Using Symfony Serializer to consume REST APIs in OOP way

How to leverage the power of Symfony Serializer to elegantly talk to REST APIs

How It Started

Lately, I needed to upgrade to ZohoCRM API v2.
“Cool, their new API has a PHP SDK, that’s going to be implemented easy!”
Boy, was I wrong.
Those who haven’t tried this before – be warned – you will wander in the woods of abstract documentation, back and forth between their developer portal, Github source of their PHP SDK, trying to find some examples.
I lost two days banging my head on it and in the end decided to write a small API client as a wrapper for their REST API instead.

The Usual

My usual routine when writing an API client would start with fetching a response with Guzzle, json_decode-ing a response, a series of if/else/switch-es and manual mapping of associative arrays to my entities.
And, for POST-ing and PUT-ing data back, I’d have a similar, ugly block of code that would dump my entity into an associative array.
I’ve always looked at those pieces of my code as especially fragile and error prone. Let me give you a hint:

What’s Wrong With This?

  • IDEs can’t really help you when working with associative arrays, since there’s no type information and it’s not really the OOP way
  • mapping logic from API response and back to API request payload is duplicated ( Project::fromApiResponse and Project::toApiRequestpayload methods)

That got me thinking – There must be a better way of doing this..

With Symfony Serializer

As you might have guessed, Symfony Serializer is a standalone component that can be installed via Composer:
composer require symfony/serializer
While PropertyAccess component isn’t necessarily required, if you notice that some of the property/method mappings while (de)serializing don’t work, try installing it too and let it do it’s magic:
composer require symfony/property-access

So, I rewrote my code to something like this:

What’s Different?

  • API responses and request payloads nicely fit in objects (Project and ProjectCollection), not associative arrays.
  • Object properties are type hinted to their types in JSON, so there’s no doubt what’s their type and no incidental type-juggling.
    If the type on the property doesn’t match the type in JSON, Serializer will throw an exception.
  • Have you noticed I didn’t have to tell the Serializer to map to the Project class?
    He auto-mapped ProjectCollection::result to an array of Project instances, by reading the ProjectCollection::setResult() type hint.
  • Mapping is done by @SerializedName annotation and it’s not duplicated
  • On the downside, this method requires a separate class for every type of embedded object.

To Wrap Up

Now I felt that my API client code was more maintainable.
Exceptions were thrown early on, mapping was explicit and deduplicated.
Even though I ended up with more files and lines of code, I felt it was worth it.
Oh, and a bonus – wrapping it in objects, instead of associative arrays, made it easier for me to test them.

Symfony: Deprecated @Route and @Method Annotations

Doing a composer update on my pet project got me a bunch of deprecation notices:

The “Sensio\Bundle\FrameworkExtraBundle\Configuration\Route” annotation is deprecated since version 5.2. Use “Symfony\Component\Routing\Annotation\Route” instead.

OK, that sounds familiar..
I’ve always had a dilemma when auto-importing @Route in PHPStorm,which of the two should I choose.
Guess I went with the wrong one 🙁
Correcting was easy, yet boring:

The other depreecation looked similar:

The “Sensio\Bundle\FrameworkExtraBundle\Configuration\Method” annotation is deprecated since version 5.2. Use “Symfony\Component\Routing\Annotation\Route” instead.

OK, what? @Method is deprecated, use @Route ? Go home, Symfony, you are drunk 😀
Well.. turns out the message is sane. The new way of specifying the HTTP method is in the @Route annotation, under methods key.
So, I’ve corrected:

To this:

After updating all the annotations, I was left was a cryptic deprecation notice:

Enabling the “sensio_framework_extra.router.annotations” configuration is deprecated since version 5.2. Set it to false and use the “Symfony\Component\Routing\Annotation\Route” annotation from Symfony itself.

I say “cryptic” because I did not enable that configuration myself. It was enabled by default. And there’s a pull request to make it disabled by default.
Until that pull request gets merged, the fix that worked for me was to create a config/packages/framework_extra.yaml and disable those annotations explicitly:

Yay! No more deprecation notices 🙂

Symfony: Inject Doctrine Repositories in Controllers

I’ve come across a cool pattern in Symfony 4 – injecting repositories instead of EntityManager

// Before
public function showAllUsers(EntityManagerInterface $em)
    {
    $users = $em->getRepository(User::class)->findAll();

    return $this->json($users);
    }

// After
public function showAllUsers(UserRepository $userRepository)
    {
    $users = $userRepository->findAll();
    
    return $this->json($users);
    }

In the latter, we keep the scope limited only to the repository we need, not holding on to the whole EntityManager.
It’s possible because all your repositories are automatically registered as services and hence can be autowired.
Previous Symfony versions allowed you to register repositories as services manually and event that was tedious, because it required a custom factory etc.

