Refactor ResourceDeleteSubscriber

This commit is contained in:
Kamil Grygierzec 2024-09-27 17:45:54 +02:00
parent 3d39f05fdf
commit 5691cea019
No known key found for this signature in database
GPG key ID: 7F54EE42DAD4B9E9
7 changed files with 277 additions and 43 deletions

View file

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\EventListener;
use Sylius\Bundle\CoreBundle\Provider\FlashBagProvider;
use Sylius\Component\Core\Exception\ResourceDeleteException;
use Sylius\Resource\ResourceActions;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final readonly class ResourceDeleteExceptionListener
{
public function __construct(
private UrlGeneratorInterface $router,
private RequestStack $requestStack,
) {
}
public function onResourceDeleteException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
if (!$exception instanceof ResourceDeleteException) {
return;
}
FlashBagProvider::getFlashBag($this->requestStack)->add('error', [
'message' => 'sylius.resource.delete_error',
'parameters' => ['%resource%' => $exception->getResourceName()],
]);
$eventRequest = $event->getRequest();
$requestAttributes = $eventRequest->attributes;
$originalRoute = $requestAttributes->get('_route', '');
$referrer = $eventRequest->headers->get('referer');
if (null !== $referrer) {
$event->setResponse(new RedirectResponse($referrer));
return;
}
$event->setResponse($this->createRedirectResponse($originalRoute, ResourceActions::INDEX));
}
private function createRedirectResponse(string $originalRoute, string $targetAction): RedirectResponse
{
$redirectRoute = str_replace(ResourceActions::DELETE, $targetAction, $originalRoute);
return new RedirectResponse($this->router->generate($redirectRoute));
}
}

View file

