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. But, in order to use all of it’s magic, I suggest installing the whole pack:
composer require symfony/serializer-pack

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() DocBlock.
    (under the hood, it’s using DocBlock parser to do that – thanks Paul Rijke for pointing that out)
  • 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.

4 thoughts on “Using Symfony Serializer to consume REST APIs in OOP way”

  1. This is really sweet idea. Very elegant approach.

    Have you considered creating a library to connect Serializer with some HTTP client and a few helper methods to simplify the implementation? It might be interesting to fully define the API using annotated objects and some interfaces to enumerate API endpoints.

    1. Thank you, Josef!
      It’s an interesting suggestion, it would definetely lessen the amount of boilerplate code.
      I’ll have a shot on that on my next API-related task, thanks!

  2. Love the idea but I tried to mimmic this but I must be missing something. Can you expand the sample to a full working example?

    My implementation ends up with an asso. array and not with objects. So I must be missing the part of automatic mapping to the Project object.

  3. Hi Nebkam,

    I really love the idea, but I can’t seem to get it working. I must be missing parts that I think is left out in your example code.

    Do you have a full working example that I can run to see what is happening?

Leave a Reply

Your email address will not be published. Required fields are marked *