Symfony vs Zend Framework – PHP Framework comparison: Documentation, Serialization, and Mailers

Symfony and Zend Framework are two major PHP frameworks, used by PHP developers all over the world to create high-performance web applications using the MVC pattern. But while they both enjoy popularity, they are distinctively different in terms of approaches and solutions they employ, performance as well as their advantages and disadvantages. Let’s go over various aspects of Symfony and Zend Framework. For starters, I’m going to tackle the documentation and then go through serialization and mailers.

My experience with Symfony and Zend Framework

Symfony has been my first-choice framework for over 5 years. As much as I enjoy it, it was Zend Framework that first allowed me to work in the MVC architecture. In total, I spent a couple of years with ZF2 (from one of the first beta releases). I have to say that I wondered how Zend Framework would feel to me now after I dedicated so much of my recent time almost exclusively to Symfony.

To make a fair comparison, I will go through all major aspects of both Symfony and Zend Framework. Let’s start with documentation.

I – Framework setup for Symfony and Zend

In both the Symfony and ZF documentation, Composer is used for the installation process and a PHP built-in server is recommended as the quickest way to start developing with any of the frameworks. But the docs go on to describe more approaches.

The Symfony documentation suggests you use the Symfony console commands for that, while the docs for ZF mention PHP -S directly. It’s worth pointing out that the ZF Framework’s Getting Started part is a step-by-step tutorial about the development process of a simple web application. Symfony’s docs is more elaborate when it comes to explaining various aspects and possibilities, but it seems to be missing a sample application, which explains basic concepts in a consistent way.

For code organization, Zend Framework uses „modules”. Symfony has a similar concept named „bundles”. However, starting from Symfony 4, the docs recommend a „bundle-less applications” approach. Previously, single AppBundle was promoted as a best practice. Personally, I’m not a fan of modules/bundles. I have seen way too many applications, which suffered as a result of the fact that their devs simply didn’t manage to keep modules/bundles independent from each other.

symfony logo

The Symfony docs are elaborate, but they’re not always very practical

II – Routing and Controllers in Zend and Symfony docs

This is the moment when you really learn what frameworks are all about and their basic concepts begin to surface.

Once you read the chapter “Creating pages” of the Symfony documentation, you are ready to create a really simple, but fully working page that displays a random number. The chapter describes the essentials of routing, controllers, and templating. It also links to subsequent chapters, where you can get more detailed knowledge of each of these topics.

In the beginning, the docs introduce the YAML format for routing configuration. In later parts, it points out that annotations are recommended for routing. This is a topic that often leads to flame wars. Personally, I don’t see any benefits of not using annotations at this moment. Perhaps, you potentially have it easier to look for defined routes, if defined in one file, but you can also use the Symfony console to list defined routes so it’s a really weak argument.

On the other hand, the Zend Framework documentation shows how to create routing and controllers for an example application – a very practical approach. At the end of the chapter, you know where to create empty template files, but you still can’t see any effects of your work. The routing configuration shown in the documentation is tightly coupled with controller and action names. For some applications, a convention like this may be quite useful. However, in most cases, it may be restrictive. At this point, the docs show only one approach for routing configuration – a PHP array stored in one file with the configuration of controllers and paths to view files.

Zend Framework’s docs are very useful, but it doesn’t make me love some of the choices ZF’s authors made

III – Database and Models by documentation

The “Databases and models” chapter of the Zend Framework documentation introduces the concept of the Models and Table Data Gateway pattern. The ZF example application uses SQLite as the database engine. As far as I’m concerned, it’s a good choice. It limits potential problems with the configuration and helps focus on writing actual PHP code. I’m positively surprised with the practice of injecting framework dependency (TableGatewayInterface), instead of class inheritance shown in the implementation of AlbumTable.

Service configuration is, again, added to the same class as previous configurations, in the form of anonymous php functions. At the very least, this time you are informed that instead of anonymous functions, you may use standalone classes.

Subsequently, you learn how to change the controller and its configuration in order to inject the newly created AlbumTable. This was the moment I realized just how awesome Symfony’s auto-wiring feature is.