Since EntityManager is mostly used for persisting and deleting entities, you can add a few helper methods to your repository(ies):

/**
 * @param object $entity
 */
public function save($entity)
    {
    $this->_em->persist($entity);
    $this->_em->flush();
    }

/**
 * @param object $entity
 */
public function delete($entity)
    {
    $this->_em->remove($entity);
    $this->_em->flush();
    }

$this->_em property is already available, if you’re extending the default ServiceEntityRepository

Then, you can shorten your create/delete controllers as well:

// Before
public function createUser(Request $request, EntityManagerInterface $em)
    {
    $user = new User();
    // request handling, form validation..
    
    $em->persist($user);
    $em->flush();

    return $this->json($user);
    }

public function deleteUser(User $user, EntityManagerInterface $em)
    {
    $em->remove($user);
    $em->flush();

    return new Response('', Response::HTTP_NO_CONTENT);
    }

// After
public function createUser(Request $request, UserRepository $userRepository)
    {
    $user = new User();
    // request handling, form validation..
    
    $userRepository->save($user);

    return $this->json($user);
    }

public function deleteUser(User $user, UserRepository $userRepository)
    {
    $userRepository->delete($user);

    return new Response('', Response::HTTP_NO_CONTENT);
    }

EDIT: first version of the post did not utilize the DI for fetching the EntityManager and did an unfair comparison. This is corrected and the actual difference between these two approaches is probably trivial now.

Simplicity vs Power: Symfony Forms

Disclaimer: This article covers using Symfony Framework without the Doctrine ORM

As a follow-up on a conversation on Twitter, I decided to compile our dev team’s experience in using Symfony Forms for almost three years in production.

Coming from a CodeIgniter background, we’ve found Symfony’s Form and Security component to be the most powerful and most complex parts of the framework.

Domain & Type – Duplication

Splitting the form generation/handling into two parts was one of the biggest changes for us. We followed the Symfony guidelines and avoided polluting our controllers with $this->createFormBuilder() but since we don’t use Doctrine entities, every form required us to make a dedicated domain object and a dedicated type object.

This led to us to duplicate a lot of form-related stuff. Symfony has very powerful validation and it’s a pity it has to be split across form domains and their corresponding types.
For example, you can @Assert\Choice for a specific field, but you still have to repeat those choices when adding a ChoiceType; you can @Assert\Type("integer") a field, but you still have to mark it as an IntegerType.
Now, I understand, splitting the form information from form representation is a good thing in theory, but in reality, it was more of a chore than a boost.

Our specific business case required us to  build a lot of forms (133), so duplication hit us hard.
Following the official cookbook article to reduce duplication didn’t work for us, so we ended up not using inherit_data. Eventually, using the plain OO inheritance proved better.

Form Modifiers

If you have to implement multi-step AJAX forms or have some kind of dependent AJAX filters in your form, chances are you’ll get an advice to use them.

Don’t.
We’ve found them so complicated (sometimes even impossible) to setup, test and debug that, if we had another opportunity, would skip using Symfony Forms in AJAX-heavy scenarios altogether.

Debugging Form Validation

Now, this may be because of the first issue – when validation is split between the domain and the type, it’s hard to find out which one caused it.
Or, this could be because of the modular nature of form building (every type inside a form type can have it’s own child type and so on), so it’s not so easy to flatten those errors and find out where and why it occurred.

Either way, every time a form isValid returns false, we know we’re in for a serious head-scratching.

To Conclude..

I didn’t mean for this to be a rant on Symfony Form component’s complexity.
It is a very sophisticated and thoroughly tested library we’d use on a project any day. It has helped us numerous times and we’re always discovering new ways in optimizing using it (it has changed a lot since we’ve started using it in Symfony 2.2).
The point I’m trying to get across is, while powerful, this library has a steep learning curve, a lot of gotchas and one needs really to master it in order to use it effectively, beyond the simplest use cases.

Handling MongoDB Dates in PHP (New Driver)

In case you’re enthusiastic like me and wanted to use the new MongoDB driver for PHP ASAP, you’ve might have stumbled upon differences in date handling.

Untill now, when you’ve wanted to properly insert dates, you’d construct new MongoDate and insert it as such. Well, there is no such class any more in the new driver, and to insert a date properly, you need to use new MongoDB\BSON\UTCDatetime. Notice the difference in constructor arguments, though – the new UTCDateTime accepts Unix timestamp in milliseconds, not seconds like MongoDate did.

Of course, you have the use the new class when querying as well:

$docs = $collection->find([
 'ts' => [
  '$gt' =>new MongoDB\BSON\UTCDatetime($unix_timestamp * 1000)
  ]
 ]);

And when you fetch your objects, you can use toDateTime to get native PHP’s DateTime and format it as you’d like.