vendor/easycorp/easyadmin-bundle/src/Field/Configurator/CommonPreConfigurator.php line 183

  1. <?php
  2. namespace EasyCorp\Bundle\EasyAdminBundle\Field\Configurator;
  3. use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
  4. use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
  5. use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
  6. use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface;
  7. use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
  8. use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
  9. use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory;
  10. use EasyCorp\Bundle\EasyAdminBundle\Field\AvatarField;
  11. use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;
  12. use Symfony\Component\PropertyAccess\Exception\AccessException;
  13. use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
  14. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  15. use function Symfony\Component\String\u;
  16. use function Symfony\Component\Translation\t;
  17. use Symfony\Contracts\Translation\TranslatableInterface;
  18. /**
  19.  * @author Javier Eguiluz <javier.eguiluz@gmail.com>
  20.  */
  21. final class CommonPreConfigurator implements FieldConfiguratorInterface
  22. {
  23.     private PropertyAccessorInterface $propertyAccessor;
  24.     private EntityFactory $entityFactory;
  25.     public function __construct(PropertyAccessorInterface $propertyAccessorEntityFactory $entityFactory)
  26.     {
  27.         $this->propertyAccessor $propertyAccessor;
  28.         $this->entityFactory $entityFactory;
  29.     }
  30.     public function supports(FieldDto $fieldEntityDto $entityDto): bool
  31.     {
  32.         // this configurator applies to all kinds of properties
  33.         return true;
  34.     }
  35.     public function configure(FieldDto $fieldEntityDto $entityDtoAdminContext $context): void
  36.     {
  37.         $translationDomain $context->getI18n()->getTranslationDomain();
  38.         // if a field already has set a value, someone has written something to
  39.         // it (as a virtual field or overwrite); don't modify the value in that case
  40.         $isReadable true;
  41.         if (null === $value $field->getValue()) {
  42.             try {
  43.                 $value null === $entityDto->getInstance() ? null $this->propertyAccessor->getValue($entityDto->getInstance(), $field->getProperty());
  44.             } catch (AccessException|UnexpectedTypeException) {
  45.                 $isReadable false;
  46.             }
  47.             $field->setValue($value);
  48.             if (null === $field->getFormattedValue()) {
  49.                 $field->setFormattedValue($value);
  50.             }
  51.         }
  52.         $label $this->buildLabelOption($field$translationDomain$context->getCrud()->getCurrentPage());
  53.         $field->setLabel($label);
  54.         $isRequired $this->buildRequiredOption($field$entityDto);
  55.         $field->setFormTypeOption('required'$isRequired);
  56.         $isSortable $this->buildSortableOption($field$entityDto);
  57.         $field->setSortable($isSortable);
  58.         $isVirtual $this->buildVirtualOption($field$entityDto);
  59.         $field->setVirtual($isVirtual);
  60.         $templatePath $this->buildTemplatePathOption($context$field$entityDto$isReadable);
  61.         $field->setTemplatePath($templatePath);
  62.         $doctrineMetadata $entityDto->hasProperty($field->getProperty()) ? $entityDto->getPropertyMetadata($field->getProperty())->all() : [];
  63.         $field->setDoctrineMetadata($doctrineMetadata);
  64.         if (null !== $helpMessage $this->buildHelpOption($field$translationDomain)) {
  65.             $field->setHelp($helpMessage);
  66.             $field->setFormTypeOptionIfNotSet('help'$helpMessage);
  67.             $field->setFormTypeOptionIfNotSet('help_html'true);
  68.         }
  69.         if ('' !== $field->getCssClass()) {
  70.             $field->setFormTypeOptionIfNotSet('row_attr.class'$field->getCssClass());
  71.         }
  72.         if (null !== $field->getTextAlign()) {
  73.             $field->setFormTypeOptionIfNotSet('attr.data-ea-align'$field->getTextAlign());
  74.         }
  75.         $field->setFormTypeOptionIfNotSet('label'$field->getLabel());
  76.     }
  77.     private function buildHelpOption(FieldDto $fieldstring $translationDomain): ?TranslatableInterface
  78.     {
  79.         $help $field->getHelp();
  80.         if (null === $help || $help instanceof TranslatableInterface) {
  81.             return $help;
  82.         }
  83.         return '' === $help null t($help$field->getTranslationParameters(), $translationDomain);
  84.     }
  85.     /**
  86.      * @return TranslatableInterface|string|false|null
  87.      */
  88.     private function buildLabelOption(FieldDto $fieldstring $translationDomain, ?string $currentPage)
  89.     {
  90.         // don't autogenerate a label for these special fields (there's a dedicated configurator for them)
  91.         if (FormField::class === $field->getFieldFqcn()) {
  92.             $label $field->getLabel();
  93.             if ($label instanceof TranslatableInterface) {
  94.                 return $label;
  95.             }
  96.             return (null === $label || false === $label || '' === $label) ? $label t($label$field->getTranslationParameters(), $translationDomain);
  97.         }
  98.         // if an Avatar field doesn't define its label, don't autogenerate it for the 'index' page
  99.         // (because the table of the 'index' page looks better without a header in the avatar column)
  100.         if (Action::INDEX === $currentPage && null === $field->getLabel() && AvatarField::class === $field->getFieldFqcn()) {
  101.             $field->setLabel(false);
  102.         }
  103.         // it field doesn't define its label explicitly, generate an automatic
  104.         // label based on the field's field name
  105.         if (null === $label $field->getLabel()) {
  106.             $label $this->humanizeString($field->getProperty());
  107.         }
  108.         if ('' === $label) {
  109.             return $label;
  110.         }
  111.         // don't translate labels in form-related pages because Symfony Forms translates
  112.         // labels automatically and that causes false "translation is missing" errors
  113.         if (\in_array($currentPage, [Crud::PAGE_EDITCrud::PAGE_NEW], true)) {
  114.             return $label;
  115.         }
  116.         if ($label instanceof TranslatableInterface) {
  117.             return $label;
  118.         }
  119.         return t($label$field->getTranslationParameters(), $translationDomain);
  120.     }
  121.     private function buildSortableOption(FieldDto $fieldEntityDto $entityDto): bool
  122.     {
  123.         if (null !== $isSortable $field->isSortable()) {
  124.             return $isSortable;
  125.         }
  126.         return $entityDto->hasProperty($field->getProperty());
  127.     }
  128.     private function buildVirtualOption(FieldDto $fieldEntityDto $entityDto): bool
  129.     {
  130.         return !$entityDto->hasProperty($field->getProperty());
  131.     }
  132.     private function buildTemplatePathOption(AdminContext $adminContextFieldDto $fieldEntityDto $entityDtobool $isReadable): string
  133.     {
  134.         if (null !== $templatePath $field->getTemplatePath()) {
  135.             return $templatePath;
  136.         }
  137.         // if field has a value set, don't display it as inaccessible (needed e.g. for virtual fields)
  138.         if (!$isReadable && null === $field->getValue()) {
  139.             return $adminContext->getTemplatePath('label/inaccessible');
  140.         }
  141.         if (null === $templateName $field->getTemplateName()) {
  142.             throw new \RuntimeException(sprintf('Fields must define either their templateName or their templatePath. None given for "%s" field.'$field->getProperty()));
  143.         }
  144.         return $adminContext->getTemplatePath($templateName);
  145.     }
  146.     private function buildRequiredOption(FieldDto $fieldEntityDto $entityDto): bool
  147.     {
  148.         if (null !== $isRequired $field->getFormTypeOption('required')) {
  149.             return $isRequired;
  150.         }
  151.         // consider that virtual properties are not required
  152.         if (!$entityDto->hasProperty($field->getProperty())) {
  153.             return false;
  154.         }
  155.         $doctrinePropertyMetadata $entityDto->getPropertyMetadata($field->getProperty());
  156.         // If at least one join column of an association field isn't nullable then the field is "required" by default, otherwise the field is optional
  157.         if ($entityDto->isAssociation($field->getProperty())) {
  158.             $associatedEntityMetadata $this->entityFactory->getEntityMetadata($doctrinePropertyMetadata->get('targetEntity'));
  159.             foreach ($doctrinePropertyMetadata->get('joinColumns', []) as $joinColumn) {
  160.                 $propertyNameInAssociatedEntity $joinColumn['referencedColumnName'];
  161.                 $associatedPropertyMetadata $associatedEntityMetadata->fieldMappings[$propertyNameInAssociatedEntity] ?? [];
  162.                 $isNullable $associatedPropertyMetadata['nullable'] ?? true;
  163.                 if (false === $isNullable) {
  164.                     return true;
  165.                 }
  166.             }
  167.             return false;
  168.         }
  169.         // TODO: check if it's correct to never make a boolean value required
  170.         // I guess it's correct because Symfony Forms treat NULL as FALSE by default (i.e. in the database the value won't be NULL)
  171.         if ('boolean' === $doctrinePropertyMetadata->get('type')) {
  172.             return false;
  173.         }
  174.         return false === $doctrinePropertyMetadata->get('nullable');
  175.     }
  176.     private function humanizeString(string $string): string
  177.     {
  178.         $uString u($string);
  179.         $upperString $uString->upper()->toString();
  180.         // this prevents humanizing all-uppercase labels (e.g. 'UUID' -> 'U u i d')
  181.         // and other special labels which look better in uppercase
  182.         if ($uString->toString() === $upperString || \in_array($upperString, ['ID''URL'], true)) {
  183.             return $upperString;
  184.         }
  185.         return $uString
  186.             ->replaceMatches('/([A-Z])/''_$1')
  187.             ->replaceMatches('/[_\s]+/'' ')
  188.             ->trim()
  189.             ->lower()
  190.             ->title(true)
  191.             ->toString();
  192.     }
  193. }