REST APIs with Symfony2: The Easy Way – updated

updated:  February 3, 2015

Developing a RESTful API with Symfony has been covered multiple times by many people.  If you want to learn how to implement it “the right way”, I suggest reading William Durand’s post. If you want to implement it “the best way”, you’ll want to read Giulio De Donato’s post. If you want to do it “the easy way”, keep reading.

The Easy Way

Install the Voryx REST Generator bundle and go through the Installation and Configuration instructions.

That was easy, wasn’t it?  Now let’s start using it…

First, you’ll need to make sure that you have a working Symfony project with a database and schema created, with the demo bundle still installed.  If you need help getting your project setup, take a look at the Symfony Getting Started Guide.

For this tutorial, you’ll need to create a new Entity by running the following command:

php app/console doctrine:generate:entity --entity=AppBundle:Post --format=annotation --fields="name:string(255) description:string(255)" --no-interaction

The generated entity will look something like this:

namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Post
*
* @ORM\Table()
* @ORM\Entity
*/
class Post
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;

/**
* @ORM\Column(name="description", type="string", length=255)
*/
private $description;

...

}

Make sure you update your database schema by running:

php app/console doctrine:schema:update --force

Forms

Forms are the standard way to get information into your application.  They provide a layer that restricts the fields that can be submitted and provide validation.  Since this is an API, the form will not be rendered as HTML, they’re strictly used for inbound actions like POST, PUT and PATCH. At the moment, the REST generator bundle does not create the form for you (even though it says that it does), so you’ll need to generate one manually:

update:  With the current version forms and routes will be created during the generation process.
php app/console doctrine:generate:form AppBundle:Post

Generate the REST controller

php app/console voryx:generate:rest --entity="AppBundle:Post"

This adds a new controller called PostRESTController with the following REST actions: GET, POST, PUT, PATCH and DELETE.

Routes

The last thing that you’ll need to do, is make sure that you have a route setup.
api_posts:
type: rest
resource: “@AppBundle/Controller/PostRESTController.php”
prefix: /api

Using the API

Now we’re ready to start using the API. Open up your favorite terminal application and try these commands.

Make sure to replace http://localhost:8000 with your local domain.

Creating a new post (POST)
curl -i -H "Content-Type: application/json" -X POST -d '{"name" : "Test Post", "description" : "This is a test post"}' http://localhost:8000/app_dev.php/api/posts
Updating (PUT)
curl -i -H "Content-Type: application/json" -X PUT -d '{"name" : "Test Post 1", "description" : "This is an updated test post"}' http://localhost:8000/app_dev.php/api/posts/1
Partial update (PATCH)
curl -i -H "Content-Type: application/json" -X PATCH -d '{"name" : "Awesome Post 3"}' http://localhost:8000/app_dev.php/api/posts/1
Get all posts (GET)
curl http://localhost:8000/app_dev.php/api/posts
Get one post (GET)
curl http://localhost:8000/app_dev.php/api/posts/1
Delete (DELETE)
curl -X DELETE  http://localhost:8000/app_dev.php/api/posts/1

CORS

Make sure that the prefix in your route matches the CORS path you set in the Symfony app config. Configuring CORS is outside of the scope of this post, but the defaults in the REST Generator documentation should work in most cases. You’ll want to change these settings for a production environment.

That’s pretty much it. You just created a RESTful CRUD API without writing a single line of code.

