vendor/easycorp/easyadmin-bundle/src/Twig/EasyAdminTwigExtension.php line 107

Open in your IDE?
  1. <?php
  2. namespace EasyCorp\Bundle\EasyAdminBundle\Twig;
  3. use Doctrine\ORM\Mapping\ClassMetadata;
  4. use EasyCorp\Bundle\EasyAdminBundle\Configuration\ConfigManager;
  5. use EasyCorp\Bundle\EasyAdminBundle\Router\EasyAdminRouter;
  6. use EasyCorp\Bundle\EasyAdminBundle\Security\AuthorizationChecker;
  7. use Symfony\Component\HttpKernel\Kernel;
  8. use Symfony\Component\Intl\Countries;
  9. use Symfony\Component\Intl\Exception\MissingResourceException;
  10. use Symfony\Component\Intl\Intl;
  11. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  12. use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
  13. use Symfony\Contracts\Translation\TranslatorInterface;
  14. use Twig\Environment;
  15. use Twig\Extension\AbstractExtension;
  16. use Twig\TwigFilter;
  17. use Twig\TwigFunction;
  18. /**
  19.  * Defines the filters and functions used to render the bundle's templates.
  20.  *
  21.  * @author Javier Eguiluz <javier.eguiluz@gmail.com>
  22.  */
  23. class EasyAdminTwigExtension extends AbstractExtension
  24. {
  25.     private $configManager;
  26.     private $propertyAccessor;
  27.     private $easyAdminRouter;
  28.     private $debug;
  29.     private $logoutUrlGenerator;
  30.     /** @var TranslatorInterface|null */
  31.     private $translator;
  32.     private $authorizationChecker;
  33.     public function __construct(ConfigManager $configManagerPropertyAccessorInterface $propertyAccessorEasyAdminRouter $easyAdminRouterbool $debug, ?LogoutUrlGenerator $logoutUrlGenerator$translatorAuthorizationChecker $authorizationChecker)
  34.     {
  35.         $this->configManager $configManager;
  36.         $this->propertyAccessor $propertyAccessor;
  37.         $this->easyAdminRouter $easyAdminRouter;
  38.         $this->debug $debug;
  39.         $this->logoutUrlGenerator $logoutUrlGenerator;
  40.         $this->translator $translator;
  41.         $this->authorizationChecker $authorizationChecker;
  42.     }
  43.     /**
  44.      * {@inheritdoc}
  45.      */
  46.     public function getFunctions()
  47.     {
  48.         return [
  49.             new TwigFunction('easyadmin_render_field_for_*_view', [$this'renderEntityField'], ['is_safe' => ['html'], 'needs_environment' => true]),
  50.             new TwigFunction('easyadmin_config', [$this'getBackendConfiguration']),
  51.             new TwigFunction('easyadmin_entity', [$this'getEntityConfiguration']),
  52.             new TwigFunction('easyadmin_path', [$this'getEntityPath']),
  53.             new TwigFunction('easyadmin_action_is_enabled', [$this'isActionEnabled']),
  54.             new TwigFunction('easyadmin_action_is_enabled_for_*_view', [$this'isActionEnabled']),
  55.             new TwigFunction('easyadmin_get_action', [$this'getActionConfiguration']),
  56.             new TwigFunction('easyadmin_get_action_for_*_view', [$this'getActionConfiguration']),
  57.             new TwigFunction('easyadmin_get_actions_for_*_item', [$this'getActionsForItem']),
  58.             new TwigFunction('easyadmin_logout_path', [$this'getLogoutPath']),
  59.             new TwigFunction('easyadmin_read_property', [$this'readProperty']),
  60.             new TwigFunction('easyadmin_is_granted', [$this'isGranted']),
  61.         ];
  62.     }
  63.     /**
  64.      * {@inheritdoc}
  65.      */
  66.     public function getFilters()
  67.     {
  68.         $filters = [
  69.             new TwigFilter('easyadmin_truncate', [$this'truncateText'], ['needs_environment' => true]),
  70.             new TwigFilter('easyadmin_urldecode''urldecode'),
  71.             new TwigFilter('easyadmin_form_hidden_params', [$this'getFormHiddenParams']),
  72.             new TwigFilter('easyadmin_filesize', [$this'fileSize']),
  73.         ];
  74.         if (Kernel::VERSION_ID >= 40200) {
  75.             $filters[] = new TwigFilter('transchoice', [$this'transchoice']);
  76.         }
  77.         return $filters;
  78.     }
  79.     public function fileSize(int $bytes): string
  80.     {
  81.         $size = ['B''K''M''G''T''P''E''Z''Y'];
  82.         $factor = (int) floor(log($bytes) / log(1024));
  83.         return (int) ($bytes / (1024 ** $factor)).@$size[$factor];
  84.     }
  85.     /**
  86.      * Returns the entire backend configuration or the value corresponding to
  87.      * the provided key. The dots of the key are automatically transformed into
  88.      * nested keys. Example: 'assets.css' => $config['assets']['css'].
  89.      *
  90.      * @param string|null $key
  91.      *
  92.      * @return mixed
  93.      */
  94.     public function getBackendConfiguration($key null)
  95.     {
  96.         return $this->configManager->getBackendConfig($key);
  97.     }
  98.     /**
  99.      * Returns the entire configuration of the given entity.
  100.      *
  101.      * @param string $entityName
  102.      *
  103.      * @return array|null
  104.      */
  105.     public function getEntityConfiguration($entityName)
  106.     {
  107.         return null !== $this->getBackendConfiguration('entities.'.$entityName)
  108.             ? $this->configManager->getEntityConfig($entityName)
  109.             : null;
  110.     }
  111.     /**
  112.      * @param object|string $entity
  113.      * @param string        $action
  114.      * @param array         $parameters
  115.      *
  116.      * @return string
  117.      */
  118.     public function getEntityPath($entity$action, array $parameters = [])
  119.     {
  120.         return $this->easyAdminRouter->generate($entity$action$parameters);
  121.     }
  122.     /**
  123.      * Renders the value stored in a property/field of the given entity. This
  124.      * function contains a lot of code protections to avoid errors when the
  125.      * property doesn't exist or its value is not accessible. This ensures that
  126.      * the function never generates a warning or error message when calling it.
  127.      *
  128.      * @param Environment $twig
  129.      * @param string      $view          The view in which the item is being rendered
  130.      * @param string      $entityName    The name of the entity associated with the item
  131.      * @param object      $item          The item which is being rendered
  132.      * @param array       $fieldMetadata The metadata of the actual field being rendered
  133.      *
  134.      * @return string
  135.      *
  136.      * @throws \Exception
  137.      */
  138.     public function renderEntityField(Environment $twig$view$entityName$item, array $fieldMetadata)
  139.     {
  140.         $entityConfiguration $this->configManager->getEntityConfig($entityName);
  141.         $hasCustomTemplate !== strpos($fieldMetadata['template'], '@EasyAdmin/');
  142.         $templateParameters = [];
  143.         try {
  144.             $templateParameters $this->getTemplateParameters($entityName$view$fieldMetadata$item);
  145.             // if the field defines a custom template, render it (no matter if the value is null or inaccessible)
  146.             if ($hasCustomTemplate) {
  147.                 return $twig->render($fieldMetadata['template'], $templateParameters);
  148.             }
  149.             if (false === $templateParameters['is_accessible']) {
  150.                 return $twig->render($entityConfiguration['templates']['label_inaccessible'], $templateParameters);
  151.             }
  152.             if (null === $templateParameters['value']) {
  153.                 return $twig->render($entityConfiguration['templates']['label_null'], $templateParameters);
  154.             }
  155.             if (empty($templateParameters['value']) && \in_array($fieldMetadata['dataType'], ['image''file''array''simple_array'])) {
  156.                 return $twig->render($templateParameters['entity_config']['templates']['label_empty'], $templateParameters);
  157.             }
  158.             return $twig->render($fieldMetadata['template'], $templateParameters);
  159.         } catch (\Exception $e) {
  160.             if ($this->debug) {
  161.                 throw $e;
  162.             }
  163.             return $twig->render($entityConfiguration['templates']['label_undefined'], $templateParameters);
  164.         }
  165.     }
  166.     private function getTemplateParameters($entityName$view, array $fieldMetadata$item)
  167.     {
  168.         $fieldName $fieldMetadata['property'];
  169.         $fieldType $fieldMetadata['dataType'];
  170.         $parameters = [
  171.             'backend_config' => $this->getBackendConfiguration(),
  172.             'entity_config' => $this->configManager->getEntityConfig($entityName),
  173.             'field_options' => $fieldMetadata,
  174.             'item' => $item,
  175.             'view' => $view,
  176.         ];
  177.         if ($this->propertyAccessor->isReadable($item$fieldName)) {
  178.             $parameters['value'] = $this->propertyAccessor->getValue($item$fieldName);
  179.             $parameters['is_accessible'] = true;
  180.         } else {
  181.             $parameters['value'] = null;
  182.             $parameters['is_accessible'] = false;
  183.         }
  184.         if ('image' === $fieldType) {
  185.             $parameters $this->addImageFieldParameters($parameters);
  186.         }
  187.         if ('file' === $fieldType) {
  188.             $parameters $this->addFileFieldParameters($parameters);
  189.         }
  190.         if ('association' === $fieldType) {
  191.             $parameters $this->addAssociationFieldParameters($parameters);
  192.         }
  193.         if ('country' === $fieldType) {
  194.             $parameters['value'] = null !== $parameters['value'] ? strtoupper($parameters['value']) : null;
  195.             $parameters['country_name'] = $this->getCountryName($parameters['value']);
  196.         }
  197.         if ('avatar' === $fieldType) {
  198.             $parameters['image_height'] = $fieldMetadata['height'];
  199.             if ($fieldMetadata['is_image_url'] ?? false) {
  200.                 $parameters['image_url'] = $parameters['value'];
  201.             } else {
  202.                 $parameters['image_url'] = null === $parameters['value'] ? null sprintf('https://www.gravatar.com/avatar/%s?s=%d&d=mp'md5($parameters['value']), $parameters['image_height']);
  203.             }
  204.         }
  205.         // when a virtual field doesn't define it's type, consider it a string
  206.         if (true === $fieldMetadata['virtual'] && null === $parameters['field_options']['dataType']) {
  207.             $parameters['value'] = (string) $parameters['value'];
  208.         }
  209.         return $parameters;
  210.     }
  211.     private function addImageFieldParameters(array $templateParameters)
  212.     {
  213.         // add the base path only to images that are not absolute URLs (http or https) or protocol-relative URLs (//)
  214.         if (null !== $templateParameters['value'] && === preg_match('/^(http[s]?|\/\/)/i'$templateParameters['value'])) {
  215.             $templateParameters['value'] = isset($templateParameters['field_options']['base_path'])
  216.                 ? rtrim($templateParameters['field_options']['base_path'], '/').'/'.ltrim($templateParameters['value'], '/')
  217.                 : '/'.ltrim($templateParameters['value'], '/');
  218.         }
  219.         $templateParameters['uuid'] = md5($templateParameters['value']);
  220.         return $templateParameters;
  221.     }
  222.     private function addFileFieldParameters(array $templateParameters)
  223.     {
  224.         // add the base path only to files that are not absolute URLs (http or https) or protocol-relative URLs (//)
  225.         if (null !== $templateParameters['value'] && === preg_match('/^(http[s]?|\/\/)/i'$templateParameters['value'])) {
  226.             $templateParameters['value'] = isset($templateParameters['field_options']['base_path'])
  227.                 ? rtrim($templateParameters['field_options']['base_path'], '/').'/'.ltrim($templateParameters['value'], '/')
  228.                 : '/'.ltrim($templateParameters['value'], '/');
  229.         }
  230.         $templateParameters['filename'] = $templateParameters['field_options']['filename'] ?? basename($templateParameters['value']);
  231.         return $templateParameters;
  232.     }
  233.     private function addAssociationFieldParameters(array $templateParameters)
  234.     {
  235.         $targetEntityConfig $this->configManager->getEntityConfigByClass($templateParameters['field_options']['targetEntity']);
  236.         // the associated entity is not managed by EasyAdmin
  237.         if (null === $targetEntityConfig) {
  238.             return $templateParameters;
  239.         }
  240.         $isShowActionAllowed = !\in_array('show'$targetEntityConfig['disabled_actions']);
  241.         if ($templateParameters['field_options']['associationType'] & ClassMetadata::TO_ONE) {
  242.             if ($this->propertyAccessor->isReadable($templateParameters['value'], $targetEntityConfig['primary_key_field_name'])) {
  243.                 $primaryKeyValue $this->propertyAccessor->getValue($templateParameters['value'], $targetEntityConfig['primary_key_field_name']);
  244.             } else {
  245.                 $primaryKeyValue null;
  246.             }
  247.             // get the string representation of the associated *-to-one entity
  248.             if (method_exists($templateParameters['value'], '__toString')) {
  249.                 $templateParameters['value'] = (string) $templateParameters['value'];
  250.             } elseif (null !== $primaryKeyValue) {
  251.                 $templateParameters['value'] = sprintf('%s #%s'$targetEntityConfig['name'], $primaryKeyValue);
  252.             } else {
  253.                 $templateParameters['value'] = null;
  254.             }
  255.             // if the associated entity is managed by EasyAdmin, and the "show"
  256.             // action is enabled for the associated entity, display a link to it
  257.             if (null !== $targetEntityConfig && null !== $primaryKeyValue && $isShowActionAllowed) {
  258.                 $templateParameters['link_parameters'] = [
  259.                     'action' => 'show',
  260.                     'entity' => $targetEntityConfig['name'],
  261.                     // casting to string is needed because entities can use objects as primary keys
  262.                     'id' => (string) $primaryKeyValue,
  263.                 ];
  264.             }
  265.         }
  266.         if ($templateParameters['field_options']['associationType'] & ClassMetadata::TO_MANY) {
  267.             // if the associated entity is managed by EasyAdmin, and the "show"
  268.             // action is enabled for the associated entity, display a link to it
  269.             if (null !== $targetEntityConfig && $isShowActionAllowed) {
  270.                 $templateParameters['link_parameters'] = [
  271.                     'action' => 'show',
  272.                     'entity' => $targetEntityConfig['name'],
  273.                     'primary_key_name' => $targetEntityConfig['primary_key_field_name'],
  274.                 ];
  275.             }
  276.         }
  277.         return $templateParameters;
  278.     }
  279.     /**
  280.      * Checks whether the given 'action' is enabled for the given 'entity'.
  281.      *
  282.      * @param string $view
  283.      * @param string $action
  284.      * @param string $entityName
  285.      *
  286.      * @return bool
  287.      */
  288.     public function isActionEnabled($view$action$entityName)
  289.     {
  290.         return $this->configManager->isActionEnabled($entityName$view$action);
  291.     }
  292.     /**
  293.      * Returns the full action configuration for the given 'entity' and 'view'.
  294.      *
  295.      * @param string $view
  296.      * @param string $action
  297.      * @param string $entityName
  298.      *
  299.      * @return array
  300.      */
  301.     public function getActionConfiguration($view$action$entityName)
  302.     {
  303.         return $this->configManager->getActionConfig($entityName$view$action);
  304.     }
  305.     /**
  306.      * Returns the actions configured for each item displayed in the given view.
  307.      * This method is needed because some actions are displayed globally for the
  308.      * entire view (e.g. 'new' action in 'list' view).
  309.      *
  310.      * @param string $view
  311.      * @param string $entityName
  312.      *
  313.      * @return array
  314.      */
  315.     public function getActionsForItem($view$entityName)
  316.     {
  317.         try {
  318.             $entityConfig $this->configManager->getEntityConfig($entityName);
  319.         } catch (\Exception $e) {
  320.             return [];
  321.         }
  322.         $disabledActions $entityConfig['disabled_actions'];
  323.         $viewActions $entityConfig[$view]['actions'];
  324.         $actionsExcludedForItems = [
  325.             'list' => ['new''search'],
  326.             'edit' => [],
  327.             'new' => [],
  328.             'show' => [],
  329.         ];
  330.         $excludedActions $actionsExcludedForItems[$view];
  331.         return array_filter($viewActions, function ($action) use ($excludedActions$disabledActions) {
  332.             return !\in_array($action['name'], $excludedActions) && !\in_array($action['name'], $disabledActions);
  333.         });
  334.     }
  335.     /*
  336.      * Copied from the official Text Twig extension.
  337.      *
  338.      * code: https://github.com/twigphp/Twig-extensions/blob/master/lib/Twig/Extensions/Extension/Text.php
  339.      * author: Henrik Bjornskov <hb@peytz.dk>
  340.      * copyright holder: (c) 2009 Fabien Potencier
  341.      *
  342.      * @return string
  343.      */
  344.     public function truncateText(Environment $env$value$length 64$preserve false$separator '...')
  345.     {
  346.         try {
  347.             $value = (string) $value;
  348.         } catch (\Exception $e) {
  349.             $value '';
  350.         }
  351.         if (mb_strlen($value$env->getCharset()) > $length) {
  352.             if ($preserve) {
  353.                 // If breakpoint is on the last word, return the value without separator.
  354.                 if (false === ($breakpoint mb_strpos($value' '$length$env->getCharset()))) {
  355.                     return $value;
  356.                 }
  357.                 $length $breakpoint;
  358.             }
  359.             return rtrim(mb_substr($value0$length$env->getCharset())).$separator;
  360.         }
  361.         return $value;
  362.     }
  363.     public function getFormHiddenParams(array $paramsstring $prefix): iterable
  364.     {
  365.         foreach ($params as $key => $value) {
  366.             $key $prefix.'['.$key.']';
  367.             if (\is_array($value)) {
  368.                 yield from $this->getFormHiddenParams($value$key);
  369.             } else {
  370.                 yield $key => $value;
  371.             }
  372.         }
  373.     }
  374.     /**
  375.      * Remove this filter when the Symfony's requirement is equal or greater than 4.2
  376.      * and use the built-in trans filter instead with a %count% parameter.
  377.      */
  378.     public function transchoice($message$count, array $arguments = [], $domain null$locale null)
  379.     {
  380.         if (null === $this->translator) {
  381.             return strtr($message$arguments);
  382.         }
  383.         return $this->translator->trans($messagearray_merge(['%count%' => $count], $arguments), $domain$locale);
  384.     }
  385.     /**
  386.      * This reimplementation of Symfony's logout_path() helper is needed because
  387.      * when no arguments are passed to the getLogoutPath(), it's common to get
  388.      * exceptions and there is no way to recover from them in a Twig template.
  389.      */
  390.     public function getLogoutPath()
  391.     {
  392.         if (null === $this->logoutUrlGenerator) {
  393.             return;
  394.         }
  395.         try {
  396.             return $this->logoutUrlGenerator->getLogoutPath();
  397.         } catch (\Exception $e) {
  398.             return;
  399.         }
  400.     }
  401.     public function readProperty($objectOrArray, ?string $propertyPath)
  402.     {
  403.         if (null === $propertyPath) {
  404.             return null;
  405.         }
  406.         if ('__toString' === $propertyPath) {
  407.             try {
  408.                 return (string) $objectOrArray;
  409.             } catch (\Exception $e) {
  410.                 return null;
  411.             }
  412.         }
  413.         try {
  414.             return $this->propertyAccessor->getValue($objectOrArray$propertyPath);
  415.         } catch (\Exception $e) {
  416.             return null;
  417.         }
  418.     }
  419.     public function isGranted($permissions$subject null): bool
  420.     {
  421.         return $this->authorizationChecker->isGranted($permissions$subject);
  422.     }
  423.     private function getCountryName(?string $countryCode): ?string
  424.     {
  425.         if (null === $countryCode) {
  426.             return null;
  427.         }
  428.         // Compatibility with Symfony versions before 4.3
  429.         if (!class_exists(Countries::class)) {
  430.             return Intl::getRegionBundle()->getCountryName($countryCode) ?? null;
  431.         }
  432.         try {
  433.             return Countries::getName($countryCode);
  434.         } catch (MissingResourceException $e) {
  435.             return null;
  436.         }
  437.     }
  438. }