Skip to content

Commit 7f96a25

Browse files
committed
feat: Allow custom object construction and custom property binds for ClassPropertiesPrimitiveTypeAdapter
1 parent a813023 commit 7f96a25

18 files changed

+316
-114
lines changed

src/GoodPhp/Serialization/SerializerBuilder.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
use GoodPhp\Serialization\TypeAdapter\Primitive\BuiltIn\Nullable\NullableTypeAdapterFactory;
1313
use GoodPhp\Serialization\TypeAdapter\Primitive\BuiltIn\ScalarMapper;
1414
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\ClassPropertiesPrimitiveTypeAdapterFactory;
15+
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Constructing\NoConstructorObjectFactory;
1516
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming\BuiltInNamingStrategy;
1617
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming\NamingStrategy;
1718
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming\SerializedNameAttributeNamingStrategy;
18-
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\ObjectClassFactory;
19+
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property\DefaultBoundClassPropertyFactory;
1920
use GoodPhp\Serialization\TypeAdapter\Primitive\Illuminate\CollectionMapper;
2021
use GoodPhp\Serialization\TypeAdapter\Primitive\MapperMethods\MapperMethodFactory;
2122
use GoodPhp\Serialization\TypeAdapter\Primitive\MapperMethods\MapperMethodsPrimitiveTypeAdapterFactoryFactory;
@@ -116,7 +117,8 @@ public function build(): Serializer
116117
->addMapperLast(new DateTimeMapper())
117118
->addFactoryLast(new ClassPropertiesPrimitiveTypeAdapterFactory(
118119
new SerializedNameAttributeNamingStrategy($this->namingStrategy ?? BuiltInNamingStrategy::PRESERVING),
119-
new ObjectClassFactory(),
120+
new NoConstructorObjectFactory(),
121+
new DefaultBoundClassPropertyFactory(),
120122
))
121123
->addFactoryLast(new FromPrimitiveJsonTypeAdapterFactory());
122124

src/GoodPhp/Serialization/TypeAdapter/Primitive/ClassProperties/BoundClassProperty.php

-67
This file was deleted.

src/GoodPhp/Serialization/TypeAdapter/Primitive/ClassProperties/ClassPropertiesPrimitiveTypeAdapter.php

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Closure;
66
use GoodPhp\Serialization\TypeAdapter\Exception\MultipleMappingException;
7+
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property\BoundClassProperty;
78
use GoodPhp\Serialization\TypeAdapter\Primitive\PrimitiveTypeAdapter;
89
use Illuminate\Support\Collection;
910

src/GoodPhp/Serialization/TypeAdapter/Primitive/ClassProperties/ClassPropertiesPrimitiveTypeAdapterFactory.php

+12-16
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77
use GoodPhp\Reflection\Type\NamedType;
88
use GoodPhp\Reflection\Type\Type;
99
use GoodPhp\Serialization\Serializer;
10+
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Constructing\ObjectFactory;
1011
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Naming\NamingStrategy;
12+
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property\BoundClassPropertyFactory;
1113
use GoodPhp\Serialization\TypeAdapter\Primitive\PrimitiveTypeAdapter;
1214
use GoodPhp\Serialization\TypeAdapter\TypeAdapterFactory;
13-
use TenantCloud\Standard\Optional\Optional;
1415

