API - GET a Single BlogPost


In this video we are going to implement the first of two GET actions. We will use GET to get (ahem) single resources, and collections of resources.

By this I mean you will be able to send in a request for an individual BlogPost (by id), and in the next video, we will also be able to GET a collection of BlogPost's, in that case without needing an id.

The URLs may make this a little clearer:

http://api.symfony-3.dev/app_dev.php/posts/{someIdHere}

Which would return the JSON representation of a BlogPost matching whatever id you passed in.

And then, in the next video we will implement the functionality to GET a collection of BlogPost's where the URL would be :

http://api.symfony-3.dev/app_dev.php/posts

Which would return a JSON array of BlogPost entries.

Keep It Simple - But Also, Unrealistic

Ok, so that's the idea. And to begin with we will implement the simplest possible approach to return a result.

The thing is though, in anything but a quick demo API, the following logic won't be good enough. At this stage all we are demonstrating is the basics.

Once you understand this and start to feel comfortable that - hey, Symfony is actually a pretty nice platform to build an API with - then I would suggest watching this course, if not in full, then at least in the parts that sound interesting to you / your project.

But, enough words. Let's see some code:

<?php

// /src/AppBundle/Controller/BlogPostsController.php

namespace AppBundle\Controller;

use AppBundle\Entity\BlogPost;
use FOS\RestBundle\View\View;
use FOS\RestBundle\Controller\Annotations;
use FOS\RestBundle\View\RouteRedirectView;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Routing\ClassResourceInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

/**
 * Class BlogPostsController
 * @package AppBundle\Controller
 */
class BlogPostsController extends FOSRestController implements ClassResourceInterface
{
    /**
     * Gets an individual Blog Post
     *
     * @param int $id
     * @return mixed
     * @throws \Doctrine\ORM\NoResultException
     * @throws \Doctrine\ORM\NonUniqueResultException
     *
     * @ApiDoc(
     *     output="AppBundle\Entity\BlogPost",
     *     statusCodes={
     *         200 = "Returned when successful",
     *         404 = "Return when not found"
     *     }
     * )
     */
    public function getAction(int $id)
    {
        return $this->getDoctrine()->getRepository('AppBundle:BlogPost')->find($id);
    }
}

There we go, as easy as that - and thanks to the power of FOSRESTBundle - we are now returning JSON representations of any BlogPost entries in our database.

Fairly fantastic, in my opinion.

Note that our controller action is called getAction. As we are implementing ClassResourceInterface to make use of implicit resource naming, we don't need to include the word BlogPost in our action names - it is inferred from the name of the Controller itself (BlogPostsController).

However, that will make our automatically generated route names a little messy.

Improving Things

Because our entity has two words in it - Blog and Post (BlogPost), FOSRESTBundle will get a little mixed up and create some funky routes such as :

get_blogs_posts GET /blogs/{id}/posts

I don't like this. Changing it is fairly easy, though maybe not so immediately obvious. We need another annotation on our Controller:

use FOS\RestBundle\Controller\Annotations\RouteResource;

/**
 * Class BlogPostsController
 * @package AppBundle\Controller
 *
 * @RouteResource("post")
 */
class BlogPostsController extends FOSRestController implements ClassResourceInterface
{

By specifying the RouteResource, we can influence the naming of our routes and URLs:

get_post GET /posts/{id}

Nicer. Thankfully in FOSRESTBundle 2.0, there is further enhancement on route naming.

At this stage, and assuming you have populated your database in some way, we should be able to call URLs such as:

http://api.symfony-3.dev/app_dev.php/posts/36

or

http://api.symfony-3.dev/app_dev.php/posts/48

etc.

And get a valid result, which should be the JSON representation of the BlogPost with the passed in id:

{
    "id": 36,
    "title": "a blog post title here",
    "body": "a blog post body here"
}

Moving Queries To The Repository

The first improvement I am going to make is to remove the find query from the controller action.

This is not going to truly solve the problem that we have here - that this particular controller action is currently heavily tied to Doctrine. A better approach here, in my opinion, would be to mask that implementation behind a repository - but it is a trade off against simplicity.

Instead, this first step is to simply move to the query logic to the BlogPost entity repository:

<?php

// /src/AppBundle/Entity/Repository/BlogPostRepository.php

namespace AppBundle\Entity\Repository;

use Doctrine\ORM\EntityRepository;

class BlogPostRepository extends EntityRepository
{
    public function createFindOneByIdQuery(int $id)
    {
        $query = $this->_em->createQuery(
            "
            SELECT bp
            FROM AppBundle:BlogPost bp
            WHERE bp.id = :id
            "
        );

        $query->setParameter('id', $id);

        return $query;
    }
}

And to use this, we need to make a change in the BlogPost entity annotations also:

// /src/AppBundle/Entity/BlogPost.php

/**
 * @ORM\Entity(repositoryClass="AppBundle\Entity\Repository\BlogPostRepository")
 * @ORM\Table(name="blog_post")
 * @JMSSerializer\ExclusionPolicy("all")
 */
class BlogPost implements \JsonSerializable
{

And we would also need to declare this as its own service:

# /app/config/services.yml

services:
    crv.doctrine_entity_repository.blog_post:
        class: Doctrine\ORM\EntityRepository
        factory: ["@doctrine", getRepository]
        arguments:
            - AppBundle\Entity\BlogPost

This will help us more when we come to implementing pagination, sorting, and filtering, a little later on. The additional benefit here is that it makes our queries that much more explicit. This comes in handy as our project grows, as we can be very specific about exactly what should and should not be returned.

Because of our forthcoming requirements for pagination, I would return a Query object here instead of returning the result. Again, this is a trade off.

That said, we won't be paginating, sorting, or filtering individual results, but for later consistency it feels right to return the $query from this action.

With this in place, we can update the controller action:

    /**
     * Gets an individual Blog Post
     *
     * @param int $id
     * @return mixed
     * @throws \Doctrine\ORM\NoResultException
     * @throws \Doctrine\ORM\NonUniqueResultException
     *
     * @ApiDoc(
     *     output="AppBundle\Entity\BlogPost",
     *     statusCodes={
     *         200 = "Returned when successful",
     *         404 = "Return when not found"
     *     }
     * )
     */
    public function getAction(int $id)
    {
        return $this->getBlogPostRepository()->createFindOneByIdQuery($id)->getSingleResult();
    }

I've left the docblock / annotations in there as this highlights something won't behave as expected - which is that we won't get a 404 if the record is not found. We will end up getting a 500 error if a non-existent record is requested.

This goes back to what I mentioned earlier about simplicity versus real world. We really do need some error checking:

    public function getAction(int $id)
    {
        $blogPost = $this->getBlogPostRepository()->createFindOneByIdQuery($id)->getSingleResult();

        if ($blogPost === null) {
            return new View(null, Response::HTTP_NOT_FOUND);
        }

        return $blogPost;
    }

Now it should behave as annotated.

Episodes