Skip to content

Commit 9ab125b

Browse files
Add error handling on write processor (#1049)
| Q | A | --------------- | ----- | Bug fix? | yes | New feature? | yes | BC breaks? | no | Deprecations? | no | Related tickets | | License | MIT This will handle this part of the resource controller: https://github.com/Sylius/SyliusResourceBundle/blob/1.14/src/Bundle/Controller/ResourceController.php#L356 It will fix that error on Sylius E-commerce: ![image](https://github.com/user-attachments/assets/3f9a9b0a-9afb-4f0b-ba82-6fe6862aa9c1) To allow throwing exceptions on writing process, we should move the flash processing directly on the write processor. It's easier to catch error and add the right flash message. Then writing failure will still call the respond processing. **After** ![Capture d’écran du 2025-07-10 09-55-34](https://github.com/user-attachments/assets/3c0aed60-8ce7-4376-8ae7-c72fafe0a586)
2 parents 9963374 + 6bf5526 commit 9ab125b

File tree

12 files changed

+223
-18
lines changed

12 files changed

+223
-18
lines changed

src/Bundle/Resources/config/services/state/processor.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
</imports>
1818

1919
<services>
20+
<service id="sylius.state_processor.main" alias="sylius.state_processor.respond" />
21+
2022
<service id="sylius.state_processor.respond" class="Sylius\Resource\State\Processor\RespondProcessor">
2123
<argument type="service" id="sylius.state_responder" />
2224
</service>
23-
<service id="sylius.state_processor.main" alias="sylius.state_processor.respond" />
2425

2526
<service id="sylius.state_processor.write" class="Sylius\Resource\State\Processor\WriteProcessor" decorates="sylius.state_processor.main" decoration-priority="100">
2627
<argument type="service" id=".inner" />

src/Component/spec/Symfony/Session/Flash/FlashHelperSpec.php

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ function it_is_initializable(): void
4141
$this->shouldHaveType(FlashHelper::class);
4242
}
4343

