Implement a RESTful web service using Symfony2June 27, 2014

Why a RESTful web service?

The case for REST is best illustrated by example.

Non RESTful Reading of Resources:

<h1>Stephen King</h1>
<p>Author of 'Misery', 'The Stand' and more recently 'The Dome', Stephen King is 
indeed a King of...</p>

RESTful Reading of Resources:

        "name":"Stephen King",
        "intro":"Author of 'Misery', 'The Stand' and more recently 'The Dome', Stephen King is indeed a King of..."

Non RESTful Deletion of Resource:

<form method="POST" action="">
    <select name="author">
        <?php foreach($authors as $author): ?>
        <option value="<?php $author->getId() ?>"><?php $author->getName() ?></option>
        <?php endforeach ?>
    <input type="submit" value="Delete Author">

RESTful Deletion of Resource:

Non-RESTful Posting of a New Resource or Updating an Existing Resource:

<form method="POST" action="">
    Author Name: <input type='text' name='name' />
    Author Hometown: <input type='text' name='hometown' />
    Author Age: <input type='text' name='age' />
    <input type="submit" value="Submit Author">

RESTful Posting of a New Resource or Updating an Existing Resource:

        "name":"Stephen King",
        "hometown":"Portland, Maine",

In the RESTful examples, the user of the web service doesn’t have to know specific URLs of pages or query-strings in order to receive, update or delete resources; all they have to know is the name of the resource and the action they want to perform on it.

Furthermore, they can use JSON or XML format to update or post a new resource, and they can request either a JSON, XML, or HTML response, depending on their needs.

Finally, when there is an error either with the request or the response, a HTTP error code is returned which adequately informs the user of the correct nature of the error. These error codes will remain a consistent standard so that no matter which web service you are using you’ll never be forced to learn a whole new set of error codes and what they mean.

The More Common HTTP response codes

Code Name Meaning
200 OK No errors
204 Success No errors, returning no content; i.e. as in a successful DELETE operation
400 Bad Request Server doesn’t understand request (due to bad syntax perhaps)
401 Unauthorized The resource/operation is protected by authentication and authorization
403 Forbidden The resource/operation is not accessible to anyone
404 Not Found The resource could not be found
405 Method Not Allowed The resource can have certain actions applied to it (PUT, POST, GET) but not others (DELETE), for example
500 Internal Server Error More than likely bad configuration or an error in your code

There are many more errors (especially of the 4xx variety) but I’ve only listed the most common ones here, as the other ones hardly ever come up… Well, not for me at least.

How the HTTP Request looks to the webserver

In order to see how HTTP requests look to web servers you need to be able to modify the request headers. In order to do this you need more than a web browser – you need a HTTP command-line program such as HTTPIE or cURL, or a REST console plugin for a browser such as Firefox’s RESTClient or Chrome’s REST Console, etc, then you will be able to see the request and its response in finer detail.

In HTTPIE you specify what type of request you’re sending, i.e. GET, PUT, POST or DELETE before specifying the URL. Optionally you can modify extra headers and even send JSON or XML data in the body of the request in case of a PUT or POST request.

Below is a simple GET request. I’ve used the -h switch which tells HTTPIE to only display headers in the response. If you leave it out, the complete source-code of the HTML page will quickly scroll past in your terminal.

C:\me> HTTP GET -h HTTP/1.1 200 OK Cache-Control: public, no-cache="Set-Cookie", max-age=60 Content-Encoding: gzip Content-Length: 39037 Content-Type: text/html; charset=utf-8 Date: Fri, 27 Jun 2014 06:58:08 GMT Expires: Fri, 27 Jun 2014 06:59:08 GMT Last-Modified: Fri, 27 Jun 2014 06:58:08 GMT Set-Cookie: prov=4251ab7c-6785-4313-ad55-29e538b97d0a;; expires=Fri, 01-Jan-2055 00:00:00 GMT; path=/; HttpOnly Vary: * X-Frame-Options: SAMEORIGIN

The request was accepted and a response with content sent, as specified by the 200 OK response code.

