vendor/easycorp/easyadmin-bundle/src/Field/Configurator/AssociationConfigurator.php line 151

Open in your IDE?
  1. <?php
  2. namespace EasyCorp\Bundle\EasyAdminBundle\Field\Configurator;
  3. use Doctrine\ORM\EntityRepository;
  4. use Doctrine\ORM\PersistentCollection;
  5. use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
  6. use EasyCorp\Bundle\EasyAdminBundle\Config\Option\EA;
  7. use EasyCorp\Bundle\EasyAdminBundle\Config\Option\TextAlign;
  8. use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
  9. use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface;
  10. use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
  11. use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
  12. use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory;
  13. use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
  14. use EasyCorp\Bundle\EasyAdminBundle\Form\Type\CrudAutocompleteType;
  15. use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
  16. use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
  17. use Symfony\Component\PropertyAccess\PropertyAccessor;
  18. use Symfony\Contracts\Translation\TranslatorInterface;
  19. /**
  20.  * @author Javier Eguiluz <javier.eguiluz@gmail.com>
  21.  */
  22. final class AssociationConfigurator implements FieldConfiguratorInterface
  23. {
  24.     private $entityFactory;
  25.     private $adminUrlGenerator;
  26.     private $translator;
  27.     public function __construct(EntityFactory $entityFactoryAdminUrlGenerator $adminUrlGeneratorTranslatorInterface $translator)
  28.     {
  29.         $this->entityFactory $entityFactory;
  30.         $this->adminUrlGenerator $adminUrlGenerator;
  31.         $this->translator $translator;
  32.     }
  33.     public function supports(FieldDto $fieldEntityDto $entityDto): bool
  34.     {
  35.         return AssociationField::class === $field->getFieldFqcn();
  36.     }
  37.     public function configure(FieldDto $fieldEntityDto $entityDtoAdminContext $context): void
  38.     {
  39.         $propertyName $field->getProperty();
  40.         if (!$entityDto->isAssociation($propertyName)) {
  41.             throw new \RuntimeException(sprintf('The "%s" field is not a Doctrine association, so it cannot be used as an association field.'$propertyName));
  42.         }
  43.         $targetEntityFqcn $field->getDoctrineMetadata()->get('targetEntity');
  44.         // the target CRUD controller can be NULL; in that case, field value doesn't link to the related entity
  45.         $targetCrudControllerFqcn $field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER)
  46.             ?? $context->getCrudControllers()->findCrudFqcnByEntityFqcn($targetEntityFqcn);
  47.         $field->setCustomOption(AssociationField::OPTION_CRUD_CONTROLLER$targetCrudControllerFqcn);
  48.         if (AssociationField::WIDGET_AUTOCOMPLETE === $field->getCustomOption(AssociationField::OPTION_WIDGET)) {
  49.             $field->setFormTypeOption('attr.data-ea-widget''ea-autocomplete');
  50.         }
  51.         // check for embedded associations
  52.         $propertyNameParts explode('.'$propertyName);
  53.         if (\count($propertyNameParts) > 1) {
  54.             // prepare starting class for association
  55.             $targetEntityFqcn $entityDto->getPropertyMetadata($propertyNameParts[0])->get('targetEntity');
  56.             array_shift($propertyNameParts);
  57.             $metadata $this->entityFactory->getEntityMetadata($targetEntityFqcn);
  58.             foreach ($propertyNameParts as $association) {
  59.                 if (!$metadata->hasAssociation($association)) {
  60.                     throw new \RuntimeException(sprintf('There is no association for the class "%s" with name "%s"'$targetEntityFqcn$association));
  61.                 }
  62.                 // overwrite next class from association
  63.                 $targetEntityFqcn $metadata->getAssociationTargetClass($association);
  64.                 // read next association metadata
  65.                 $metadata $this->entityFactory->getEntityMetadata($targetEntityFqcn);
  66.             }
  67.             $accessor = new PropertyAccessor();
  68.             $targetCrudControllerFqcn $field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER);
  69.             $field->setFormTypeOptionIfNotSet('class'$targetEntityFqcn);
  70.             try {
  71.                 $relatedEntityId $accessor->getValue($entityDto->getInstance(), $propertyName.'.'.$metadata->getIdentifierFieldNames()[0]);
  72.                 $relatedEntityDto $this->entityFactory->create($targetEntityFqcn$relatedEntityId);
  73.                 $field->setCustomOption(AssociationField::OPTION_RELATED_URL$this->generateLinkToAssociatedEntity($targetCrudControllerFqcn$relatedEntityDto));
  74.                 $field->setFormattedValue($this->formatAsString($relatedEntityDto->getInstance(), $relatedEntityDto));
  75.             } catch (UnexpectedTypeException $e) {
  76.                 // this may crash if something in the tree is null, so just do nothing then
  77.             }
  78.         } else {
  79.             if ($entityDto->isToOneAssociation($propertyName)) {
  80.                 $this->configureToOneAssociation($field);
  81.             }
  82.             if ($entityDto->isToManyAssociation($propertyName)) {
  83.                 $this->configureToManyAssociation($field);
  84.             }
  85.         }
  86.         if (true === $field->getCustomOption(AssociationField::OPTION_AUTOCOMPLETE)) {
  87.             $targetCrudControllerFqcn $field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER);
  88.             if (null === $targetCrudControllerFqcn) {
  89.                 throw new \RuntimeException(sprintf('The "%s" field cannot be autocompleted because it doesn\'t define the related CRUD controller FQCN with the "setCrudController()" method.'$field->getProperty()));
  90.             }
  91.             $field->setFormType(CrudAutocompleteType::class);
  92.             $autocompleteEndpointUrl $this->adminUrlGenerator
  93.                 ->unsetAll()
  94.                 ->set('page'1// The autocomplete should always start on the first page
  95.                 ->setController($field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER))
  96.                 ->setAction('autocomplete')
  97.                 ->set(AssociationField::PARAM_AUTOCOMPLETE_CONTEXT, [
  98.                     EA::CRUD_CONTROLLER_FQCN => $context->getRequest()->query->get(EA::CRUD_CONTROLLER_FQCN),
  99.                     'propertyName' => $propertyName,
  100.                     'originatingPage' => $context->getCrud()->getCurrentPage(),
  101.                 ])
  102.                 ->generateUrl();
  103.             $field->setFormTypeOption('attr.data-ea-autocomplete-endpoint-url'$autocompleteEndpointUrl);
  104.         } else {
  105.             $field->setFormTypeOptionIfNotSet('query_builder', static function (EntityRepository $repository) use ($field) {
  106.                 // TODO: should this use `createIndexQueryBuilder` instead, so we get the default ordering etc.?
  107.                 // it would then be identical to the one used in autocomplete action, but it is a bit complex getting it in here
  108.                 $queryBuilder $repository->createQueryBuilder('entity');
  109.                 if ($queryBuilderCallable $field->getCustomOption(AssociationField::OPTION_QUERY_BUILDER_CALLABLE)) {
  110.                     $queryBuilderCallable($queryBuilder);
  111.                 }
  112.                 return $queryBuilder;
  113.             });
  114.         }
  115.     }
  116.     private function configureToOneAssociation(FieldDto $field): void
  117.     {
  118.         $field->setCustomOption(AssociationField::OPTION_DOCTRINE_ASSOCIATION_TYPE'toOne');
  119.         if (false === $field->getFormTypeOption('required')) {
  120.             $field->setFormTypeOptionIfNotSet('attr.placeholder'$this->translator->trans('label.form.empty_value', [], 'EasyAdminBundle'));
  121.         }
  122.         $targetEntityFqcn $field->getDoctrineMetadata()->get('targetEntity');
  123.         $targetCrudControllerFqcn $field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER);
  124.         $targetEntityDto null === $field->getValue()
  125.             ? $this->entityFactory->create($targetEntityFqcn)
  126.             : $this->entityFactory->createForEntityInstance($field->getValue());
  127.         $field->setFormTypeOptionIfNotSet('class'$targetEntityDto->getFqcn());
  128.         $field->setCustomOption(AssociationField::OPTION_RELATED_URL$this->generateLinkToAssociatedEntity($targetCrudControllerFqcn$targetEntityDto));
  129.         $field->setFormattedValue($this->formatAsString($field->getValue(), $targetEntityDto));
  130.     }
  131.     private function configureToManyAssociation(FieldDto $field): void
  132.     {
  133.         $field->setCustomOption(AssociationField::OPTION_DOCTRINE_ASSOCIATION_TYPE'toMany');
  134.         // associations different from *-to-one cannot be sorted
  135.         $field->setSortable(false);
  136.         $field->setFormTypeOptionIfNotSet('multiple'true);
  137.         /* @var PersistentCollection $collection */
  138.         $field->setFormTypeOptionIfNotSet('class'$field->getDoctrineMetadata()->get('targetEntity'));
  139.         if (null === $field->getTextAlign()) {
  140.             $field->setTextAlign(TextAlign::RIGHT);
  141.         }
  142.         $field->setFormattedValue($this->countNumElements($field->getValue()));
  143.     }
  144.     private function formatAsString($entityInstanceEntityDto $entityDto): ?string
  145.     {
  146.         if (null === $entityInstance) {
  147.             return null;
  148.         }
  149.         if (method_exists($entityInstance'__toString')) {
  150.             return (string) $entityInstance;
  151.         }
  152.         if (null !== $primaryKeyValue $entityDto->getPrimaryKeyValue()) {
  153.             return sprintf('%s #%s'$entityDto->getName(), $primaryKeyValue);
  154.         }
  155.         return $entityDto->getName();
  156.     }
  157.     private function generateLinkToAssociatedEntity(?string $crudControllerEntityDto $entityDto): ?string
  158.     {
  159.         if (null === $crudController) {
  160.             return null;
  161.         }
  162.         // TODO: check if user has permission to see the related entity
  163.         return $this->adminUrlGenerator
  164.             ->setController($crudController)
  165.             ->setAction(Action::DETAIL)
  166.             ->setEntityId($entityDto->getPrimaryKeyValue())
  167.             ->unset(EA::MENU_INDEX)
  168.             ->unset(EA::SUBMENU_INDEX)
  169.             ->includeReferrer()
  170.             ->generateUrl();
  171.     }
  172.     private function countNumElements($collection): int
  173.     {
  174.         if (null === $collection) {
  175.             return 0;
  176.         }
  177.         if (is_countable($collection)) {
  178.             return \count($collection);
  179.         }
  180.         if ($collection instanceof \Traversable) {
  181.             return iterator_count($collection);
  182.         }
  183.         return 0;
  184.     }
  185. }