Skip to content

Commit

Permalink
feat: Allow custom object construction and custom property binds for …
Browse files Browse the repository at this point in the history
…ClassPropertiesPrimitiveTypeAdapter
  • Loading branch information
autaut03 committed Aug 18, 2022
1 parent a813023 commit 7f96a25
Show file tree
Hide file tree
Showing 18 changed files with 316 additions and 114 deletions.
6 changes: 4 additions & 2 deletions src/GoodPhp/Serialization/SerializerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
use GoodPhp\Serialization\TypeAdapter\Primitive\BuiltIn\Nullable\NullableTypeAdapterFactory;
use GoodPhp\Serialization\TypeAdapter\Primitive\BuiltIn\ScalarMapper;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\ClassPropertiesPrimitiveTypeAdapterFactory;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Constructing\NoConstructorObjectFactory;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming\BuiltInNamingStrategy;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming\NamingStrategy;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming\SerializedNameAttributeNamingStrategy;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\ObjectClassFactory;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property\DefaultBoundClassPropertyFactory;
use GoodPhp\Serialization\TypeAdapter\Primitive\Illuminate\CollectionMapper;
use GoodPhp\Serialization\TypeAdapter\Primitive\MapperMethods\MapperMethodFactory;
use GoodPhp\Serialization\TypeAdapter\Primitive\MapperMethods\MapperMethodsPrimitiveTypeAdapterFactoryFactory;
Expand Down Expand Up @@ -116,7 +117,8 @@ public function build(): Serializer
->addMapperLast(new DateTimeMapper())
->addFactoryLast(new ClassPropertiesPrimitiveTypeAdapterFactory(
new SerializedNameAttributeNamingStrategy($this->namingStrategy ?? BuiltInNamingStrategy::PRESERVING),
new ObjectClassFactory(),
new NoConstructorObjectFactory(),
new DefaultBoundClassPropertyFactory(),
))
->addFactoryLast(new FromPrimitiveJsonTypeAdapterFactory());

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Closure;
use GoodPhp\Serialization\TypeAdapter\Exception\MultipleMappingException;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property\BoundClassProperty;
use GoodPhp\Serialization\TypeAdapter\Primitive\PrimitiveTypeAdapter;
use Illuminate\Support\Collection;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@
use GoodPhp\Reflection\Type\NamedType;
use GoodPhp\Reflection\Type\Type;
use GoodPhp\Serialization\Serializer;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Constructing\ObjectFactory;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming\NamingStrategy;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property\BoundClassPropertyFactory;
use GoodPhp\Serialization\TypeAdapter\Primitive\PrimitiveTypeAdapter;
use GoodPhp\Serialization\TypeAdapter\TypeAdapterFactory;
use TenantCloud\Standard\Optional\Optional;

