api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.43k stars 866 forks source link

Is there any simple way to perform pre-delete validation? #388

Closed akozhemiakin closed 8 years ago

akozhemiakin commented 8 years ago

For example, imagine that we have some system where we have Users and Projects assigned to them and we want to prevent User from being deleted if he still has some projects assigned to him. Am I right that for now there is no way to do this using standard controller?

soyuka commented 8 years ago

Use events ? https://api-platform.com/doc/1.0/api-bundle/the-event-system

akozhemiakin commented 8 years ago

@soyuka How? The delete action in standard controller dispatches only one event ("pre-delete").

public function deleteAction(Request $request, $id)
{
    $resource = $this->getResource($request);
    $object = $this->findOrThrowNotFound($resource, $id);
    $this->get('event_dispatcher')->dispatch(Events::PRE_DELETE, new DataEvent($resource, $object));
    return new Response(null, 204);
}

The only way to intercept this action is to register pre-delete event handler with higher priority and to throw exception. But I do not want to throw some random exception, I want to return some response containing the information about error. Like, for example, it is done in the post or put action during the validation phase:

public function cpostAction(Request $request)
{
    ...
    return $this->getErrorResponse($violations);
}

protected function getErrorResponse(ConstraintViolationListInterface $violations)
{
    return new Response($this->get('serializer')->normalize($violations, 'hydra-error'), 400);
}
soyuka commented 8 years ago

Without thinking to much I think that you could inject the @request_stack in your event listener to handle the request yourself, or maybe use another listener (doctrine, kernel).

Actually in master, the event system documented for 1.x has been removed (commit).

You may take a look at the ValidationViewListener. Exception throwed in it are handled through listeners and normalized with Hydra to return informations with a correct HTTP code. Those are not random exceptions :).

akozhemiakin commented 8 years ago

Massive commit xD I should dig into it before I can say anything about it.

As for ValidationViewListener, I believe (and onKernelView method description proves it) that it is executed after controller execution. So the object will be already deleted at that time. Am I missing something?

akozhemiakin commented 8 years ago

And yes, of course I can use kernel listeners, and doctrine listeners and many other things to achieve what I want. But, I believe, that if APIBundle is responsible for validating data during POST and PUT requests than it should give developer the possibility to validate any request to the API using some standard approach.

soyuka commented 8 years ago

I'm using the master branch so I can't really give you the best way with 1.0.

From what I see in ValidationListener it's only handling POST and PUT requests, and I would do the same if I wanted to pre-validate a DELETE request. POST and PUT are handling validation groups, but I'm not sure they are handled with DELETE requests.

If I'm not mistaken, in the master branch, controller (for example DeleteItemAction) returns the element to be deleted, and it's then removed with doctrine in the ManagerViewListener.

Sorry about the amount of informations, I try to help the best I can (I'm a symfony newbie and still learning ;)).

akozhemiakin commented 8 years ago

Oh, I thought you show me proposed commit from some pull request, my mistake. Actually I'm working with a master branch too.

From what I see in ValidationListener it's only handling POST and PUT requests, and I would do the same if I wanted to pre-validate a DELETE request. POST and PUT are handling validation groups, but I'm not sure they are handled with DELETE requests.

ValidationListener responds to the Symfony kernel.view event. This event fires AFTER the controller action execution and is intended to handle custom (other than standard symfony Response) response objects. So it has nothing to do with pre-validation.

If I'm not mistaken, in the master branch, controller (for example DeleteItemAction) returns the element to be deleted, and it's then removed with doctrine in the ManagerViewListener.

In 1.x Controller does not behave this way. After loading the resource it just fires PRE_DELETE event and Dunglas\ApiBundle\Doctrine\EventSubscriber subscribed to it performs the "dirty job". As for new Action based mechanics in master I'm going to look into it.

Ah, no problem. Appreciate for your help anyway)

dunglas commented 8 years ago

Should be easier with the new event system of v2.

soullivaneuh commented 5 years ago

Should be easier with the new event system of v2.

I'm looking at the doc and I don't see any reference.

Could we use this to prevent deletion? https://api-platform.com/docs/core/validation how?

If not, how should it be prepared?

