Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

The Serializer: Swiss-Army Knife of APIs

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

The Serializer: Swiss-Army Knife of APIs

Hey guys! Welcome to episode 2 of our RESTFUL API’s in the real world series. In episode 1 we covered a lot of the basics. Phew! And explained really important confusing terms like representations, resources, idempotency, GET, POST, PUT, PATCH and a lot of other things that really make it difficult to learn REST. Because, really before that you had no idea what anybody was talking about. At This point I hope that you feel like you have a nice base because we’re going to take that base and start doing some interesting things like talking about serializers, hypermedia HATEOAS, documentation and a ton more.

Tip

Hey, go download the starting code for this repository right on this page!

I’ve already started by downloading the code for Episode 2 onto my computer. So I’m going to move over to the web/ directory, which is the document root for our project, and use the built-in PHP Web server to get things running:

cd /path/to/downloaded/code
cd web
php -S localhost:8000

Let’s try that out in our browser... and there we go.

You can login as ryan@knplabs.com with password foo - very secure - and see the web side of our site. There’s a web side of our site and an API version of our site, which we created in Episode 1.

The project is pretty simple, but awesome, you create programmers, give them a tag line, select from of our avatars and then battle a project. So we’ll start a battle here, fight against “InstaFaceTweet”... some magic happens, and boom! Our happy coder has defeated the InstaFaceTweet project.

So from an API perspective, you can see that we have a number of resources: we have programmers, we have projects and we also have battles. And they all relate to each other, which will be really important for episode 2.

What We’ve Already Accomplished

The API code all lives inside of the src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php file. And in episode 1, we created endpoints for listing programmers, showing a single programmer, updating a programmer and deleting one:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

class ProgrammerController extends BaseController
{
    // ...

    public function newAction(Request $request)
    {
        // ...
    }

    // other methods for updating, deleting etc
}

We also used Behat to make some nice scenarios for this:

# features/api/programmer.feature
Feature: Programmer
  # ...

  Background:
    Given the user "weaverryan" exists

  Scenario: Create a programmer
    Given I have the payload:
      """
      {
        "nickname": "ObjectOrienter",
        "avatarNumber" : "2",
        "tagLine": "I'm from a test!"
      }
      """
    When I request "POST /api/programmers"
    Then the response status code should be 201
    And the "Location" header should be "/api/programmers/ObjectOrienter"

  # ... additional scenarios

This makes sure that our API doesn’t break. It also lets us design our API, before we think about implementing it.

Whenever I have an API, I like to have an object for each resource. Before we started in episode 1, I created a Programmer object. And this is actually what we’re allowing the API user to update, and we’re using this object to send data back to the user:

<?php

// src/KnpU/CodeBattle/Model/Programmer.php
// ...

class Programmer
{
    public $id;

    public $nickname;

    public $avatarNumber;

    public $tagLine;

    // ... a few more properties
}

Serialization: Turning Objects into JSON or XML

So one of the key things we were doing was turning objects into JSON. For example, let’s look in showAction. We’re querying the database for a Programmer object, using a simple database-system I created in the background. Then ultimately, we pass that object into the serializeProgrammer function, which is a simple function we wrote inside this same class. It just takes a Programmer object and manually turns it into an array:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

public function showAction($nickname)
{
    $programmer = $this->getProgrammerRepository()->findOneByNickname($nickname);

    // ...
    $data = $this->serializeProgrammer($programmer);
    $response = new JsonResponse($data, 200);

    return $response;
}

// ...
private function serializeProgrammer(Programmer $programmer)
{
    return array(
        'nickname' => $programmer->nickname,
        'avatarNumber' => $programmer->avatarNumber,
        'powerLevel' => $programmer->powerLevel,
        'tagLine' => $programmer->tagLine,
    );
}

This transformation from an object into an array is really important because we’re going to be doing it for every resource across all of our endpoints.

Installing JMS Serializer

The first thing we’re going to talk about is a library that makes this a lot easier and a lot more powerful, called a serializer. The one I like is called jms/serializer, so let’s Google for that. I’ll show you how this works as we start using it. But the first step to installing any library is to bring it in via Composer.

