commercetools-8.x-1.2-alpha1/src/EventSubscriber/CommercetoolsExceptionSubscriber.php
src/EventSubscriber/CommercetoolsExceptionSubscriber.php
<?php
namespace Drupal\commercetools\EventSubscriber;
use Commercetools\Exception\ApiClientException;
use Commercetools\Exception\UnauthorizedException;
use Drupal\commercetools\CommercetoolsService;
use Drupal\commercetools\Exception\CommercetoolsGraphqlErrorException;
use Drupal\commercetools\Exception\CommercetoolsMissingPermissionException;
use Drupal\commercetools\Exception\CommercetoolsOperationFailedException;
use Drupal\Core\Link;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\Core\Utility\Error;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Subscribes to kernel exceptions events and generates a Span Event.
*/
class CommercetoolsExceptionSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
use LoggerChannelTrait;
const REQUEST_FORMAT_HTML = 'html';
/**
* Constructs the CommercetoolsExceptionEventSubscriber object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* A Symfony container.
*/
public function __construct(
protected ContainerInterface $container,
) {
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
// Set weight to 60 to execute before the ExceptionLoggingSubscriber
// that logs the not found pages.
KernelEvents::EXCEPTION => ['onException', 60],
];
}
/**
* {@inheritdoc}
*/
public function onException(ExceptionEvent $event) {
// Act only on Commercetools related exceptions.
$eClass = get_class($event->getThrowable());
if (
!str_starts_with($eClass, 'Drupal\commercetools')
&& !str_starts_with($eClass, 'Commercetools\\')
) {
return;
}
$e = $event->getThrowable();
if ($e instanceof CommercetoolsOperationFailedException) {
$this->logException($e);
}
$configFactory = $this->container->get('config.factory');
$errorLevel = $configFactory->get('system.logging')->get('error_level');
// For the expected basic errors on the module side - displaying a more
// detailed errors with explanations on how to fix them.
$ctApi = $this->container->get('commercetools.api');
$displayConnectionError = $configFactory->get(CommercetoolsService::CONFIGURATION_NAME)->get(CommercetoolsService::CONFIG_DISPLAY_CONNECTION_ERRORS);
if ($displayConnectionError) {
$accessConfigured = $ctApi->isAccessConfigured();
$settingsLink = Link::fromTextAndUrl(t('commercetools settings page'), Url::fromRoute('commercetools.settings'))->toString();
$displayMessageLevel = MessengerInterface::TYPE_ERROR;
if (!$accessConfigured) {
$displayMessageLevel = MessengerInterface::TYPE_WARNING;
$displayMessage = $this->t('You have not configured the commercetools credentials. Open the @link and set the credentials.', [
'@link' => $settingsLink,
]) . ' ' . $this->addDemoModuleLink();
}
elseif (
$e instanceof CommercetoolsOperationFailedException
|| $e instanceof CommercetoolsGraphqlErrorException
) {
// The text coming from variables is predictable, we have translate it.
// phpcs:ignore
$displayMessage = $this->t($e->getMessage());
if ($e instanceof CommercetoolsGraphqlErrorException) {
$displayMessage = 'commercetools GraphQL error: ' . $displayMessage;
}
if (
$errorLevel !== ERROR_REPORTING_HIDE
&& $ePrevious = $e->getPrevious()
) {
if ($ePrevious instanceof UnauthorizedException) {
$displayMessage = $this->t('Commercetools API responds with the access denied error using the configured credentials. Check the credentials and the API host on the @link page and check the logs for the more detailed information.', [
'@link' => $settingsLink,
]) . ' ' . $this->addDemoModuleLink();
}
else {
$displayMessage .= "\n\nParent error: " . $ePrevious->getMessage();
$displayMessage = rtrim($displayMessage, '. ') . '. ';
if ($ePrevious instanceof BadResponseException || $ePrevious instanceof RequestException) {
$ePreviousResponse = $ePrevious->getResponse();
if ($ePreviousResponse) {
$errorMessages = $this->parseGraphqlErrorResponse($ePreviousResponse->getBody()->__toString());
$displayMessage .= " Response error: " . implode("\n", $errorMessages) . "\n\n";
}
}
}
}
}
elseif ($e instanceof CommercetoolsMissingPermissionException) {
$displayMessage = $this->t('The operation failed because of the missing permissions on the commercetools account using the configured credentials. Check the permission configuration on the commercetools Merchant Center.', [
'@link' => $settingsLink,
]);
}
else {
// Do nothing to display the original exception.
}
if (
isset($displayMessage)
&& $event->getRequest()->getRequestFormat() == self::REQUEST_FORMAT_HTML
) {
if ($this->container->has('messenger')) {
$this->container->get('messenger')->addMessage(nl2br($displayMessage), $displayMessageLevel);
// Fall back to the Drupal default access denied page to display
// the message.
$e = new AccessDeniedHttpException($displayMessage, $e);
$event->setThrowable($e);
return;
}
}
else {
$ePrevious = $e->getPrevious();
if ($ePrevious) {
$parentMessage = $ePrevious->getMessage();
$parentClass = get_class($ePrevious);
$e = new CommercetoolsOperationFailedException("$displayMessage Parent exception ($parentClass): $parentMessage", 0, $e);
$event->setThrowable($e);
}
}
}
}
/**
* Adds the demo module information.
*/
private function addDemoModuleLink() {
if ($this->container->has('module_handler')) {
$moduleHandler = $this->container->get('module_handler');
if ($moduleHandler->moduleExists('commercetools_demo')) {
return $this->t('Also, you can configure the demo credentials on the @link page.', [
'@link' => Link::fromTextAndUrl('commercetools Demo configuration', Url::fromRoute('commercetools_demo.settings'))->toString(),
]);
}
else {
return $this->t('Also, you can <a href=@url>install the "commercetools Demo" module</a> to test the functionality with pre-configured commercetools accounts.', [
'@url' => Url::fromRoute('system.modules_list', options: ['fragment' => 'module-commercetools-demo'])->toString(),
]);
}
}
}
/**
* Parses the GraphQL error response and returns an array of error texts.
*
* @param string $response
* The GraphQL response text.
*
* @return array|null
* An array with error messages, or null if no messages found.
*/
private function parseGraphqlErrorResponse(string $response): ?array {
$data = json_decode($response, TRUE);
if (isset($data['errors'])) {
foreach ($data['errors'] as $error) {
if (isset($error['message'])) {
$messages[] = $error['message'];
}
}
}
return $messages ?? NULL;
}
/**
* Logs a Commercetools API exception.
*/
protected function logException(\Throwable $e): void {
$error = Error::decodeException($e);
$previous = $e->getPrevious();
if ($previous instanceof ApiClientException) {
$error['@message'] .= '; ' . $previous->getMessage();
}
$this->getLogger('commercetools_api')
->error(Error::DEFAULT_ERROR_MESSAGE, $error);
}
}