We also get a first look at the template file. Zend Framework uses PHP as templating engine by default. It’s very powerful but it also allows inexperienced developers to implement business logic on the view layer. It might also be at times difficult to understand for template designers.

See also: How to TDD your MVP?

IV – Forms in Symfony/Zend documentation

The last part of the Zend Framework docs introduces form processing and validation. Building forms in ZF is a little bit simpler than in Symfony, but I still prefer the Symfony way. ZF promotes the add method, which accepts an array as an argument. Of course, it would be very hard or even nearly impossible to design a nice and simple interface for building forms because of how many different types of input fields there are. Still, the name and type should be provided as separate arguments, like they are in Symfony Forms.

For validation, the Zend\InputFilter component is used. It proved even less enjoyable than the previous solution. I’m an opponent of defining validation rules as annotations, promoted by the Symfony documentation. But the ZF docs make me want to scream because it’s necessary to implement some interface methods and build validation rules using PHP arrays. It makes it practically impossible to completely keep the business logic out of the framework code.

Using forms in the controller requires some boilerplate code, like setInputFilter() and setData() methods. In the section about editing existing data, I realized that functionality similar to Symfony’s ParamConverters is missing in ZF (or at least it is not introduced in this part of the documentation). As a result, developers need to get parameters from the route and find data in the database manually.

If you have seen how Twig templates work with Symfony Forms, you will agree with me that the approach shown in the ZF documentation is very overwhelming. I guess it’s partly the result of using PHP as templating engine. Still, the requirement of setting HTML attributes of input fields at the top of the template file is something I can’t stand.

See also: Symfony APIs done quick and right – webinar video

Symfony vs Zend documentation – summary

While we can’t judge a framework by its documentation, I have to say that I like „Getting Started with Zend Framework” for its consistent examples that show basic concepts of the framework.

I’m a bit disappointed with the Symfony documentation. It sure has a lot of details but it’s hard to find the information needed to build an actual working application in it. However, the ZF docs made me worried about some aspects of the framework’s architecture, including (but not limited to) form building and validation. Perhaps, I’m simply not experienced enough with ZF to know all there is to solving these seemingly common problems.

I think that it is worth it to mention that Zend Framework provides a lot of components that are missing in Symfony. It includes common aspects of web applications such as pagination as well as some more peculiar ones (e.g. Barcode).

Still, I have to say that to me, personally, the winner of this round has to be Symfony. However, I have to note that I’m planning to take an even deeper look into some specific components of both frameworks. Perhaps, the next time it will be Zend Framework that will emerge victorious – these things are always changing, and it’s important to keep track of what tool is better when. This is especially true with custom projects and solutions that don’t follow a beaten path. 

Want to make beautiful PHP together?

Tell us what you need in your project, and we’ll tell you how we can make PHP sing for you!

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. Let’s take 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 sometime 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 as a simple app, in which there are individuals (Persons) with unique names, e-mails, birthdates, 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 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 persons – 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 into a specific format like JSON or XML. For deserialization, the process looks similar – the format is decoded to an array, and the array is 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 the 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…

A screenshot of Circular Reference Exception.

