@@ -50,386 +50,31 @@ $primitiveAdapter = $serializer->adapter(
5050 PrimitiveTypeAdapter::class,
5151 NamedType::wrap(Item::class, [Carbon::class])
5252);
53- $primitiveAdapter->serialize(new Item(...)) // -> ['int' => 123, ...]
53+ // -> ['int' => 123, ...]
54+ $primitiveAdapter->serialize(new Item(...));
5455
5556$jsonAdapter = $serializer->adapter(
5657 JsonTypeAdapter::class,
5758 NamedType::wrap(Item::class, [PrimitiveType::int()])
5859);
59- $jsonAdapter->deserialize('{"int": 123, ...}') // -> new Item(123, ...)
60+ // new Item(123, ...)
61+ $jsonAdapter->deserialize('{"int": 123, ...}');
6062```
6163
62- ### Custom mappers
64+ ## Documentation
6365
64- Mappers are the simplest form customizing serialization of types. All you have
65- to do is to mark a method with either ` #[MapTo()] ` or ` #[MapFrom] ` attribute,
66- specify the type in question as first parameter or return type and the serializer
67- will handle the rest automatically. A single mapper may have as many map methods as you wish.
68-
69- ``` php
70- final class DateTimeMapper
71- {
72- #[MapTo(PrimitiveTypeAdapter::class)]
73- public function serialize(DateTime $value): string
74- {
75- return $value->format(DateTimeInterface::RFC3339_EXTENDED);
76- }
77-
78- #[MapFrom(PrimitiveTypeAdapter::class)]
79- public function deserialize(string $value): DateTime
80- {
81- return new DateTime($value);
82- }
83- }
84-
85- $serializer = (new SerializerBuilder())
86- ->addMapperLast(new DateTimeMapper())
87- ->build();
88- ```
89-
90- With mappers, you can even handle complex types - such as generics or inheritance:
91-
92- ``` php
93- final class ArrayMapper
94- {
95- #[MapTo(PrimitiveTypeAdapter::class)]
96- public function to(array $value, Type $type, Serializer $serializer): array
97- {
98- $itemAdapter = $serializer->adapter(PrimitiveTypeAdapter::class, $type->arguments[1]);
99-
100- return array_map(fn ($item) => $itemAdapter->serialize($item), $value);
101- }
102-
103- #[MapFrom(PrimitiveTypeAdapter::class)]
104- public function from(array $value, Type $type, Serializer $serializer): array
105- {
106- $itemAdapter = $serializer->adapter(PrimitiveTypeAdapter::class, $type->arguments[1]);
107-
108- return array_map(fn ($item) => $itemAdapter->deserialize($item), $value);
109- }
110- }
111-
112- final class BackedEnumMapper
113- {
114- #[MapTo(PrimitiveTypeAdapter::class, new BaseTypeAcceptedByAcceptanceStrategy(BackedEnum::class))]
115- public function to(BackedEnum $value): string|int
116- {
117- return $value->value;
118- }
119-
120- #[MapFrom(PrimitiveTypeAdapter::class, new BaseTypeAcceptedByAcceptanceStrategy(BackedEnum::class))]
121- public function from(string|int $value, Type $type): BackedEnum
122- {
123- $enumClass = $type->name;
124-
125- return $enumClass::tryFrom($value);
126- }
127- }
128- ```
129-
130- ## Type adapter factories
131-
132- Besides type mappers which satisfy most of the needs, you can use type adapter factories
133- to precisely control how each type is serialized.
134-
135- The idea is the following: when building a serializer, you add all of the factories you want
136- to use in order of priority:
137-
138- ``` php
139- (new SerializerBuilder())
140- ->addMapperLast(new TestMapper()) // #2 - then this one
141- ->addFactoryLast(new TestFactory()) // #3 - and this one last
142- ->addFactory(new TestFactory()) // #1 - attempted first
143- ```
144-
145- A factory has the following signature:
146-
147- ``` php
148- public function create(string $typeAdapterType, Type $type, Attributes $attributes, Serializer $serializer): ?TypeAdapter
149- ```
150-
151- If you return ` null ` , the next factory is called. Otherwise, the returned type adapter is used.
152-
153- The serialized is entirely built using type adapter factories. Every type that is
154- supported out-of-the-box also has it's factory and can be overwritten just by doing
155- ` ->addFactoryLast() ` . Type mappers are also just fancy adapter factories under the hood.
156-
157- This is how you can use them:
158-
159- ``` php
160- class NullableTypeAdapterFactory implements TypeAdapterFactory
161- {
162- public function create(string $typeAdapterType, Type $type, Attributes $attributes, Serializer $serializer): ?TypeAdapter
163- {
164- if ($typeAdapterType !== PrimitiveTypeAdapter::class || !$type instanceof NullableType) {
165- return null;
166- }
167-
168- return new NullableTypeAdapter(
169- $serializer->adapter($typeAdapterType, $type->innerType, $attributes),
170- );
171- }
172- }
173-
174- class NullableTypeAdapter implements PrimitiveTypeAdapter
175- {
176- public function __construct(
177- private readonly PrimitiveTypeAdapter $delegate,
178- ) {
179- }
180-
181- public function serialize(mixed $value): mixed
182- {
183- if ($value === null) {
184- return null;
185- }
186-
187- return $this->delegate->serialize($value);
188- }
189-
190- public function deserialize(mixed $value): mixed
191- {
192- if ($value === null) {
193- return null;
194- }
195-
196- return $this->delegate->deserialize($value);
197- }
198- }
199- ```
200-
201- In this example, ` NullableTypeAdapterFactory ` handles all nullable types. When a non-nullable
202- type is given, it returns ` null ` . That means that the next in "queue" type adapter will be
203- called. When a nullable is given, it returns a new type adapter instance which has two
204- methods: ` serialize ` and ` deserialize ` . They do exactly what they're called.
205-
206- ## Naming of keys
207-
208- By default, serializer preserves the naming of keys, but this is easily customizable (in order of priority):
209-
210- - specify a custom property name using the ` #[SerializedName] ` attribute
211- - specify a custom naming strategy per class using the ` #[SerializedName] ` attribute
212- - specify a custom global (default) naming strategy (use one of the built-in or write your own)
213-
214- Here's an example:
215-
216- ``` php
217- (new SerializerBuilder())->namingStrategy(BuiltInNamingStrategy::SNAKE_CASE);
218-
219- // Uses snake_case by default
220- class Item1 {
221- public function __construct(
222- public int $keyName, // appears as "key_name" in serialized data
223- #[SerializedName('second_key')] public int $firstKey, // second_key
224- #[SerializedName(BuiltInNamingStrategy::PASCAL_CASE)] public int $thirdKey, // THIRD_KEY
225- ) {}
226- }
227-
228- // Uses PASCAL_CASE by default
229- #[SerializedName(BuiltInNamingStrategy::PASCAL_CASE)]
230- class Item2 {
231- public function __construct(
232- public int $keyName, // KEY_NAME
233- ) {}
234- }
235- ```
236-
237- Out of the box, strategies for ` snake_case ` , ` camelCase ` and ` PascalCase ` are provided,
238- but it's trivial to implement your own:
239-
240- ``` php
241- class PrefixedNaming implements NamingStrategy {
242- public function __construct(
243- private readonly string $prefix,
244- ) {}
245-
246- public function translate(PropertyReflection $property): string
247- {
248- return $this->prefix . $property->name();
249- }
250- }
251-
252- #[SerializedName(new PrefixedNaming('$'))]
253- class SiftTrackData {}
254- ```
255-
256- ## Required, nullable, optional and default values
257-
258- By default, if a property is missing in serialized payload:
259-
260- - nullable properties are just set to null
261- - properties with a default value - use the default value
262- - optional properties are set to ` MissingValue::INSTANCE `
263- - any other throw an exception
264-
265- Here's an example:
266-
267- ``` php
268- class Item {
269- public function __construct(
270- public ?int $first, // set to null
271- public bool $second = true, // set to true
272- public Item $third = new Item(...), // set to Item instance
273- public int|MissingValue $fourth, // set to MissingValue::INSTANCE
274- public int $fifth, // required, throws if missing
275- ) {}
276- }
277-
278- // all keys missing -> throws for 'fifth' property
279- $adapter->deserialize([])
280-
281- // only required property -> uses null, default values and optional
282- $adapter->deserialize(['fifth' => 123]);
283-
284- // all properties -> fills all values
285- $adapter->deserialize(['first' => 123, 'second' => false, ...]);
286- ```
287-
288- ## Flattening
289-
290- Sometimes the same set of keys/types is shared between multiple other models. You could
291- use inheritance for this, but we believe in composition over inheritance and hence provide
292- a simple way to achieve the same behaviour without using inheritance:
293-
294- Here's an example:
295-
296- ``` php
297- class Pagination {
298- public function __construct(
299- public readonly int $perPage,
300- public readonly int $total,
301- ) {}
302- }
303-
304- class UsersPaginatedList {
305- public function __construct(
306- #[Flatten]
307- public readonly Pagination $pagination,
308- /** @var User[] */
309- public readonly array $users,
310- ) {}
311- }
312-
313- // {"perPage": 25, "total": 100, "users": []}
314- $adapter->serialize(
315- new UsersPaginatedList(
316- pagination: new Pagination(25, 100),
317- users: [],
318- )
319- );
320- ```
321-
322- ## Use default value for unexpected
323-
324- There are situations where you're deserializing data from a third party that doesn't have an API documentation
325- or one that can't keep a backwards compatibility promise. One such case is when a third party uses an enum
326- and you expect that new enum values might get added in the future by them. For example, imagine this structure:
327-
328- ``` php
329- enum CardType: string
330- {
331- case CLUBS = 'clubs';
332- case DIAMONDS = 'diamonds';
333- case HEARTS = 'hearts';
334- case SPADES = 'spades';
335- }
336-
337- readonly class Card {
338- public function __construct(
339- public CardType $type,
340- public string $value,
341- ) {}
342- }
343- ```
344-
345- If you get an unexpected value for ` type ` , you'll get an exception:
346-
347- ``` php
348- // UnexpectedEnumValueException: Expected one of [clubs, diamonds, hearts, spades], but got 'joker'
349- $adapter->deserialize('{"type": "joker"}');
350- ```
351-
352- So if you suspect that might happen, add a default value you wish to use (anything) and
353- a ` #[UseDefaultForUnexpected] ` attribute:
354-
355- ``` php
356- readonly class Card {
357- public function __construct(
358- #[UseDefaultForUnexpected]
359- public CardType $type = null,
360- // Can be any other valid default value
361- #[UseDefaultForUnexpected]
362- public CardType $type2 = CardType::SPADES,
363- ) {}
364- }
365- ```
366-
367- Whenever that happens, a default value will be used instead. Optionally, you can also log such cases:
368-
369- ``` php
370- $serializer = (new SerializerBuilder())
371- ->reportUnexpectedDefault(function (BoundClassProperty $property, UnexpectedValueException $e) {
372- $log->warning("Serializer used a default for unexpected value: {$e->getMessage()}", [
373- 'property' => $property->serializedName(),
374- 'exception' => $e,
375- ]);
376- })
377- ->build();
378- ```
379-
380- ## Error handling
381-
382- This is expected to be used with client-provided data, so good error descriptions is a must.
383- These are some of the errors you'll get:
384-
385- - Expected value of type 'int', but got 'string'
386- - Expected value of type 'string', but got 'NULL'
387- - Failed to parse time string (2020 dasd) at position 5 (d): The timezone could not be found in the database
388- - Expected value of type 'string|int', but got 'boolean'
389- - Expected one of [ one, two] , but got 'five'
390- - Could not map item at key '1': Expected value of type 'string', but got 'NULL'
391- - Could not map item at key '0': Expected value of type 'string', but got 'NULL' (and 1 more errors)."
392- - Could not map property at path 'nested.field': Expected value of type 'string', but got 'integer'
393-
394- All of these are just a chain of PHP exceptions with ` previous ` exceptions. Besides
395- those messages, you have all of the thrown exceptions with necessary information.
396-
397- ## More formats
398-
399- You can add support for more formats as you wish with your own type adapters.
400- All of the existing adapters are at your disposal:
401-
402- ``` php
403- interface XmlTypeAdapter extends TypeAdapter {}
404-
405- final class FromPrimitiveXmlTypeAdapter implements XmlTypeAdapter
406- {
407- public function __construct(
408- private readonly PrimitiveTypeAdapter $primitiveAdapter,
409- ) {
410- }
411-
412- public function serialize(mixed $value): mixed
413- {
414- return xml_encode($this->primitiveAdapter->serialize($value));
415- }
416-
417- public function deserialize(mixed $value): mixed
418- {
419- return $this->primitiveAdapter->deserialize(xml_decode($value));
420- }
421- }
422- ```
66+ Basic documentation is available in [ docs/] ( docs ) . For examples, you can look at the
67+ test suite: [ tests/Integration] ( tests/Integration ) .
42368
42469## Why this over everything else?
42570
426- There are some alternatives to this, but all of them will lack at least one of these :
71+ There are some alternatives to this, but they usually lack one of the following :
42772
428- - doesn't rely on inheritance, hence allows serializing third-party classes
73+ - stupid simple internal structure: no node tree, no value/JSON wrappers, no in-repo custom reflection implementation, no PHP parsing
74+ - doesn't rely on inheritance of serializable classes, hence allows serializing third-party classes
42975- parses existing PHPDoc information instead of duplicating it through attributes
43076- supports generic types which are quite useful for wrapper types
43177- allows simple extension through mappers and complex stuff through type adapters
43278- produces developer-friendly error messages for invalid data
43379- correctly handles optional (missing keys) and ` null ` values as separate concerns
43480- simple to extend with additional formats
435- - simple internal structure: no node tree, no value/JSON wrappers, no custom reflection / PHP parsing, no inherent limitations
0 commit comments