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

Buy Access

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

Your Guides

Ryan Weaver Leanna Pelham

Questions? Conversation?

  • 2017-08-11 weaverryan

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

  • 2017-08-10 VinZ

    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);
    }
  • 2017-08-09 weaverryan

    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!

  • 2017-08-09 VinZ

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

  • 2017-08-09 weaverryan

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

    Very nice solution! Thanks for sharing :)

  • 2017-08-09 VinZ

    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);
    }
  • 2017-08-08 weaverryan

    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.co...

    I hope this at least gets you started!

  • 2017-08-07 VinZ

    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.

  • 2017-06-02 Dominik

    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 ;-)

  • 2016-08-23 weaverryan

    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!

  • 2016-08-20 Vlad

    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 phpunit.xml file

  • 2016-08-19 weaverryan

    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!

  • 2016-08-19 Vlad

    Hi Ryan,
    In ApiTestCase::setUpBeforeClass there is an empty if 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!

  • 2016-08-13 Thierno Diop

    Thx u so much its very clear now
    cheers

  • 2016-08-12 weaverryan

    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!

  • 2016-08-11 Thierno Diop

    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

  • 2016-08-11 weaverryan

    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!

  • 2016-08-10 Thierno Diop

    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 !!!!!

  • 2016-07-07 weaverryan

    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!

  • 2016-07-06 Vlad

    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,

  • 2016-05-18 weaverryan

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

  • 2016-05-17 TheLastTamurai

    Any news here?!
    ;)

  • 2016-04-29 Neandher Carlos

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

  • 2016-04-29 weaverryan

    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 :).

  • 2016-04-28 Neandher Carlos

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

  • 2016-04-13 weaverryan

    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!

  • 2016-04-07 Neandher Carlos

    You will talk about NelmioApiDocBundle in this course?