I’m opening a new tab, and going back into the root of my project, and then copying that command:

composer require jms/serializer

If you get a “command not found” for composer, then you need to install it globally in your system. Or, you can go and download it directly, and you’ll end up with a composer.phar file. In that case, you’ll run php composer.phar require instead.

Creating/Configuring the Serializer Object

While we’re waiting, let’s go back and look at the usage a little bit. What we’ll do is create an object called a “serializer”, and there’s this SerializerBuilder that helps us with this. Then we’ll pass it some data, which for us will be a Programmer object or a Battle object. And then it returns to you the actual JSON string. So it takes an object and turns it into a string:

// from the serialization documentation
$serializer = JMS\Serializer\SerializerBuilder::create()->build();
$jsonContent = $serializer->serialize($data, 'json');
echo $jsonContent; // or return it in a Response

Now this is a little bit specific to Silex, which is the framework we’re building our API on, but in Silex, you have a spot where you can globally configure objects that you want to be able to use. They’re called services. I’ll create a new global object called serializer and we’ll use code similar to what you just saw to create the serializer object. We’re doing this because it will let me access that object from any of my controllers:

// src/KnpU/CodeBattle/Application.php
// ...

private function configureServices()
{
    // ...

    $this['serializer'] = $this->share(function() use ($app) {
        // todo ...
    });
}

Before I start typing anything here, I’ll make sure everything is done downloading. Yes, it is - so I should be able to start referencing the serializer classes. Start with the SerializerBuilder that we saw. We also need to set a cache directory, because this library caches annotations that we’ll use a bit later. This is a fancy way in my app to tell it to use a cache/serializer directory at the root of my project.

There’s also a debug flag, and when that’s true, it’ll rebuild the cache automatically. Finally, the last step tells the serializer to use the same property names that are on the Programmer object as the keys on the JSON. In other words, don’t try to transform them in any way. And that’s it!

// src/KnpU/CodeBattle/Application.php
// ...

private function configureServices()
{
    // ...

    $this['serializer'] = $this->share(function() use ($app) {
        return \JMS\Serializer\SerializerBuilder::create()
            ->setCacheDir($app['root_dir'].'/cache/serializer')
            ->setDebug($app['debug'])
            ->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy())
            ->build();
    });
}

Using the Serializer Object

The important thing here is that we have a serializer object and we can access it from any of our controllers. Let’s open our ProgrammerController and rename serializeProgrammer to just serialize since it can serialize anything.

I’ve setup my application so that I can reference any of those global objects by saying $this->container['serializer']. This will look different for you: the important point is that we need to access that object we just configured.

Now, just call serialize() on it, just like the documentation told us. I’ll put json as the second argument so we get JSON. The serializer can also give us another format, like XML:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

protected function serialize($data)
{
    return $this->container['serializer']->serialize($data, 'json');
}

Perfect! Now let’s look to see where we used the serializeProgrammer function before. That old function returned an array, but serialize now returns the JSON string. So now we can return a normal Response object and just pass the JSON string that we want. The one thing we’ll lose is the Content-Type header being set to application/json, but we’ll fix that in a second:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

public function newAction(Request $request)
{
    $programmer = new Programmer();
    // ...

    $json = $this->serialize($programmer);
    $response = new Response($json, 201);

    // ... setting the Location header

    return $response;
}

Let’s go and make similar changes to the rest of our code.

In fact, when we have the collection of Programmer objects, things get much easier. We can pass the entire array of objects and it’s smart enough to know how to serialize that. You can already start to see some of the benefits of using a serializer:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

public function listAction()
{
    $programmers = $this->getProgrammerRepository()->findAll();
    $data = array('programmers' => $programmers);
    $json = $this->serialize($data);

    $response = new Response($json, 200);

    return $response;
}

Compared with what we had before, not a lot should have changed, because the serializer should give us a JSON structure with all the properties in Programmer. That’s practically the same as we were doing before.

So let’s run our tests!

php vendor/bin/behat

We’ve totally changed how a Programmer gets turned into JSON, but almost every test passes already! We’ll debug that failure next.