soullivaneuh commented 5 years ago

The only way I found is like this:

public static function getSubscribedEvents()
{
    return [
        KernelEvents::VIEW => [
            ['setup', EventPriorities::PRE_WRITE],
            ['checkForDeletion', EventPriorities::PRE_WRITE],
            ['letsEncryptInit', EventPriorities::POST_WRITE],
        ],
    ];
}

public function checkForDeletion(GetResponseForControllerResultEvent $event)
{
    if (!$event->getRequest()->isMethod('DELETE')) {
        return;
    }

    throw new AccessDeniedException('NOPE!');
}

But nothing with the validation system. Looks ok because validation is for creation/edition AFAIK.

Is it the best way to do?

Thanks.

teohhanhui commented 5 years ago

Indeed, you could use an event listener like you did. :) Or you could handle it at the persistence layer (e.g. Doctrine ORM).

piotrekkr commented 4 years ago

If anyone wondering how to validate on DELETE method:

<?php
declare(strict_types=1);

namespace App\EventSubscriber;

use ApiPlatform\Core\EventListener\EventPriorities;
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use ApiPlatform\Core\Validator\ValidatorInterface;

final class ValidateBeforeDeleteSubscriber implements EventSubscriberInterface
{
    public const DEFAULT_VALIDATION_GROUPS = [
        'item:delete'
    ];

    private $validationGroups = null;

    private $validator;

    public function __construct(ValidatorInterface $validator, array $validationGroups = null)
    {
        $this->validator = $validator;
        $this->validationGroups = $validationGroups;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::VIEW => ['validate', EventPriorities::PRE_WRITE],
        ];
    }

    /**
     * @throws ValidationException
     */
    public function validate(ViewEvent $event): void
    {
        $entity = $event->getControllerResult();
        $request = $event->getRequest();
        if (
            $entity instanceof Response
            || $request->getMethod() !== 'DELETE'
        ) {
            return;
        }
        // add your entity validators to proper groups for validation to trigger
        $this->validator->validate($entity, ['groups' => $this->validationGroups ?? self::DEFAULT_VALIDATION_GROUPS]);
    }
}

In Entity add item:delete group to validator:

<?php

declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use App\Validator\Constraints as AppAssert;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Template
 *
 * @ApiResource(
 *     collectionOperations={
 *      "POST"={"path"="/tree_templates"},
 *      "GET"={"path"="/tree_templates", "normalization_context"={"groups"={"template.collection.read"}}}
 *     },
 *     itemOperations={
 *      "PATCH"={"path"="/tree_templates/{id}"},
 *      "GET"={"path"="/tree_templates/{id}"},
 *      "DELETE"={"path"="/tree_templates/{id}"}
 *     },
 *     normalizationContext={"groups"={"template:read"}},
 *     denormalizationContext={"groups"={"template:write"}},
 *     attributes={"order"={"id": "desc"}}
 * )
 * @ApiFilter(OrderFilter::class, properties={"name"})
 * @ApiFilter(OrderFilter::class, properties={"id"})
 * @ApiFilter(SearchFilter::class, properties={"name": "istart"})
 * @ApiFilter(SearchFilter::class, properties={"treeTemplateEntries.name": "istart"})
 *
 * @AppAssert\TreeTemplateCanChange(groups={"template:write", "item:delete"})
 *
 * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
 * @ORM\Entity(repositoryClass="App\Repository\TreeTemplateRepository")
 * @ORM\Table(name="tree_template")
 */
class TreeTemplate
{
// ...
}
teohhanhui commented 4 years ago

The current recommendation would be to decorate the DataPersister (or use a custom one): https://api-platform.com/docs/core/data-persisters/

See https://api-platform.com/docs/core/extending/

benblub commented 3 years ago

If you use DataPersisters whats best answer you have to use Api Platform Validator Interface and not from Symfony!

Examble Code from DataPersister

    use ApiPlatform\Core\Validator\ValidatorInterface;

    // ...

    /**
     * @param mixed $data
     */
    public function remove($data, array $context = []): void
    {
        /** @var User $data */
        $this->validator->validate($data, ['groups' => 'Delete']);

        $this->em->remove($data);
        $this->em->flush();
    }