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.