Core: Make it easier to use simple DTO

Created on 21 Dec 2017  路  10Comments  路  Source: api-platform/core

Hello.

I have one simple need: A user should be able to call an endpoint to reset his password.
(Note: I could have been something totally different, but let's stick to this use case)

I have tried many thing to be the closest possible to API Platform. I mean, I want to use all the feature
of API Platform: de-serialization, validation, error management, documation, etc.

So I end up to write the following code that works, but I think we can do better.

A simple DTO, with validation and declared as an ApiRessource

<?php

namespace AppBundle\Api\Dto;

use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ApiResource(
 *      collectionOperations={
 *          "post"={"route_name"="api_users_forgot_password_request"},
 *      },
 *      itemOperations={},
 * )
 */
class ForgotPasswordRequest
{
    /**
     * @Assert\NotBlank()
     * @Assert\Email()
     */
    public $email;
}

Then, I had to write the associated controller:

<?php

namespace AppBundle\Controller\Api2;

use AppBundle\Api\Dto\ForgotPasswordRequest;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

class ForgotPasswordRequestController extends Controller
{
    /**
     * @Route(
     *     name="api_users_forgot_password_request",
     *     path="/users/forgot-password-request",
     *     defaults={"_api_resource_class"=ForgotPasswordRequest::class, "_api_collection_operation_name"="special"}
     * )
     * @Method("POST")
     */
    public function requestAction(ForgotPasswordRequest $data)
    {
        $errors = $this->get('validator')->validate($data);

        if (count($errors) > 0) {
            return $errors;
        }

        // My custom logic here

        return new JsonResponse(null, 204);
    }
}

So basically it works but:

  • I had hard time to find a working solution. I tried with some events without success (always an issue because it's not a real entity) and finally with a controller. It was not easy to find the custom attribute to use. I had to open all listener to understand how it really works.
  • I had to validate the entity by myself. But there is a ValidatorListener. But it is ran after the controller. Why ? cf聽#1538
  • The documentation is a bit light on using a DTO like this.

Could we do something to improve this experience ?


Anyway, thanks a lot for this project

doc help wanted

Most helpful comment

I am struggling to understand when to use a DTO and when to use an EventListener.

I feel DTO's are the way to go and I had assumed that DTO's were validated prior to receiving the data. It doesn't feel right to be referring to route names when using the listeners to decide what to do :|

All 10 comments

EDIT

I finally manage to leverage the event system. So now I don't use the a custom controller \o/

I made a ver stupid typo. Here is my fix:

     public static function getSubscribedEvents()
     {
         return [
-            KernelEvents::REQUEST => ['resolveMe', EventPriorities::PRE_READ],
-            KernelEvents::VIEW => ['sendPasswordReset', EventPriorities::PRE_WRITE],
-            KernelEvents::VIEW => ['sendForgotPasswordRequest', EventPriorities::PRE_WRITE],
-            KernelEvents::VIEW => ['updatePassword', EventPriorities::PRE_WRITE],
+            KernelEvents::REQUEST => ['resolveMe', EventPriorities::POST_READ],
+            KernelEvents::VIEW => [
+                ['sendForgotPasswordRequest', EventPriorities::PRE_VALIDATE],
+                ['sendPasswordReset', EventPriorities::PRE_WRITE],
+                ['updatePassword', EventPriorities::PRE_WRITE],
+            ]
         ];
     }

I'm closing this issue as it's solve for me.

Maybe we could keep this open? I feel there might be room for improvements in our documentation about DTO and the event system ! Would you mind contributing some of your above research to the docs?

Hello.

Thanks @soyuka . You doc is really good but indeed there is a lack.
but unfortunately I'm not able to contribute to the doc. I'm not a native EN speaker :/
And I lack time too ;)

I'm not a native EN speaker :/

Neither are most of our doc contributors!

And I lack time too ;)

We all do don't we? ;)

Don't worry though, I'm sure this can already help some people!

@dunglas , @soyuka WDYT about moving validator listener from onKernelView to another event, like "get controller args"? This allows validate DTO's before they pass to the controller and we can write some logic inside controllers (not in event listeners) before flush listener would called.

https://github.com/api-platform/core/blob/master/src/Bridge/Symfony/Validator/EventListener/ValidateListener.php#L49

I thinks it's a good idea, but this would be a big BC break :/

I have opened a PR in the documentation ;)

The Doc PR has been merged. I close the issue, but if you think there is more to do feel free to re-open.
Thanks.

I am struggling to understand when to use a DTO and when to use an EventListener.

I feel DTO's are the way to go and I had assumed that DTO's were validated prior to receiving the data. It doesn't feel right to be referring to route names when using the listeners to decide what to do :|

Was this page helpful?
0 / 5 - 0 ratings

Related issues

desmax picture desmax  路  3Comments

rockyweng picture rockyweng  路  3Comments

dunglas picture dunglas  路  3Comments

stipic picture stipic  路  3Comments

mahmoodbazdar picture mahmoodbazdar  路  3Comments