final class ClassPropertiesPrimitiveTypeAdapterFactory implements TypeAdapterFactory
{
public function __construct(
private readonly NamingStrategy $namingStrategy,
private readonly ObjectClassFactory $objectClassFactory,
private readonly ObjectFactory $objectFactory,
private readonly BoundClassPropertyFactory $boundClassPropertyFactory,
) {
}

Expand All @@ -36,21 +38,15 @@ public function create(string $typeAdapterType, Type $type, array $attributes, S
}

return new ClassPropertiesPrimitiveTypeAdapter(
fn () => $this->objectClassFactory->create($reflection->qualifiedName()),
$reflection->properties()->map(function (PropertyReflection $property) use ($serializer, $typeAdapterType, $attributes) {
$attributes = $property->attributes()->all();
$serializedName = $this->namingStrategy->translate($property->name(), $attributes);

return PropertyMappingException::rethrow($serializedName, fn () => new BoundClassProperty(
reflection: $property,
typeAdapter: $serializer->adapter(
$typeAdapterType,
$property->type(),
$attributes
),
fn () => $this->objectFactory->create($reflection),
$reflection->properties()->map(function (PropertyReflection $property) use ($reflection, $serializer, $typeAdapterType) {
$serializedName = $this->namingStrategy->translate($property->name(), $property->attributes(), $reflection->attributes());

return PropertyMappingException::rethrow($serializedName, fn () => $this->boundClassPropertyFactory->create(
property: $property,
serializedName: $serializedName,
optional: $property->type() instanceof NamedType && $property->type()->name === Optional::class,
hasDefaultValue: $property->hasDefaultValue(),
typeAdapterType: $typeAdapterType,
serializer: $serializer
));
})
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Constructing;

use GoodPhp\Reflection\Reflector\Reflection\ClassReflection;

final class NoConstructorObjectFactory implements ObjectFactory
{
/**
* @inheritDoc
*/
public function create(ClassReflection $reflection): object
{
return $reflection->newInstanceWithoutConstructor();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Constructing;

use GoodPhp\Reflection\Reflector\Reflection\ClassReflection;

interface ObjectFactory
{
/**
* @template T
*
* @param ClassReflection<T> $reflection
*
* @return T
*/
public function create(ClassReflection $reflection): object;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming;

use Illuminate\Support\Collection;
use Illuminate\Support\Str;

enum BuiltInNamingStrategy implements NamingStrategy
{
public function translate(string $name, array $attributes): string
public function translate(string $name, Collection $attributes, Collection $classAttributes): string
{
return match ($this) {
self::PRESERVING => $name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming;

use Illuminate\Support\Collection;

interface NamingStrategy
{
public function translate(string $name, array $attributes): string;
public function translate(string $name, Collection $attributes, Collection $classAttributes): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
final class SerializedName
{
public function __construct(public readonly string|NamingStrategy $nameOrStrategy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,33 @@

namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming;

use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Webmozart\Assert\Assert;

class SerializedNameAttributeNamingStrategy implements NamingStrategy
{
public function __construct(private readonly NamingStrategy $fallback)
{
}

public function translate(string $name, array $attributes): string
public function translate(string $name, Collection $attributes, Collection $classAttributes): string
{
/** @var SerializedName|null $attribute */
$attribute = Arr::first($attributes, fn (object $attribute) => $attribute instanceof SerializedName);
$attribute = $attributes->first(fn (object $attribute) => $attribute instanceof SerializedName);

if (!$attribute) {
return $this->fallback->translate($name, $attributes);
/** @var SerializedName|null $attribute */
$attribute = $classAttributes->first(fn (object $attribute) => $attribute instanceof SerializedName);

Assert::true(!$attribute || $attribute->nameOrStrategy instanceof NamingStrategy, 'Class applied #[SerializedName] must provide a naming strategy rather than a string name.');
}

if (!$attribute) {
return $this->fallback->translate($name, $attributes, $classAttributes);
}

if ($attribute->nameOrStrategy instanceof NamingStrategy) {
return $attribute->nameOrStrategy->translate($name, $attributes);
return $attribute->nameOrStrategy->translate($name, $attributes, $classAttributes);
}

return $attribute->nameOrStrategy;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property;

/**
* @template T of object
*/
interface BoundClassProperty
{
public function serializedName(): string;

/**
* @param T $object
*/
public function serialize(object $object): array;

/**
* @param T $into
*/
public function deserialize(array $data, object $into): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property;

use GoodPhp\Reflection\Reflector\Reflection\PropertyReflection;
use GoodPhp\Serialization\Serializer;

interface BoundClassPropertyFactory
{
public function create(PropertyReflection $property, string $serializedName, string $typeAdapterType, Serializer $serializer): BoundClassProperty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property;

use GoodPhp\Reflection\Reflector\Reflection\PropertyReflection;
use GoodPhp\Serialization\Serializer;

class DefaultBoundClassPropertyFactory implements BoundClassPropertyFactory
{
public function create(PropertyReflection $property, string $serializedName, string $typeAdapterType, Serializer $serializer): BoundClassProperty
{
return OptionalSkippingBoundClassProperty::wrap(
$property,
DefaultValueSkippingBoundClassProperty::wrap(
$property,
DirectlyBoundClassProperty::from($property, $serializedName, $typeAdapterType, $serializer)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property;

use GoodPhp\Reflection\Reflector\Reflection\PropertyReflection;
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\MissingValueException;

/**
* Skips fields with a default value if their value is missing.
*
* @template T
*/
class DefaultValueSkippingBoundClassProperty implements BoundClassProperty
{
public function __construct(
private readonly BoundClassProperty $delegate,
) {
}

public static function wrap(PropertyReflection $reflection, BoundClassProperty $property): BoundClassProperty
{
if (!$reflection->hasDefaultValue()) {
return $property;
}

return new self(
delegate: $property,
);
}

public function serializedName(): string
{
return $this->delegate->serializedName();
}

/**
* @inheritDoc
*/
public function serialize(object $object): array
{
return $this->delegate->serialize($object);
}

/**
* @inheritDoc
*/
public function deserialize(array $data, object $into): void
{
try {
$this->delegate->deserialize($data, $into);
} catch (MissingValueException) {
}
}
}
Loading

0 comments on commit 7f96a25

Please sign in to comment.