Symfony – Create a post Form with a Rest server

Notes

We will assume that ApiBundle have been generated with :

# php app/console generate:bundle --namespace=Acme/ApiBundle --format=yml

Composer intall

composer require "friendsofsymfony/rest-bundle" --no-update
composer require "jms/serializer-bundle" --no-update
composer require "nelmio/api-doc-bundle" --no-update
composer update

Configuration

Edit AppKernel

Add in app/AppKernel.php

<?php
// app/AppKernel.php

public function registerbundles()
{
    return array(
        // ...
        new FOS\RestBundle\FOSRestBundle(),
        new JMS\SerializerBundle\JMSSerializerBundle(),
        new Nelmio\ApiDocBundle\NelmioApiDocBundle(),
    );
}

Edit Routing

Add in app/config/routing.yml

// app/config/routing.yml
acme_api:
    type: rest
    resource: "@AcmeApiBundle/Resources/config/routing.yml"
    prefix: /api
   
NelmioApiDocBundle:
    resource: "@NelmioApiDocBundle/Resources/config/routing.yml"
    prefix: /api/doc/rest

Add in src/ApiBundle/Resources/config/routing.yml

acme_api_soap:
    path: /soap
    defaults: { _controller: AcmeApiBundle:Soap:index } 
    
acme_api_page:
    type: rest
    name_prefix: api_1_ # naming collision
    prefix: /v1
    resource: 'Acme\ApiBundle\Controller\PageController'

Edit Config

Add in app/config/config.yml

// app/config/config.yml
fos_rest:
    param_fetcher_listener: true
    view:
        view_response_listener: 'force'
        formats:
            xml: true
            json: true
        templating_formats:
            html: true
    format_listener:
        rules:
            - { path: ^/, priorities: [ html, json, xml ], fallback_format: ~, prefer_extension: false }
    exception:
        codes:
            'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
            'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT
        messages:
            'Symfony\Component\Routing\Exception\ResourceNotFoundException': true
    allowed_methods_listener: true
    access_denied_listener:
        json: true
    body_listener: true
    disable_csrf_role: ROLE_API

Create controller

Create Acme/ApiBundle/Controller/PageController.php

// src/ApiBundle/Controller
    
namespace Acme\ApiBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;
    
use FOS\RestBundle\Util\Codes;
    
use FOS\RestBundle\Controller\Annotations;
    
use FOS\RestBundle\View\View;
use FOS\RestBundle\Request\ParamFetcherInterface;
    
use Acme\ApiBundle\Form\PageType;
use Acme\ApiBundle\Model\PageInterface;
use Acme\ApiBundle\Exception\InvalidFormException;
    
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Request;
    
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
    
class PageController extends FOSRestController
{
    /**
     * Get single Page.
     *
     * @ApiDoc(
     *     resource = true,
     *     description = "Gets a Page for a given id",
     *     output = "Acme\ApiBundle\Entity\Page",
     *     statusCodes = {
     *         200 = "Returned when successful",
     *         404 = "Returned when the page is not found"
     *     }
     * )
     *
     * @Annotations\View(templateVar="page") 
     * @param int $id the page id
     * @return array
     * @throws NotFoundHttpException when page not exist
     */
    public function getPageAction($id)
    {
        $page = $this->getOr404($id);
        return $page;
    }
    
    protected function getOr404($id)
    {
        if (!($page = $this->container->get('acme_api.page.handler')->get($id))) {
            throw new NotFoundHttpException(sprintf('The resource \'%s\' was not found.',$id));
        }
        return $page;
    }
    
    /**
     * List all pages.
     *
     * @ApiDoc(
     * resource = true,
     * statusCodes = {
     * 200 = "Returned when successful"
     * }
     * )
     *
     * @Annotations\QueryParam(name="offset", requirements="\d+", nullable=true, description="Offset from which to start listing pages.")
     * @Annotations\QueryParam(name="limit", requirements="\d+", default="5", description="How many pages to return.")
     *
     * @Annotations\View(templateVar="pages")
     *
     * @param Request $request the request object
     * @param ParamFetcherInterface $paramFetcher param fetcher service
     *
     * @return array
     */
    public function getPagesAction(Request $request, ParamFetcherInterface $paramFetcher)
    {
        return $this->container->get('acme_api.page.handler')->all();
    }
    
