vendor/easycorp/easyadmin-bundle/src/Field/Configurator/AssociationConfigurator.php line 30
<?phpnamespace EasyCorp\Bundle\EasyAdminBundle\Field\Configurator;use Doctrine\ORM\EntityRepository;use Doctrine\ORM\PersistentCollection;use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;use EasyCorp\Bundle\EasyAdminBundle\Config\Action;use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;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\ControllerFactory;use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory;use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;use EasyCorp\Bundle\EasyAdminBundle\Form\Type\CrudAutocompleteType;use EasyCorp\Bundle\EasyAdminBundle\Form\Type\CrudFormType;use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;use Symfony\Component\HttpFoundation\RequestStack;use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;use Symfony\Component\PropertyAccess\PropertyAccessor;use function Symfony\Component\Translation\t;/*** @author Javier Eguiluz <javier.eguiluz@gmail.com>*/final class AssociationConfigurator implements FieldConfiguratorInterface{private EntityFactory $entityFactory;private AdminUrlGenerator $adminUrlGenerator;private RequestStack $requestStack;private ControllerFactory $controllerFactory;public function __construct(EntityFactory $entityFactory, AdminUrlGenerator $adminUrlGenerator, RequestStack $requestStack, ControllerFactory $controllerFactory){$this->entityFactory = $entityFactory;$this->adminUrlGenerator = $adminUrlGenerator;$this->requestStack = $requestStack;$this->controllerFactory = $controllerFactory;}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_EMBEDDED_CRUD_FORM_CONTROLLER)?? $context->getCrudControllers()->findCrudFqcnByEntityFqcn($targetEntityFqcn);if (true === $field->getCustomOption(AssociationField::OPTION_RENDER_AS_EMBEDDED_FORM)) {if (false === $entityDto->isToOneAssociation($propertyName)) {throw new \RuntimeException(sprintf('The "%s" association field of "%s" is a to-many association but it\'s trying to use the "renderAsEmbeddedForm()" option, which is only available for to-one associations. If you want to use a CRUD form to render to-many associations, use a CollectionField instead of the AssociationField.',$field->getProperty(),$context->getCrud()?->getControllerFqcn(),));}if (null === $targetCrudControllerFqcn) {throw new \RuntimeException(sprintf('The "%s" association field of "%s" wants to render its contents using an EasyAdmin CRUD form. However, no CRUD form was found related to this field. You can either create a CRUD controller for the entity "%s" or pass the CRUD controller to use as the first argument of the "renderAsEmbeddedForm()" method.',$field->getProperty(),$context->getCrud()?->getControllerFqcn(),$targetEntityFqcn));}$this->configureCrudForm($field, $entityDto, $propertyName, $targetEntityFqcn, $targetCrudControllerFqcn);return;}$field->setCustomOption(AssociationField::OPTION_EMBEDDED_CRUD_FORM_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_EMBEDDED_CRUD_FORM_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) {// 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_EMBEDDED_CRUD_FORM_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_EMBEDDED_CRUD_FORM_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 (null !== $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', t('label.form.empty_value', [], 'EasyAdminBundle'));}$targetEntityFqcn = $field->getDoctrineMetadata()->get('targetEntity');$targetCrudControllerFqcn = $field->getCustomOption(AssociationField::OPTION_EMBEDDED_CRUD_FORM_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 entityreturn $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;}private function configureCrudForm(FieldDto $field, EntityDto $entityDto, string $propertyName, string $targetEntityFqcn, string $targetCrudControllerFqcn): void{$field->setFormType(CrudFormType::class);$propertyAccessor = new PropertyAccessor();if (null === $entityDto->getInstance()) {$associatedEntity = null;} else {$associatedEntity = $propertyAccessor->isReadable($entityDto->getInstance(), $propertyName)? $propertyAccessor->getValue($entityDto->getInstance(), $propertyName): null;}if (null === $associatedEntity) {$targetCrudControllerAction = Action::NEW;$targetCrudControllerPageName = $field->getCustomOption(AssociationField::OPTION_EMBEDDED_CRUD_FORM_NEW_PAGE_NAME) ?? Crud::PAGE_NEW;} else {$targetCrudControllerAction = Action::EDIT;$targetCrudControllerPageName = $field->getCustomOption(AssociationField::OPTION_EMBEDDED_CRUD_FORM_EDIT_PAGE_NAME) ?? Crud::PAGE_EDIT;}$field->setFormTypeOption('entityDto',$this->createEntityDto($targetEntityFqcn, $targetCrudControllerFqcn, $targetCrudControllerAction, $targetCrudControllerPageName),);}private function createEntityDto(string $entityFqcn, string $crudControllerFqcn, string $crudControllerAction, string $crudControllerPageName): EntityDto{$entityDto = $this->entityFactory->create($entityFqcn);$crudController = $this->controllerFactory->getCrudControllerInstance($crudControllerFqcn,$crudControllerAction,$this->requestStack->getMainRequest());$fields = $crudController->configureFields($crudControllerPageName);$this->entityFactory->processFields($entityDto, FieldCollection::new($fields));return $entityDto;}}