Create a New Bundle in Symfony

Create a new skeleton bundle:

C:\me> php app/console generate:bundle

Answer the questions that come up – I’m calling this bundle the GoodReadings\AuthorsBundle, as my app name is GoodReadings (yes I know, it’s not very original; in fact, it probably almost infringes someone’s copyright). If you’re unsure about how to answer the other questions, just hit enter and the default options will be used. Type ‘yes’ to generate the example directory structure. The AppKernel.php file will be updated to include your bundle when the app is run, and the app/config/routing.yml will be updated to import your bundle’s routing file. The /hello/{name} example function in the default controller will be included by default in this routing.

Create the Author and Book Entities

Create the Author Entity:

C:\me> php app/console generate:doctrine:entity

The name will be GoodReadingsAuthorsBundle:Author. Hit enter for annotation field configuration format. The fields will be id, name, birthdate, hometown and books. For name and hometown just hit enter, then enter again for 255 character strings. For birthdate it’s ‘date’, for books it’s ‘array’. Hit enter to stop adding fields and yes for empty repository.

Create the Book Entity:

C:\me> php app/console generate:doctrine:entity

The name will be GoodReadingsAuthorsBundle:Book. Hit enter for annotation field configuration format. The fields will be id, title, published, genre and author. For title and genre just hit enter, then enter again for 255 character strings (you could create a genre entity if you wanted). For published it’s ‘date’, for author it’s ‘array’. Hit enter to stop adding fields and yes for empty repository.

We need to change the entities before updating the database schema in order to reflect the many-to-one relationship between books and author. First we’ll change Author (the updated code has been bolded):

