Last active
March 8, 2024 13:12
-
-
Save wizhippo/9043a6676ce2920676b730ba2f507655 to your computer and use it in GitHub Desktop.
Start of adding autocomplete support to EntityFilter
This file contains 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 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 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
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 :
My entities use ULIDs as primary key, maybe it's the problem ?