Johannes Wachter
Johannes Wachter
Core Developer – Sulu GmbH
Sulu Core Developer and Open-Source Enthusiast.

How to develop a bundle in the Sulu-Admin – #3: REST-API

This part of the tutorial is based on the results of the previous blog-post.

One of Sulu’s core concepts is the separation of data and presentation in the Sulu-Admin. To achieve this the Admin is developed as a single-page application with JavaScript - we talked about the basics in the last part - and a standardized RESTful-API. This enables the developer to link external data-sources into the system or export existing data.

In this part we are going to integrate a “News” endpoint in the API of Sulu. We will develop the following parts:

  • RestController - delivers data in a JSON-serialized format.
  • NewsManager - encapsulates the CRUD operations to interact with “News” items.
  • NewsEntity - a simple Doctrine entity which will be stored in the database.

For the rest of this tutorial we will use this Doctrine mapping file. It contains an id, the content and the title. This example is very basic but it can be extended for your needs.

<!-- file: Resources/config/doctrine/NewsItem.orm.xml -->

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    <entity name="Example\NewsBundle\Entity\NewsItem">
        <id name="id" type="integer" column="id">
            <generator strategy="AUTO"/>
        </id>
        <field name="title" type="string" column="title" length="255"/>
        <field name="content" type="text" column="content"/>
    </entity>
</doctrine-mapping>

To generate the PHP class put an empty "NewsItem" class into "Entity/NewsItem.php" file and use the following commands to generate the getters/setters and updating the database schema:

app/console doctrine:generate:entities ExampleNewsBundle && app/console doctrine:schema:update --force

After this step we can define our “NewsManager” class. This class uses the “EntityManager” of Doctrine to find or persist “NewsItem” entities. You can see the complete code of this manager  in the related PR of this tutorial here. So I’m only explaining the important parts of the manager class.

public function create(array $data)
{
    $entity = $this->bind(new NewsItem(), $data);
    $this->entityManager->persist($entity);

    return $entity;
}

public function update($id, array $data)
{
    $entity = $this->read($id);

    return $this->bind($entity, $data);
}

protected function bind(NewsItem $entity, array $data)
{
    $entity->setTitle($data['title']);
    $entity->setContent($this->getValue($data, 'content'));

    return $entity;
}

Both methods "create" and "update" use the "bind" to map the data between the data-array and the entity. This enables developers to extend it easily with additional features and data-fields.

Experienced users of Symfony and Doctrine will have noticed that the manager only calls "persist" but not "flush" which would save it to the database. This is a one simple aspect of the Hexagonal Architecture which is based on the "single responsibility” principle. The manager class is responsible for News-Item CRUD operations and the controller is responsible for the REST-API. This architecture will enable you to simply create fast batch-processing commands like import which only call the heavy "flush" operation when you want it.

To fully implement the Hexagonal Architecture we would also have to encapsulate the "createNewInstance" - for "create" method -, the "persist" and all the "find" methods into an interface which will be injected by the container. This could then be implemented for Symfony and Doctrine or other frameworks. See a talk about that here. For this simple example we will keep the Doctrine dependency.

This service definition enables you to publish the manager in the container and use it later in the controller class:

<!-- file: Resources/config/services.xml -->

<service id="example_news.manager" class="Example\NewsBundle\News\NewsManager">
    <argument type="service" id="doctrine.orm.entity_manager"/>
</service>

For the REST-API we use the well documented FOSRestBundle which automatically creates RESTful routes defined by the name of the action. The complete code for the controller can also be found in the related PR here.

The important parts of the controller are the return statements.

return $this->handleView($this->view($newsItem));

The FOSRestBundle will serialize the given entity with the JMS-serializer and returns it as a JSON-response.

To register this rest-routes we have to import the controller in the routing config. The following file has to be imported in the “app/config/admin/routing.yml” with the type "rest" which uses the FOSRestBundle route-loader to generate RESTful routes.

<!-- file: Resources/config/routing_api.xml -->

<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://friendsofsymfony.github.com/schema/rest"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://friendsofsymfony.github.com/schema/rest https://raw.github.com/FriendsOfSymfony/FOSRestBundle/master/Resources/config/schema/routing/rest_routing-1.0.xsd">
    <import id="news" resource="Example\NewsBundle\Controller\NewsController" type="rest"/>
</routes>
# file: app/config/admin/routing.yml

example_news:
    resource: "@ExampleNewsBundle/Resources/config/routing_api.xml"
    type: rest
    prefix: /admin/api

To check the registration of routes use following command:

app/console debug:router | grep news
 get_news                                     GET      ANY    ANY      /admin/api/news/{id}.{_format}
 get_news_list                                GET      ANY    ANY      /admin/api/news.{_format}
 post_news                                    POST     ANY    ANY      /admin/api/news.{_format}
 put_news                                     PUT      ANY    ANY      /admin/api/news/{id}.{_format}

After this we have a small working API. Don’t forget that we deliberately ignore validation and exceptions for the sake of a manageable tutorial.

That's it for the third part of the series. Next time we will introduce the form to create and update “news” items.

The code for this part of the tutorial can be found here https://github.com/sulu-io/ExampleNewsBundle/pull/3.