Reputation: 150
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 !
Upvotes: 0
Views: 915
Reputation: 3319
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.
Upvotes: 1