Skip to content

Commit

Permalink
Merge pull request #4 from blacktrs/fix-private-properties-serialize
Browse files Browse the repository at this point in the history
Fix nullable fields serializing
  • Loading branch information
blacktrs authored Sep 17, 2023
2 parents 1378d62 + 851dc40 commit 81a3409
Show file tree
Hide file tree
Showing 30 changed files with 618 additions and 106 deletions.
56 changes: 56 additions & 0 deletions .github/workflows/php.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: PHP Build

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
contents: write

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'

- name: Validate composer.json and composer.lock
run: composer validate --strict

- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run test suite
run: composer test

- name: Make code coverage badge
uses: timkrase/phpunit-coverage-badge@v1.2.1
with:
coverage_badge_path: output/coverage.svg
push_badge: false

- name: Git push to image-data branch
uses: peaceiris/actions-gh-pages@v3
with:
publish_dir: ./output
publish_branch: image-data
github_token: ${{ secrets.GITHUB_TOKEN }}
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/.idea
/.fleet
/vendor
/.phpunit.result.cache
/.php-cs-fixer.cache
/.phpunit.cache
/clover.xml
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
![Coverage](https://raw.githubusercontent.com/blacktrs/data-transformer/image-data/coverage.svg)

# Data transformer

The modern PHP library for converting arrays into objects and vice-versa.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "blacktrs/data-transformer",
"type": "library",
"version": "1.1.1",
"description": "Zero-dependency PHP array-to-object transformer",
"require": {
"php": ">=8.2"
Expand Down Expand Up @@ -35,6 +34,7 @@
"test": [
"@putenv PHP_CS_FIXER_IGNORE_ENV=true",
"php-cs-fixer fix --dry-run",
"@putenv XDEBUG_MODE=coverage",
"phpunit",
"phpstan --xdebug",
"psalm"
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
parameters:
level: 6
paths:
- src
- src
checkGenericClassInNonGenericObjectType: false
25 changes: 16 additions & 9 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd" backupGlobals="true"
cacheResult="false" colors="true">
<coverage/>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" backupGlobals="true" cacheResult="false" colors="true">
<coverage cacheDirectory=".phpunit.cache/code-coverage" includeUncoveredFiles="true">
<report>
<clover outputFile="clover.xml" />
</report>
</coverage>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>
15 changes: 14 additions & 1 deletion src/Serializer/ObjectSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,25 @@
*/
class ObjectSerializer implements ObjectSerializerInterface
{
public function __construct(private bool $includePrivateProperties = false)
{
}

public function serialize(object $object, ObjectSerializerInterface|string|null $originSerializer = null): mixed
{
$reflection = new ReflectionClass($object);
$objectItem = $this->getTransformerObject($reflection);

return $this->getObjectSerializer($objectItem, $originSerializer)->serialize($object, $this);
return $this->getObjectSerializer($objectItem, $originSerializer)
->setIncludePrivateProperties($this->includePrivateProperties)
->serialize($object, $this);
}

public function setIncludePrivateProperties(bool $includePrivateProperties): self
{
$this->includePrivateProperties = $includePrivateProperties;

return $this;
}

private function getObjectSerializer(
Expand Down
2 changes: 2 additions & 0 deletions src/Serializer/ObjectSerializerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ interface ObjectSerializerInterface
* @param ObjectSerializerInterface|class-string<ObjectSerializerInterface>|null $originSerializer
*/
public function serialize(object $object, ObjectSerializerInterface|string|null $originSerializer = null): mixed;

public function setIncludePrivateProperties(bool $includePrivateProperties): self;
}
11 changes: 10 additions & 1 deletion src/Serializer/Serializer/ArraySerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class ArraySerializer implements ObjectSerializerInterface
use Fieldable;
use Valuable;

private bool $includePrivateProperties = false;

/**
* @param ObjectSerializerInterface|class-string<ObjectSerializerInterface>|null $originSerializer
* @return array<array-key, mixed>
Expand All @@ -30,7 +32,7 @@ public function serialize(object $object, ObjectSerializerInterface|string|null
foreach ($properties as $property) {
$field = $this->getPropertyAttribute($property);

if ($field->ignoreSerialize) {
if (!$this->includePrivateProperties && $field->ignoreSerialize) {
continue;
}

Expand All @@ -41,6 +43,13 @@ public function serialize(object $object, ObjectSerializerInterface|string|null
return $serialized;
}

public function setIncludePrivateProperties(bool $includePrivateProperties): self
{
$this->includePrivateProperties = $includePrivateProperties;

return $this;
}

/**
* @param ObjectSerializerInterface|class-string<ObjectSerializerInterface>|null $serializer
*/
Expand Down
16 changes: 15 additions & 1 deletion src/Serializer/Serializer/JsonSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,27 @@ class JsonSerializer implements ObjectSerializerInterface
use Fieldable;
use Valuable;

private bool $includePrivateProperties = false;

/**
* @param ObjectSerializerInterface|class-string<ObjectSerializerInterface>|null $originSerializer
*/
public function serialize(object $object, string|ObjectSerializerInterface|null $originSerializer = null): string
{
$arraySerializer = new ArraySerializer();

return json_encode($arraySerializer->serialize($object, $originSerializer));
return json_encode(
$arraySerializer
->setIncludePrivateProperties($this->includePrivateProperties)
->serialize($object, $originSerializer),
JSON_THROW_ON_ERROR
);
}

public function setIncludePrivateProperties(bool $includePrivateProperties): self
{
$this->includePrivateProperties = $includePrivateProperties;

return $this;
}
}
118 changes: 71 additions & 47 deletions src/Transformer/Transformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

use ReflectionParameter;

use ReflectionException;

use ReflectionType;

use function array_key_exists;
use function class_exists;
use function is_string;
Expand Down Expand Up @@ -58,23 +62,10 @@ public function transform(string|object $object, iterable $data): object
return $object;
}

$args = [];
foreach ($parameters as $parameter) {
$field = $this->getParameterAttribute($parameter);
$name = $field->nameIn ?? $parameter->getName();

if (array_key_exists($name, $this->data)) {
$args[$name] = $this->getValue($field, $parameter, $name);
}
}

$object = $reflection->newInstanceWithoutConstructor();
$reflection->getConstructor()?->invokeArgs($object, $args);

return $object;
return $this->handleConstructorParameters($parameters, $reflection);
}

public function setIncludePrivateProperties(bool $includePrivateProperties): Transformer
public function setIncludePrivateProperties(bool $includePrivateProperties): self
{
$this->includePrivateProperties = $includePrivateProperties;

Expand Down Expand Up @@ -126,37 +117,7 @@ private function getValue(DataField $field, ReflectionProperty|ReflectionParamet
return $this->getValueFromObjectResolver($field, $reflectionType, $name);
}

$value = $this->data[$name];

if ($property->hasType() && $reflectionType instanceof ReflectionNamedType) {
if ($value === null && $reflectionType->allowsNull()) {
return null;
}

if ($reflectionType->isBuiltin()) {
settype($value, $reflectionType->getName());

return $value;
}

if (enum_exists($reflectionType->getName())) {
/** @var class-string<UnitEnum>|class-string<BackedEnum> $enum */
$enum = $reflectionType->getName();
if (is_subclass_of($enum, BackedEnum::class)) {
return $enum::tryFrom($value);
}

$cases = array_values(array_filter($enum::cases(), fn (UnitEnum $unitEnum) => $unitEnum->name === $value));

return $cases[0] ?? throw new TransformerException(sprintf('No enum case found for %s in %s', $value, $enum));
}

if (class_exists($reflectionType->getName())) {
return (new self())->transform($reflectionType->getName(), $value);
}
}

return $value;
return $this->getTypedValue($property, $reflectionType, $this->data[$name]);
}

/**
Expand Down Expand Up @@ -202,10 +163,73 @@ private function getValueFromObjectResolver(
): mixed {
/** @var TransformerInterface $objectTransformer */
$objectTransformer = is_string($field->objectTransformer) ? new $field->objectTransformer() : $field->objectTransformer;

/** @var class-string $objectClass */
$objectClass = $this->getType($reflectionType);

return $objectTransformer->transform($objectClass, $this->data[$name]);
}