@ -14,31 +14,12 @@ declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\EventListener;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Sylius\Bundle\CoreBundle\Provider\FlashBagProvider;
use Sylius\Resource\ResourceActions;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Sylius\Component\Core\Exception\ResourceDeleteException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final readonly class ResourceDeleteSubscriber implements EventSubscriberInterface
final readonly class ResourceDeleteListener
{
public function __construct(
private UrlGeneratorInterface $router,
private RequestStack $requestStack,
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => 'onResourceDelete',
];
}
public function onResourceDelete(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
@ -67,19 +48,7 @@ final readonly class ResourceDeleteSubscriber implements EventSubscriberInterfac
return;
}
FlashBagProvider::getFlashBag($this->requestStack)->add('error', [
'message' => 'sylius.resource.delete_error',
'parameters' => ['%resource%' => $resourceName],
]);
$referrer = $eventRequest->headers->get('referer');
if (null !== $referrer) {
$event->setResponse(new RedirectResponse($referrer));
return;
}
$event->setResponse($this->createRedirectResponse($originalRoute, ResourceActions::INDEX));
throw new ResourceDeleteException($resourceName);
}
private function getResourceNameFromRoute(string $route): string
@ -92,13 +61,6 @@ final readonly class ResourceDeleteSubscriber implements EventSubscriberInterfac
return trim(implode(' ', $routeArrayWithoutPrefixes));
}
private function createRedirectResponse(string $originalRoute, string $targetAction): RedirectResponse
{
$redirectRoute = str_replace(ResourceActions::DELETE, $targetAction, $originalRoute);
return new RedirectResponse($this->router->generate($redirectRoute));
}
private function isMethodDelete(Request $request): bool
{
return Request::METHOD_DELETE === $request->getMethod();
@ -109,6 +71,7 @@ final readonly class ResourceDeleteSubscriber implements EventSubscriberInterfac
return str_starts_with($route, 'sylius');
}
/** @param array<string, mixed> $syliusParameters */
private function isAdminSection(array $syliusParameters): bool
{
return array_key_exists('section', $syliusParameters) && 'admin' === $syliusParameters['section'];

View file

@ -26,10 +26,14 @@
<tag name="kernel.event_listener" event="sylius.locale.pre_delete" method="preDelete" />
</service>
<service id="sylius.event_subscriber.resource_delete" class="Sylius\Bundle\AdminBundle\EventListener\ResourceDeleteSubscriber">
<service id="sylius.event_listener.resource_delete_exception" class="Sylius\Bundle\AdminBundle\EventListener\ResourceDeleteExceptionListener">
<argument type="service" id="router" />
<argument type="service" id="request_stack" />
<tag name="kernel.event_subscriber" event="kernel.exception" />
<tag name="kernel.event_listener" event="kernel.exception" method="onResourceDeleteException" />
</service>
<service id="sylius.event_listener.resource_delete" class="Sylius\Bundle\AdminBundle\EventListener\ResourceDeleteListener">
<tag name="kernel.event_listener" event="kernel.exception" method="onResourceDelete" />
</service>
<service id="sylius.event_subscriber.admin_cache_control_subscriber" class="Sylius\Bundle\AdminBundle\EventListener\AdminSectionCacheControlSubscriber">

View file

@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\AdminBundle\EventListener;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Component\Core\Exception\ResourceDeleteException;
use Symfony\Component\HttpFoundation\HeaderBag;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final class ResourceDeleteExceptionListenerSpec extends ObjectBehavior
{
function let(UrlGeneratorInterface $router, RequestStack $requestStack): void
{
$this->beConstructedWith($router, $requestStack);
}
function it_does_nothing_if_exception_is_not_resource_delete_exception(
KernelInterface $kernel,
ForeignKeyConstraintViolationException $exception,
): void {
$event = new ExceptionEvent($kernel->getWrappedObject(), new Request(), HttpKernelInterface::MAIN_REQUEST, $exception->getWrappedObject());
$this->onResourceDeleteException($event)->shouldReturn(null);
}
function it_redirects_to_referer_if_present_and_adds_flash_message(
KernelInterface $kernel,
Request $request,
RequestStack $requestStack,
FlashBagInterface $flashBag,
UrlGeneratorInterface $router,
SessionInterface $session,
): void {
$request->attributes = new ParameterBag(['_route' => 'sylius_admin_product_delete']);
$request->headers = new HeaderBag(['referer' => '/admin/product/index']);
$exception = new ResourceDeleteException('Product');
$event = new ExceptionEvent($kernel->getWrappedObject(), $request->getWrappedObject(), HttpKernelInterface::MAIN_REQUEST, $exception);
$requestStack->getSession()->willReturn($session);
$session->getBag('flashes')->willReturn($flashBag);
$flashBag->add('error', ['message' => 'sylius.resource.delete_error', 'parameters' => ['%resource%' => 'Product']])->shouldBeCalled();
$router->generate(Argument::cetera())->shouldNotBeCalled();
$this->onResourceDeleteException($event);
}
function it_redirects_to_index_when_no_referer_present_and_adds_flash_message(
KernelInterface $kernel,
Request $request,
RequestStack $requestStack,
FlashBagInterface $flashBag,
UrlGeneratorInterface $router,
SessionInterface $session,
): void {
$request->attributes = new ParameterBag(['_route' => 'sylius_admin_product_delete']);
$request->headers = new HeaderBag();
$exception = new ResourceDeleteException('Product');
$event = new ExceptionEvent($kernel->getWrappedObject(), $request->getWrappedObject(), HttpKernelInterface::MAIN_REQUEST, $exception);
$requestStack->getSession()->willReturn($session);
$session->getBag('flashes')->willReturn($flashBag);
$flashBag->add('error', ['message' => 'sylius.resource.delete_error', 'parameters' => ['%resource%' => 'Product']])->shouldBeCalled();
$router->generate('sylius_admin_product_index')->willReturn('/admin/product/index');
$this->onResourceDeleteException($event);
}
}

View file

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\AdminBundle\EventListener;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Exception\ResourceDeleteException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelInterface;
final class ResourceDeleteListenerSpec extends ObjectBehavior
{
function it_does_nothing_if_exception_is_not_foreign_key_constraint(KernelInterface $kernel): void
{
$this->onResourceDelete(new ExceptionEvent($kernel->getWrappedObject(), new Request(), HttpKernelInterface::MAIN_REQUEST, new \Exception()))->shouldReturn(null);
}
function it_does_nothing_if_request_is_not_main(KernelInterface $kernel, ForeignKeyConstraintViolationException $exception): void
{
$event = new ExceptionEvent($kernel->getWrappedObject(), new Request(), HttpKernelInterface::SUB_REQUEST, $exception->getWrappedObject());
$this->onResourceDelete($event)->shouldReturn(null);
}
function it_should_throw_resource_delete_exception_if_all_conditions_are_met(KernelInterface $kernel, ForeignKeyConstraintViolationException $exception): void
{
$event = new ExceptionEvent($kernel->getWrappedObject(), new Request(), HttpKernelInterface::MAIN_REQUEST, $exception->getWrappedObject());
$this->onResourceDelete($event)->shouldThrow(ResourceDeleteException::class);
}
function it_should_do_nothing_if_method_is_not_delete(KernelInterface $kernel, ForeignKeyConstraintViolationException $exception): void
{
$request = new Request([], [], ['_route' => 'sylius_admin_product_delete', '_sylius' => ['section' => 'admin']]);
$request->setMethod('GET');
$event = new ExceptionEvent($kernel->getWrappedObject(), $request, HttpKernelInterface::MAIN_REQUEST, $exception->getWrappedObject());
$this->onResourceDelete($event)->shouldReturn(null);
}
function it_should_do_nothing_if_route_is_not_sylius(KernelInterface $kernel, ForeignKeyConstraintViolationException $exception): void
{
$request = new Request([], [], ['_route' => 'non_sylius_route', '_sylius' => ['section' => 'admin']]);
$event = new ExceptionEvent($kernel->getWrappedObject(), $request, HttpKernelInterface::MAIN_REQUEST, $exception->getWrappedObject());
$this->onResourceDelete($event)->shouldReturn(null);
}
function it_should_do_nothing_if_section_is_not_admin(KernelInterface $kernel, ForeignKeyConstraintViolationException $exception): void
{
$request = new Request([], [], ['_route' => 'sylius_admin_product_delete', '_sylius' => ['section' => 'shop']]);
$event = new ExceptionEvent($kernel->getWrappedObject(), $request, HttpKernelInterface::MAIN_REQUEST, $exception->getWrappedObject());
$this->onResourceDelete($event)->shouldReturn(null);
}
}

View file

@ -119,6 +119,11 @@
<tag name="kernel.event_listener" event="sylius.product.pre_delete" method="protectFromRemovingProductInUseByPromotionRule" />
</service>
<service id="sylius.listener.product_option_value" class="Sylius\Bundle\CoreBundle\EventListener\ProductOptionValueDeletionListener">
<argument type="service" id="sylius.repository.product_variant" />
<tag name="doctrine.orm.entity_listener" event="preRemove" entity="%sylius.model.product_option_value.class%" />
</service>
<service id="Sylius\Bundle\CoreBundle\EventListener\PostgreSQLDefaultSchemaListener">
<tag name="doctrine.event_listener" event="postGenerateSchema" method="postGenerateSchema" lazy="true" />
</service>

View file

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Component\Core\Exception;
class ResourceDeleteException extends \RuntimeException
{
public function __construct(
private string $resourceName,
string $message = '',
int $code = 0,
?\Throwable $previous = null,
) {
if (empty($message)) {
$message = sprintf('Cannot delete, the %s is in use.', $resourceName);
}
parent::__construct($message, $code, $previous);
}
public function getResourceName(): string
{
return $this->resourceName;
}
}