> APIs >

Course Overview

Login to bookmark this course

Symfony RESTful API: Hypermedia, Links & Bonuses (Course 5)

Become a pro in Symfony RESTful API. Learn about Hypermedia, HATEOAS, handling non-RESTful endpoints & more in this course.

  • 1098 students
  • EN Captions
  • EN Script
  • Certificate of Completion

Your Guides

About this course

This tutorial uses an older version of Symfony. The concepts of Hypermedia & HATEOAS are still valid. But I recommend using API Platform in modern Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.0.*", // v3.0.3
        "doctrine/orm": "^2.5", // v2.5.4
        "doctrine/doctrine-bundle": "^1.6", // 1.6.2
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // v2.10.0
        "sensio/distribution-bundle": "^5.0", // v5.0.4
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.2
        "jms/serializer-bundle": "^1.1.0", // 1.1.0
        "white-october/pagerfanta-bundle": "^1.0", // v1.0.5
        "lexik/jwt-authentication-bundle": "^1.4", // v1.4.3
        "willdurand/hateoas-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.6
        "symfony/phpunit-bridge": "^3.0", // v3.0.3
        "behat/behat": "~3.1@dev", // dev-master
        "behat/mink-extension": "~2.2.0", // v2.2
        "behat/mink-goutte-driver": "~1.2.0", // v1.2.1
        "behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
        "phpunit/phpunit": "~4.6.0", // 4.6.10
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}

After 4 courses, we've somehow avoided the hottest buzzwords in REST: Hypermedia and HATEOAS. These can make your API awesome, or could bring you to your knees with fuzzy details, missing best practices and complexity. Let's make our API awesome:

  • Linking to Resources (and Hypermedia) without hating it
  • Controlling your JSON fields with VirtualProperty and SerializedName
  • Customizing your input field names with property_path
  • The wonderful HATEOAS php library
  • HAL+JSON... and whether you want to use it or not
  • Subordinate resources!
  • Handle ugly, non-RESTful, weird endpoints with some swagger

Next courses in the APIs: REST in Symfony 2 & 3 section of the APIs Track!

29 Comments

Sort By
Login or Register to join the conversation
Default user avatar Thierno Diop 6 years ago

Hello Ryan,
What is the best way to do a single page with angular 2 and symfony 3 ?
I mean the architecture and also the deployement
Please with details because i am so confused THX!!! You are doing an awesome job here keep going !!!!!

1 | Reply |

Hey Thierno!

Ha, that's a big question! :) Maybe you can tell me what things specifically are the most confusing, and we can try to clear those up! We're planning on a ReactJS + Symfony tutorial soon (maybe also Angular after) - but if you have some specific questions, let me know!

Part of the problem is that there are many ways to do things. You might have a pure JS frontend and a pure API backend. In this case, I would still keep them in the same git repository (for convenience), but they would be isolated. Then, you need to decide how you want to handle authentication. Btw, in some pure JS frontends, people actually just have one index.html file that holds the JS code to bootstrap things. If your app is this pure, you can even deploy this file (and of course your JS files) to a CDN for faster performance.

Or, you might have a mix of a traditional web app with some pages that host reach Angular/React applications. We do exactly that on KnpU. This is a bit easier, as we just rely on our cookie/session authentication when making AJAX calls that require authentication.

Cheers!

| Reply |
Default user avatar Thierno Diop weaverryan 6 years ago

Thx you Ryan for your answer.
I see what you mean but i want to make a social network in a single page app with angular 2 in the front-end and an API in the back-end with symfony 3 but i dont know if it is a good solution to develop it in a single page however it would be really cool if i could do this without leak of performance because i dont know if there is a problem with SPA and classic site in term of performance !!
THX

| Reply |

Hey Thierno!

I don't think you need to worry about performance - that's just a big topic, and one you can worry about later (neither method is inherently faster/slower). I typically recommend doing whatever you're more familiar with if you need to build this for a client or have some budget/deadline. But if this is more of a personal project and you want to learn, go with the approach that's less familiar to you :).

Good luck!

| Reply |
Default user avatar Thierno Diop weaverryan 6 years ago

Thx u so much its very clear now
cheers

| Reply |
Kaizoku avatar Kaizoku 6 years ago

Hi team,

I'm havent seen endpoint aggregation covered so I'm asking here.
Let say I have X entities ( A, B, C, ...) with a GET endpoint each.

Now how can I do an endpoint getAll that will return all my entities (A, B, C ...) ?
Should I internaly call each endpoint and aggregate the Json ?
Or Should I create a meta App class and serialize it ?
Other ?