/**
* @param array<ReflectionParameter> $parameters
* @param ReflectionClass<object> $reflection
* @throws ReflectionException
*/
private function handleConstructorParameters(array $parameters, ReflectionClass $reflection): object
{
$args = [];
foreach ($parameters as $parameter) {
$field = $this->getParameterAttribute($parameter);
$name = $field->nameIn ?? $parameter->getName();

if (array_key_exists($name, $this->data)) {
$args[$name] = $this->getValue($field, $parameter, $name);
}
}

$object = $reflection->newInstanceWithoutConstructor();
$reflection->getConstructor()?->invokeArgs($object, $args);

return $object;
}

private function getTypedValue(
ReflectionParameter|ReflectionProperty $property,
?ReflectionType $reflectionType,
mixed $value
): mixed {
if ($property->hasType() && $reflectionType instanceof ReflectionNamedType) {
if ($value === null && $reflectionType->allowsNull()) {
return null;
}

if ($reflectionType->isBuiltin()) {
settype($value, $reflectionType->getName());

return $value;
}

if (enum_exists($reflectionType->getName())) {
return $this->getEnumValue($reflectionType, $value);
}

if (class_exists($reflectionType->getName())) {
return (new self())->transform($reflectionType->getName(), $value);
}
}

return $value;
}

private function getEnumValue(ReflectionNamedType $reflectionType, mixed $value): mixed
{
/** @var class-string<UnitEnum>|class-string<BackedEnum> $enum */
$enum = $reflectionType->getName();
if (is_subclass_of($enum, BackedEnum::class)) {
return $enum::tryFrom($value);
}

$cases = array_values(array_filter($enum::cases(), fn (UnitEnum $unitEnum) => $unitEnum->name === $value));

return $cases[0] ?? throw new TransformerException(sprintf('No enum case found for %s in %s', $value, $enum));
}
}
10 changes: 10 additions & 0 deletions tests/Fake/Item/FakeObjectWithConstructorAndGetters.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,14 @@ public function getPostcode(): int
{
return $this->postcode;
}

public function setPostcode(int $postcode): void
{
$this->postcode = $postcode;
}

public function setCity(string $city): void
{
$this->city = $city;
}
}
Loading

0 comments on commit 81a3409

Please sign in to comment.