44+
function it_adds_success_flashes_with_custom_message(
45+
Request $request,
46+
SessionInterface $session,
47+
FlashBagInterface $flashBag,
48+
): void {
49+
$operation = (new Create())->withResource(new ResourceMetadata(alias: 'app.dummy', name: 'dummy', applicationName: 'app'));
50+
$context = new Context(new RequestOption($request->getWrappedObject()));
51+
52+
$request->getSession()->willReturn($session);
53+
54+
$session->getBag('flashes')->willReturn($flashBag);
55+
56+
$flashBag->add('success', 'Custom message.')->shouldBeCalled();
57+
58+
$this->addSuccessFlash($operation, $context, 'Custom message.');
59+
}
60+
4461
function it_adds_success_flashes_with_specific_message(
4562
Request $request,
4663
SessionInterface $session,
@@ -91,7 +108,7 @@ function it_adds_success_flashes_with_fallback_message(
91108
$this->addSuccessFlash($operation, $context);
92109
}
93110

94-
function it_adds_success_flashes_with_custom_message(
111+
function it_adds_success_flashes_with_custom_message_on_its_operation(
95112
Request $request,
96113
SessionInterface $session,
97114
FlashBagInterface $flashBag,
@@ -186,6 +203,73 @@ function it_adds_success_flashes_with_humanized_message_and_plural_name_on_bulk_
186203
$this->addSuccessFlash($operation, $context);
187204
}
188205

206+
function it_adds_error_flashes_with_custom_message(
207+
Request $request,
208+
SessionInterface $session,
209+
FlashBagInterface $flashBag,
210+
): void {
211+
$operation = (new Create())->withResource(new ResourceMetadata(alias: 'app.dummy', name: 'dummy', applicationName: 'app'));
212+
$context = new Context(new RequestOption($request->getWrappedObject()));
213+
214+
$request->getSession()->willReturn($session);
215+
216+
$session->getBag('flashes')->willReturn($flashBag);
217+
218+
$flashBag->add('error', 'Custom error message.')->shouldBeCalled();
219+
220+
$this->addErrorFlash($operation, $context, 'Custom error message.');
221+
}
222+
223+
function it_adds_error_flashes_with_specific_message(
224+
Request $request,
225+
SessionInterface $session,
226+
FlashBagInterface $flashBag,
227+
TranslatorBagInterface $translator,
228+
MessageCatalogueInterface $messageCatalogue,
229+
): void {
230+
$operation = (new Create())->withResource(new ResourceMetadata(alias: 'app.dummy', name: 'dummy', applicationName: 'app'));
231+
$context = new Context(new RequestOption($request->getWrappedObject()));
232+
233+
$request->getSession()->willReturn($session);
234+
235+
$session->getBag('flashes')->willReturn($flashBag);
236+
237+
$translator->getCatalogue()->willReturn($messageCatalogue);
238+
239+
$messageCatalogue->has('app.dummy.create_error', 'flashes')->willReturn(true)->shouldBeCalled();
240+
241+
$translator->trans('app.dummy.create_error', ['%resource%' => 'Dummy'], 'flashes')->willReturn('Cannot create Dummy resource.')->shouldBeCalled();
242+
243+
$flashBag->add('error', 'Cannot create Dummy resource.')->shouldBeCalled();
244+
245+
$this->addErrorFlash($operation, $context);
246+
}
247+
248+
function it_adds_error_flashes_with_fallback_message(
249+
Request $request,
250+
SessionInterface $session,
251+
FlashBagInterface $flashBag,
252+
TranslatorBagInterface $translator,
253+
MessageCatalogueInterface $messageCatalogue,
254+
): void {
255+
$operation = (new Create())->withResource(new ResourceMetadata(alias: 'app.dummy', name: 'dummy', applicationName: 'app'));
256+
$context = new Context(new RequestOption($request->getWrappedObject()));
257+
258+
$request->getSession()->willReturn($session);
259+
260+
$session->getBag('flashes')->willReturn($flashBag);
261+
262+
$translator->getCatalogue()->willReturn($messageCatalogue);
263+
264+
$messageCatalogue->has('app.dummy.create_error', 'flashes')->willReturn(false)->shouldBeCalled();
265+
266+
$translator->trans('sylius.resource.create_error', ['%resource%' => 'Dummy'], 'flashes')->willReturn('Cannot create Dummy resource.')->shouldBeCalled();
267+
268+
$flashBag->add('error', 'Cannot create Dummy resource.')->shouldBeCalled();
269+
270+
$this->addErrorFlash($operation, $context);
271+
}
272+
189273
function it_translates_flashes_from_event_when_translator_is_not_a_bag(
190274
Request $request,
191275
SessionInterface $session,

src/Component/src/Doctrine/Common/State/RemoveProcessor.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313

1414
namespace Sylius\Resource\Doctrine\Common\State;
1515

16+
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
1617
use Doctrine\Persistence\ManagerRegistry;
1718
use Doctrine\Persistence\ObjectManager as DoctrineObjectManager;
1819
use Sylius\Resource\Context\Context;
20+
use Sylius\Resource\Exception\DeleteResourceException;
1921
use Sylius\Resource\Metadata\Operation;
2022
use Sylius\Resource\Reflection\ClassInfoTrait;
2123
use Sylius\Resource\State\ProcessorInterface;
@@ -34,8 +36,12 @@ public function process(mixed $data, Operation $operation, Context $context): mi
3436
return null;
3537
}
3638

37-
$manager->remove($data);
38-
$manager->flush();
39+
try {
40+
$manager->remove($data);
41+
$manager->flush();
42+
} catch (ForeignKeyConstraintViolationException) {
43+
throw new DeleteResourceException();
44+
}
3945

4046
return null;
4147
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\Resource\Exception;
15+
16+
class DeleteResourceException extends WriteResourceException
17+
{
18+
public function __construct(
19+
?string $resourceName = null,
20+
string $message = '',
21+
int $code = 0,
22+
?\Throwable $previous = null,
23+
) {
24+
if (empty($message)) {
25+
$message = sprintf('Cannot delete, the %s is in use.', $resourceName ?? 'resource');
26+
}
27+
28+
parent::__construct($resourceName, $message, $code, $previous);
29+
}
30+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\Resource\Exception;
15+
16+
class WriteResourceException extends RuntimeException
17+
{
18+
public function __construct(
19+
private readonly ?string $resourceName = null,
20+
string $message = '',
21+
int $code = 0,
22+
?\Throwable $previous = null,
23+
) {
24+
parent::__construct($message, $code, $previous);
25+
}
26+
27+
public function getResourceName(): ?string
28+
{
29+
return $this->resourceName;
30+
}
31+
}

src/Component/src/State/Processor/FlashProcessor.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Sylius\Resource\Metadata\Operation;
1919
use Sylius\Resource\State\ProcessorInterface;
2020
use Sylius\Resource\Symfony\Session\Flash\FlashHelperInterface;
21+
use Symfony\Component\HttpFoundation\Request;
2122
use Symfony\Component\HttpFoundation\Response;
2223

2324
/**
@@ -50,8 +51,19 @@ public function process(mixed $data, Operation $operation, Context $context): mi
5051
return $this->processor->process($data, $operation, $context);
5152
}
5253

53-
$this->flashHelper->addSuccessFlash($operation, $context);
54+
$this->addFlash($request, $operation, $context);
5455

5556
return $this->processor->process($data, $operation, $context);
5657
}
58+
59+
private function addFlash(Request $request, Operation $operation, Context $context): void
60+
{
61+
if ($request->attributes->has('error')) {
62+
$this->flashHelper->addErrorFlash($operation, $context);
63+
64+
return;
65+
}
66+
67+
$this->flashHelper->addSuccessFlash($operation, $context);
68+
}
5769
}

src/Component/src/State/Processor/WriteProcessor.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace Sylius\Resource\State\Processor;
1515

1616
use Sylius\Resource\Context\Context;
17+
use Sylius\Resource\Context\Option\RequestOption;
18+
use Sylius\Resource\Exception\WriteResourceException;
1719
use Sylius\Resource\Metadata\Operation;
1820
use Sylius\Resource\State\ProcessorInterface;
1921
use Symfony\Component\HttpFoundation\Response;
@@ -39,6 +41,13 @@ public function process(mixed $data, Operation $operation, Context $context): mi
3941
return $this->processor->process($data, $operation, $context);
4042
}
4143

42-
return $this->processor->process($this->locatorProcessor->process($data, $operation, $context), $operation, $context);
44+
try {
45+
return $this->processor->process($this->locatorProcessor->process($data, $operation, $context), $operation, $context);
46+
} catch (WriteResourceException $exception) {
47+
$request = $context->get(RequestOption::class)?->request();
48+
$request?->attributes->set('error', $exception->getMessage());
49+
50+
return $this->processor->process(null, $operation, $context);
51+
}
4352
}
4453
}

src/Component/src/Symfony/Session/Flash/FlashHelper.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@ public function __construct(
3434
) {
3535
}
3636

37-
public function addSuccessFlash(Operation $operation, Context $context): void
37+
public function addSuccessFlash(Operation $operation, Context $context, ?string $message = null): void
3838
{
39-
$this->addFlashFromOperation($operation, $context, 'success');
39+
$this->addFlashFromOperation($operation, $context, 'success', $message);
40+
}
41+
42+
public function addErrorFlash(Operation $operation, Context $context, ?string $message = null): void
43+
{
44+
$this->addFlashFromOperation($operation, $context, 'error', $message);
4045
}
4146

4247
public function addFlashFromEvent(GenericEvent $event, Context $context): void
@@ -46,9 +51,9 @@ public function addFlashFromEvent(GenericEvent $event, Context $context): void
4651
$this->addFlash($message, $event->getMessageType(), $context);
4752
}
4853

49-
private function addFlashFromOperation(Operation $operation, Context $context, string $type): void
54+
private function addFlashFromOperation(Operation $operation, Context $context, string $type, ?string $message): void
5055
{
51-
$message = $this->buildOperationMessage($operation, $type);
56+
$message ??= $this->buildOperationMessage($operation, $type);
5257

5358
$this->addFlash($message, $type, $context);
5459
}
@@ -74,8 +79,10 @@ private function buildOperationMessage(Operation $operation, string $type): stri
7479
$resource = $operation->getResource();
7580
Assert::notNull($resource);
7681

77-
$key = $operation->getNotificationMessage() ?? sprintf('%s.%s.%s', $resource->getApplicationName() ?? '', $resource->getName() ?? '', $operation->getShortName() ?? '');
78-
$fallbackKey = sprintf('sylius.resource.%s', $operation->getShortName() ?? '');
82+
$translationKeySuffix = sprintf('%s%s', $operation->getShortName() ?? '', 'error' === $type ? '_error' : '');
83+
$key = 'success' === $type ? $operation->getNotificationMessage() : null;
84+
$key ??= sprintf('%s.%s.%s', $resource->getApplicationName() ?? '', $resource->getName() ?? '', $translationKeySuffix);
85+
$fallbackKey = sprintf('sylius.resource.%s', $translationKeySuffix);
7986

8087
$parameters = $this->getTranslationParameters($operation);
8188

src/Component/src/Symfony/Session/Flash/FlashHelperInterface.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
*/
2323
interface FlashHelperInterface
2424
{
25-
public function addSuccessFlash(Operation $operation, Context $context): void;
25+
public function addSuccessFlash(Operation $operation, Context $context, ?string $message = null): void;
26+
27+
public function addErrorFlash(Operation $operation, Context $context, ?string $message = null): void;
2628

2729
public function addFlashFromEvent(GenericEvent $event, Context $context): void;
2830
}

src/Component/tests/State/Processor/FlashProcessorTest.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Sylius\Resource\State\Processor\FlashProcessor;
2323
use Sylius\Resource\State\ProcessorInterface;
2424
use Sylius\Resource\Symfony\Session\Flash\FlashHelperInterface;
25+
use Symfony\Component\HttpFoundation\ParameterBag;
2526
use Symfony\Component\HttpFoundation\Request;
2627
use Symfony\Component\HttpFoundation\Response;
2728

@@ -47,11 +48,12 @@ protected function setUp(): void
4748
}
4849

4950
/** @test */
50-
public function it_adds_flash(): void
51+
public function it_adds_success_flash(): void
5152
{
5253
$request = $this->prophesize(Request::class);
5354
$operation = $this->prophesize(HttpOperation::class);
5455

56+
$request->attributes = new ParameterBag();
5557
$request->getRequestFormat()->willReturn('html')->shouldBeCalled();
5658
$request->isMethodSafe()->willReturn(false)->shouldBeCalled();
5759

@@ -66,6 +68,27 @@ public function it_adds_flash(): void
6668
$this->flashProcessor->process(['foo' => 'fighters'], $operation->reveal(), $context);
6769
}
6870

71+
/** @test */
72+
public function it_adds_error_flash(): void
73+
{
74+
$request = $this->prophesize(Request::class);
75+
$operation = $this->prophesize(HttpOperation::class);
76+
77+
$request->attributes = new ParameterBag(['error' => 'Cannot delete, the resource is in use.']);
78+
$request->getRequestFormat()->willReturn('html')->shouldBeCalled();
79+
$request->isMethodSafe()->willReturn(false)->shouldBeCalled();
80+
81+
$operation->canWrite()->willReturn(null)->shouldBeCalled();
82+
83+
$context = new Context(new RequestOption($request->reveal()));
84+
85+
$this->decorated->process(['foo' => 'fighters'], $operation, $context)->willReturn(['foo' => 'fighters'])->shouldBeCalled();
86+
87+
$this->flashHelper->addErrorFlash($operation, $context)->shouldBeCalled();
88+
89+
$this->flashProcessor->process(['foo' => 'fighters'], $operation->reveal(), $context);
90+
}
91+
6992
/** @test */
7093
public function it_does_nothing_when_controller_result_is_a_response(): void
7194
{

0 commit comments

Comments
 (0)