Search code examples
phpsymfonydryfosrestbundle

Symfony 2 - keeping the code DRY - FOSRestBundle


I'm currently using Symfony2 to create (and learn how to) a REST API. I'm using FOSRestBundle and i've created an "ApiControllerBase.php" with the following :

<?php
namespace Utopya\UtopyaBundle\Controller;


use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * Class ApiControllerBase
 *
 * @package Utopya\UtopyaBundle\Controller
 */
abstract class ApiControllerBase extends FOSRestController
{
    /**
     * @param string $entityName
     * @param string $entityClass
     *
     * @return array
     * @throws NotFoundHttpException
     */
    protected function getObjects($entityName, $entityClass)
    {
        $dataRepository = $this->container->get("doctrine")->getRepository($entityClass);

        $entityName = $entityName."s";
        $data = $dataRepository->findAll();
        foreach ($data as $object) {
            if (!$object instanceof $entityClass) {
                throw new NotFoundHttpException("$entityName not found");
            }
        }

        return array($entityName => $data);
    }

    /**
     * @param string  $entityName
     * @param string  $entityClass
     * @param integer $id
     *
     * @return array
     * @throws NotFoundHttpException
     */
    protected function getObject($entityName, $entityClass, $id)
    {
        $dataRepository = $this->container->get("doctrine")->getRepository($entityClass);

        $data = $dataRepository->find($id);
        if (!$data instanceof $entityClass) {
            throw new NotFoundHttpException("$entityName not found");
        }

        return array($entityClass => $data);
    }

    /**
     * @param FormTypeInterface $objectForm
     * @param mixed             $object
     * @param string            $route
     *
     * @return Response
     */
    protected function processForm(FormTypeInterface $objectForm, $object, $route)
    {
        $statusCode = $object->getId() ? 204 : 201;

        $em = $this->getDoctrine()->getManager();

        $form = $this->createForm($objectForm, $object);
        $form->submit($this->container->get('request_stack')->getCurrentRequest());

        if ($form->isValid()) {
            $em->persist($object);
            $em->flush();

            $response = new Response();
            $response->setStatusCode($statusCode);

            // set the `Location` header only when creating new resources
            if (201 === $statusCode) {
                $response->headers->set('Location',
                    $this->generateUrl(
                        $route, array('id' => $object->getId(), '_format' => 'json'),
                        true // absolute
                    )
                );
            }

            return $response;
        }

        return View::create($form, 400);
    }
}

This handles getting one object with a given id, all objects and process a form. But to use this i have to create as many controller as needed. By example : GameController.

<?php

namespace Utopya\UtopyaBundle\Controller;

use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Utopya\UtopyaBundle\Entity\Game;
use Utopya\UtopyaBundle\Form\GameType;

/**
 * Class GameController
 *
 * @package Utopya\UtopyaBundle\Controller
 */
class GameController extends ApiControllerBase
{
    private $entityName = "Game";
    private $entityClass = 'Utopya\UtopyaBundle\Entity\Game';

    /**
     * @Rest\View()
     */
    public function getGamesAction()
    {
        return $this->getObjects($this->entityName, $this->entityClass);
    }

    /**
     * @param int $id
     *
     * @return array
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
     * @Rest\View()
     */
    public function getGameAction($id)
    {
        return $this->getObject($this->entityName, $this->entityClass, $id);
    }

    /**
     * @return mixed
     */
    public function postGameAction()
    {
        return $this->processForm(new GameType(), new Game(), "get_game");
    }
}

This sound not so bad to me but there's a main problem : if i want to create another controller (by example Server or User or Character), i'll have to do the same process and i don't want to since it'll be the same logic.

Another "maybe" problem could be my $entityName and $entityClass.

Any idea or could i make this better ?

Thank-you !

===== Edit 1 =====

I think i made up my mind. For those basics controllers. I would like to be able to "configure" instead of "repeat".

By example i could make a new node in config.yml with the following :

#config.yml
mynode:
    game:
        entity: 'Utopya\UtopyaBundle\Entity\Game'

This is a very basic example but is it possible to make this and transform it into my GameController with 3 methods routes (getGame, getGames, postGame) ?

I just want some leads if i can really achieve with this way or not, if yes with what components ? (Config, Router, etc.)

If no, what could i do? :)

Thanks !