Thx,

| Reply |

Hey Kaizoku,

I think calling each endpoint for every entity and aggregate results is not good for performance if you're talking about calling them by cURL requests. But if you're talking about calling those methods in your application internally that will return JSON - I think it's a valid case and you can do this way. Though, it depends, probably serialize them all at once, i.e. in one run would be better for performance than if you serialize them separately and aggregate JSON results. I'd recommend to measure performance and choose one or another based on the test results. But if performance would be very similar - do the easiest way for you.

I hope this helps.

Cheers!

1 | Reply |
Default user avatar Tamurai 6 years ago

Any news here?!
;)

| Reply |

This week! Wed or Thu actually. So nice timing :D

2 | Reply |
Default user avatar Dominik 6 years ago

I just want to drop a quick "THANK YOU" here for your great courses. Not only have I learned a ton about Symfony and REST but also a lot about good coding style in general. And your videos are a piece of art for itself. I know how much work goes into the designing, recording and production of such great videos. So THUMBS UP ;-)

| Reply |
Vladimir Z. avatar Vladimir Z. 6 years ago

Hi Ryan,
What is the proper way to send images and PDF files in a REST API? Should it be sent as a base64-encoded string, or as binary?
Thanks,

| Reply |

Hey Vlad!

When it comes to files, I think the best answer is to do whatever you would normally do on the web - i.e. return the same data (so in this case, probably binary) and content-type as normal. Also, if you look at GitHub's API (they're my goto to look at, because it's used by many people and is actually quite nice to use), if you ever ask them for a "download", they return JSON, with a key on that JSON to the URL the client can use to go fetch that assets. Then, I assume, they just serve that asset from a CDN. But, returning the PDF directly from a controller is also fine.

Cheers!

1 | Reply |
Vladimir Z. avatar Vladimir Z. 6 years ago edited

Hi Ryan,
In <strong>ApiTestCase::setUpBeforeClass</strong> there is an empty <strong>if</strong> statement:


    public static function setUpBeforeClass()
    {
        $handler = HandlerStack::create();

        $handler->push(Middleware::history(self::$history));
        $handler->push(Middleware::mapRequest(function(RequestInterface $request) {
            $path = $request->getUri()->getPath();
            if (strpos($path, '/app_test.php') !== 0) {
                $path = '/app_test.php' . $path;
            }
            $uri = $request->getUri()->withPath($path);

            return $request->withUri($uri);
        }));

        $baseUrl = getenv('TEST_BASE_URL');
        if ($baseUrl) {

        }
        self::$staticClient = new Client([
            'base_uri' => $baseUrl,
            'http_errors' => false,
            'handler' => $handler
        ]);

        self::bootKernel();
    }

specifically:


if ($baseUrl) {

}

What should go there?
Thanks!

| Reply |

Hi Vlad!

Yes, you caught me - I saw this recently :). I actually don't remember what I was thinking here. In fact, my only idea was that I was thinking about doing a (!$baseUrl), and then throwing an exception to help the user know this isn't set. I'm not sure if that's what I was thinking... but it's either that or nothing: I haven't seen any bug or problem with the code that I now realized I forgot to fix.

Unless you can see something I may have meant? That would be awesome :).

Cheers!

| Reply |
Vladimir Z. avatar Vladimir Z. weaverryan 6 years ago edited

How about this:


$baseUrl = getenv('TEST_BASE_URL');
if (!$baseUrl) {
        static::fail('No TEST_BASE_URL environmental variable set in phpunit.xml.');
}

I've also added:


stopOnError="true"
stopOnFailure="true"

to the <strong>phpunit.xml</strong> file

| Reply |

I like it!

I've just updated the starting code to add the static::fail() line :) (ref https://github.com/knpunive.... I kept the stopOnError stuff the way it was - I don't feel too strongly about those settings, and the user can override them.

Thanks for the suggestion!

| Reply |

Hi Ryan,
I haven't found anything about file upload through an API? Let's say I have a Chat entity that can receive a text (easy) but also an image, how can I deal with it? I'm using VichUploaderBundle and VichUploaderSerializationBundle in my project but I can't figure out how to make this work.

| Reply |

Yo VinZ!