…an exception, more precisely, CircularReferenceException. The reason for this is that our Person instance has a reference to the second Person instance (in the friend’s 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 who 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.

A screenshot of Missing Constructor Arguments Exception.

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 the 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 the existing objects. 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 the 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 the 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 the 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 results 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 an infinite 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. Unfortunately, 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 by 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 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 friend’s 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.

The result:

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 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 is 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.

Make PHP part of this complete balanced project!

Get served with some expert advice, product design help and a side of PHP

Part III: Mailers

The issue of sending and managing emails is one of the most common challenges in PHP web development. Symfony and Zend Framework both have their own solutions to help developers take care of it. In the 3rd part of my Symfony vs Zend Framework comparison, I am taking a look at their email components and create both simple and more complex email messages.

In the previous parts of my Symfony vs Zend Framework series, I talked about the issue of serialization and documentation. This time, I will be tackling what is perhaps an even more common aspect of web development: sending emails.

Some of the most typical types of email we include are “welcome” messages, emails with reset links for passwords, order confirmations and notifications. Frameworks can indeed be very helpful in doing all that and then some. But how do the Zend Mail and the recently released (May 2019 with Symfony 4.3) Symfony Mailer components compare in this aspect?

Zend Mail

Let’s start with Zend Framework’s email component – the Zend Mail. It consists of two concepts – the Message represents a single message and the Transport handles the process of sending it (e.g. via SMTP or PHP’s mail() function). Should it be necessary, one can always implement a custom version of Transport to handle other delivery services.

Zend Mail can be installed with Composer, but if one wants to use SMTP for message delivery, Service Manager is also required.

$ composer require zendframework/zend-mail
$ composer require zendframework/zend-servicemanager

In order to send a simple email message, the example code from the documentation will suffice.

I’m now running a local MailHog instance in the Docker container so I can see the results.

IMAGEmailhog test subject test email

As you can see, it takes fewer than 20 lines of code to send a really simple message. However, since HTML-based emails are now more common, I am now going to make a more complex email, which includes both plain text and HTML body as well as an attachment.

This time, we need a lot more lines of code to create an email. Separate objects need to be made:

  • one that represents the plain text message,
  • one that represents the HTML message,
  • one that integrates the two above,
  • one for image attachment,
  • one for integrating the image with the rest of the content.

Only then, we can create a Message instance similar to the one in the previous example and then send it via Transport.

The result can be seen below – a message that includes both text and HTML content.

An example of a text and html email in Zend.

An example of a multipart email in Zend.

Symfony Mailer

As I said before, the Mailer is one of the latest additions to the Symfony framework. Before, the recommended method of sending emails in Symfony applications was Swift Mailer. With the new addition, the whole process was significantly simplified.

Much like in Zend Mail, Symfony Mailer introduces the Transport and Email concepts. The latter is a part of the Mime component, which serves as a dependency of Symfony Mailer. The package allows us to use Transport to send emails via Sendmail or SMTP, but there are also additional packages, which make it possible to use other delivery services such as Mailgun, SendGrid, or Amazon SES.

Let’s start by installing the dependency via Composer.

$ composer require symfony/mailer

Now we can initialize Transport and send a simple message.

Let’s now use MailHog to catch the emails.

A screenshot of a plain text message in Symfony.

Sending a simple plain text message again required fewer than 20 lines of code. Now, let’s try to send one with plain text, HTML content, and an attachment.

The resulting message that includes both plain text and HTML can be seen below.

An example of a text and html email in Symfony.

An example of a multipart email in Symfony.

Creating a message like this with Symfony Mailer required only two additional method calls – one for HTML content and one for the attachment.

What have we learned, kids? 

Our little test showed that while sending simple emails takes about an equal amount of code and effort in both Symfony and Zend Framework, sending complex messages is significantly easier with Symfony Mailer. It also provides support for more delivery services as separate packages. Out of the two, Zend Mail is much older so it may have been fairer to compare it with Swift Mailer. Still, when it comes to complex and numerous email messages, Swift Mailer was also superior.

However, there is one aspect of the process in which it is Zend Mail has the upper hand – reading emails. Zend Mail provides special classes for fetching messages from mailboxes via POP3 or IMAP protocols. It’s a very important argument for using it when it comes to building a mail client in PHP.

Our projects made a real change!

See our clients’ success in case studies

CTO vs Status Quo

Find the exact rules Tech Managers used to organize IT & deliver solutions the Board praised.

Read here

The Software House is promoting EU projects and driving innovation with the support of EU funds

What would you like to do?

    Your personal data will be processed in order to handle your question, and their administrator will be The Software House sp. z o.o. with its registered office in Gliwice. Other information regarding the processing of personal data, including information on your rights, can be found in our Privacy Policy.

    This site is protected by reCAPTCHA and the Google
    Privacy Policy and Terms of Service apply.

    We regard the TSH team as co-founders in our business. The entire team from The Software House has invested an incredible amount of time to truly understand our business, our users and their needs.

    Eyass Shakrah

    Co-Founder of Pet Media Group


    Thank you for your inquiry!

    We'll be back to you shortly to discuss your needs in more detail.