diff --git a/src/Controller/CRUDController.php b/src/Controller/CRUDController.php index b7245b8d91..902a700f57 100644 --- a/src/Controller/CRUDController.php +++ b/src/Controller/CRUDController.php @@ -170,11 +170,10 @@ public function batchActionDelete(ProxyQueryInterface $query): Response ); } catch (ModelManagerException $e) { // NEXT_MAJOR: Remove this catch. - $this->handleModelManagerException($e); - + $errorMessage = $this->handleModelManagerException($e); $this->addFlash( 'sonata_flash_error', - $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle') + $errorMessage ?? $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle') ); } catch (ModelManagerThrowable $e) { $errorMessage = $this->handleModelManagerThrowable($e); @@ -229,7 +228,7 @@ public function deleteAction(Request $request): Response ); } catch (ModelManagerException $e) { // NEXT_MAJOR: Remove this catch. - $this->handleModelManagerException($e); + $errorMessage = $this->handleModelManagerException($e); if ($this->isXmlHttpRequest($request)) { return $this->renderJson(['result' => 'error']); @@ -237,7 +236,7 @@ public function deleteAction(Request $request): Response $this->addFlash( 'sonata_flash_error', - $this->trans( + $errorMessage ?? $this->trans( 'flash_delete_error', ['%name%' => $this->escapeHtml($objectName)], 'SonataAdminBundle' @@ -295,7 +294,6 @@ public function editAction(Request $request): Response if (null !== $preResponse) { return $preResponse; } - $this->admin->setSubject($existingObject); $objectId = $this->admin->getNormalizedIdentifier($existingObject); \assert(null !== $objectId); @@ -334,7 +332,7 @@ public function editAction(Request $request): Response return $this->redirectTo($request, $existingObject); } catch (ModelManagerException $e) { // NEXT_MAJOR: Remove this catch. - $this->handleModelManagerException($e); + $errorMessage = $this->handleModelManagerException($e); $isFormValid = false; } catch (ModelManagerThrowable $e) { @@ -610,7 +608,7 @@ public function createAction(Request $request): Response return $this->redirectTo($request, $newObject); } catch (ModelManagerException $e) { // NEXT_MAJOR: Remove this catch. - $this->handleModelManagerException($e); + $errorMessage = $this->handleModelManagerException($e); $isFormValid = false; } catch (ModelManagerThrowable $e) { @@ -1107,13 +1105,13 @@ protected function getBaseTemplate(): string /** * @throws \Exception + * + * @return string|null A custom error message to display in the flag bag instead of the generic one */ - protected function handleModelManagerException(\Exception $exception): void + protected function handleModelManagerException(\Exception $exception) { if ($exception instanceof ModelManagerThrowable) { - $this->handleModelManagerThrowable($exception); - - return; + return $this->handleModelManagerThrowable($exception); } @trigger_error(sprintf( @@ -1132,6 +1130,8 @@ protected function handleModelManagerException(\Exception $exception): void $context['previous_exception_message'] = $exception->getPrevious()->getMessage(); } $this->getLogger()->error($exception->getMessage(), $context); + + return null; } /** diff --git a/tests/App/Controller/CustomModelManagerExceptionMessageController.php b/tests/App/Controller/CustomModelManagerExceptionMessageController.php new file mode 100644 index 0000000000..2aba1eb8d2 --- /dev/null +++ b/tests/App/Controller/CustomModelManagerExceptionMessageController.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\App\Controller; + +use Sonata\AdminBundle\Controller\CRUDController; + +/** + * @phpstan-extends CRUDController + */ +final class CustomModelManagerExceptionMessageController extends CRUDController +{ + public const ERROR_MESSAGE = 'message from model manager exception'; + + protected function handleModelManagerException(\Exception $exception): string + { + return self::ERROR_MESSAGE; + } +} diff --git a/tests/App/Controller/CustomModelManagerThrowableMessageController.php b/tests/App/Controller/CustomModelManagerThrowableMessageController.php new file mode 100644 index 0000000000..080b267e5f --- /dev/null +++ b/tests/App/Controller/CustomModelManagerThrowableMessageController.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\App\Controller; + +use Sonata\AdminBundle\Controller\CRUDController; +use Sonata\AdminBundle\Exception\ModelManagerThrowable; + +/** + * @phpstan-extends CRUDController + */ +final class CustomModelManagerThrowableMessageController extends CRUDController +{ + public const ERROR_MESSAGE = 'message from model manager throwable'; + + protected function handleModelManagerThrowable(ModelManagerThrowable $exception): string + { + return self::ERROR_MESSAGE; + } +} diff --git a/tests/Controller/CRUDControllerTest.php b/tests/Controller/CRUDControllerTest.php index e102513c8f..7b4b55fcea 100644 --- a/tests/Controller/CRUDControllerTest.php +++ b/tests/Controller/CRUDControllerTest.php @@ -32,6 +32,8 @@ use Sonata\AdminBundle\Request\AdminFetcherInterface; use Sonata\AdminBundle\Security\Handler\AclSecurityHandlerInterface; use Sonata\AdminBundle\Templating\MutableTemplateRegistryInterface; +use Sonata\AdminBundle\Tests\App\Controller\CustomModelManagerExceptionMessageController; +use Sonata\AdminBundle\Tests\App\Controller\CustomModelManagerThrowableMessageController; use Sonata\AdminBundle\Tests\Fixtures\Controller\BatchAdminController; use Sonata\AdminBundle\Tests\Fixtures\Controller\BatchOtherController; use Sonata\AdminBundle\Tests\Fixtures\Controller\PreCRUDController; @@ -683,6 +685,84 @@ public function testBatchActionDeleteWithModelManagerExceptionInDebugMode(): voi $this->controller->batchActionDelete($this->createMock(ProxyQueryInterface::class)); } + public function testBatchActionDeleteWithModelManagerExceptionAndCustomError(): void + { + $modelManager = $this->createMock(ModelManagerInterface::class); + + $exception = new ModelManagerException( + $message = 'message', + 1234, + new \Exception($previousExceptionMessage = 'very useful message') + ); + + $modelManager->expects(static::once()) + ->method('batchDelete') + ->willReturnCallback(static function () use ($exception): void { + throw $exception; + }); + + $this->admin->expects(static::once()) + ->method('getModelManager') + ->willReturn($modelManager); + + $this->admin->expects(static::once()) + ->method('getFilterParameters') + ->willReturn(['foo' => 'bar']); + + $customController = new CustomModelManagerExceptionMessageController(); + $customController->setContainer($this->container); + $customController->configureAdmin($this->request); + + $result = $customController->batchActionDelete($this->createMock(ProxyQueryInterface::class)); + + static::assertInstanceOf(RedirectResponse::class, $result); + static::assertSame( + [CustomModelManagerExceptionMessageController::ERROR_MESSAGE], + $this->session->getFlashBag()->get('sonata_flash_error') + ); + static::assertSame('list?filter%5Bfoo%5D=bar', $result->getTargetUrl()); + } + + public function testBatchActionDeleteWithModelManagerThrowableCustomErrorAndHandleThrowableOnly(): void + { + $modelManager = $this->createMock(ModelManagerInterface::class); + + $exception = new ModelManagerException( + $message = 'message', + 1234, + new \Exception($previousExceptionMessage = 'very useful message') + ); + + $modelManager->expects(static::once()) + ->method('batchDelete') + ->willReturnCallback(static function () use ($exception): void { + throw $exception; + }); + + $this->admin->expects(static::once()) + ->method('getModelManager') + ->willReturn($modelManager); + + $this->admin->expects(static::once()) + ->method('getFilterParameters') + ->willReturn(['foo' => 'bar']); + + $customController = new CustomModelManagerThrowableMessageController(); + $customController->setContainer($this->container); + $customController->configureAdmin($this->request); + + $this->translator->expects(static::never())->method('trans'); + + $result = $customController->batchActionDelete($this->createMock(ProxyQueryInterface::class)); + + static::assertInstanceOf(RedirectResponse::class, $result); + static::assertSame( + [CustomModelManagerThrowableMessageController::ERROR_MESSAGE], + $this->session->getFlashBag()->get('sonata_flash_error') + ); + static::assertSame('list?filter%5Bfoo%5D=bar', $result->getTargetUrl()); + } + public function testShowActionNotFoundException(): void { $this->request->attributes->set($this->admin->getIdParameter(), 21); @@ -1350,6 +1430,80 @@ public function testDeleteActionError(string $expectedToStringValue, string $toS static::assertSame('list', $response->getTargetUrl()); } + public function testDeleteActionWithModelManagerExceptionCustomError(): void + { + $this->request->attributes->set($this->admin->getIdParameter(), 21); + + $object = new \stdClass(); + + $this->admin->expects(static::once())->method('getObject')->willReturn($object); + $this->admin->expects(static::once())->method('checkAccess')->with(static::equalTo('delete')); + + $this->translator->expects(static::never())->method('trans'); + + $exception = new ModelManagerException( + $message = 'message', + 1234, + new \Exception($previousExceptionMessage = 'very useful message') + ); + + $this->admin->expects(static::once()) + ->method('delete') + ->willReturnCallback(static function () use ($exception): void { + throw $exception; + }); + + $this->request->setMethod(Request::METHOD_DELETE); + $this->request->request->set('_sonata_csrf_token', 'csrf-token-123_sonata.delete'); + + $customController = new CustomModelManagerExceptionMessageController(); + $customController->setContainer($this->container); + $customController->configureAdmin($this->request); + + $response = $customController->deleteAction($this->request); + + static::assertInstanceOf(RedirectResponse::class, $response); + static::assertSame([CustomModelManagerExceptionMessageController::ERROR_MESSAGE], $this->session->getFlashBag()->get('sonata_flash_error')); + static::assertSame('list', $response->getTargetUrl()); + } + + public function testDeleteActionWithModelManagerThrowableCustomError(): void + { + $this->request->attributes->set($this->admin->getIdParameter(), 21); + + $object = new \stdClass(); + + $this->admin->expects(static::once())->method('getObject')->willReturn($object); + $this->admin->expects(static::once())->method('checkAccess')->with(static::equalTo('delete')); + + $this->translator->expects(static::never())->method('trans'); + + $exception = new ModelManagerException( + $message = 'message', + 1234, + new \Exception($previousExceptionMessage = 'very useful message') + ); + + $this->admin->expects(static::once()) + ->method('delete') + ->willReturnCallback(static function () use ($exception): void { + throw $exception; + }); + + $this->request->setMethod(Request::METHOD_DELETE); + $this->request->request->set('_sonata_csrf_token', 'csrf-token-123_sonata.delete'); + + $customController = new CustomModelManagerThrowableMessageController(); + $customController->setContainer($this->container); + $customController->configureAdmin($this->request); + + $response = $customController->deleteAction($this->request); + + static::assertInstanceOf(RedirectResponse::class, $response); + static::assertSame([CustomModelManagerThrowableMessageController::ERROR_MESSAGE], $this->session->getFlashBag()->get('sonata_flash_error')); + static::assertSame('list', $response->getTargetUrl()); + } + public function testDeleteActionInvalidCsrfToken(): void { $this->request->attributes->set($this->admin->getIdParameter(), 21); @@ -1652,6 +1806,176 @@ public function testEditActionError(string $expectedToStringValue, string $toStr static::assertSame(['sonata_flash_error' => ['flash_edit_error']], $this->session->getFlashBag()->all()); } + public function testEditActionWithModelManagerExceptionAndCustomError(): void + { + $this->request->attributes->set($this->admin->getIdParameter(), 21); + + $object = new \stdClass(); + + $this->admin->expects(static::once()) + ->method('getObject') + ->willReturn($object); + + $this->admin->expects(static::once()) + ->method('checkAccess') + ->with(static::equalTo('edit')); + + $this->admin + ->method('getNormalizedIdentifier') + ->with(static::equalTo($object)) + ->willReturn('foo_normalized'); + + $form = $this->createMock(Form::class); + + $this->admin->expects(static::once()) + ->method('getForm') + ->willReturn($form); + + $form->expects(static::once()) + ->method('isSubmitted') + ->willReturn(true); + + $form->expects(static::once()) + ->method('isValid') + ->willReturn(true); + + $this->translator->expects(static::never())->method('trans'); + + $this->request->setMethod(Request::METHOD_POST); + + $formView = $this->createMock(FormView::class); + + $form + ->method('createView') + ->willReturn($formView); + + $form->expects(static::once()) + ->method('getData') + ->willReturn($object); + + $this->twig + ->expects(static::once()) + ->method('render') + ->with('@SonataAdmin/CRUD/edit.html.twig', [ + 'admin' => $this->admin, + 'base_template' => '@SonataAdmin/standard_layout.html.twig', + 'action' => 'edit', + 'form' => $formView, + 'object' => $object, + 'objectId' => 'foo_normalized', + ]); + + $customController = new CustomModelManagerExceptionMessageController(); + $customController->setContainer($this->container); + $customController->configureAdmin($this->request); + + $exception = new ModelManagerException( + $message = 'message', + 1234, + new \Exception($previousExceptionMessage = 'very useful message') + ); + + $this->admin->expects(static::once()) + ->method('update') + ->willReturnCallback(static function () use ($exception): void { + throw $exception; + }); + + $response = $customController->editAction($this->request); + + static::assertInstanceOf(Response::class, $response); + + static::assertSame( + ['sonata_flash_error' => [CustomModelManagerExceptionMessageController::ERROR_MESSAGE]], + $this->session->getFlashBag()->all() + ); + } + + public function testEditActionWithModelManagerThrowableAndCustomError(): void + { + $this->request->attributes->set($this->admin->getIdParameter(), 21); + + $object = new \stdClass(); + + $this->admin->expects(static::once()) + ->method('getObject') + ->willReturn($object); + + $this->admin->expects(static::once()) + ->method('checkAccess') + ->with(static::equalTo('edit')); + + $this->admin + ->method('getNormalizedIdentifier') + ->with(static::equalTo($object)) + ->willReturn('foo_normalized'); + + $form = $this->createMock(Form::class); + + $this->admin->expects(static::once()) + ->method('getForm') + ->willReturn($form); + + $form->expects(static::once()) + ->method('isSubmitted') + ->willReturn(true); + + $form->expects(static::once()) + ->method('isValid') + ->willReturn(true); + + $this->translator->expects(static::never())->method('trans'); + + $this->request->setMethod(Request::METHOD_POST); + + $formView = $this->createMock(FormView::class); + + $form + ->method('createView') + ->willReturn($formView); + + $form->expects(static::once()) + ->method('getData') + ->willReturn($object); + + $this->twig + ->expects(static::once()) + ->method('render') + ->with('@SonataAdmin/CRUD/edit.html.twig', [ + 'admin' => $this->admin, + 'base_template' => '@SonataAdmin/standard_layout.html.twig', + 'action' => 'edit', + 'form' => $formView, + 'object' => $object, + 'objectId' => 'foo_normalized', + ]); + + $customController = new CustomModelManagerThrowableMessageController(); + $customController->setContainer($this->container); + $customController->configureAdmin($this->request); + + $exception = new ModelManagerException( + $message = 'message', + 1234, + new \Exception($previousExceptionMessage = 'very useful message') + ); + + $this->admin->expects(static::once()) + ->method('update') + ->willReturnCallback(static function () use ($exception): void { + throw $exception; + }); + + $response = $customController->editAction($this->request); + + static::assertInstanceOf(Response::class, $response); + + static::assertSame( + ['sonata_flash_error' => [CustomModelManagerThrowableMessageController::ERROR_MESSAGE]], + $this->session->getFlashBag()->all() + ); + } + public function testEditActionAjaxSuccess(): void { $this->request->attributes->set($this->admin->getIdParameter(), 21); @@ -2324,6 +2648,168 @@ public function testCreateActionWithModelManagerException(string $expectedToStri static::assertSame(['sonata_flash_error' => ['flash_create_error']], $this->session->getFlashBag()->all()); } + public function testCreateActionWithModelManagerExceptionAndCustomError(): void + { + $this->admin->expects(static::once()) + ->method('checkAccess') + ->with(static::equalTo('create')); + + $this->admin + ->method('getClass') + ->willReturn(\stdClass::class); + + $object = new \stdClass(); + + $this->admin->expects(static::once()) + ->method('getNewInstance') + ->willReturn($object); + + $form = $this->createMock(Form::class); + + $this->admin->expects(static::once()) + ->method('getForm') + ->willReturn($form); + + $form->expects(static::once()) + ->method('isValid') + ->willReturn(true); + + $this->translator->expects(static::never())->method('trans'); + + $form->expects(static::once()) + ->method('isSubmitted') + ->willReturn(true); + + $form->expects(static::once()) + ->method('getData') + ->willReturn($object); + + $this->request->setMethod(Request::METHOD_POST); + + $formView = $this->createMock(FormView::class); + + $form + ->method('createView') + ->willReturn($formView); + + $this->twig + ->expects(static::once()) + ->method('render') + ->with('@SonataAdmin/CRUD/edit.html.twig', [ + 'admin' => $this->admin, + 'base_template' => '@SonataAdmin/standard_layout.html.twig', + 'action' => 'create', + 'form' => $formView, + 'object' => $object, + 'objectId' => null, + ]); + + $customController = new CustomModelManagerExceptionMessageController(); + $customController->setContainer($this->container); + $customController->configureAdmin($this->request); + + $exception = new ModelManagerException( + $message = 'message', + 1234, + new \Exception($previousExceptionMessage = 'very useful message') + ); + + $this->admin->expects(static::once()) + ->method('create') + ->willReturnCallback(static function () use ($exception): void { + throw $exception; + }); + + $response = $customController->createAction($this->request); + + static::assertInstanceOf(Response::class, $response); + static::assertSame( + ['sonata_flash_error' => [CustomModelManagerExceptionMessageController::ERROR_MESSAGE]], + $this->session->getFlashBag()->all() + ); + } + + public function testCreateActionWithModelManagerThrowableAndCustomError(): void + { + $this->admin->expects(static::once()) + ->method('checkAccess') + ->with(static::equalTo('create')); + + $this->admin + ->method('getClass') + ->willReturn(\stdClass::class); + + $object = new \stdClass(); + + $this->admin->expects(static::once()) + ->method('getNewInstance') + ->willReturn($object); + + $form = $this->createMock(Form::class); + + $this->admin->expects(static::once()) + ->method('getForm') + ->willReturn($form); + + $form->expects(static::once()) + ->method('isValid') + ->willReturn(true); + + $this->translator->expects(static::never())->method('trans'); + + $form->expects(static::once()) + ->method('isSubmitted') + ->willReturn(true); + + $form->expects(static::once()) + ->method('getData') + ->willReturn($object); + + $this->request->setMethod(Request::METHOD_POST); + + $formView = $this->createMock(FormView::class); + + $form + ->method('createView') + ->willReturn($formView); + + $this->twig + ->expects(static::once()) + ->method('render') + ->with('@SonataAdmin/CRUD/edit.html.twig', [ + 'admin' => $this->admin, + 'base_template' => '@SonataAdmin/standard_layout.html.twig', + 'action' => 'create', + 'form' => $formView, + 'object' => $object, + 'objectId' => null, + ]); + + $customController = new CustomModelManagerThrowableMessageController(); + $customController->setContainer($this->container); + $customController->configureAdmin($this->request); + + $exception = new ModelManagerException( + $message = 'message', + 1234, + new \Exception($previousExceptionMessage = 'very useful message') + ); + + $this->admin->expects(static::once()) + ->method('create') + ->willReturnCallback(static function () use ($exception): void { + throw $exception; + }); + + $response = $customController->createAction($this->request); + + static::assertInstanceOf(Response::class, $response); + static::assertSame( + ['sonata_flash_error' => [CustomModelManagerThrowableMessageController::ERROR_MESSAGE]], + $this->session->getFlashBag()->all() + ); + } + public function testCreateActionAjaxSuccess(): void { $object = new \stdClass();