- <?php
- namespace EasyCorp\Bundle\EasyAdminBundle\Field\Configurator;
- use Doctrine\ORM\EntityRepository;
- use Doctrine\ORM\PersistentCollection;
- use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
- use EasyCorp\Bundle\EasyAdminBundle\Config\Option\EA;
- use EasyCorp\Bundle\EasyAdminBundle\Config\Option\TextAlign;
- use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
- use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface;
- use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
- use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
- use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory;
- use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
- use EasyCorp\Bundle\EasyAdminBundle\Form\Type\CrudAutocompleteType;
- use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
- use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
- use Symfony\Component\PropertyAccess\PropertyAccessor;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
-  * @author Javier Eguiluz <javier.eguiluz@gmail.com>
-  */
- final class AssociationConfigurator implements FieldConfiguratorInterface
- {
-     private $entityFactory;
-     private $adminUrlGenerator;
-     private $translator;
-     public function __construct(EntityFactory $entityFactory, AdminUrlGenerator $adminUrlGenerator, TranslatorInterface $translator)
-     {
-         $this->entityFactory = $entityFactory;
-         $this->adminUrlGenerator = $adminUrlGenerator;
-         $this->translator = $translator;
-     }
-     public function supports(FieldDto $field, EntityDto $entityDto): bool
-     {
-         return AssociationField::class === $field->getFieldFqcn();
-     }
-     public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
-     {
-         $propertyName = $field->getProperty();
-         if (!$entityDto->isAssociation($propertyName)) {
-             throw new \RuntimeException(sprintf('The "%s" field is not a Doctrine association, so it cannot be used as an association field.', $propertyName));
-         }
-         $targetEntityFqcn = $field->getDoctrineMetadata()->get('targetEntity');
-         // the target CRUD controller can be NULL; in that case, field value doesn't link to the related entity
-         $targetCrudControllerFqcn = $field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER)
-             ?? $context->getCrudControllers()->findCrudFqcnByEntityFqcn($targetEntityFqcn);
-         $field->setCustomOption(AssociationField::OPTION_CRUD_CONTROLLER, $targetCrudControllerFqcn);
-         if (AssociationField::WIDGET_AUTOCOMPLETE === $field->getCustomOption(AssociationField::OPTION_WIDGET)) {
-             $field->setFormTypeOption('attr.data-ea-widget', 'ea-autocomplete');
-         }
-         // check for embedded associations
-         $propertyNameParts = explode('.', $propertyName);
-         if (\count($propertyNameParts) > 1) {
-             // prepare starting class for association
-             $targetEntityFqcn = $entityDto->getPropertyMetadata($propertyNameParts[0])->get('targetEntity');
-             array_shift($propertyNameParts);
-             $metadata = $this->entityFactory->getEntityMetadata($targetEntityFqcn);
-             foreach ($propertyNameParts as $association) {
-                 if (!$metadata->hasAssociation($association)) {
-                     throw new \RuntimeException(sprintf('There is no association for the class "%s" with name "%s"', $targetEntityFqcn, $association));
-                 }
-                 // overwrite next class from association
-                 $targetEntityFqcn = $metadata->getAssociationTargetClass($association);
-                 // read next association metadata
-                 $metadata = $this->entityFactory->getEntityMetadata($targetEntityFqcn);
-             }
-             $accessor = new PropertyAccessor();
-             $targetCrudControllerFqcn = $field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER);
-             $field->setFormTypeOptionIfNotSet('class', $targetEntityFqcn);
-             try {
-                 $relatedEntityId = $accessor->getValue($entityDto->getInstance(), $propertyName.'.'.$metadata->getIdentifierFieldNames()[0]);
-                 $relatedEntityDto = $this->entityFactory->create($targetEntityFqcn, $relatedEntityId);
-                 $field->setCustomOption(AssociationField::OPTION_RELATED_URL, $this->generateLinkToAssociatedEntity($targetCrudControllerFqcn, $relatedEntityDto));
-                 $field->setFormattedValue($this->formatAsString($relatedEntityDto->getInstance(), $relatedEntityDto));
-             } catch (UnexpectedTypeException $e) {
-                 // this may crash if something in the tree is null, so just do nothing then
-             }
-         } else {
-             if ($entityDto->isToOneAssociation($propertyName)) {
-                 $this->configureToOneAssociation($field);
-             }
-             if ($entityDto->isToManyAssociation($propertyName)) {
-                 $this->configureToManyAssociation($field);
-             }
-         }
-         if (true === $field->getCustomOption(AssociationField::OPTION_AUTOCOMPLETE)) {
-             $targetCrudControllerFqcn = $field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER);
-             if (null === $targetCrudControllerFqcn) {
-                 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()));
-             }
-             $field->setFormType(CrudAutocompleteType::class);
-             $autocompleteEndpointUrl = $this->adminUrlGenerator
-                 ->unsetAll()
-                 ->set('page', 1) // The autocomplete should always start on the first page
-                 ->setController($field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER))
-                 ->setAction('autocomplete')
-                 ->set(AssociationField::PARAM_AUTOCOMPLETE_CONTEXT, [
-                     EA::CRUD_CONTROLLER_FQCN => $context->getRequest()->query->get(EA::CRUD_CONTROLLER_FQCN),
-                     'propertyName' => $propertyName,
-                     'originatingPage' => $context->getCrud()->getCurrentPage(),
-                 ])
-                 ->generateUrl();
-             $field->setFormTypeOption('attr.data-ea-autocomplete-endpoint-url', $autocompleteEndpointUrl);
-         } else {
-             $field->setFormTypeOptionIfNotSet('query_builder', static function (EntityRepository $repository) use ($field) {
-                 // TODO: should this use `createIndexQueryBuilder` instead, so we get the default ordering etc.?
-                 // it would then be identical to the one used in autocomplete action, but it is a bit complex getting it in here
-                 $queryBuilder = $repository->createQueryBuilder('entity');
-                 if ($queryBuilderCallable = $field->getCustomOption(AssociationField::OPTION_QUERY_BUILDER_CALLABLE)) {
-                     $queryBuilderCallable($queryBuilder);
-                 }
-                 return $queryBuilder;
-             });
-         }
-     }
-     private function configureToOneAssociation(FieldDto $field): void
-     {
-         $field->setCustomOption(AssociationField::OPTION_DOCTRINE_ASSOCIATION_TYPE, 'toOne');
-         if (false === $field->getFormTypeOption('required')) {
-             $field->setFormTypeOptionIfNotSet('attr.placeholder', $this->translator->trans('label.form.empty_value', [], 'EasyAdminBundle'));
-         }
-         $targetEntityFqcn = $field->getDoctrineMetadata()->get('targetEntity');
-         $targetCrudControllerFqcn = $field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER);
-         $targetEntityDto = null === $field->getValue()
-             ? $this->entityFactory->create($targetEntityFqcn)
-             : $this->entityFactory->createForEntityInstance($field->getValue());
-         $field->setFormTypeOptionIfNotSet('class', $targetEntityDto->getFqcn());
-         $field->setCustomOption(AssociationField::OPTION_RELATED_URL, $this->generateLinkToAssociatedEntity($targetCrudControllerFqcn, $targetEntityDto));
-         $field->setFormattedValue($this->formatAsString($field->getValue(), $targetEntityDto));
-     }
-     private function configureToManyAssociation(FieldDto $field): void
-     {
-         $field->setCustomOption(AssociationField::OPTION_DOCTRINE_ASSOCIATION_TYPE, 'toMany');
-         // associations different from *-to-one cannot be sorted
-         $field->setSortable(false);
-         $field->setFormTypeOptionIfNotSet('multiple', true);
-         /* @var PersistentCollection $collection */
-         $field->setFormTypeOptionIfNotSet('class', $field->getDoctrineMetadata()->get('targetEntity'));
-         if (null === $field->getTextAlign()) {
-             $field->setTextAlign(TextAlign::RIGHT);
-         }
-         $field->setFormattedValue($this->countNumElements($field->getValue()));
-     }
-     private function formatAsString($entityInstance, EntityDto $entityDto): ?string
-     {
-         if (null === $entityInstance) {
-             return null;
-         }
-         if (method_exists($entityInstance, '__toString')) {
-             return (string) $entityInstance;
-         }
-         if (null !== $primaryKeyValue = $entityDto->getPrimaryKeyValue()) {
-             return sprintf('%s #%s', $entityDto->getName(), $primaryKeyValue);
-         }
-         return $entityDto->getName();
-     }
-     private function generateLinkToAssociatedEntity(?string $crudController, EntityDto $entityDto): ?string
-     {
-         if (null === $crudController) {
-             return null;
-         }
-         // TODO: check if user has permission to see the related entity
-         return $this->adminUrlGenerator
-             ->setController($crudController)
-             ->setAction(Action::DETAIL)
-             ->setEntityId($entityDto->getPrimaryKeyValue())
-             ->unset(EA::MENU_INDEX)
-             ->unset(EA::SUBMENU_INDEX)
-             ->includeReferrer()
-             ->generateUrl();
-     }
-     private function countNumElements($collection): int
-     {
-         if (null === $collection) {
-             return 0;
-         }
-         if (is_countable($collection)) {
-             return \count($collection);
-         }
-         if ($collection instanceof \Traversable) {
-             return iterator_count($collection);
-         }
-         return 0;
-     }
- }
-