TSantos Serializer Library¶
Welcome to TSantos Serializer documentation page! This page will show you how to install and use the TSantos Serializer library.
Introduction¶
With the growth of micro-services a good serializer tool should be use to expose your data over the internet. Such tools should be able to read data (commonly PHP objects) and encode it to any format to be consumed by API client. TSantos Serializer was built focused on performance and APIs which requires fast responses, low CPU and low memory usage as non-functional requirements. Moreover, this library tries to do all this process without loosing the benefits of data-mapping and flexible configurations.
Summary¶
Installation¶
Composer¶
Open a command console, enter your project directory and execute:
$ composer require tsantos/serializer
This command requires you to have Composer installed globally, as explained in the installation chapter of the Composer documentation.
Symfony Applications¶
If your application is based on Symfony Framework, you can easily install this library through the Official Bundle.
Usage¶
All the (de-)serialization processes needs an instance of SerializerInterface which can be easily created through the SerializerBuilder. All the examples in this page will consider that the serializer instance is already created in a way like this:
$serializer = (new SerializerBuilder())
->build();
Serialize Objects¶
The simplest way to serialize an object is pass a data to SerializerInterface::serialize method:
$post = new App\Entity\Post(100, 'Post Title');
$encoded = $serializer->serialize($post);
echo $encoded; // {"id":100, "name":"Post Title"}
Collections¶
You can serialize a collection of objects in the same way you serialize a single object:
$comments = ... ;
$encoded = $serializer->serialize($comments);
echo $encoded; // [{"content":"Comment 1"}, {"content":"Comment 2"}]
Handling Circular Reference¶
Circular references occurs when an object A serializes an object B which in turns serializes the A again. This library is able to detect such situation and throws the CircularReferenceException exception. In addition you can control how many times the same object will be serialized before the exception is thrown:
try {
$post = ... ;
$context = (new SerializationContext())->setCircularReferenceCount(3);
$encoded = $serializer->serialize($post, $context);
echo $encoded; // {"id":100, "name":"Post Title"}
} catch (\TSantos\Serializer\Exception\CircularReferenceException $e) {
}
Deserialize Objects¶
The inverse operation (e.g: deserialization) is quite simple as serializing objects. You just need to provide the type and format of the data being deserialized:
$json = '{"id":100, "name":"Post Title"}';
$post = $serializer->deserialize($json, Post::class, 'json');
echo get_class($post); // App\Entity\Post
Object Instantiator¶
The serializer will instantiate a new class based on the type parameter of the method SerializerInterface::deserialize. By default it uses the Doctrine Object instantiator to create new instances but you can define your own implementation and configure the serializer to use it:
use TSantos\Serializer\ObjectInstantiator\ObjectInstantiatorInterface;
class MyObjectInstantiator implements ObjectInstantiatorInterface
{
public function create(string $type, array $data, DeserializationContext $context)
{
return new $type();
}
}
and then:
$serializer = (new SerializerBuilder())
->setObjectInstantiator(new MyObjectInstantiator())
->build();
Targeting the Deserialization¶
The serializer can populate the data into an existing object instead of instantiate a fresh instance:
$json = '{"name":"Post Title"}';
$post = ...;
$context = (new DeserializationContext())->setTarget($post);
$post = $serializer->deserialize($json, Post::class, 'json', $post);
Normalizers¶
Normalizers are powerful services that handles a specific data type and returns its handled version.
Built-in Normalizers¶
- ObjectNormalizer:
- Is the most important normalizer in this library. It can receive the object being serialized and normalize it to array.
- CollectionNormalizer:
- This normalizer iterates over a collection of objects and serializes each of them.
- JsonNormalizer:
- This normalizer checks whether the object being serialized implements the JsonSerializable interface and call the method jsonSerialize to normalized the data.
Custom normalizers can be easily added to serializer:
class AuthorNormalizer implements NormalizerInterface
{
public function normalize($data, SerializationContext $context)
{
return $data->getUsername();
}
public function supportsNormalization($data, SerializationContext $context): bool
{
return $data instanceof Author;
}
}
and then:
$builder = (new SerializerBuilder())
->addNormalizer(new AuthorNormalizer())
->build();
Encoders¶
Encoders are services that encodes a normalized data into a specific format and vice-versa.
Built-in Encoders¶
- JsonEncoder:
- Encodes and decode data in JSON format.
Event Listeners¶
Event listeners gives you the ability to hook into a serialization process. They ables you the opportunity to change the data before and after a serialization process.:
$builder = (new SerializerBuilder())
->addListener(Events::PRE_SERIALIZATION, function (PreSerializationEvent $event) {
/** @var Post $post */
$post = $event->getObject();
$post->setSummary('modified summary');
})
->build();
Event Subscribers¶
Instead of adding listener through closures, you can add event subscribers to add listeners to serializer:
class MyEventSubscriber implements EventSubscriberInterface
{
public static function getListeners(): array
{
return [
Events::PRE_SERIALIZATION => 'onPreSerialization',
Events::POST_SERIALIZATION => 'onPostSerialization',
Events::PRE_DESERIALIZATION => 'onPreDeserialization',
Events::POST_DESERIALIZATION => 'onPostDeserialization',
];
}
public function onPreSerialization(PreSerializationEvent $event): void {}
public function onPostSerialization(PostSerializationEvent $event): void {}
public function onPreDeserialization(PreDeserializationEvent $event): void {}
public function onPostDeserialization(PostDeserializationEvent $event): void {}
}
and then:
$builder = (new SerializerBuilder())
->addSubscriber(new MyEventSubscriber())
->build();
Events¶
- Events::PRE_SERIALIZATION:
- Listeners have the opportunity to change the state of the object before the serialization.
- Events::POST_SERIALIZATION::
- Listeners have the opportunity to change the array generated by de serialization.
- Events::PRE_DESERIALIZATION::
- Listeners have the opportunity to change the array generated by the deserialization.
- Events::POST_DESERIALIZATION::
- Listeners have the opportunity to do some validations after the deserialization finishes.
Caching¶
The serializer can cache two types of information:
- the generated hydrator classes
- the class metadata.
Hydrator Cache¶
You should provide the location where the generated serializer classes will be stored. Defaults to /tmp/serializer/hydrators:
$serializer = (new SerializerBuilder())
->setHydratorDir(__DIR__ . '/var/cache/serializer/hydrators')
->build();
Metadata Cache¶
To avoid parsing all classes to read its metadata data all the time, the serializer can cache the metadata and use it on the subsequent request:
$serializer = (new SerializerBuilder())
->setMetadataCacheDir(__DIR__ . '/var/cache/serializer/metadata')
->build();
Built-in metadata cache strategies:
- FileCache:
- Will be automatically configured when provide a directory like the bellow example.
- DoctrineCacheAdapter:
Any class implementing Cache interface of Doctrine
$serializer = (new SerializerBuilder()) ->setMetadataCache(new DoctrineCacheAdapter( new \Doctrine\Common\Cache\RedisCache(...) )) ->build();
- PsrCacheAdapter:
Any class implementing CacheItemPoolInterface interface.
$serializer = (new SerializerBuilder()) ->setMetadataCache(new PsrCacheAdapter( $psrCache )) ->build();
Hydrator Generation¶
This library generates PHP classes (e.g: hydrator) that will convert objects to array representation and vice-versa. Those classes are automatically generated based on you class mapping and stored in somewhere defined in your project. Therefore, to avoid unnecessary I/O to generate those classes, you can configure the strategy when generating them.
- FileNotExists:
This strategy will generate the hydrators only if they don’t exist in filesystem. Good for development environments.
$serializer = (new SerializerBuilder()) ->setHydratorGenerationStrategy(HydratorLoader::AUTOGENERATE_FILE_NOT_EXISTS) ->build();
- Always:
The hydrators will be generated regardless of its existence. Good for debugging.
$serializer = (new SerializerBuilder()) ->setHydratorGenerationStrategy(HydratorLoader::AUTOGENERATE_ALWAYS) ->build();
- Never:
The serializer will never check the hydrator’s existence and will never generate them. This strategy improves the performance in production environment.
$serializer = (new SerializerBuilder()) ->setHydratorGenerationStrategy(HydratorLoader::AUTOGENERATE_NEVER) ->build();
Mapping¶
Telling to serializer how it should transform your data is a very important step to assert that the transformation will not serialize the data with a wrong data type.
Zero User Mapping¶
TSantos Serializer will do its better to extract the must important metadata information from your class by reading its structure without the needing add custom annotations on every single property of your class. All you need to do is to write classes with good type-hint parameters and proper return types:
namespace App\Entity;
class Post
{
private $id;
private $title;
private $comments;
private $author;
public function __construct(int $id) { ... } // mutator
public function setTitle(string $title): { ... } // mutator
public function addComment(Comment $comment) { ... } // mutator
public function getAuthor(): Author { ... } // accessor
}
The serializer is smart enough to extract the data types from the property’s mutators and accessors and will (de-)serialize them respecting those types.
The previous example is a good start point because you don’t need to worry about the boring process of mapping all properties of all classes you want to serialize and is enough to cover the must use cases. However, you don’t have any control of what data should be serialized and when it should be serialized. That’s why you should use the mapping mechanism if you want a refined control over the serialization process.
The Serializer Builder¶
Before going ahead with mapping options, lets see how you should use the Serializer Builder to tell the serialize where are your mapping information:
$builder = new SerializerBuilder();
$serializer = $builder
->addMetadataDir('App\Document', '/project/config/serializer') // yaml metadata files
->addMetadataDir('App\Entity', '/project/src/Entity') // annotation metadata files
->build();
Note
Because the builder accepts many metadata directories, you can mix the supported mapping formats in the same serializer instance.
Note
You need to require the Symfony Yaml component to map your classes using the YAML format
$ composer require symfony/yaml
Note
You need to require the Doctrine Annotations component to map your classes using annotations syntax
$ composer require doctrine/annotations
and then enable the annotation reader in the Serializer Builder:
$serializer = $builder
->enableAnnotations()
->build();
Options Reference¶
BaseClass¶
Define what class the generated class should extends
/**
* @BaseClass("My\Custom\Class")
*/
class Post {}
App\Entity\Post:
baseClass: "My\Custom\Class"
<class name="App\Entity\Post" base-class="My\Custom\Class">
ExposeAs¶
The serialized name
/**
* @ExposeAs("full_name")
*/
private $fullName;
properties:
fullName:
exposeAs: "full_name"
<property name="fullName" type="integer" expose-as="full_name" />
Getter¶
The accessor method to read the value
/**
* @Getter("getMyCustomFullName")
*/
private $fullName;
properties:
fullName:
getter: "getMyCustomFullName"
<property name="fullName" getter="getMyCustomFullName" />
Tip
If you omit the getter option, the serializer will try to guess the getter automatically
Groups¶
The list of groups that the property can be serialized
/**
* @Groups({"web","v1"})
*/
private $fullName;
properties:
fullName:
groups: ["web", "v1"]
<property name="fullName" groups="web,v1" />
<!-- or -->
<property name="fullName">
<groups>
<value>web</value>
<value>v1</value>
</groups>
</property>
Options¶
A key/value used by metadata configurators
/**
* @Options({"format":"Y-m-d"})
*/
private $birthday;
properties:
birthday:
options: {"format":"Y-m-d"}
<property name="birthday">
<options>
<option name="format">Y-m-d</option>
</options>
</property>
Tip
Metadata configurators can access the property’s options to modify its behavior.
Read Only¶
The property cannot be deserialized
/**
* @ReadOnly
*/
private $id;
properties:
id:
readOnly: true
<property name="id" read-only="true">
Read Value Filter¶
A filter applied to the property value before encoding
/**
* @ReadValueFilter("strtolower($value)")
*/
private $username;
properties:
username:
readValueFilter: "strtolower($value)"
<property name="username" read-value-filter="strtolower($value)" />
Tip
Metadata configurators can change the read-value-filter to customize the input/output of property’s values.
Setter¶
The mutator method to write the value
/**
* @Setter("setMyCustomFullName")
*/
private $fullName;
properties:
fullName:
getter: "setMyCustomFullName"
<property name="fullName" getter="setMyCustomFullName" />
Tip
If you omit the setter option, the serializer will try to guess the setter automatically.
Type¶
The data type of mapped property
/**
* @Type("integer")
*/
private $id;
properties:
id:
type: "integer"
<property name="id" type="integer" />
Tip
If you omit the type, the serializer will try to guess the type automatically.
Virtual Property¶
Mark a method as a virtual property. Its return will be encoded within the properties data.
/**
* @VirtualProperty
*/
public function getAge(): int
{
...
}
virtualProperties:
getAge: ~
<virtual-property name="getAge" />
Tip
If you omit the type option, the serializer will try to guess the type automatically thanks to metadata configurators.
Write Value Filter¶
A filter applied to the property value before writing it to objects
/**
* @WriteValueFilter("\DateTime::createFromFormat('Y-m-d', $value)")
*/
private $birthday;
properties:
birthday:
writeValueFilter: "\DateTime::createFromFormat('Y-m-d', $value)"
<property name="username" write-value-filter="\DateTime::createFromFormat('Y-m-d', $value)" />
Tip
Metadata configurators can change the write-value-filter to customize the input/output of property’s values.
Performance¶
There is no difference in terms of performance between the mapping formats. In fact, the metadata generated by the mapping will be cached and reused in the next serialization operation, so you can choose the most comfortable format for you.
Benchmark¶
There are a lot of other libraries that does the same job as TSantos Serializer like JMS Serializer and Symfony Serializer. However, after making some benchmark in those library, I realized that they have a considerable overhead when serializing some data.
That’s why I decided to write a different technique to serialize complex objects and the results were very satisfactory:

As you can see, TSantos Serializer is 6x and 7x faster than Symfony Serializer and JMS Serializer respectively. This benchmark was generated through the PHPBench.
The source code of this benchmark is available on GitHub so you can clone and run it by yourself.
Performance Notes¶
The serialization process can be separated in two main operations: compile time and runtime.
- Compile time:
- Operation that compiles the class metadata and generate the hydrator classes. The compile time has a considerable number of I/O operations which can reduce the performance of your application and you should avoid operations in production environment.
- Runtime:
- Operation that transforms the data through the hydrators. Very fast after the class metadata is already compiled.
Another important topic about performance is how the serializer will read and write data from your objects. By using explicit accessors and mutators is slightly faster then using reflection to access private/protected properties. Keep this in mind in order to boost your application.