08 August 2019
Symfony vs Zend Framework – PHP Framework comparison Part II: Serialization
Web developers work with APIs a lot. Sometimes, the goal is to use an existing API to achieve something (client). At other times, they provide a whole new API for others to use. One way or the other, they are faced with the need to serialize and deserialize data. In today’s article, I’m going to continue my Symfony vs Zend Framework series by taking a closer look at serializers and the issue of serialization in PHP in a broader sense.
If you haven’t yet seen the first part of my Symfony vs Zend Framework series on documentation, I recommend you read it first. Without further ado – let’s get to serialization.
According to the standard definition, serialization is the process of translating an object’s state or data structures into a format that can be stored as well as reconstructed some time in the future.
We do this a lot in our work as PHP developers. Still, serializers really do come in handy sometimes. Thanks to popular PHP frameworks, we don’t need to implement it ourselves every time.
Since this is the Symfony vs Zend Framework series, I’m going to compare the Symfony Serializer and the zend-serializer – the serialization components of these two frameworks.
Let’s use a simple API as an example (I recommend you give this guide on how to make an API by my colleague a try). It serves a simple app, in which there are individuals (Persons) with unique names, e-mails, birthdate, friends (other instances of Persons) and an optional photo. The e-mail and birthdate should not be visible publicly. The birthdate will be replaced with the age for the public view. Also, if a Person doesn’t have a photo, a URL with a placeholder must be provided.
This is how our Person and Photo classes look.
For the purpose of this article, we will have two instances of Person – John and Jimmy. They are friends with each other. Jimmy has a photo.
Serialization with Symfony Serializer
The Symfony Serializer component performs serialization in two steps. First, an object is normalized to an array. Then, the array is encoded to a specific format like JSON or XML. For deserialization, the process looks similar – the format is decoded to an array and the array in denormalized to an object. In this architecture, encoders deal with formats and arrays, while normalizers deal with arrays and objects.
Symfony Serializer comes with a bunch of useful encoders, such as JsonEncoder, XmlEncoder or CsvEncoder. Some of the most popular normalizers are ObjectNormalizer, DateTimeNormalizer or GetSetMethodNormalizer.
That’s the theory. Let’s try it out in practice.
Let’s install the serializer with composer.
> composer install symfony/serializer
In an app based on the Symfony Framework, you can type-hint SerializerInterface argument and Dependency Injection container will inject a pre-configured Serializer instance. Here, we will do it manually for easier comparison with the Zend Serializer.
As both of our classes have defined getters methods, we can use GeSetMethodNormalizer, which is simpler than ObjectNormalizer and doesn’t have additional requirements (ObjectNormalizer requires the PropertyAccess component).
And the result is…
…an exception, more precisely, CircularReferenceException. The reason for this is that our Person instance has a reference to the second Person instance (in the friends array), which subsequently has a reference to the first one. The result is an infinity loop and Serializer doesn’t know how to deal with it. Of course, it’s not an uncommon situation and the Serializer Component is prepared for that. We just need to define CircularReferenceHandler. For example, we can return an object’s pid there.
This time, everything works fine and we get the JSON representation of our objects
This is most definitely not the result we wanted. Instead of a formatted birthDate, we get all the details of the DateTime instance. This can be fixed by registering DateTimeNormalizer.
Additionally, we wanted the photo’s URL, rather detailed information and a URL to a default image if there is a Person doesn’t have a photo. To solve these problems, we need to implement our own Normalizer and register it.
The result is now much closer to our requirements
Now we can take care of hiding some information. To achieve that ,we will use serialization groups. Serialization groups can be defined with annotations, XML or YAML files. For our example, we will use annotations.
NOTE: Personally I’m not a big fan of annotations in code and I strongly recommend XML or XAML files for this. But annotations work great for presentation purposes in articles and other media.
Let’s install the doctrine/annotations package.
> composer require doctrine/annotations
By modifying our PersonNormalizer, we can display a Person’s age instead of birthday.
Let’s check how it works when we use a “public” serialization group
… and a “private” one
Both results meet our requirements.
Deserialization with Symfony Serializer
Serialization is just one aspect of the serialization component. Let’s have a look at deserialization.
Let’s say that we need to create simple API endpoints for a Person. We want to be able to create new instances of Persons and make updates of name, e-mail and birthdate.
Let’s start by deserializing a new Person instance. For now we leave the Serializer configuration as it was when we successfully did the serialization.
Again, we get an exception – MissingConstructorArgumentsException. The constructor requires the ‘pid’ parameter and it’s missing in our JSON. Providing it in JSON doesn’t seem like a good idea – we should generate it server-side instead.
Keep in mind – in a real-world application, we probably won’t have to provide pid in constructor, but it still may happen that we want to have a required parameter with a default value during the deserialization.
Symfony Serializer has an out-of-the-box solution for that. We just need to provide default_constructor_arguments in the context argument.
We created new instances. We can now try to edit existing object. To achieve that during deserialization, we must provide object_to_populate in context.
$person from this example is an existing instance of the Person class. It may have been fetched from some repository.
This time, we don’t get an exception, but our object wasn’t changed. The reason is that we don’t have setter methods in our Person class. To make it work, we can add setters in the Person class or register PropertyNormalizer which uses Reflection to access public, protected and private properties.
Let’s assume that we can’t change our model’s code. We will use second solution.
Name and e-mail properties were updated exactly as we wanted.
Our example app shows that the Symfony Serializer works great as a serialization library. With some tweaking and a little bit of extra PHP, we managed to meet our requirements. The truth is that it is more capable than we showed here. It could also be easier to use if we used Symfony Serializer in a Symfony-based application in which part of configuration is already done for us.
Serialization with Zend Serializer
The first thing we may notice while reading Zend Serializer’s documentation is the fact that this component is focused on serialization understood as the process of translating PHP types and structures to and from different representations.
The list of provided Adapters (which may be seen as equivalents of Symfony Serializer’s Encoders) suggests that authors thought about other use cases, such as storing objects in cache. Because of that, we can use 3 different PHP-specific serializers (PhpSerialize, IgBinary and PhpCode Adapters). There is also a JSON Adapter, which we will focus on today.
To be able to achieve similar results, we will also need the Zend Hydrator component. According to the component’s documentation “Hydration is the act of populating an object from a set of data” and “zend-hydrator is a simple component to provide mechanisms both for hydrating objects, as well as extracting data sets from them.” As you can see, it provides similar functionalities as Normalizers in Symfony Serializer.
Let’s install both components and start with implementing our use case.
composer require zendframework/zend-serializer
composer require zendframework/zend-hydrator
Now we can try to serialize our Person instance.
We use ClassMethodsHydrator because it should work in a similar way to GetSetMethodNormalizer from Symfony. Let’s look at the result.
It is far from what we want, but still somewhat closer than our first result from the Symfony Serializer.
First of all, both of the libraries have different default “naming strategy”. Symfony Serializer by default returns camelCase keys (w.g.: birthDay) and with Zend we get underscore_separated keys (birth_day). Both libraries allow us to easily change that setting. We just need to pass false as (the first) underscoreSeparatedKeys argument of the ClassMethodsHydrator constructor.
Another difference is the date format. This can be solved by using a concept named Strategies. They are used to manipulate key/value pairs during the hydration and extraction in Hydrators. We will use DateTimeFormatterStrategy, which supports DateTimeInterface instances and formats them into strings during the extraction.
We can create our own Strategy to handle the photo value according to our requirements.
The last problem is the fact that we got an array with an empty object as the value of friends. It’s because Hydrators extract values and returns them without performing the extraction if the value is not scalar. There is also Strategy for that. We can use CollectionStrategy, which must be instantiated with the Hydrator instance and class name. It will perform the extraction on each array element.
Let’s see how our code evolved.
And the code of our PersonPhotoStrategy.
We won’t get any result with the current code because of the circular reference between Persons. Unfortunately, the Zend Hydrator doesn’t have any protection from that and as a result our application enters into infinity loop. We need to implement our own mechanism that can prevent such situations.
Let’s take a look at a simple CircularReferenceHandlingHydrator implementation.
It decorates another Hydrator and, during the extraction, checks if there is a circular reference. If there isn’t, the decorated Hydrator’s extract() is called. Otherwise it returns the object’s pid.
Now we can initialize our Hydrator and use it.
Let’s take a look at the result we got.
There is a small difference compared to the result we got with Symfony Serializer. When the circular reference occurs, we get an object with the pid property and previously it was just a string with pid. We can probably achieve such results, but I don’t want to complicate the code at this moment.
Let’s focus on our next requirement – serializing a specific set of attributes. Unfortunate;y, Zend Serializer and Zend Hydrator don’t have a concept similar to serialization groups. The only way I found to define which properties should be exposed is writing your own implementation of Zend\Hydrator\Filter\FilterInterface. According to the docs, “Hydrator filters allow you to manipulate the behavior of the extract() operation.”
Our GroupFilter …
… and the way we can use it with Hydrator.
To be able to expose the Person’s age instead of the birthdate, I added the getAge() method in the Person class.
I’m not happy with that but I didn’t find any other way to add something to an extracted array.
The result for the public “group”…
… and for the private one.
Both meet our requirements.
Deserialization with Zend Serializer & Zend Hydrator
Now we can take care of the deserialization process. For starters, we will try with the same configuration of Serializer and Hydrator that we used in the last step.
Again, we need to perform two steps instead of one. First, unserialize a JSON string to an array and then hydrate data from an array to an object. Let’s have a look at the code.
Some readers may notice the “code smell” – we need to use Reflection to create an empty Person instance. Unfortunately, Zend Hydrator can’t initialize object instances and we need to provide one which will be populated during the hydration. Also, the Person’s constructor has required arguments, so we need to do some “magic”.
As a result of executing our code we get:
Our object is empty. This is because of the lack of setter methods. Zend Hydrator has ReflectionHydrator, so we can use it to avoid adding setter methods.
The rest of the code remains unchanged. Let’s check the result.
There are still issues with two properties: pid and friends. Both are null and both should not be. The first one should get a string value and the second one should be an empty array.
After a few hours spent on looking for the best solution by analyzing Hydrator’s code and heated discussions with my colleagues, I’m quite disappointed with the solution we found, but it’s working.
To fix our problems, we are merging an unserialized array with a hard-coded array that has default values for pid and friends.
Setting a pid value is just a little bit less elegant than with Symfony Serializer – we still need to provide a “generated” pid. Solving this problem with an uninitialized friends property concerns me more. We need to duplicate “logic” from the constructor in another place in the application.
Finally, we get what we wanted.
For the last part of our comparison, we will take care of editing the existing Person instance.
We can use the same Hydrator configuration we implemented for creating a Person instance.
$person is an existing Person instance fetched from the Repository.
This part works as we expected.
We managed to meet all of our requirements with Zend Serializer (and Zend Hydrator).
Because of differences in the architecture when compared to Symfony Serializer, we needed a bit more of extra PHP code to make it work as we wanted.
Based on what we did, it might seem that the Zend Serializer with Zend Hydrator are a bad choice for a serialization library. But, as it usually is in our line of work, the correct answer is always “it depends”.
I would not recommend using Zend Serializer when we want to serialize complex objects to JSON responses, but it may be a good choice when we want to serialize/deserialize simple DTOs with a flat structure.
So what do you think about my Symfony vs Zend Framework serialization comparison? Do you have any questions? Feel free to contact me. And if you have a specific PHP project in mind, use the form at the Contact page to consult with the The Software House (initial consultations are free of charge).