    /**
     * Presents the form to use to create a new page.
     *
     * @ApiDoc(
     * resource = true,
     * statusCodes = {
     * 200 = "Returned when successful"
     * }
     * )
     *
     * @Annotations\View(
     * templateVar = "form"
     * )
     *
     * @return FormTypeInterface
     */
    public function newPageAction()
    {
        return $this->createForm(new PageType());
    }
    
    /**
     * Create a Page from the submitted data.
     *
     * @ApiDoc(
     * resource = true,
     * description = "Creates a new page from the submitted data.",
     * input = "Acme\ApiBundle\Form\PageType",
     * statusCodes = {
     * 200 = "Returned when successful",
     * 400 = "Returned when the form has errors"
     * }
     * )
     *
     * @Annotations\View(
     * template = "AcmeApiBundle:Page:newPage.html.twig",
     * statusCode = Codes::HTTP_BAD_REQUEST,
     * templateVar = "form"
     * )
     *
     * @param Request $request the request object
     *
     * @return FormTypeInterface|View
     */
    public function postPageAction(Request $request)
    {
        try {
            // Hey Page handler create a new Page.
            $form = new PageType();
            $newPage = $this->container->get('acme_api.page.handler')->post(
                $request->request->get($form->getName())
            );
            $routeOptions = array(
                'id' => $newPage->getId(),
                '_format' => $request->get('_format')
            );
            return $this->routeRedirectView('api_1_get_page', $routeOptions, Codes::HTTP_CREATED);
        } catch (InvalidFormException $exception) {
            return $exception->getForm();
        }
    }
}

Create exeception

Create src/ApiBundle/Exception/InvalidFormException.php file

// src/ApiBundle/Exception/InvalidFormException.php
    
namespace Acme\ApiBundle\Exception;
    
class InvalidFormException extends \RuntimeException
{
    protected $form;
    public function __construct($message, $form = null)
    {
        parent::__construct($message);
        $this->form = $form;
    }
    /**
     * @return array|null
     */
    public function getForm()
    {
        return $this->form;
    }
}

Create Rest Form

// src/ApiBundle/Form/PageType.php
namespace Acme\ApiBundle\Form;
    
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
    
class PageType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('body')
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\ApiBundle\Entity\Page'
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'page';
    }
}

Create Handler

// src/ApiBundle/Handler/PageHandler.php
    
namespace Acme\ApiBundle\Handler;
    
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\FormFactoryInterface;
use Acme\APIBundle\Model\PageInterface;
use Acme\APIBundle\Form\PageType;
use Acme\APIBundle\Exception\InvalidFormException;

class PageHandler implements PageHandlerInterface
{
    private $om;
    private $entityClass;
    private $repository;
    private $formFactory;

    public function __construct(ObjectManager $om, $entityClass, FormFactoryInterface $formFactory)
    {
        $this->om = $om;
        $this->entityClass = $entityClass;
        $this->repository = $this->om->getRepository($this->entityClass);
        $this->formFactory = $formFactory;
    }
    
    public function get($id)
    {
        return $this->repository->find($id);
    }
    
    /**
     * Get a list of Pages.
     *
     * @return array
     */
    public function all()
    {
        return $this->repository->findBy(array(), null, null, null);
    }
    
    /**
     * Processes the form.
     *
     * @param PageInterface $page
     * @param array $parameters
     * @param String $method
     *
     * @return PageInterface
     *
     * @throws \Acme\ApiBundle\Exception\InvalidFormException
     */
    private function processForm(PageInterface $page, array $parameters, $method = "PUT")
    {
        $form = $this->formFactory->create(new PageType(), $page, array('method' => $method));
        $form->submit($parameters, 'PATCH' !== $method);
        $page = $form->getData();
        $this->om->persist($page);
        $this->om->flush($page);
        return $page;
    }
    
    private function createPage()
    {
        return new $this->entityClass();
    }
    
    /**
     * Create a new Page.
     *
     * @param array $parameters
     *
     * @return PageInterface
     */
    public function post(array $parameters)
    {
        $page = $this->createPage(); // factory method create an empty Page
    
        // Process form does all the magic, validate and hydrate the Page Object.
        return $this->processForm($page, $parameters, 'POST');
    }
}

Create Handler interface

