REST webservice with symfony

REST webservice with symfony

July 2, 2011 by Pietrino Atzeni

There are a lot of tutorials, links, and publications about REST webservices. And there is something already well documented about the symfony framework and how it’s used to produce webservices with a REST protocol. There are even some plugins that implement REST with symfony. (See here, here and here.)

What we’ll see here is a practical way to write a symfony REST webservice, extending the Jobeet application originally developed for the symfony framework (you can find here the latest code for symfony 1.2, and here for symfony 1.4).

If you don’t know what REST is, check out how Ryan Tomayko explained REST to his wife.

Webservice scenario

First of all, let’s suppose we developed a dynamic website (http://www.jobeet.org) built on top of a PHP framework (symfony) and that uses an ORM engine (Doctrine). The site is up and running, and our client is very happy with her business.

After being successful for a while, she decides that a brand new mobile app is required to make the website shine on iPhones. Without losing any feature on the main website, and to minimize costs and time to develop, the most obvious solution is to develop a webservice build on top of the symfony app, to avoid code duplication (remember the DRY principle?).

So, here we still have a single web server, that now produces HTML, stylesheets and javascript code, and XML or JSON resources following REST standards.

We (ehm, Sensio) already developed the original app, so we will not split the controller in two, but add some new views, and generate a new module.

Routing and webservice module structure

To define a common structure for all the available resources, let’s define some routes:

# apps/frontend/config/routing.yml

ws_list
:
  url
: /rest/:model.:sf_format
  class
: sfRequestRoute
  param
: { module: webservice, action: list, sf_format: xml }
  requirements
:
    sf_method
: [GET]

ws_create
:
  url
: /rest/:model.:sf_format
  class
: sfRequestRoute
  param
: { module: webservice, action: create, sf_format: xml }
  requirements
:
    sf_method
: [POST]

ws_get
:
  url
: /rest/:model/:id.:sf_format
  class
: sfRequestRoute
  param
: { module: webservice, action: get, sf_format: xml, column: id }
  requirements
:
    id
: \d+
    sf_method
: [GET]

ws_update
:
  url
: /rest/:model/:id.:sf_format
  class
: sfRequestRoute
  param
: { module: webservice, action: update, sf_format: xml, column: id }
  requirements
:
    id
: \d+
    sf_method
: [PUT]

ws_delete
:
  url
: /rest/:model/:id.:sf_format
  class
: sfRequestRoute
  param
: { module: webservice, action: delete, sf_format: xml, column: id }
  requirements
:
    id
: \d+
    sf_method
: [DELETE]

The first rule will get the list of jobs (GET), and to create a new one (POST). The second one will retrieve a single job (GET), will update it (PUT) and will delete it (DELETE).

The module webservice contains methods to perform every actions for the model: if no model is defined, or if the model does not exist, an error will be returned. The list and find methods will forward to the corresponding actions if the methods are set correctly.

// apps/frontend/modules/webservice/actions/actions.class.php

class webserviceActions extends sfActions
{

  public function preExecute()
  {
    $this->model = $this->getRequest()->getParameter('model');

    if(!class_exists($this->model))
      $this->forward('webservice', 'error');
  }

  public function executeList(sfWebRequest $request)
  {
    $this->objects = Doctrine_Core::getTable($this->model)->findAll();
  }

  public function executeCreate(sfWebRequest $request)
  {
  }

  public function executeGet(sfWebRequest $request)
  {
  }

  public function executeUpdate(sfWebRequest $request)
  {
  }

  public function executeDelete(sfWebRequest $request)
  {
  }

  public function executeError(sfWebRequest $request)
  {
    $model = $request->getParameter('model');

    $this->getResponse()->setStatusCode(500);

    $this->error = ($model != null) ? "Invalid model: " . $model : "Undefined model";
  }

}

Listing all jobs

Following REST standards, we should be able to get all jobs with

curl http://jobeet.localhost/rest/jobeetjob.xml

and the result should be

<?xml version="1.0" encoding="utf-8"?>
<objects>
  <object id="1">
    <id>1</id>
    <category_id>1</category_id>
    <type>full-time</type>
    <company>Sensio Labs</company>
    <logo>sensio-labs.gif</logo>
    <url>http://www.sensiolabs.com/</url>
    <position>Web Developer</position>
    <location>Paris, France</location>
    <description>You&#039;ve already developed websites with symfony and you want to work
with Open-Source technologies. You have a minimum of 3 years
experience in web development with PHP or Java and you wish to
participate to development of Web 2.0 sites using the best
frameworks available.
</description>
    <how_to_apply>Send your resume to fabien.potencier [at] sensio.com
</how_to_apply>
    <token>job_sensio_labs</token>
    <is_public>1</is_public>
    <is_activated>1</is_activated>
    <email>job@example.com</email>
    <expires_at>2010-10-10 00:00:00</expires_at>
    <created_at>2011-06-30 17:12:48</created_at>
    <updated_at>2011-06-30 17:12:48</updated_at>
  </object>
  <object id="2">
    ...
</objects>

The webservice list action retrieves all the jobs:

// apps/frontend/modules/webservice/actions/actions.class.php

public function executeList(sfWebRequest $request)
{
  if($request->isMethod(sfRequest::POST))
    $this->forward('webservice', 'create');

  $this->objects = Doctrine_Core::getTable($this->model)->findAll();
}

The xml view cycles through all the objects, and renders all the fields:

// apps/frontend/modules/webservice/templates/listSuccess.xml.php

<?xml version="1.0" encoding="utf-8"?>
<objects>
<?php foreach ($objects as $object) : ?>
  <object id="<?php echo $object['id'] ?>">
<?php foreach ($object as $key => $value): ?>
    <<?php echo $key ?>><?php echo $value ?></<?php echo $key ?>>
<?php endforeach ?>
  </object>
<?php endforeach ?>
</objects>

Getting one job only

To retrieve only one job, identified by its id, we call

curl http://jobeet.localhost/rest/jobeetjob/1.xml

and the result should be

<?xml version="1.0" encoding="utf-8"?>
<object id="1">
  <id>1</id>
  <category_id>1</category_id>
  <type>full-time</type>
  <company>Sensio Labs</company>
  <logo>sensio-labs.gif</logo>
  <url>http://www.sensiolabs.com/</url>
  <position>Web Developer</position>
  <location>Paris, France</location>
  <description>You&#039;ve already developed websites with symfony and you want to work
with Open-Source technologies. You have a minimum of 3 years
experience in web development with PHP or Java and you wish to
participate to development of Web 2.0 sites using the best
frameworks available.
</description>
  <how_to_apply>Send your resume to fabien.potencier [at] sensio.com
</how_to_apply>
  <token>job_sensio_labs</token>
  <is_public>1</is_public>
  <is_activated>1</is_activated>
  <email>job@example.com</email>
  <expires_at>2010-10-10 00:00:00</expires_at>
  <created_at>2011-06-30 17:12:48</created_at>
  <updated_at>2011-06-30 17:12:48</updated_at>
</object>

The webservice get action retrieves one job, given its ID:

// apps/frontend/modules/webservice/actions/actions.class.php

public function executeGet(sfWebRequest $request)
{
  $id = $request->getParameter('id');

  $this->object = Doctrine_Core::getTable($this->model)->findOneById($id);

  $this->setTemplate('resource');
}

Again, the xml view cycles through the object fields:

// apps/frontend/modules/webservice/templates/findSuccess.xml.php

<?xml version="1.0" encoding="utf-8"?>
<object id="<?php echo $object['id'] ?>">
<?php foreach ($object as $key => $value): ?>
  <<?php echo $key ?>><?php echo $value ?></<?php echo $key ?>>
<?php endforeach ?>
</object>

In the next tutorial, we’ll see how to create and update jobs. See there then…

Links

REST on Wikipedia
Web Services and Symfony
The origina Jobeet website
How Ryan Tomayko explained REST to his wife

This entry was posted in Consulting, Web Solutions and tagged , , , . Bookmark the permalink.

6 Comments

  1. July 4, 2011 by jamen

    Hi thanks for your post , it was quite easy to follow and well written. I would like to ask if your example could actually be “repurposed” easily for another simple database that has been just loaded into mysql to set it up as a web service? Would there be any missing steps in between as compared to the well developed Jobeet page ? Thanks !

    • July 4, 2011 by Pietrino Atzeni

      Hi Jamen, actually defining a simple database and applying what I’m writing here is even easier. I didn’t put a new database since it would take a post by itself, but you just need to define a model here, the relative routing and then the actions that render the correct format. In the next tutorial, I’ll show the create, put and delete methods, so stay tuned! B))

      • July 5, 2011 by jamen

        Hi Pietrino ,

        Thanks for preparing for your upcoming tutorials as well :) Your tutorial really came at a very important time to me. Thanks ! I’ve tried to recreate the Jobeet Tutorial and up till day 3 where all basic data has been loaded up into mySQL. I’ve followed through your steps and the browser returned a “Page not found, Server returned a 404 response ” ? I’ve followed exactly to the dot here , what might be the problem ? Thanks Pietrino !

        • July 5, 2011 by Pietrino Atzeni

          Hi Jamen,
          as I specified in the tutorial, the best is to start from the complete Jobeet tutorial (day 22). Your 404 could be related to the routing, so clear your cache, and check that the routes exist (symfony app:routes frontend). Also, the model in day 3 is Job, not JobeetJob (that’s because Jobeet is defined as a prefix in day 20).

          • July 5, 2011 by jamen

            Hi Pietrino ,
            Yup sure , I’ve tried it too , still trying to figure out how to work. After clearing cache still doesn’t work.

            Actually its stated in page 40 of the tutorial (chapt 3) that Job is the module created for the JobeetJob model … So which one is true ?

          • July 7, 2011 by Pietrino Atzeni

            Hi Jamen,
            in the tutorial shown, the model we are referring to is JobeetJob, and the corresponding routing uses the model. If you want to test it, use something like “curl http://jobeet.localhost/frontend_dev.php/rest/jobeetjob.xml“. You should see the symfony debug log. In the part 3, I’ll show how to test the webservice, so maybe that will help too.

Leave a Reply

eight + = 12