Symfpony-Project.org

About

This page is part of our Symfony Live 2011 talk about REST web services with symfony and Symfony2. There are major differences between the two frameworks, which can both be listed as RESTful, even if their REST implementations are very different.

The aim of this page is to compare the frameworks best practices and out-of-the-box solutions for creating a REST web service over HTTP. Learn how symfony 1 and Symfony2 can help you with the routing, the validation, the output formats and the cache...

Slides

Here are some useful resources:

 

Routing

The routing is the component in charge of finding the right controller to execute in response to an incoming request.

symfony

The routing of symfony is rather powerful : you can use different matching and generating classes (like sfDoctrineRoute), generate route collections...

        pony_list:
          url: /pony.:sf_format
          param: { module: pony, action: index, sf_format: xml }
          class: sfRequestRoute
          requirements: { sf_method: GET, sf_format: (xml|json|yml) }

        pony_show:
          url: /pony/:slug.:sf_format
          param: { module: pony, action: show, sf_format: xml }
          class: sfDoctrineRoute
          options: { model: Pony, column: slug, type: object }
          requirements: { sf_method: GET, sf_format: (xml|json|yml) }
      

Symfony2

The Router component is quite simpler than the symfony one's, it's all about matching pattern and generating URL ONLY.

Here is the configuration file, nothing much to say about it, but look at the _controller parameter: no more action / module - you just declare a callable controller. It can be anything, even a static method from a custom class.

        pony_list:
            pattern:      /pony.{_format}
            defaults:     { _controller: CleverAgeSymfponyBundle:Default:index, _format: xml }
            requirements: { _format: (xml|json), _method: GET }

        pony_show:
            pattern:      /pony/{slug}.{_format}
            defaults:     { _controller: CleverAgeSymfponyBundle:Default:show, _format: xml }
            requirements: { _format: (xml|json), _method: GET, slug: "[a-z0-9-]+" }
      

Take a look at ParamConverter if you want a sfDoctrineRoute-like input.

Validation

The Validation process has been entirely rebuilt from symfony 1 to Symfony2.

symfony

There are two validators declarations: one in sfForm, the other in Doctrine. They can't work together, so we usually use the sfValidators from sfForm.

        $pony_form = new PonyForm();

        $pony_form->bind($values);

        if ($pony_form->isValid())
        {
          // You can save the Pony here
        }
      

Symfony2

Using the annotations, you can define the fields validators directly in Doctrine entities or in any other class. You can also declare validators in separate YAML, XML or PHP files. Write once, use everywhere!

        class Pony
        {
            /**
             * @var integer $id
             *
             * @orm:Column(name="id", type="integer")
             * @orm:Id
             * @orm:GeneratedValue(strategy="IDENTITY")
             */
            private $id;

            /**
             * @var string $name
             *
             * @gedmo:Sluggable
             * @orm:Column(name="name", type="string", length=110)
             * @validation:NotBlank()
             * @validation:MinLength(3)
             */
            private $name;
      

And to validate an object:

        $validator = $this->get('validator');

        if (0 === count($validator->validate($pony)))
        {
          // Pony is valid \o/
        }
      

There is a lot more to say about validation, so don't hesitate to take a look at the whole SymfponyBundle and the official documentation.

Output format

There is no true serializer concept in symfony, but Symfony2 introduces a Serializer component. Here is how to encode XML Ponies with both versions of the framework.

symfony

As there is no Serializer component, we must use the Doctrine Dumper. It works fine but we will not be able to customise its output.

        $query = // New DoctrineQuery fetching some ponies!
        $query->execute()->exportTo('xml');
      

It is rather quick and dirty, there is no CDATA, no namespace... But it is the only solution out of the box. For decoding a payload, you may use Doctrine_Parser::load and fromArray().

Symfony2

Introducing the new Serializer component, composed of Normalizers and Encoders:

        use Symfony\Component\Serializer;

        // Init the Serializer
        $serializer = new Serializer\Serializer();
        $serializer->addNormalizer(new Serializer\Normalizer\CustomNormalizer());
        $serializer->setEncoder('xml', new Serializer\Encoder\XmlEncoder());

        $xml_pony = $serializer->encode($pony, 'xml'); // Return an XML string

        $pony_from_xml = $serializer->denormalizeObject( // Return a Pony instance
          $serializer->decode($xml_pony, 'xml'), // Return an array from the XML
          'CleverAge\SymfponyBundle\Entity\Pony',
          'xml'
        );
      

Caching your Resources

Caching is really important. You must set it up, even for a short time. Your server charge will definitely thank you.

symfony

The full page cache is set in each application cache.yml and activated in settings.yml. Resources will be stored on the server hard drive, but you can specify a custom store, like Memcached for instance.

        # settings.yml
        prod:
          .settings:
            no_script_name:         true
            cache:                  false

        # cache.yml
        default:
          enabled:     true
          with_layout: true
          lifetime:    120
      

Off course, only the safe method GET gets stored into the cache. The invalidation is kind of easy too :

        if ($cache = $this->getContext()->getViewCacheManager())
        {
          $cache->remove('pony/index');
          $cache->remove('pony/show?slug='.$slug);
        }
      

Symfony2

The Symfony2 cache is... HTTP. In fact, there is no full page cache inside Symfony2. Check out the documentation or the code below - the secret is to use a reverse-proxy, and to correctly set the HTTP expiration and validation headers. No configuration needed!

        $response->setPublic();
        $response->setSharedMaxAge(120);
        return $response;
      

The cache proxy will store this resource for 120 seconds. The downside is that there is no invalidation method. Therefore, it is possible to associate the expiration to a validation method, which can reduce the expiration time.

        $response->setPublic();
        $response->setSharedMaxAge(60);
        $response->setETag(\md5(\serialize($pony))); // Should be a method in Pony

        if ($response->isNotModified($request))
        {
            // return the 304 Response immediately
            return $response;
        }
        else
        {
            // do some stuff and return a full Response
            $response->setContent(
                $this->getSerializer($_format)->encode($pony, $_format)
            );
            return $response;
        }