// src/ApiBundle/Handler/PageHandlerInterface.php
    
namespace Acme\ApiBundle\Handler;
    
use Acme\ApiBundle\Model\PageInterface;
interface PageHandlerInterface
{
    /**
     * Get a Page given the identifier
     *
     * @api
     *
     * @param mixed $id
     *
     * @return PageInterface
     */
    public function get($id);
    /**
     * Get a list of Pages.
     *
     * @param int $limit the limit of the result
     * @param int $offset starting from the offset
     *
     * @return array
     */
    public function all();
    /**
     * Post Page, creates a new Page.
     *
     * @api
     *
     * @param array $parameters
     *
     * @return PageInterface
     */
    public function post(array $parameters);
}

Create page interface

// src/ApiBundle/Model/PageInterface.php
    
namespace Acme\ApiBundle\Model;
    
Interface PageInterface
{
    /**
     * Set title
     *
     * @param string $title
     * @return PageInterface
     */
    public function setTitle($title);
    /**
     * Get title
     *
     * @return string
     */
    public function getTitle();
    /**
     * Set body
     *
     * @param string $body
     * @return PageInterface
     */
    public function setBody($body);
    /**
     * Get body
     *
     * @return string
     */
    public function getBody();
}

Edit config

Add at the beginning of app/config/config.yml

imports:
    - { resource: @AcmeApiBundle/Resources/config/services.xml }

Edit service

// src/ApiBundle/Resources/config/services.xml
<?xml version="1.0" ?>
    
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <parameters>
        <parameter key="acme_api.page.handler.class">Acme\ApiBundle\Handler\PageHandler</parameter>
        <parameter key="acme_api.page.class">Acme\ApiBundle\Entity\Page</parameter>
    </parameters>
    <services>
        <service id="acme_api.page.handler" class="%acme_api.page.handler.class%">
            <argument type="service" id="doctrine.orm.entity_manager" />
            <argument>%acme_api.page.class%</argument>
            <argument type="service" id="form.factory"></argument>
        </service>
    </services>
</container>

Create Twig pages

Create getPage.html.twig

// src/ApiBundle/Resources/views/Page/getPage.html.twig

<h1>{{ page.id }} - {{ page.title }}</h1>
<p>
{{ page.body }}
</p>

Create getPages.html.twig

// src/ApiBundle/Resources/views/Page/getPages.html.twig
{% block content %}
    <h1 class="title">{{ 'page.list'|trans({}, 'AcmeApiBundle') }}</h1>
    
    <ul id="page-list">
    {% for page in pages %}
        <li>
            <a href="{{ path('api_1_get_page', {'id': page.id}) }}">{{ page.title }}</a>
        </li>
        {% else %}
            <li>{{ 'page.list.empty'|trans({}, 'AcmeApiBundle') }}</li>
    {% endfor %}
    </ul>
    <p>
        <a href="{{ path('api_1_new_page') }}">{{ 'page.new.link'|trans({}, 'AcmeApiBundle') }}</a>
    </p>
{% endblock %}

Create newPage.html.twig

// src/ApiBundle/Resources/views/Page/newPage.html.twig
{% block content %}
    <h1>Create Page Form</h1>
    {% if (form is not null) %}
        <form action="{{ url('api_1_post_page') }}" method="POST" {{ form_enctype(form) }}>
        {{ form_widget(form) }}
        <input type="submit" value="submit">
        </form>
    {% endif %}
{% endblock %}

Update database

Execute:

php app/console doctrine:generate:entity --entity=AcmeApiBundle:Page --format=annotation --fields="title:string(255) body:text" --no-interaction
php app/console doctrine:schema:update --force

This will create the entity Page.php, update this file with:

// src //ApiBundle/Entity/Page.php

use Acme\ApiBundle\Model\PageInterface;

class Page implements PageInterface
{
    ...

Test the service

Now you can test the service with:

curl -i -H "Accept: application/json"  localhost:8000/api/v1/pages/10

or directly from your browser :

http://localhost:8000/api/v1/pages

And you can submit a form:

curl -X POST -d ‘{“page”:{“title”:”title1″,”body”:”body1″}}’ http://localhost:8000/api/v1/pages.json –header “Content-Type:application/json”

Raphaël has written 45 articles

One thought on “Symfony – Create a post Form with a Rest server

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>