namespace GoodReadings\AuthorsBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\ManyToMany; use Doctrine\Common\Collections\ArrayCollection; /** * @ORM\Table(name="authors") * @ORM\Entity(repositoryClass="GoodReadings\AuthorsBundle\Entity\AuthorRepository") */ class Author { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(name="name", type="string", length=255, unique=true) */ private $name; /** * @ORM\Column(name="hometown", type="string", length=255) */ private $hometown; /** * @ORM\Column(name="birthdate", type="date") */ private $birthdate; /** * @ORM\ManyToOne(targetEntity="Book", mappedBy="author") */ protected $books; public function __construct() { $this->books = new ArrayCollection(); } public function getId() { return $this->id; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setHometown($hometown) { $this->hometown = $hometown; return $this; } public function getHometown() { return $this->hometown; } public function setBirthdate($birthdate) { $this->birthdate = $birthdate; return $this; } public function getBirthdate() { return $this->birthdate; } public function setBooks($books) { $this->books = $books; return $this; } public function getBooks() { return $this->books; } }

One author has written and published many books, and Book is another Entity in our app. The Author’s Book objects are stored in an ArrayCollection which has to be initalized during construction of the Author object. The Many-to-one relationship between Books and Author has to be reflected in annotations in both the Author and Book entities.

Edit Book:

namespace DigitalRep\AuthorsBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table(name="books") * @ORM\Entity(repositoryClass="DigitalRep\AuthorsBundle\Entity\BookRepository") */ class Book {     /**     * @ORM\Column(name="id", type="integer")     * @ORM\Id     * @ORM\GeneratedValue(strategy="AUTO")     */     private $id;     /**     * @ORM\Column(name="title", type="string", length=255)     */     private $title;     /**     * @ORM\Column(name="genre", type="string", length=255)     */     private $genre;     /**     * @ORM\Column(name="published", type="date")     */     private $published;     /**     * @ORM\ManyToOne(targetEntity="Author", inversedBy="books")     * @ORM\JoinColumn(name="author_id", referencedColumnName="id")     */     private $author;     public function getId()     {         return $this->id;     }     public function setTitle($title)     {         $this->title = $title;         return $this;     }     public function getTitle()     {         return $this->title;     }     public function setGenre($genre)     {         $this->genre = $genre;         return $this;     }     public function getGenre()     {         return $this->genre;     }     public function setPublished($published)     {         $this->published = $published;         return $this;     }     public function getPublished()     {         return $this->published;     }     public function setAuthor($author)     {         $this->author = $author;         return $this;     }     public function getAuthor()     {         return $this->author;     } }

Now we update the database schema with our updated Entities:

C:/me> php app/console doctrine:schema:update ----force Updating database schema... Database schema updated successfully! "3" queries were executed

The Data

Accessing the URL will yield an error because there aren’t any Author objects yet. I’ll leave it up to you to decide how you want to insert some Authors and Books… I’ll just supply you with some sample data should you need it (leave the id as null but keep the order of insertion):

Id Author Name Author Birthdate Author Hometown
1 Stephen King 21/09/1947 Portland, Maine
2 Margaret Attwood 18/11/1939 Ottowa, Ontario
3 Philip K. Dick 16/12/1928 Chicago, Illinois
4 Elizabeth Bowen 07/06/1889 Dublin, Ireland
5 Chuck Palahniuk 21/02/1962 Pasco, Washington

And it would be remiss of us if we didn’t add some books (again, leave the book id null):

Book Id Author Id Book Title Book Genre Publish Date
1 1 Misery Psychological Horror 08/06/1987
2 2 The Handmaid’s Tale Speculative Fiction 01/01/1985
3 3 A Scanner Darkly Science Fiction 12/03/1977
4 4 The Death of the Heart Classic / Literature 01/01/1938
5 5 Fight Club Satirical Novel 17/08/1996
6 1 The Langoliers Science Fiction 01/09/1990
7 5 Choke Satire 22/05/2001
8 3 Do Androids Dream of Electric Sheep? Philisophical Fiction 01/01/1968

Controller Actions

Use the console to generate C(reate)R(ead)U(pdate)D(elete) operations for your controller (AuthorController):

C:/me> php app/console generate:doctrine:crud

Enter the Entity name, GoodReadingsAuthorsBundle:Author, type yes to generate write methods, yml for routing configuration format, and then enter for the routes prefix, which is /author by default. Finally enter to confirm.

For the routing, you should have in app/config/routing.yml:

goodreadings_authors: resource: "@GoodReadingsAuthorsBundle/Resources/config/routing.yml" prefix: /

And in src/GoodReadings/AuthorsBundle/Resources/config/routing.yml

goodreadings_authors_author: resource: "@GoodReadingsAuthorsBundle/Resources/config/routing/author.yml" prefix: /author

And in src/GoodReadings/AuthorsBundle/Resources/config/routing/author.yml

author: pattern: / defaults: { _controller: "GoodReadingsAuthorsBundle:Author:index" } author_show: pattern: /{id}/show defaults: { _controller: "GoodReadingsAuthorsBundle:Author:show" } author_new: pattern: /new defaults: { _controller: "GoodReadingsAuthorsBundle:Author:new" } author_create: pattern: /create defaults: { _controller: "GoodReadingsAuthorsBundle:Author:create" } requirements: { _method: post } author_edit: pattern: /{id}/edit defaults: { _controller: "GoodReadingsAuthorsBundle:Author:edit" } author_update: pattern: /{id}/update defaults: { _controller: "GoodReadingsAuthorsBundle:Author:update" } requirements: { _method: post|put } author_delete: pattern: /{id}/delete defaults: { _controller: "GoodReadingsAuthorsBundle:Author:delete" } requirements: { _method: post|delete }
C:/me> php app/console router:debug

will show you the routes available in your app, and as you can see, they are not quite RESTful routes. They also include extra routes for the showing of forms, which we don’t need for web services

Name Method Path Should be
author ANY /author/ GET /author/
author_show ANY /author/{id}/show GET /author/{id}
author_new ANY /author/new Delete this
author_create POST /author/create POST /author/
author_edit ANY /author/{id}/edit PUT /author/{id}
author_update POST|PUT /author/{id}/update Delete this
author_delete POST|DELETE /author/{id}/delete DELETE /author/{id}

Changing the first two is really easy; simply change the routes in routing.yml:

author: pattern: / defaults: { _controller: "GoodReadingsAuthorsBundle:Author:index" } requirements: { _method: get } author_show: pattern: /{id} defaults: { _controller: "GoodReadingsAuthorsBundle:Author:show" } requirements: { _method: get }

These two routes now show all authors and a single author identified by id respectively. But we want the data in json format, don’t we? All you have to do here is encode the response in json in the controller (don’t forget to use Symfony\Component\HttpFoundation\Response;):

/** * Lists all Author entities. */ public function indexAction() { $em = $this->getDoctrine()->getManager(); $entities = $em->getRepository('GoodReadingsAuthorsBundle:Author')->findAll(); foreach($entities as $ent) { $books = $ent->getBooks(); foreach($books as $book) { $booklist[] = array( "id" => $book->getId(), "title" => $book->getTitle(), "genre" => $book->getGenre(), "published" => $book->getPublished()); } $authors[] = array ( "id" => $ent->getId(), "name" => $ent->getName(), "birthdate" => $ent->getBirthdate(), "hometown" => $ent->getHometown(), "books" => $booklist); } return new Response(json_encode(array('authors' => $authors))); } /** * Finds and display an Author entity. */ public function showAction($id) { $em = $this->getDoctrine()->getManager(); $entity = $em->getRepository('GoodReadingsAuthorsBundle:Author')->find($id); if (!$entity) { throw $this->createNotFoundException('Unable to find Author entity.'); } $author = array ( "id" => $entity->getId(), "name" => $entity->getName(), "birthdate" => $entity->getBirthdate(), "hometown" => $entity->getHometown() ); return new Response(json_encode($author)); }

The reason we have to populate an array manually using the Entity’s getter methods before passing it to json_encode rather than just calling json_encode on the object or the array of objects is because the Entity’s properties are all private. To get around this you can use Serialization – and that is outside the scope of this tutorial.

The routing for the remaining methods (create, edit and delete) are as follows:

author_create: pattern: / defaults: { _controller: "GoodReadingsAuthorsBundle:Author:create" } requirements: { _method: post } author_edit: pattern: /{id} defaults: { _controller: "GoodReadingsAuthorsBundle:Author:edit" } requirements: { _method: put } author_delete: pattern: /{id} defaults: { _controller: "GoodReadingsAuthorsBundle:Author:delete" } requirements: { _method: delete }

To create, you must supply the create action with a valid json object representing the author, for example:

{ "author": { "name":"Maya Angelou", "birthdate":"1928-04-01", "hometown":"St. Louis, Missouri" } }

You will need to grab this object from the incoming Request parameter in the create action:

public function createAction(Request $request) { $em = $this->getDoctrine()->getManager(); $data = $this->getRequest()->getContent(); $author = json_decode($data, true); $newAuthor = new Author(); $newAuthor->setName($author["author"]["name"]); $newAuthor->setBirthdate($author["author"]["birthdate"]); $newAuthor->setHometown($author["author"]["hometown"]); $em->persist($newAuthor); $em->flush(); $author_array = array( "id" => $newAuthor->getId(), "name" => $newAuthor->getName(), "birthdate" => $newAuthor->getBirthdate(), "hometown" => $newAuthor->getHometown(), ); return new Response(json_encode($author_array)); }

To test that it works you will need to use HTTPIE, CURL or a Rest console.

Delete is incredibly easy (don’t forget to use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;):

/** * Deletes an Author entity. * @ParamConverter("author", class="GoodReadingsAuthorsBundle:Author") */ public function deleteAction(Author $author) { $em = $this->getDoctrine()->getManager(); $em->remove($author); $em->flush(); return new Response("Deleted."); }

And finally, edit is a bastardized version of create, which I think given the examples above, you can figure out for yourself! Hint: use both Author and Request as parameters, find the Author you need to update, update the properties of the author based on the json data from the request and then persist it to the database. Finally, return the Author object in the Response.

Once you do all that, you will have a RESTful web service that was made with the Symfony2 framework. Too easy!

Category: Tutorials

Thoughts onImplement a RESTful web service using Symfony2

Leave a Reply

Your email address will not be published. Required fields are marked *