-
-
Save wizhippo/9043a6676ce2920676b730ba2f507655 to your computer and use it in GitHub Desktop.
<?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; | |
} | |
} |
<?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 | |
); | |
} | |
} | |
} |
{# @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 %} |
Hi @wizhippo!
I found this gist through google search. Is this something you're planning to upstream?
Issue EasyCorp/EasyAdminBundle#4244, I asked for input and got not. Not sure if they are interested.
@wizhippo The autocomplete filter works fine but it would be very nice if its support nested associations.
It crash for me, "The file "../../src/EasyAdmin/Filter/Configurator" does not exist"
Anyone knows how to get this autocomplete feature to work with nested relations? I am aware of EasyCorp/EasyAdminBundle#4882 but the problem is that it does not support autocompletion like this gist does, which is really important for relations with many results.
It doesn't work for me. When I submit the form with an item, I get an error telling me that the choice is not part of the list
It doesn't work for me. When I submit the form with an item, I get an error telling me that the choice is not part of the list
I just updated it to code I am currently using that is known to work. Not sure it it will fix your issue.
It doesn't work for me. When I submit the form with an item, I get an error telling me that the choice is not part of the list
I just updated it to code I am currently using that is known to work. Not sure it it will fix your issue.
In your last update, you used the "AutocompleteEntityFilterType", but this class doesn't exists. Is it a form type you created ?
EDIT : OK I used your old gist : https://gist.github.com/wizhippo/597bf56cd69dfd3682d8a0eee67ac5c0
But yes, I've still the same error, it's weird
I'm using this filter for a collection, like this :
public function configureFilters(Filters $filters): Filters
{
return parent::configureFilters($filters)
->add(AutocompleteEntityFilter::new('users', 'Utilisateurs'))
;
}
My entities use ULIDs as primary key, maybe it's the problem ?
Many Thanks, it helps a lot !!
Do you have the complete trace? At first glance it looks like '$doctrineMetadata->get('targetEntity')' may not be working for you and setting the correct class.