Skip to content

Commit

Permalink
feature #1473 Translatable exception messages (javiereguiluz)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the master branch (closes #1473).

Discussion
----------

Translatable exception messages

I was working on #1424 when I realized that the proposed solution was too complicated. The problem was that we can't translate exceptions inside the templates (the natural place) because we don't store the translation parameters anywhere.

So, I propose to create a new `ExceptionContext` class to store all the related information about exceptions. The idea would be to add in the translation files the public message displayed to the end-user. In contrast, the debug message stored in the log file (and displayed in the `dev` environment) would always be in English.

In this example, a French user would see this in the browser:

![exception-public](https://cloud.githubusercontent.com/assets/73419/22000123/ca158084-dc3d-11e6-9109-700f2c8aa4c8.png)

...and this message in the log file:

![exception-private](https://cloud.githubusercontent.com/assets/73419/22000129/d0d2c648-dc3d-11e6-9b1b-fdacabb9e55e.png)

---

This PR is still WIP but ... what do you think? Thanks!

Commits
-------

f62d3ea Translatable exception messages
  • Loading branch information
javiereguiluz committed Jan 19, 2017
2 parents 7a69d8f + f62d3ea commit 83f7353
Show file tree
Hide file tree
Showing 19 changed files with 208 additions and 67 deletions.
3 changes: 2 additions & 1 deletion Configuration/ConfigManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace JavierEguiluz\Bundle\EasyAdminBundle\Configuration;

use JavierEguiluz\Bundle\EasyAdminBundle\Exception\UndefinedEntityException;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
Expand Down Expand Up @@ -84,7 +85,7 @@ public function getEntityConfig($entityName)
{
$backendConfig = $this->getBackendConfig();
if (!isset($backendConfig['entities'][$entityName])) {
throw new \InvalidArgumentException(sprintf('Entity "%s" is not managed by EasyAdmin.', $entityName));
throw new UndefinedEntityException(array('entity_name' => $entityName));
}

return $backendConfig['entities'][$entityName];
Expand Down
4 changes: 2 additions & 2 deletions Controller/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function indexAction(Request $request)

$action = $request->query->get('action', 'list');
if (!$this->isActionAllowed($action)) {
throw new ForbiddenActionException(array('action' => $action, 'entity' => $this->entity['name']));
throw new ForbiddenActionException(array('action' => $action, 'entity_name' => $this->entity['name']));
}

return $this->executeDynamicMethod($action.'<EntityName>Action');
Expand Down Expand Up @@ -338,7 +338,7 @@ protected function deleteAction()
$this->em->remove($entity);
$this->em->flush();
} catch (ForeignKeyConstraintViolationException $e) {
throw new EntityRemoveException(array('entity' => $this->entity['name']));
throw new EntityRemoveException(array('entity_name' => $this->entity['name']));
}

$this->dispatch(EasyAdminEvents::POST_REMOVE, array('entity' => $entity));
Expand Down
2 changes: 1 addition & 1 deletion EventListener/RequestPostInitializeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ private function findCurrentItem(array $entityConfig, $itemId)
{
$manager = $this->doctrine->getManagerForClass($entityConfig['class']);
if (null === $entity = $manager->getRepository($entityConfig['class'])->find($itemId)) {
throw new EntityNotFoundException(array('entity' => $entityConfig, 'entity_id' => $itemId));
throw new EntityNotFoundException(array('entity_name' => $entityConfig['name'], 'entity_id_name' => $entityConfig['primary_key_field_name'], 'entity_id_value' => $itemId));
}

return $entity;
Expand Down
38 changes: 23 additions & 15 deletions Exception/BaseException.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,40 @@
*/
class BaseException extends \RuntimeException
{
// the HTTP status code of the Response created for the exception
protected $message;
// this is the error message that can be safely displayed to end users
private $safeMessage;
// this is the full error message displayed only in 'dev' environment and logs
private $statusCode;
private $context;

/**
* @param string $errorMessage
* @param string $proposedSolution
* @param int $statusCode
* @param ExceptionContext $context
*/
public function __construct($errorMessage, $proposedSolution = '', $statusCode = 500)
public function __construct(ExceptionContext $context)
{
$this->safeMessage = $errorMessage;
$this->message = sprintf('Error: %s Solution: %s', $errorMessage, $proposedSolution);
$this->statusCode = $statusCode;
$this->message = $context->getDebugMessage();
$this->context = $context;
}

public function getSafeMessage()
public function getContext()
{
return $this->safeMessage;
return $this->context;
}

public function getPublicMessage()
{
return $this->context->getPublicMessage();
}

public function getDebugMessage()
{
return $this->context->getDebugMessage();
}

public function getParameters()
{
return $this->context->getParameters();
}

public function getStatusCode()
{
return $this->statusCode;
return $this->context->getStatusCode();
}
}
10 changes: 7 additions & 3 deletions Exception/EntityNotFoundException.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ class EntityNotFoundException extends BaseException
{
public function __construct(array $parameters = array())
{
$errorMessage = sprintf('The "%s" entity with "%s = %s" does not exist in the database.', $parameters['entity']['name'], $parameters['entity']['primary_key_field_name'], $parameters['entity_id']);
$proposedSolution = sprintf('Check that the mentioned entity hasn\'t been deleted by mistake.');
$exceptionContext = new ExceptionContext(
'exception.entity_not_found',
sprintf('The "%s" entity with "%s = %s" does not exist in the database. The entity may have been deleted by mistake or by a "cascade={"remove"}" operation executed by Doctrine.', $parameters['entity_name'], $parameters['entity_id_name'], $parameters['entity_id_value']),
$parameters,
404
);

parent::__construct($errorMessage, $proposedSolution, 404);
parent::__construct($exceptionContext);
}
}
10 changes: 7 additions & 3 deletions Exception/EntityRemoveException.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ class EntityRemoveException extends BaseException
{
public function __construct(array $parameters = array())
{
$errorMessage = sprintf('You can\'t delete this "%s" item because other items depend on it in the database.', $parameters['entity']);
$proposedSolution = "Don't delete this item or change the database configuration to allow deleting it.";
$exceptionContext = new ExceptionContext(
'exception.entity_remove',
sprintf('There is a ForeignKeyConstraintViolationException for the Doctrine entity associated with "%s". Solution: disable the "delete" action for this entity or configure the "cascade={"remove"}" attribute for the related property in the Doctrine entity.', $parameters['entity_name']),
$parameters,
404
);

parent::__construct($errorMessage, $proposedSolution, 404);
parent::__construct($exceptionContext);
}
}
73 changes: 73 additions & 0 deletions Exception/ExceptionContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/*
* This file is part of the EasyAdminBundle.
*
* (c) Javier Eguiluz <javier.eguiluz@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace JavierEguiluz\Bundle\EasyAdminBundle\Exception;

/**
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class ExceptionContext
{
private $publicMessage;
private $debugMessage;
private $parameters;
private $statusCode;

public function __construct($publicMessage, $debugMessage = '', $parameters = array(), $statusCode = 500)
{
$this->publicMessage = $publicMessage;
$this->debugMessage = $debugMessage;
$this->parameters = $parameters;
$this->statusCode = $statusCode;
}

public function getPublicMessage()
{
return $this->publicMessage;
}

public function getDebugMessage()
{
return $this->debugMessage;
}

public function getParameters()
{
return $this->parameters;
}

public function getTranslationParameters()
{
return $this->transformIntoTranslationPlaceholders($this->parameters);
}

public function getStatusCode()
{
return $this->statusCode;
}

private function transformIntoTranslationPlaceholders(array $parameters)
{
$placeholders = array();
foreach ($parameters as $key => $value) {
if ('%' !== $key[0]) {
$key = '%'.$key;
}
if ('%' !== substr($key, -1)) {
$key = $key.'%';
}

$placeholders[$key] = $value;
}

return $placeholders;
}
}
36 changes: 22 additions & 14 deletions Exception/FlattenException.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
*/
class FlattenException extends BaseFlattenException
{
/** @var string */
private $safeMessage;
/** @var ExceptionContext */
private $context;

public static function create(\Exception $exception, $statusCode = null, array $headers = array())
{
Expand All @@ -29,25 +29,33 @@ public static function create(\Exception $exception, $statusCode = null, array $

/** @var FlattenException $e */
$e = parent::create($exception, $statusCode, $headers);
$e->setStatusCode($exception->getStatusCode());
$e->setSafeMessage($exception->getSafeMessage());
$e->context = $exception->getContext();

return $e;
}

/**
* @return string
*/
public function getSafeMessage()
public function getPublicMessage()
{
return $this->safeMessage;
return $this->context->getPublicMessage();
}

/**
* @param string $safeMessage
*/
public function setSafeMessage($safeMessage)
public function getDebugMessage()
{
$this->safeMessage = $safeMessage;
return $this->context->getDebugMessage();
}

public function getParameters()
{
return $this->context->getParameters();
}

public function getTranslationParameters()
{
return $this->context->getTranslationParameters();
}

public function getStatusCode()
{
return $this->context->getStatusCode();
}
}
10 changes: 7 additions & 3 deletions Exception/ForbiddenActionException.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ class ForbiddenActionException extends BaseException
{
public function __construct(array $parameters = array())
{
$errorMessage = sprintf('The requested "%s" action is not allowed for the "%s" entity.', $parameters['action'], $parameters['entity']);
$proposedSolution = sprintf('Remove the "%s" action from the "disabled_actions" option, which can be configured globally for the entire backend or locally for the "%s" entity.', $parameters['action'], $parameters['entity']);
$exceptionContext = new ExceptionContext(
'exception.forbidden_action',
sprintf('The requested "%s" action is not allowed for the "%s" entity. Solution: remove the "%s" action from the "disabled_actions" option, which can be configured globally for the entire backend or locally for the "%s" entity.', $parameters['action'], $parameters['entity_name'], $parameters['action'], $parameters['entity_name']),
$parameters,
403
);

parent::__construct($errorMessage, $proposedSolution, 403);
parent::__construct($exceptionContext);
}
}
10 changes: 7 additions & 3 deletions Exception/NoEntitiesConfiguredException.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ class NoEntitiesConfiguredException extends BaseException
{
public function __construct(array $parameters = array())
{
$errorMessage = 'Your backend is empty because you haven\'t configured any Doctrine entity to manage.';
$proposedSolution = 'Open your "app/config/config.yml" file and configure the backend under the "easy_admin" key.';
$exceptionContext = new ExceptionContext(
'exception.no_entities_configured',
'The backend is empty because you haven\'t configured any Doctrine entity to manage. Solution: open your "app/config/config.yml" file and configure the backend under the "easy_admin" key.',
$parameters,
500
);

parent::__construct($errorMessage, $proposedSolution);
parent::__construct($exceptionContext);
}
}
10 changes: 7 additions & 3 deletions Exception/UndefinedEntityException.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ class UndefinedEntityException extends BaseException
{
public function __construct(array $parameters = array())
{
$errorMessage = sprintf('The "%s" entity is not defined in the configuration of your backend.', $parameters['entity_name']);
$proposedSolution = sprintf('Open your "app/config/config.yml" file and add the "%s" entity to the list of entities managed by EasyAdmin. NOTE: If your project worked before, this error may be caused by a change introduced by EasyAdmin 1.12.0 version. Check out https://github.com/javiereguiluz/EasyAdminBundle/releases/tag/v1.12.0 for more details.', $parameters['entity_name']);
$exceptionContext = new ExceptionContext(
'exception.undefined_entity',
sprintf('The "%s" entity is not defined in the configuration of your backend. Solution: open your "app/config/config.yml" file and add the "%s" entity to the list of entities managed by EasyAdmin.', $parameters['entity_name'], $parameters['entity_name']),
$parameters,
404
);

parent::__construct($errorMessage, $proposedSolution);
parent::__construct($exceptionContext);
}
}
22 changes: 22 additions & 0 deletions Resources/translations/EasyAdminBundle.en.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,28 @@
<source>show.remaining_items</source>
<target><![CDATA[{1} there is another item not displayed in this listing|]1,Inf] %count% other items are not displayed in this listing]]></target>
</trans-unit>

<!-- Exceptions -->
<trans-unit id="exception.entity_not_found">
<source>exception.entity_not_found</source>
<target>This item is no longer available.</target>
</trans-unit>
<trans-unit id="exception.entity_remove">
<source>exception.entity_remove</source>
<target>This item can't be deleted because other items depend on it.</target>
</trans-unit>
<trans-unit id="exception.forbidden_action">
<source>exception.forbidden_action</source>
<target>The requested action can't be performed on this item.</target>
</trans-unit>
<trans-unit id="exception.no_entities_configured">
<source>exception.no_entities_configured</source>
<target>The application is not properly configured.</target>
</trans-unit>
<trans-unit id="exception.undefined_entity">
<source>exception.undefined_entity</source>
<target>The application is not properly configured for this kind of items.</target>
</trans-unit>
</body>
</file>
</xliff>
15 changes: 9 additions & 6 deletions Resources/views/css/easyadmin.css.twig
Original file line number Diff line number Diff line change
Expand Up @@ -1096,27 +1096,30 @@ body.show .form-control.text {
{# -------------------------------------------------------------------------
ERROR PAGES
------------------------------------------------------------------------- #}
body.error .content-wrapper {
align-items: center;
display: flex;
}
body.error .error-description {
background: {{ colors.white }};
border: 1px solid {{ colors.gray_lighter }};
box-shadow: 0 0 3px {{ colors.gray_light }};
margin: 2em auto 2em;
max-width: 90%;
min-height: 150px;
max-width: 96%;
padding: 0;
}
body.error .error-description h1 {
background: {{ colors.danger }};
color: {{ colors.white }};
font-size: 18px;
font-weight: bold;
margin-top: 0;
margin: 0;
padding: 10px;
text-transform: uppercase;
}
body.error .error-message {
font-size: 16px;
padding: 15px;
padding: 45px 30px;
text-align: center;
}

{# =========================================================================
Expand Down Expand Up @@ -1287,7 +1290,7 @@ body.error .error-message {
}

body.error .error-description {
max-width: 70%;
max-width: 550px;
}

.pagination > li > a,
Expand Down

0 comments on commit 83f7353

Please sign in to comment.