Youri_G
Youri_G

Reputation: 219

Symfony 2: How to use the ParamConverter with a PUT method to get or create an entity object

I need to implement an API with a PUT method and I would like to use the ParamConverter in my Controller to find an existing entity object, or if the entity object doesn't exist, to create a new one.

However the standard Symfony ParamConverter returns an exception if it doesn't find the entity object in the repository.

Do you have any ideas to do that in a nice and clean way ? Thx.

Here is an example of what I would like to do (I use FOS REST Bundle to handle the PUT request):

/**
 * @param Request $request
 * @return View
 *
 * @ParamConverter("video")
 *
 */
public function putVideosAction(Request $request, Video $video)
{
    try {
        return $this->getHandlerVideos()->put($video, $request->request->all());
    } catch (InvalidFormException $e) {
        return $e->getForm();
    }
}

Upvotes: 2

Views: 1838

Answers (2)

lolmx
lolmx

Reputation: 521

You'll have to create your own custom paramConverter.

First, here is what you want to write in your controller:

/**
 * @ParamConverter("video", class = "MyBundle:Video", converter = "my_param_converter")
 * @param Request $request
 * @param Video $video
 * @return \Symfony\Component\HttpFoundation\Response
 */
public function putVideosAction(Request $request, Video $video)
{
    // your code..
}

Now let's write the my_param_converter!

use Doctrine\Common\Persistence\ManagerRegistry;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
// ...

class MyParamConverter implements ParamConverterInterface
{
    private $registry;

    /**
     * @param ManagerRegistry $registry
     */
    public function __construct(ManagerRegistry $registry = null)
    {
        $this->registry = $registry;
    }

    /**
     * Check if object supported by our paramConverter
     *
     * @param ParamConverter $configuration
     */
    public function supports(ParamConverter $configuration)
    {
        // In this case we can do nothing and just return
        if (null === $this->registry || !count($this->registry->getManagers())) {
            return false;
        }

        // Check if the class is set in configuration
        if(null === $configuration->getClass()) {
            return false;
        }

        // Get actual entity manager for class
        $em = $this->registry->getManagerForClass($configuration->getClass());

        // Check what you need to check...

        return true;
    }

    public function apply(Request $request, ParamConverter $configuration)
    {
        $videoId = $request->attributes->get('video');

        if(null === videoId) {
            throw new \InvalidArgumentException('Route attribute is missing');
        }

        // Get actual entity manager for class
        $em = $this->registry->getManagerForClass($configuration->getClass());

        $repository = $em->getRepository($configuration->getClass());

        // Try to find the video
        $video = $$repository->findOneById($videoId);

        if($video === null || !($video instanceof Video)) {
            // Here you can create your new video object
        }

        // Map video to the route's parameter
        $request->attributes->set($configuration->getName(), $video);
    }
}

Once your new paramConverter wrote, declare it as a service:

services:
    app.param_converter.my_param_converter:
        class: YourBundle\Path\To\MyParamConverter
        tags:
            - { name: request.param_converter, converter: my_param_converter }
        arguments:
            - @?doctrine 

Here you're done!

My answer is largely inspired by this article and hope is helpful.

Upvotes: 4

john saulnier
john saulnier

Reputation: 41

Here's a solution. Please give me your thoughts on it.

In your controller, I would do that:

/**
 * @param Request $request
 * @return View
 *
 * @Rest\Put()
 * @Rest\View()
 *
 * @ParamConverter("video", converter="app_get_or_create_entity_converter", options={"repository_method" = "findOneById"})
 */
public function putVideosAction(Request $request, Video $video)
{
    try {
        $video = $this->getHandlerVideos()->put($video, $request->request->all());
        return $video;
    } catch (InvalidFormException $e) {
        return $e->getForm();
    }
}

I would write a dynamic param converter that way:

    class GetOrCreateEntityConverter implements \Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface
{
    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * @var ManagerRegistry $registry Manager registry
     */
    private $registry;

    /**
     * @param ManagerRegistry $registry
     * @param EntityManagerInterface $entityManager
     */
    public function __construct(ManagerRegistry $registry, EntityManagerInterface $entityManager)
    {
        $this->registry = $registry;
        $this->entityManager = $entityManager;
    }

    public function supports(ParamConverter $configuration)
    {
        if ('app_get_or_create_entity_converter' !== $configuration->getConverter()) {
            return false;
        }

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * Applies converting
     *
     * @throws \InvalidArgumentException When route attributes are missing
     * @throws NotFoundHttpException     When object not found
     */
    public function apply(Request $request, ParamConverter $configuration)
    {
        $name    = $configuration->getName();
        $options = $configuration->getOptions();
        $class   = $configuration->getClass();
        $repository = $this->entityManager->getRepository($class);

        $repositoryMethod = $options['repository_method'];

        if (!is_callable([$repository, $repositoryMethod])) {
            throw new \BadMethodCallException($repositoryMethod . ' function does not exist.', 405);
        }

        $entity = $repository->$repositoryMethod($id);

        if (null === $entity) {
            $entity = new $class;
        }

        $request->attributes->set($name, $entity);
    }
}

If you ask why I return a form in the catch, please go and see https://github.com/liuggio/symfony2-rest-api-the-best-2013-way/blob/master/src/Acme/BlogBundle/Controller/PageController.php

Upvotes: 4

Related Questions