Leave a comment!

12
Login or Register to join the conversation
Default user avatar

no video download?

3 Reply

Hey!

This is taking a bit longer (biggest screencast ever for us), which is why we released without the video download. But we're encoding things right now - the download should be available on a day or 2 :).

Cheers!

Reply
Quentin L. Avatar
Quentin L. Avatar Quentin L. | posted 3 years ago | edited

Hi! I'm guetting this error when I try to access localhost:8000 :
Fatal error: Uncaught UnexpectedValueException: The stream or file "C:\xampp\htdocs\rest-ep2\src\KnpU\CodeBattle/../../../logs/development.log" could not be opened: failed to open stream: No such file or directory in C:\xampp\htdocs\rest-ep2\vendor\monolog\monolog\src\Monolog\Handler\StreamHandler.php:70 Stack trace: #0 C:\xampp\htdocs\rest-ep2\vendor\monolog\monolog\src\Monolog\Handler\AbstractProcessingHandler.php(37): Monolog\Handler\StreamHandler->write(Array) #1 C:\xampp\htdocs\rest-ep2\vendor\monolog\monolog\src\Monolog\Logger.php(244): Monolog\Handler\AbstractProcessingHandler->handle(Array) #2 C:\xampp\htdocs\rest-ep2\vendor\monolog\monolog\src\Monolog\Logger.php(320): Monolog\Logger->addRecord(500, 'UnexpectedValue...', Array) #3 C:\xampp\htdocs\rest-ep2\vendor\silex\silex\src\Silex\Provider\MonologServiceProvider.php(83): Monolog\Logger->addCritical('UnexpectedValue...', Array) #4 [internal function]: Silex\Provider\MonologServiceProvider->Silex\Provider\{closure}(Object(UnexpectedValueException), 500) #5 C:\xam in C:\xampp\htdocs\rest-ep2\vendor\monolog\monolog\src\Monolog\Handler\StreamHandler.php on line 70
I'm on Windows 10 and I already ran composer install. What can I do to make the project work?

Reply

Hey @Quentin!

Hmm, sorry about the problem! I'm not sure why Monolog is angry, but the solution *could* be as easy as this (so let's try the easy solution first!): create a new "logs" directory at the root of your project (so, put this "logs" directory in the same directory as the "src" directory).

Let me know if this fixes it - it works for us, but it could be something that's affecting Windows for some reason. If this *does* fix it, we can add that directory to the code download so that it's always there :).

Cheers!

Reply
Quentin L. Avatar

Yep, fixed it! Thank you!

Reply
Johannes J. Avatar
Johannes J. Avatar Johannes J. | posted 3 years ago

Hi!

I don't seem to find the download link to the project files anywhere ...
https://github.com/knpunive... is the right one?
And if yes: Trying to log into the application, i get an SQL error.
Additionally, i see that your jQuery dependency from code.google.com ain't available anymore.

Would be terrific if the code to the course could be reviewed and updated.

Thanks alot from Salzburg, Austria!

Reply

Hey Jjarolim,

See the big orange button in the right top corner of any video page called "Download" :) When you hover over it - you will see "Course Code" button to download start and finish course code. Here's a screenshot of this button: https://imgur.com/a/0mpMTuC

The correct repository behind this course is: https://github.com/knpunive...

> And if yes: Trying to log into the application, i get an SQL error

Before running the project you need to perform a few simple steps, like install composer, create DB, create schema, etc. All the required steps and their correct order you can find in README file located in downloaded code. Difficult to say something more about it without seeing the exact error.

I also just downloaded source code and double-checked the homepage - no JS errors for me, both links work for me:

http://code.jquery.com/jque...
http://netdna.bootstrapcdn....

Cheers!

Reply
Default user avatar
Default user avatar Neandher Carlos | posted 5 years ago | edited

Hi Ryan! I hope i'm not being boring...