Solution

  • I'd like to show my approach here.

    I've created a base controller for the API, and I stick with the routing that's generated with FOSRest's rest routing type. Hence, the controller looks like this:

    <?php
    
    use FOS\RestBundle\Controller\Annotations\View;
    
    use \Symfony\Component\EventDispatcher\EventDispatcherInterface;
    use Nelmio\ApiDocBundle\Annotation\ApiDoc;
    use Psr\Log\LoggerInterface;
    use Symfony\Component\Form\FormFactoryInterface,
        Symfony\Bridge\Doctrine\RegistryInterface,
        Symfony\Component\Security\Core\SecurityContextInterface,
        Doctrine\Common\Persistence\ObjectRepository;
    
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
    use Symfony\Component\Security\Core\Exception\AccessDeniedException;
    
    abstract class AbstractRestController 
    {
    
        /**
         * @var \Symfony\Component\Form\FormFactoryInterface
         */
        protected $formFactory;
    
        /**
         * @var string
         */
        protected $formType;
    
        /**
         * @var string
         */
        protected $entityClass;
    
        /**
         * @var SecurityContextInterface
         */
        protected $securityContext;
    
        /**
         * @var RegistryInterface
         */
        protected $doctrine;
    
        /**
         * @var EventDispatcherInterface
         */
        protected $dispatcher;
    
        /**
         * @param FormFactoryInterface     $formFactory
         * @param RegistryInterface        $doctrine
         * @param SecurityContextInterface $securityContext
         * @param LoggerInterface          $logger
         */
        public function __construct(
            FormFactoryInterface $formFactory,
            RegistryInterface $doctrine,
            SecurityContextInterface $securityContext,
            EventDispatcherInterface $dispatcher
        )
        {
            $this->formFactory = $formFactory;
            $this->doctrine = $doctrine;
            $this->securityContext = $securityContext;
            $this->dispatcher = $dispatcher;
        }
    
        /**
         * @param string $formType
         */
        public function setFormType($formType)
        {
            $this->formType = $formType;
        }
    
        /**
         * @param string $entityClass
         */
        public function setEntityClass($entityClass)
        {
            $this->entityClass = $entityClass;
        }
    
        /**
         * @param null  $data
         * @param array $options
         *
         * @return \Symfony\Component\Form\FormInterface
         */
        public function createForm($data = null, $options = array())
        {
            return $this->formFactory->create(new $this->formType(), $data, $options);
        }
    
        /**
         * @return RegistryInterface
         */
        public function getDoctrine()
        {
            return $this->doctrine;
        }
    
        /**
         * @return \Doctrine\ORM\EntityRepository
         */
        public function getRepository()
        {
            return $this->doctrine->getRepository($this->entityClass);
        }
    
        /**
         * @param Request $request
         *
         * @View(serializerGroups={"list"}, serializerEnableMaxDepthChecks=true)
         */
        public function cgetAction(Request $request)
        {
            $this->logger->log('DEBUG', 'CGET ' . $this->entityClass);
    
            $offset = null;
            $limit  = null;
    
            if ($range = $request->headers->get('Range')) {
                list($offset, $limit) = explode(',', $range);
            }
    
            return $this->getRepository()->findBy(
                [],
                null,
                $limit,
                $offset
            );
        }
    
        /**
         * @param int $id
         *
         * @return object
         *
         * @View(serializerGroups={"show"}, serializerEnableMaxDepthChecks=true)
         */
        public function getAction($id)
        {
            $this->logger->log('DEBUG', 'GET ' . $this->entityClass);
    
            $object = $this->getRepository()->find($id);
    
            if (!$object) {
                throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
            }
    
            return $object;
        }
    
        /**
         * @param Request $request
         *
         * @return \Symfony\Component\Form\Form|\Symfony\Component\Form\FormInterface
         *
         * @View()
         */
        public function postAction(Request $request)
        {
            $object = new $this->entityClass();
    
            $form = $this->createForm($object);
            $form->submit($request);
    
            if ($form->isValid()) {
                $this->doctrine->getManager()->persist($object);
                $this->doctrine->getManager()->flush($object);
    
                return $object;
            }
    
            return $form;
        }
    
        /**
         * @param Request $request
         * @param int     $id
         *
         * @return \Symfony\Component\Form\FormInterface
         *
         * @View()
         */
        public function putAction(Request $request, $id)
        {
            $object = $this->getRepository()->find($id);
    
            if (!$object) {
                throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
            }
    
            $form = $this->createForm($object);
            $form->submit($request);
    
            if ($form->isValid()) {
                $this->doctrine->getManager()->persist($object);
                $this->doctrine->getManager()->flush($object);
    
                return $object;
            }
    
            return $form;
        }
    
        /**
         * @param Request $request
         * @param         $id
         *
         * @View()
         *
         * @return object|\Symfony\Component\Form\FormInterface
         */
        public function patchAction(Request $request, $id)
        {
            $this->logger->log('DEBUG', 'PATCH ' . $this->entityClass);
    
            $object = $this->getRepository()->find($id);
    
            if (!$object) {
                throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
            }
    
            $form = $this->createForm($object);
            $form->submit($request, false);
    
            if ($form->isValid()) {
                $this->doctrine->getManager()->persist($object);
                $this->doctrine->getManager()->flush($object);
    
                return $object;
            }
    
            return $form;
        }
    
        /**
         * @param int $id
         *
         * @return array
         *
         * @View()
         */
        public function deleteAction($id)
        {
            $this->logger->log('DEBUG', 'DELETE ' . $this->entityClass);
    
            $object = $this->getRepository()->find($id);
    
            if (!$object) {
                throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
            }
    
            $this->doctrine->getManager()->remove($object);
            $this->doctrine->getManager()->flush($object);
    
            return ['success' => true];
        }
    
    } 
    

    It has methods for most of RESTful actions. Next, when I want to implement a CRUD for an entity, that's what I do:

    The abstract controller is registered in the DI as an abstract service:

    my_api.abstract_controller:
      class: AbstractRestController
      abstract: true
      arguments:
        - @form.factory
        - @doctrine
        - @security.context
        - @logger
        - @event_dispatcher
    

    Next, when I'm implementing a CRUD for a new entity, I create an empty class and a service definition for it:

    class SettingController extends AbstractRestController implements ClassResourceInterface {} 
    

    Note that the class implements ClassResourceInterface. This is necessary for the rest routing to work.

    Here's the service declaration for this controller:

    api.settings.controller.class: MyBundle\Controller\SettingController
    api.settings.form.class: MyBundle\Form\SettingType
    
    my_api.settings.controller:
      class: %api.settings.controller.class%
      parent: my_api.abstract_controller
      calls:
        - [  setEntityClass, [ MyBundle\Entity\Setting ] ]
        - [  setFormType,    [ %my_api.settings.form.class% ] ]
    

    Then, I'm just including the controller in routing.yml as the FOSRestBundle doc states, and it's done.