Last active
February 25, 2025 20:07
-
-
Save wizhippo/9043a6676ce2920676b730ba2f507655 to your computer and use it in GitHub Desktop.
Start of adding autocomplete support to EntityFilter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\EasyAdmin\Filter; | |
use Doctrine\Common\Collections\ArrayCollection; | |
use Doctrine\DBAL\Types\Type; | |
use Doctrine\ORM\Mapping\MappingException; | |
use Doctrine\ORM\Query\Expr\Orx; | |
use Doctrine\ORM\QueryBuilder; | |
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Filter\FilterInterface; | |
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto; | |
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto; | |
use EasyCorp\Bundle\EasyAdminBundle\Dto\FilterDataDto; | |
use EasyCorp\Bundle\EasyAdminBundle\Filter\FilterTrait; | |
use EasyCorp\Bundle\EasyAdminBundle\Form\Filter\Type\EntityFilterType; | |
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\ComparisonType; | |
use Symfony\Component\Uid\Ulid; | |
use Symfony\Component\Uid\Uuid; | |
final class AutocompleteEntityFilter implements FilterInterface | |
{ | |
use FilterTrait; | |
public static function new(string $propertyName, $label = null): self | |
{ | |
return (new self()) | |
->setFilterFqcn(__CLASS__) | |
->setProperty($propertyName) | |
->setLabel($label) | |
->setFormType(EntityFilterType::class) | |
->setFormTypeOption('translation_domain', 'EasyAdminBundle') | |
; | |
} | |
public function apply( | |
QueryBuilder $queryBuilder, | |
FilterDataDto $filterDataDto, | |
?FieldDto $fieldDto, | |
EntityDto $entityDto | |
): void { | |
$alias = $filterDataDto->getEntityAlias(); | |
$property = $filterDataDto->getProperty(); | |
$comparison = $filterDataDto->getComparison(); | |
$parameterName = $filterDataDto->getParameterName(); | |
$value = $filterDataDto->getValue(); | |
$isMultiple = $filterDataDto->getFormTypeOption('value_type_options.multiple'); | |
if ($entityDto->isToManyAssociation($property)) { | |
// the 'ea_' prefix is needed to avoid errors when using reserved words as assocAlias ('order', 'group', etc.) | |
// see https://github.com/EasyCorp/EasyAdminBundle/pull/4344 | |
$assocAlias = 'ea_'.$filterDataDto->getParameterName(); | |
$queryBuilder->leftJoin(sprintf('%s.%s', $alias, $property), $assocAlias); | |
if (0 === \count($value)) { | |
$queryBuilder->andWhere(sprintf('%s %s', $assocAlias, $comparison)); | |
} else { | |
$orX = new Orx(); | |
$orX->add(sprintf('%s %s (:%s)', $assocAlias, $comparison, $parameterName)); | |
if ('NOT IN' === $comparison) { | |
$orX->add(sprintf('%s IS NULL', $assocAlias)); | |
} | |
$queryBuilder->andWhere($orX) | |
->setParameter($parameterName, $this->processParameterValue($queryBuilder, $value)) | |
; | |
} | |
} elseif (null === $value || ($isMultiple && 0 === \count($value))) { | |
$queryBuilder->andWhere(sprintf('%s.%s %s', $alias, $property, $comparison)); | |
} else { | |
$orX = new Orx(); | |
$orX->add(sprintf('%s.%s %s (:%s)', $alias, $property, $comparison, $parameterName)); | |
if (ComparisonType::NEQ === $comparison) { | |
$orX->add(sprintf('%s.%s IS NULL', $alias, $property)); | |
} | |
$queryBuilder->andWhere($orX) | |
->setParameter($parameterName, $this->processParameterValue($queryBuilder, $value)) | |
; | |
} | |
} | |
private function processParameterValue(QueryBuilder $queryBuilder, $parameterValue) | |
{ | |
if (!$parameterValue instanceof ArrayCollection) { | |
return $this->processSingleParameterValue($queryBuilder, $parameterValue); | |
} | |
return $parameterValue->map(fn ($element) => $this->processSingleParameterValue($queryBuilder, $element)); | |
} | |
private function processSingleParameterValue(QueryBuilder $queryBuilder, $parameterValue) | |
{ | |
$entityManager = $queryBuilder->getEntityManager(); | |
try { | |
$classMetadata = $entityManager->getClassMetadata(\get_class($parameterValue)); | |
} catch (\Throwable $e) { | |
// only reached if $parameterValue does not contain an object of a managed | |
// entity, return as we only need to process bound entities | |
return $parameterValue; | |
} | |
try { | |
$identifierType = $classMetadata->getTypeOfField($classMetadata->getSingleIdentifierFieldName()); | |
} catch (MappingException $e) { | |
throw new \RuntimeException( | |
sprintf( | |
'The EntityFilter does not support entities with a composite primary key or entities without an identifier. Please check your entity "%s".', | |
\get_class($parameterValue) | |
) | |
); | |
} | |
$identifierValue = $entityManager->getUnitOfWork()->getSingleIdentifierValue($parameterValue); | |
if (('uuid' === $identifierType && $identifierValue instanceof Uuid) | |
|| ('ulid' === $identifierType && $identifierValue instanceof Ulid)) { | |
try { | |
return Type::getType($identifierType)->convertToDatabaseValue( | |
$identifierValue, | |
$entityManager->getConnection()->getDatabasePlatform() | |
); | |
} catch (\Throwable $e) { | |
// if the conversion fails we cannot process the uid parameter value | |
return $parameterValue; | |
} | |
} | |
return $parameterValue; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\EasyAdmin\Filter\Configurator; | |
use App\EasyAdmin\Filter\AutocompleteEntityFilter; | |
use App\Form\Filter\Type\AutocompleteEntityFilterType; | |
use EasyCorp\Bundle\EasyAdminBundle\Config\Option\EA; | |
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext; | |
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Filter\FilterConfiguratorInterface; | |
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto; | |
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto; | |
use EasyCorp\Bundle\EasyAdminBundle\Dto\FilterDto; | |
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator; | |
final class AutocompleteEntityFilterConfigurator implements FilterConfiguratorInterface | |
{ | |
private AdminUrlGenerator $adminUrlGenerator; | |
public function __construct(AdminUrlGenerator $adminUrlGenerator) | |
{ | |
$this->adminUrlGenerator = $adminUrlGenerator; | |
} | |
public function supports( | |
FilterDto $filterDto, | |
?FieldDto $fieldDto, | |
EntityDto $entityDto, | |
AdminContext $context | |
): bool { | |
return AutocompleteEntityFilter::class === $filterDto->getFqcn(); | |
} | |
public function configure( | |
FilterDto $filterDto, | |
?FieldDto $fieldDto, | |
EntityDto $entityDto, | |
AdminContext $context | |
): void { | |
$propertyName = $filterDto->getProperty(); | |
if (!$entityDto->isAssociation($propertyName)) { | |
return; | |
} | |
$doctrineMetadata = $entityDto->getPropertyMetadata($propertyName); | |
// TODO: add the 'em' form type option too? | |
$filterDto->setFormTypeOptionIfNotSet('value_type_options.class', $doctrineMetadata->get('targetEntity')); | |
$filterDto->setFormTypeOptionIfNotSet( | |
'value_type_options.multiple', | |
$entityDto->isToManyAssociation($propertyName) | |
); | |
$filterDto->setFormTypeOptionIfNotSet('value_type_options.attr.data-ea-widget', 'ea-autocomplete'); | |
if ($entityDto->isToOneAssociation($propertyName)) { | |
// don't show the 'empty value' placeholder when all join columns are required, | |
// because an empty filter value would always return no result | |
$numberOfRequiredJoinColumns = \count( | |
array_filter( | |
$doctrineMetadata->get('joinColumns'), | |
static fn (array $joinColumn): bool => false === ($joinColumn['nullable'] ?? false) | |
) | |
); | |
$someJoinColumnsAreNullable = \count( | |
$doctrineMetadata->get('joinColumns') | |
) !== $numberOfRequiredJoinColumns; | |
if ($someJoinColumnsAreNullable) { | |
$filterDto->setFormTypeOptionIfNotSet('value_type_options.placeholder', 'label.form.empty_value'); | |
} | |
} | |
$targetEntityFqcn = $doctrineMetadata->get('targetEntity'); | |
$targetCrudControllerFqcn = $context->getCrudControllers()->findCrudFqcnByEntityFqcn($targetEntityFqcn); | |
if ($targetCrudControllerFqcn) { | |
$filterDto->setFormTypeOptionIfNotSet('value_type', AutocompleteEntityFilterType::class); | |
$filterDto->setFormTypeOptionIfNotSet('value_type_options.class', $doctrineMetadata->get('targetEntity')); | |
$filterDto->setFormTypeOptionIfNotSet( | |
'value_type_options.multiple', | |
$entityDto->isToManyAssociation($propertyName) | |
); | |
$filterDto->setFormTypeOptionIfNotSet('value_type_options.attr.data-widget', 'select2'); | |
$autocompleteEndpointUrl = $this->adminUrlGenerator | |
->set('page', 1) | |
->setController($targetCrudControllerFqcn) | |
->setAction('autocomplete') | |
->setEntityId(null) | |
->unset(EA::SORT) | |
->set('autocompleteContext', [ | |
EA::CRUD_CONTROLLER_FQCN => $context->getRequest()->query->get(EA::CRUD_CONTROLLER_FQCN), | |
'propertyName' => $propertyName, | |
'originatingPage' => $context->getCrud()->getCurrentAction(), | |
]) | |
->generateUrl() | |
; | |
$filterDto->setFormTypeOption( | |
'value_type_options.attr.data-ea-autocomplete-endpoint-url', | |
$autocompleteEndpointUrl | |
); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} | |
{% extends '@!EasyAdmin/crud/form_theme.html.twig' %} | |
{% block ea_autocomplete_widget %} | |
{{ form_widget(form.autocomplete, { attr: attr|merge({ required: required }) }) }} | |
<script> | |
document.dispatchEvent(new Event('ea.collection.item-added')); | |
</script> | |
{% endblock ea_autocomplete_widget %} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Many Thanks, it helps a lot !!