vendor/easycorp/easyadmin-bundle/src/Factory/FieldFactory.php line 108

  1. <?php
  2. namespace EasyCorp\Bundle\EasyAdminBundle\Factory;
  3. use Doctrine\DBAL\Types\Types;
  4. use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
  5. use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
  6. use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
  7. use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
  8. use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
  9. use EasyCorp\Bundle\EasyAdminBundle\Field\ArrayField;
  10. use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
  11. use EasyCorp\Bundle\EasyAdminBundle\Field\DateField;
  12. use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
  13. use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
  14. use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;
  15. use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
  16. use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
  17. use EasyCorp\Bundle\EasyAdminBundle\Field\NumberField;
  18. use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
  19. use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
  20. use EasyCorp\Bundle\EasyAdminBundle\Field\TimeField;
  21. use EasyCorp\Bundle\EasyAdminBundle\Form\Type\EaFormRowType;
  22. use EasyCorp\Bundle\EasyAdminBundle\Form\Type\EasyAdminTabType;
  23. use EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider;
  24. use EasyCorp\Bundle\EasyAdminBundle\Security\Permission;
  25. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  26. /**
  27.  * @author Javier Eguiluz <javier.eguiluz@gmail.com>
  28.  */
  29. final class FieldFactory
  30. {
  31.     private static array $doctrineTypeToFieldFqcn = [
  32.         Types::ARRAY => ArrayField::class,
  33.         Types::BIGINT => TextField::class,
  34.         Types::BINARY => TextareaField::class,
  35.         Types::BLOB => TextareaField::class,
  36.         Types::BOOLEAN => BooleanField::class,
  37.         Types::DATE_MUTABLE => DateField::class,
  38.         Types::DATE_IMMUTABLE => DateField::class,
  39.         Types::DATEINTERVAL => TextField::class,
  40.         Types::DATETIME_MUTABLE => DateTimeField::class,
  41.         Types::DATETIME_IMMUTABLE => DateTimeField::class,
  42.         Types::DATETIMETZ_MUTABLE => DateTimeField::class,
  43.         Types::DATETIMETZ_IMMUTABLE => DateTimeField::class,
  44.         Types::DECIMAL => NumberField::class,
  45.         Types::FLOAT => NumberField::class,
  46.         Types::GUID => TextField::class,
  47.         Types::INTEGER => IntegerField::class,
  48.         Types::JSON => TextField::class,
  49.         Types::OBJECT => TextField::class,
  50.         Types::SIMPLE_ARRAY => ArrayField::class,
  51.         Types::SMALLINT => IntegerField::class,
  52.         Types::STRING => TextField::class,
  53.         Types::TEXT => TextareaField::class,
  54.         Types::TIME_MUTABLE => TimeField::class,
  55.         Types::TIME_IMMUTABLE => TimeField::class,
  56.     ];
  57.     private AdminContextProvider $adminContextProvider;
  58.     private AuthorizationCheckerInterface $authorizationChecker;
  59.     private iterable $fieldConfigurators;
  60.     public function __construct(AdminContextProvider $adminContextProviderAuthorizationCheckerInterface $authorizationCheckeriterable $fieldConfigurators)
  61.     {
  62.         $this->adminContextProvider $adminContextProvider;
  63.         $this->authorizationChecker $authorizationChecker;
  64.         $this->fieldConfigurators $fieldConfigurators;
  65.     }
  66.     public function processFields(EntityDto $entityDtoFieldCollection $fields): void
  67.     {
  68.         $this->preProcessFields($fields$entityDto);
  69.         $context $this->adminContextProvider->getContext();
  70.         $currentPage $context->getCrud()->getCurrentPage();
  71.         $isDetailOrIndex \in_array($currentPage, [Crud::PAGE_INDEXCrud::PAGE_DETAIL], true);
  72.         foreach ($fields as $fieldDto) {
  73.             if ((null !== $currentPage && false === $fieldDto->isDisplayedOn($currentPage))
  74.                 || false === $this->authorizationChecker->isGranted(Permission::EA_VIEW_FIELD$fieldDto)) {
  75.                 $fields->unset($fieldDto);
  76.                 continue;
  77.             }
  78.             // "form rows" only make sense in pages that contain forms
  79.             if ($isDetailOrIndex && EaFormRowType::class === $fieldDto->getFormType()) {
  80.                 $fields->unset($fieldDto);
  81.                 continue;
  82.             }
  83.             // when creating new entities with "useEntryCrudForm" on an edit page we must
  84.             // explicitly check for the "new" page because $currentPage will be "edit"
  85.             if ((null === $entityDto->getInstance()) && !$fieldDto->isDisplayedOn(Crud::PAGE_NEW)) {
  86.                 $fields->unset($fieldDto);
  87.                 continue;
  88.             }
  89.             foreach ($this->fieldConfigurators as $configurator) {
  90.                 if (!$configurator->supports($fieldDto$entityDto)) {
  91.                     continue;
  92.                 }
  93.                 $configurator->configure($fieldDto$entityDto$context);
  94.             }
  95.             foreach ($fieldDto->getFormThemes() as $formThemePath) {
  96.                 $context?->getCrud()?->addFormTheme($formThemePath);
  97.             }
  98.             $fields->set($fieldDto);
  99.         }
  100.         $isPageWhereTabsAreVisible \in_array($currentPage, [Crud::PAGE_DETAILCrud::PAGE_EDITCrud::PAGE_NEW], true);
  101.         if ($isPageWhereTabsAreVisible) {
  102.             $this->checkOrphanTabFields($fields$context);
  103.         }
  104.         $entityDto->setFields($fields);
  105.     }
  106.     private function preProcessFields(FieldCollection $fieldsEntityDto $entityDto): void
  107.     {
  108.         if ($fields->isEmpty()) {
  109.             return;
  110.         }
  111.         // this is needed to handle this edge-case: the list of fields include one or more form panels,
  112.         // but the first fields of the list don't belong to any panel. We must create an automatic empty
  113.         // form panel for those "orphaned fields" so they are displayed as expected
  114.         $firstFieldIsAFormPanel $fields->first()->isFormDecorationField();
  115.         foreach ($fields as $fieldDto) {
  116.             if (!$firstFieldIsAFormPanel && $fieldDto->isFormDecorationField()) {
  117.                 $fields->prepend(FormField::addPanel()->getAsDto());
  118.                 break;
  119.             }
  120.         }
  121.         foreach ($fields as $fieldDto) {
  122.             if (Field::class !== $fieldDto->getFieldFqcn()) {
  123.                 continue;
  124.             }
  125.             // this is a virtual field, so we can't autoconfigure it
  126.             if (!$entityDto->hasProperty($fieldDto->getProperty())) {
  127.                 continue;
  128.             }
  129.             if ($fieldDto->getProperty() === $entityDto->getPrimaryKeyName()) {
  130.                 $guessedFieldFqcn IdField::class;
  131.             } else {
  132.                 $doctrinePropertyType $entityDto->getPropertyMetadata($fieldDto->getProperty())->get('type');
  133.                 $guessedFieldFqcn self::$doctrineTypeToFieldFqcn[$doctrinePropertyType] ?? null;
  134.                 if (null === $guessedFieldFqcn) {
  135.                     throw new \RuntimeException(sprintf('The Doctrine type of the "%s" field is "%s", which is not supported by EasyAdmin. For Doctrine\'s Custom Mapping Types have a look at EasyAdmin\'s field docs.'$fieldDto->getProperty(), $doctrinePropertyType));
  136.                 }
  137.             }
  138.             $fields->set($this->transformField($fieldDto$guessedFieldFqcn));
  139.         }
  140.     }
  141.     // transforms a generic Field class into a specific <type>Field class (e.g. DateTimeField)
  142.     private function transformField(FieldDto $fieldDtostring $newFieldFqcn): FieldDto
  143.     {
  144.         /** @var FieldDto $newField */
  145.         $newField $newFieldFqcn::new($fieldDto->getProperty())->getAsDto();
  146.         $newField->setUniqueId($fieldDto->getUniqueId());
  147.         $newField->setFieldFqcn($newFieldFqcn);
  148.         $newField->setDisplayedOn($fieldDto->getDisplayedOn());
  149.         $newField->setValue($fieldDto->getValue());
  150.         $newField->setFormattedValue($fieldDto->getFormattedValue());
  151.         $newField->setCssClass(trim($newField->getCssClass().' '.$fieldDto->getCssClass()));
  152.         $newField->setColumns($fieldDto->getColumns());
  153.         $newField->setTranslationParameters($fieldDto->getTranslationParameters());
  154.         $newField->setAssets($newField->getAssets()->mergeWith($fieldDto->getAssets()));
  155.         foreach ($fieldDto->getFormThemes() as $formThemePath) {
  156.             $newField->addFormTheme($formThemePath);
  157.         }
  158.         $customFormTypeOptions $fieldDto->getFormTypeOptions();
  159.         $defaultFormTypeOptions $newField->getFormTypeOptions();
  160.         $newField->setFormTypeOptions(array_merge($defaultFormTypeOptions$customFormTypeOptions));
  161.         $customFieldOptions $fieldDto->getCustomOptions()->all();
  162.         $defaultFieldOptions $newField->getCustomOptions()->all();
  163.         $mergedFieldOptions array_merge($defaultFieldOptions$customFieldOptions);
  164.         $newField->setCustomOptions($mergedFieldOptions);
  165.         if (null !== $fieldDto->getLabel()) {
  166.             $newField->setLabel($fieldDto->getLabel());
  167.         }
  168.         if (null !== $fieldDto->isVirtual()) {
  169.             $newField->setVirtual($fieldDto->isVirtual());
  170.         }
  171.         if (null !== $fieldDto->getTextAlign()) {
  172.             $newField->setTextAlign($fieldDto->getTextAlign());
  173.         }
  174.         if (null !== $fieldDto->isSortable()) {
  175.             $newField->setSortable($fieldDto->isSortable());
  176.         }
  177.         if (null !== $fieldDto->getPermission()) {
  178.             $newField->setPermission($fieldDto->getPermission());
  179.         }
  180.         if (null !== $fieldDto->getHelp()) {
  181.             $newField->setHelp($fieldDto->getHelp());
  182.         }
  183.         if (null !== $fieldDto->getFormType()) {
  184.             $newField->setFormType($fieldDto->getFormType());
  185.         }
  186.         // don't copy the template name and path from the original Field class
  187.         // (because they are just 'crud/field/text' and ' @EasyAdmin/crud/field/text.html.twig')
  188.         // and use the template name/path from the new specific field (e.g. 'crud/field/datetime')
  189.         return $newField;
  190.     }
  191.     /**
  192.      * When rendering fields using tabs, all fields must belong to some tab.
  193.      */
  194.     private function checkOrphanTabFields(FieldCollection $fieldsAdminContext $context): void
  195.     {
  196.         $hasTabs false;
  197.         $isTabField = static fn (FieldDto $fieldDto) => EasyAdminTabType::class === $fieldDto->getFormType();
  198.         $isFormField = static fn (FieldDto $fieldDto) => FormField::class === $fieldDto->getFieldFqcn();
  199.         foreach ($fields as $fieldDto) {
  200.             if ($isTabField($fieldDto)) {
  201.                 $hasTabs true;
  202.                 break;
  203.             }
  204.         }
  205.         if (!$hasTabs || $isTabField($fields->first())) {
  206.             return;
  207.         }
  208.         $orphanFieldNames = [];
  209.         foreach ($fields as $field) {
  210.             if ($isTabField($field)) {
  211.                 break;
  212.             }
  213.             if ($isFormField($field)) {
  214.                 continue;
  215.             }
  216.             $orphanFieldNames[] = $field->getProperty();
  217.         }
  218.         throw new \RuntimeException(sprintf('The "%s" page of "%s" uses tabs to display its fields, but the following fields don\'t belong to any tab: %s. Use "FormField::addTab(\'...\')" to add a tab before those fields.'$context->getCrud()->getCurrentPage(), $context->getCrud()->getControllerFqcn(), implode(', '$orphanFieldNames)));
  219.     }
  220. }