Of course, it depends... like everything :). Let me give you a quick answer to get you started. File uploads through an API don't always look like file uploads through a browser. For non-huge file uploads, I would probably have an endpoint where the user sends the file. For example, if I'm uploading my avatarImage for my user account, I might make a PUT to /users/{username}/avatar. In this, the entire body of the request would be the image file's contents. I would then read those manually in my controller with $request->getContent() and then save the file somewhere. I'm not sure if VichUploaderBundle can help you with this or not: it's usually good at taking an UploadedFile object and (a) moving the file for you and (b) setting the filename field on your entity. But in this case, you start with the file contents, and then it's pretty easy to save that where you want, create whatever filename you want, and then update your entity yourself. That bundle may have something to help with this - I'm just not sure. And I've never heard of the VichUploaderSerializationBundle before this, but I suspect this is more helpful for serializing from your entity object to JSON, but not the other way around.

Btw, another alternative is to send your contents up simply on a key of some JSON data (but base 64 encoded). That is exactly what GitHub's API does: https://developer.github.com/v3/repos/contents/#create-a-file

I hope this at least gets you started!

1 | Reply |

Thanks weaverryan, in the meantime, I choose to receive a base64 encoded image through JSON data, and then transform it into an UploadedFIle object and pass it to my FormType. Looks like it does the trick.


    public function createAction(Request $request)
    {
        $chat = new Chat();
        $form = $this->createForm(new ChatType(), $chat);
        $data = json_decode($request->getContent(), true);
        if (array_key_exists('imageFile', $data)) {
            $uploadedFile = new UploadedBase64EncodedFile(new Base64EncodedFile($data['imageFile']));
            unset($data['imageFile']);
        }
        $form->submit($data);
        if (!$form->isValid()) {
            $this->throwApiProblemValidationException($form);
        }
        if (isset($uploadedFile)) {
            $chat->setImageFile($uploadedFile);
        }
        $em = $this->getDoctrine()->getManager();
        $em->persist($chat);
        $em->flush();
        return $this->createApiResponse($chat);
    }
| Reply |

Ah, super cool! I assume the UploadedBase64EncodedFile is from this library? https://github.com/hshn/bas...

Very nice solution! Thanks for sharing :)

| Reply |

weaverryan thanks :) only problem is I don't get any validation for the image :/

| Reply |

Hey VinZ!

Hmm. There might be an easy way to solve that. If you create an imageFile field on your form that is a "file" type... then you might actually be able to put the UploadedBase64EncodedFile object onto the $data array before calling submit. In other words, you're basically manually adding the uploaded file to the submitted data, so that it is present for the form to process:


if (array_key_exists('imageFile', $data)) {
    $uploadedFile = new UploadedBase64EncodedFile(new Base64EncodedFile($data['imageFile']));
    $data['imageFile'] = $uploadedFile;
}

Oh, another idea! Even simpler, just move your setImageFile call before the $form->isValid(). As long as the data is on your entity object, it will be validated (even if the field is not actually part of your form).

Anyways, let me know if either of those work :).

Cheers!

| Reply |

Hey weaverryan, your first solution worked nicely thanks! I thought I had tried it before and wasn't working, but obviously, no! Full working code:


 public function createAction(Request $request, $id)
    {
        $chat = new Chat();
        $form = $this->createForm(new ChatType(), $chat);
        $data = json_decode($request->getContent(), true);
        if (array_key_exists('imageFile', $data)) {
            $uploadedFile = new UploadedBase64EncodedFile(new Base64EncodedFile($data['imageFile']));
            $data['imageFile'] = $uploadedFile;
        }
        $form->submit($data);
        if (!$form->isValid()) {
            $this->throwApiProblemValidationException($form);
        }
        $em = $this->getDoctrine()->getManager();
        $em->persist($chat);
        $em->flush();

        return $this->createApiResponse($chat);
    }
| Reply |

Awesome! Thanks again for sharing your working solution! Very interesting!

| Reply |
Default user avatar Neandher Carlos 6 years ago

You will talk about NelmioApiDocBundle in this course?

| Reply |

Not in this one, but I LOVE that bundle. I *am* considering an episode 6 about documentation - basically moving the planned ep3 from the Silex rest series over here as ep 6.

I've already finished recording this course, and I'm *really* excited about it - watch for it in a few weeks!

| Reply |
Default user avatar Neandher Carlos weaverryan 6 years ago

About episode 6, my vote is: YEEEEES!!!!! - Sorry, Brazil crazy things!
What do you think about: https://apigility.org
:)

| Reply |

I think it's a good idea. In my limited experience, it's crazy verbose - I don't love how Zend usually does things, but I keep an eye on it :).

| Reply |
Default user avatar Neandher Carlos weaverryan 6 years ago

Understand. That is made in Symfony: https://api-platform.com. I keep an eye on it too.

| Reply |

Delete comment?

Share this comment

astronaut with balloons in space

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