1516
final class ClassPropertiesPrimitiveTypeAdapterFactory implements TypeAdapterFactory
1617
{
1718
public function __construct(
1819
private readonly NamingStrategy $namingStrategy,
19-
private readonly ObjectClassFactory $objectClassFactory,
20+
private readonly ObjectFactory $objectFactory,
21+
private readonly BoundClassPropertyFactory $boundClassPropertyFactory,
2022
) {
2123
}
2224

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

3840
return new ClassPropertiesPrimitiveTypeAdapter(
39-
fn () => $this->objectClassFactory->create($reflection->qualifiedName()),
40-
$reflection->properties()->map(function (PropertyReflection $property) use ($serializer, $typeAdapterType, $attributes) {
41-
$attributes = $property->attributes()->all();
42-
$serializedName = $this->namingStrategy->translate($property->name(), $attributes);
43-
44-
return PropertyMappingException::rethrow($serializedName, fn () => new BoundClassProperty(
45-
reflection: $property,
46-
typeAdapter: $serializer->adapter(
47-
$typeAdapterType,
48-
$property->type(),
49-
$attributes
50-
),
41+
fn () => $this->objectFactory->create($reflection),
42+
$reflection->properties()->map(function (PropertyReflection $property) use ($reflection, $serializer, $typeAdapterType) {
43+
$serializedName = $this->namingStrategy->translate($property->name(), $property->attributes(), $reflection->attributes());
44+
45+
return PropertyMappingException::rethrow($serializedName, fn () => $this->boundClassPropertyFactory->create(
46+
property: $property,
5147
serializedName: $serializedName,
52-
optional: $property->type() instanceof NamedType && $property->type()->name === Optional::class,
53-
hasDefaultValue: $property->hasDefaultValue(),
48+
typeAdapterType: $typeAdapterType,
49+
serializer: $serializer
5450
));
5551
})
5652
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Constructing;
4+
5+
use GoodPhp\Reflection\Reflector\Reflection\ClassReflection;
6+
7+
final class NoConstructorObjectFactory implements ObjectFactory
8+
{
9+
/**
10+
* @inheritDoc
11+
*/
12+
public function create(ClassReflection $reflection): object
13+
{
14+
return $reflection->newInstanceWithoutConstructor();
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Constructing;
4+
5+
use GoodPhp\Reflection\Reflector\Reflection\ClassReflection;
6+
7+
interface ObjectFactory
8+
{
9+
/**
10+
* @template T
11+
*
12+
* @param ClassReflection<T> $reflection
13+
*
14+
* @return T
15+
*/
16+
public function create(ClassReflection $reflection): object;
17+
}

src/GoodPhp/Serialization/TypeAdapter/Primitive/ClassProperties/Naming/BuiltInNamingStrategy.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

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

5+
use Illuminate\Support\Collection;
56
use Illuminate\Support\Str;
67

78
enum BuiltInNamingStrategy implements NamingStrategy
89
{
9-
public function translate(string $name, array $attributes): string
10+
public function translate(string $name, Collection $attributes, Collection $classAttributes): string
1011
{
1112
return match ($this) {
1213
self::PRESERVING => $name,

src/GoodPhp/Serialization/TypeAdapter/Primitive/ClassProperties/Naming/NamingStrategy.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

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

5+
use Illuminate\Support\Collection;
6+
57
interface NamingStrategy
68
{
7-
public function translate(string $name, array $attributes): string;
9+
public function translate(string $name, Collection $attributes, Collection $classAttributes): string;
810
}

src/GoodPhp/Serialization/TypeAdapter/Primitive/ClassProperties/Naming/SerializedName.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use Attribute;
66

7-
#[Attribute(Attribute::TARGET_PROPERTY)]
7+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
88
final class SerializedName
99
{
1010
public function __construct(public readonly string|NamingStrategy $nameOrStrategy)

src/GoodPhp/Serialization/TypeAdapter/Primitive/ClassProperties/Naming/SerializedNameAttributeNamingStrategy.php

+13-5
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,33 @@
22

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

5-
use Illuminate\Support\Arr;
5+
use Illuminate\Support\Collection;
6+
use Webmozart\Assert\Assert;
67

78
class SerializedNameAttributeNamingStrategy implements NamingStrategy
89
{
910
public function __construct(private readonly NamingStrategy $fallback)
1011
{
1112
}
1213

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

1819
if (!$attribute) {
19-
return $this->fallback->translate($name, $attributes);
20+
/** @var SerializedName|null $attribute */
21+
$attribute = $classAttributes->first(fn (object $attribute) => $attribute instanceof SerializedName);
22+
23+
Assert::true(!$attribute || $attribute->nameOrStrategy instanceof NamingStrategy, 'Class applied #[SerializedName] must provide a naming strategy rather than a string name.');
24+
}
25+
26+
if (!$attribute) {
27+
return $this->fallback->translate($name, $attributes, $classAttributes);
2028
}
2129

2230
if ($attribute->nameOrStrategy instanceof NamingStrategy) {
23-
return $attribute->nameOrStrategy->translate($name, $attributes);
31+
return $attribute->nameOrStrategy->translate($name, $attributes, $classAttributes);
2432
}
2533

2634
return $attribute->nameOrStrategy;

src/GoodPhp/Serialization/TypeAdapter/Primitive/ClassProperties/ObjectClassFactory.php

-20
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property;
4+
5+
/**
6+
* @template T of object
7+
*/
8+
interface BoundClassProperty
9+
{
10+
public function serializedName(): string;
11+
12+
/**
13+
* @param T $object
14+
*/
15+
public function serialize(object $object): array;
16+
17+
/**
18+
* @param T $into
19+
*/
20+
public function deserialize(array $data, object $into): void;
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property;
4+
5+
use GoodPhp\Reflection\Reflector\Reflection\PropertyReflection;
6+
use GoodPhp\Serialization\Serializer;
7+
8+
interface BoundClassPropertyFactory
9+
{
10+
public function create(PropertyReflection $property, string $serializedName, string $typeAdapterType, Serializer $serializer): BoundClassProperty;
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property;
4+
5+
use GoodPhp\Reflection\Reflector\Reflection\PropertyReflection;
6+
use GoodPhp\Serialization\Serializer;
7+
8+
class DefaultBoundClassPropertyFactory implements BoundClassPropertyFactory
9+
{
10+
public function create(PropertyReflection $property, string $serializedName, string $typeAdapterType, Serializer $serializer): BoundClassProperty
11+
{
12+
return OptionalSkippingBoundClassProperty::wrap(
13+
$property,
14+
DefaultValueSkippingBoundClassProperty::wrap(
15+
$property,
16+
DirectlyBoundClassProperty::from($property, $serializedName, $typeAdapterType, $serializer)
17+
)
18+
);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\Property;
4+
5+
use GoodPhp\Reflection\Reflector\Reflection\PropertyReflection;
6+
use GoodPhp\Serialization\TypeAdapter\Primitive\ClassProperties\MissingValueException;
7+
8+
/**
9+
* Skips fields with a default value if their value is missing.
10+
*
11+
* @template T
12+
*/
13+
class DefaultValueSkippingBoundClassProperty implements BoundClassProperty
14+
{
15+
public function __construct(
16+
private readonly BoundClassProperty $delegate,
17+
) {
18+
}
19+
20+
public static function wrap(PropertyReflection $reflection, BoundClassProperty $property): BoundClassProperty
21+
{
22+
if (!$reflection->hasDefaultValue()) {
23+
return $property;
24+
}
25+
26+
return new self(
27+
delegate: $property,
28+
);
29+
}
30+
31+
public function serializedName(): string
32+
{
33+
return $this->delegate->serializedName();
34+
}
35+
36+
/**
37+
* @inheritDoc
38+
*/
39+
public function serialize(object $object): array
40+
{
41+
return $this->delegate->serialize($object);
42+
}
43+
44+
/**
45+
* @inheritDoc
46+
*/
47+
public function deserialize(array $data, object $into): void
48+
{
49+
try {
50+
$this->delegate->deserialize($data, $into);
51+
} catch (MissingValueException) {
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)