24 responses to “REST APIs with Symfony2: The Easy Way – updated

  1. Hi Dan,

    We were very exited to see your repo and tutorial. It worked perfectly for “Post” Entities but for no other entity names what so ever. Pity. Maybe we are doing something stupid.

    We would like to see the Controller cleaner as well. Our pattern is to keep the persistence related code out of the Controller.

    1. Thanks Andre. The goal of this project is to mimic the functionality of the built in CRUD generator, so you get basic REST functionality working quickly. In the future, we may take a look at ways to thin out the controller.

  2. Hi,
    Really good job!
    But when do you think it will be working with related entities? It will be perfect!

    Thanks

  3. Not working, put, patch and post, any idea???

    Where copy the follow code:

    ->add(
    ‘user’, ‘voryx_entity’, array(
    ‘class’ => ‘Acme\Bundle\Entity\User’
    )
    )

    1. Jesus,

      Can you give me some more info on what you’re trying to accomplish? Are you getting any errors?

      The code you referenced is for embedding related entities into the form. You need to make sure the User entity exists and that it has a relationship to the parent entity.

      For example, if you have a many to one relationship between Post and User, you would define the relation in the Post entity like this:

      /**
      * @ORM\ManyToOne(targetEntity=”User”, inversedBy=”posts”)
      * @ORM\JoinColumn(name=”user_id”, referencedColumnName=”id”)
      */
      protected $user;

      And then add the following to PostType:

      ->add(
      ‘user’, ‘voryx_entity’, array(
      ‘class’ => ‘Acme\Bundle\Entity\User’
      )
      )

      Thanks,
      Dave

  4. Hi David,

    I am a new bee in symfony and am trying to implement REST in application.

    Using composer I tried to install Voryx REST Generator bundle, but it is throwing error and asking for FOSRestBundle of version 1.4.* which is not yet available at GitHib.

    Please guide me.

  5. Hello, becouse it is necessary to add “id” Primary key

    if (!in_array(‘id’, $metadata->identifier)) {
    throw new \RuntimeException(‘The REST api generator expects the entity object has a primary key field named “id” with a getId() method.’);
    }

    my Entity

    class Estado
    {
    /**
    * @var string
    *
    * @ORM\Column(name=”codi_edo”, type=”string”, length=11, nullable=false)
    * @ORM\Id
    * @ORM\GeneratedValue(strategy=”IDENTITY”)
    */
    private $codiEdo = ”;

    /**
    * Get id
    *
    * @return string
    */
    public function getCodiEdo()
    {
    return $this->codiEdo;
    }
    }

    Change

    class Estado
    {
    /**
    * @var string
    *
    * @ORM\Column(name=”codi_edo”, type=”string”, length=11, nullable=false)
    * @ORM\Id
    * @ORM\GeneratedValue(strategy=”IDENTITY”)
    */
    private $id = ”;

    /**
    * Get id
    *
    * @return string
    */
    public function getId()
    {
    return $this->id;
    }
    }

    //@todo lookup entity’s identifier. Assuming that “id” is the identifier
    if (is_array($data) && isset($data[‘id’])) {
    return $data[‘id’];
    }

    //”id” should be
    $identifier = $metadata->identifier // codiEdo or id

    $identifier = str_replace(array(‘_’), ‘ ‘, $identifier);
    $identifier = strtolower(preg_replace(‘~(?<=\\w)([A-Z])~', '_$1', $identifier));
    $propety_id = trim(ucwords(str_replace('_', ' ', $identifier)));// codiEdo or id
    $method_name = trim(ucwords(str_replace('_', ' ','get_' . $identifier))); // getCodiEdo or getId

  6. Hi David,

    Great tutorial! Very easy to follow and understand. I’ve just delved into the world of Symfony2 and ‘restful’ APIs.

    I’ve followed all the steps you described. I can’t seem to get it to work. I’ve attempted to test the POST method by adding a test post using the Postman addon in Chrome. However I get a ‘404 Not Found’ message.

    In the prod.log file the following entry is added:
    “[2014-10-08 14:38:45] request.ERROR: Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\NotFoundHttpException: “No route found for “POST /api/posts”” at /home/saeed/rightmorsel-api/rm-api/app/cache/prod/classes.php line 1980 {“exception”:”[object] (Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException: No route found for \”POST /api/posts\” at /home/saeed/rightmorsel-api/rm-api/app/cache/prod/classes.php:1980, Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException: at /home/saeed/rightmorsel-api/rm-api/app/cache/prod/appProdUrlMatcher.php:32)”} []”

    I’ve tried to debug the routes by running ‘sudo app/console router:debug’ and it returns:

    “get_post GET ANY ANY /api/posts/{entity}.{_format}
    get_posts GET ANY ANY /api/posts.{_format}
    post_post POST ANY ANY /api/posts.{_format}
    put_post PUT ANY ANY /api/posts/{entity}.{_format}
    patch_post PATCH ANY ANY /api/posts/{entity}.{_format}
    delete_post DELETE ANY ANY /api/posts/{entity}.{_format}”

    I’m stumped as to what else I can do. Appreciate if you can point me in the right direction.

  7. Saeed,

    In Postman, you must specify the Content-Type as ‘application/json’ or use the path /api/posts.json. If you don’t specify the format, it’ll default to html, which isn’t configured by default.

    1. Thanks for the reply David.

      I followed your suggestion. I have now pin pointed the issue to $form->isValid() returning false.

      There doesn’t seem to be any error messages in the form. I also tried getErrorsAsString() but this also returns an empty string.

      Appreciate any tips on debugging the isValid() method.

      Many thanks,
      Saeed.

  8. Sincere apologies, I didn’t follow your advice properly. I set the Content-Type to ‘application/json’ and it now works as accepted!

    Thanks for all your help. Once again, great tutorial, looking forward to reading more from you!

  9. Hi David,

    Just trying to understand your tutorial. Quick question; the routes for the generated REST controller have ‘posts’ after the ‘api’, so the path for each route per action in the controller is: ‘/api/posts/…’ – how and where is the ‘posts’ part defined?

    Many thanks.

    1. Saeed,

      Sorry for taking so long to get back to you.

      The “post” segment of the url is generated automatically by the FOS RestBundle. It uses the controller name to come up with the resource name.

      You can override it by using a RouteResource annotation, like this:

      /**
      * @RouteResource(“Posts1”)
      */
      class PostsController
      {

  10. Hey David,

    At the end of the tutorial you said “Next time we’ll cover Dealing with Related Entities”. I can’t find that article, I’m guessing you haven’t written that one yet. Can you tell me if you still plan on doing that? I’d love to know how to get nested routes to work automatically. So things like /posts/1/user

    Thanks!

    1. Evert,

      Sorry, I haven’t written that article yet. If I find some time, I’ll try to get it out.

      The goal of the generator was to quickly get basic CRUD like functionality up and working, so it’ll probably never fully support sub-resources.

      However, it shouldn’t be too difficult for you to add that functionality to the generated controllers.

      Here’s some more information on implementing sub-resources:

      https://github.com/FriendsOfSymfony/FOSRestBundle/blob/master/Resources/doc/6-automatic-route-generation_multiple-restful-controllers.md

  11. Hi David

    You have written a superb article, Am new to symfony but still i find you step easy to implement and work on it.
    Thanks a lot for such a simple and easy understanding article.

    Am able to execute everything you have written but now i have something more to be done, As we can get the specific result by just passing the ID, is there any way that i can get the result filter according the name as well or in future can i add more filter criteria to it.

    Thanks in advance.

    1. Sarang,

      I’m glad you’ve found this post helpful.

      You can filter the results like this:

      curl http://localhost:8000/app_dev.php/api/posts?filters%5Bname%5D=Sarang

      Here are all of the filter options:

      * @QueryParam(name=”offset”, requirements=”\d+”, nullable=true, description=”Offset from which to start listing notes.”)
      * @QueryParam(name=”limit”, requirements=”\d+”, default=”20″, description=”How many notes to return.”)
      * @QueryParam(name=”order_by”, nullable=true, array=true, description=”Order by fields. Must be an array ie. &order_by[name]=ASC&order_by[description]=DESC”)
      * @QueryParam(name=”filters”, nullable=true, array=true, description=”Filter by fields. Must be an array ie. &filters[id]=3″)

Leave a Reply