But I cloned the repository ( https://github.com/knpuniversity/rest-ep2/ ) and ran the tests with Behat, and got not only one error, but three. To solve the 2 errors I have to do this:

### Application ###


//$data['type'] = 'http://localhost:8000/api/docs/errors#'.$data['type'];

//from rest episode one

if ($data['type'] != 'about:blank') {
    $data['type'] = 'http://localhost:8000/api/docs/errors#'.$data['type'];
}

### programmer.feature ###


Scenario: Error response on invalid JSON
...
And the "type" property should equal "invalid_body_format" ????
And the "type" property should contain "/api/docs/errors#invalid_body_format" //from rest episode one, but reference is undefined

So, i have to add the reference: https://github.com/knpuniversity/rest/blob/master/features/api/ApiFeatureContext.php#L262

Reply

Hey!

Sorry - I missed this message! If you download the code from the repository, you get the starting code at the beginning of episode *1*. To get the code at the start of episode 2, you'll need to download it from this page (which is only available now to subscribers). I just downloaded the starting code for episode 2 and all of the tests pass - without making these changes. The changes you listed above *are* correct, but they're included in the starting episode 2 code download (since we made those changes near the end of episode 1.

And no worries - I like hearing about potential bugs in the tutorial - we want them to be clear for everyone!

Cheers!

Reply
Default user avatar
Default user avatar 3amprogrammer | posted 5 years ago

Hey Ryan! I have a question. We have this CRUD operations, which are great, but what if I have a quiz and I need and entry point for answering the question? What should my route look like? What my representation (json) should look like? If I have a table answer_user, should I create a controller UserAnswersController and put crud operations there?

By now I have an action in QuestionsController called answer that contains all this logic of updtaing answer_user table.

Reply

Hey 3amprogrammer!

Oh man, this type of question was one of the *hardest* things for me to really understand with REST. So, it's a great question :). But mostly, the answer is: it doesn't matter. Do your best to think of what makes sense, implement it, and never look back.

Now, let's look at your situation. Probably, you will have some GET endpoint to return the details of a specific question, e.g.:

GET /questions/{id}

Thinking philosophically, to answer this question, you're not "modifying" the question. So, a simple PUT to this URL doesn't make sense. Next, can we think of the "answers" as its own resource? If so, then, even if we don't *need* this endpoint, it should make sense to be able to make a GET request to /question/{id}/answers to return *all* answers to this question. And yes, I think, philosophically, that makes some sense :).

So, I would do this:

POST /questions/{id}/answers

But, if you don't think it makes sense that a "user answer" is its own resource, and rather, that "answering a question" is more of an "action", then this route starts to fall apart and not really fit cleanly into REST. And that's ok! Sometimes, you have an endpoint that is just awkward for REST. In those cases, I recommend making a POST request to the resource (i.e. /questions/{id}) with a URL fragment at the end that describes the "action" being taken. So:

POST /questions/{id}}/answer

I talk about these "weird" endpoints here: https://knpuniversity.com/s... and here: https://knpuniversity.com/s...

As far as a controller, I would probably put it into QuestionController, but if there were a few endpoints related to user answers, I then might create a separate UserAnswerController.

Let me know if this helped - sorry for the slow reply this time :)

1 Reply
Default user avatar

When is going to be possible to download videos of this screencast series?

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "silex/silex": "~1.0", // v1.3.2
        "symfony/twig-bridge": "~2.1", // v2.7.3
        "symfony/security": "~2.4", // v2.7.3
        "doctrine/dbal": "^2.5.4", // v2.5.4
        "monolog/monolog": "~1.7.0", // 1.7.0
        "symfony/validator": "~2.4", // v2.7.3
        "symfony/expression-language": "~2.4", // v2.7.3
        "jms/serializer": "~0.16", // 0.16.0
        "willdurand/hateoas": "~2.3" // v2.3.0
    },
    "require-dev": {
        "behat/mink": "~1.5", // v1.5.0
        "behat/mink-goutte-driver": "~1.0.9", // v1.0.9
        "behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
        "behat/behat": "~2.5", // v2.5.5
        "behat/mink-extension": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~5.7.0", // 5.7.27
        "guzzle/guzzle": "~3.7" // v3.9.3
    }
}
userVoice