vendor/symfony/ux-autocomplete/src/Doctrine/EntitySearchUtil.php line 91

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\UX\Autocomplete\Doctrine;
  11. use Doctrine\ORM\QueryBuilder;
  12. use Symfony\Component\Uid\Ulid;
  13. use Symfony\Component\Uid\Uuid;
  14. /**
  15.  * Adapted from EasyCorp/EasyAdminBundle.
  16.  *
  17.  * @experimental
  18.  */
  19. class EntitySearchUtil
  20. {
  21.     public function __construct(private EntityMetadataFactory $metadataFactory)
  22.     {
  23.     }
  24.     /**
  25.      * Adapted from easycorp/easyadmin EntityRepository.
  26.      */
  27.     public function addSearchClause(QueryBuilder $queryBuilderstring $querystring $entityClass, array $searchableProperties null): void
  28.     {
  29.         $entityMetadata $this->metadataFactory->create($entityClass);
  30.         $lowercaseQuery mb_strtolower($query);
  31.         $isNumericQuery is_numeric($query);
  32.         $isSmallIntegerQuery ctype_digit($query) && $query >= -32768 && $query <= 32767;
  33.         $isIntegerQuery ctype_digit($query) && $query >= -2147483648 && $query <= 2147483647;
  34.         $isUuidQuery class_exists(Uuid::class) && Uuid::isValid($query);
  35.         $isUlidQuery class_exists(Ulid::class) && Ulid::isValid($query);
  36.         $dqlParameters = [
  37.             // adding '0' turns the string into a numeric value
  38.             'numeric_query' => is_numeric($query) ? $query $query,
  39.             'uuid_query' => $query,
  40.             'text_query' => '%'.$lowercaseQuery.'%',
  41.             'words_query' => explode(' '$lowercaseQuery),
  42.         ];
  43.         $entitiesAlreadyJoined = [];
  44.         $searchableProperties = empty($searchableProperties) ? $entityMetadata->getAllPropertyNames() : $searchableProperties;
  45.         $expressions = [];
  46.         foreach ($searchableProperties as $propertyName) {
  47.             if ($entityMetadata->isAssociation($propertyName)) {
  48.                 // support arbitrarily nested associations (e.g. foo.bar.baz.qux)
  49.                 $associatedProperties explode('.'$propertyName);
  50.                 $numAssociatedProperties \count($associatedProperties);
  51.                 if (=== $numAssociatedProperties) {
  52.                     throw new \InvalidArgumentException(sprintf('The "%s" property included in the setSearchFields() method is not a valid search field. When using associated properties in search, you must also define the exact field used in the search (e.g. \'%s.id\', \'%s.name\', etc.)'$propertyName$propertyName$propertyName));
  53.                 }
  54.                 $originalPropertyName $associatedProperties[0];
  55.                 $originalPropertyMetadata $entityMetadata->getPropertyMetadata($originalPropertyName);
  56.                 $associatedEntityDto $this->metadataFactory->create($originalPropertyMetadata['targetEntity']);
  57.                 for ($i 0$i $numAssociatedProperties 1; ++$i) {
  58.                     $associatedEntityName $associatedProperties[$i];
  59.                     $associatedEntityAlias SearchEscaper::escapeDqlAlias($associatedEntityName);
  60.                     $associatedPropertyName $associatedProperties[$i 1];
  61.                     if (!\in_array($associatedEntityName$entitiesAlreadyJoinedtrue)) {
  62.                         $parentEntityName === $i $queryBuilder->getRootAliases()[0] : $associatedProperties[$i 1];
  63.                         $queryBuilder->leftJoin($parentEntityName.'.'.$associatedEntityName$associatedEntityAlias);
  64.                         $entitiesAlreadyJoined[] = $associatedEntityName;
  65.                     }
  66.                     if ($i $numAssociatedProperties 2) {
  67.                         $propertyMetadata $associatedEntityDto->getPropertyMetadata($associatedPropertyName);
  68.                         $targetEntity $propertyMetadata['targetEntity'];
  69.                         $associatedEntityDto $this->metadataFactory->create($targetEntity);
  70.                     }
  71.                 }
  72.                 $entityName $associatedEntityAlias;
  73.                 $propertyName $associatedPropertyName;
  74.                 $propertyDataType $associatedEntityDto->getPropertyDataType($propertyName);
  75.             } else {
  76.                 $entityName $queryBuilder->getRootAliases()[0];
  77.                 $propertyDataType $entityMetadata->getPropertyDataType($propertyName);
  78.             }
  79.             $isSmallIntegerProperty 'smallint' === $propertyDataType;
  80.             $isIntegerProperty 'integer' === $propertyDataType;
  81.             $isNumericProperty \in_array($propertyDataType, ['number''bigint''decimal''float']);
  82.             // 'citext' is a PostgreSQL extension (https://github.com/EasyCorp/EasyAdminBundle/issues/2556)
  83.             $isTextProperty \in_array($propertyDataType, ['string''text''citext''array''simple_array']);
  84.             $isGuidProperty \in_array($propertyDataType, ['guid''uuid']);
  85.             $isUlidProperty 'ulid' === $propertyDataType;
  86.             // this complex condition is needed to avoid issues on PostgreSQL databases
  87.             if (
  88.                 ($isSmallIntegerProperty && $isSmallIntegerQuery) ||
  89.                 ($isIntegerProperty && $isIntegerQuery) ||
  90.                 ($isNumericProperty && $isNumericQuery)
  91.             ) {
  92.                 $expressions[] = $queryBuilder->expr()->eq(sprintf('%s.%s'$entityName$propertyName), ':query_for_numbers');
  93.                 $queryBuilder->setParameter('query_for_numbers'$dqlParameters['numeric_query']);
  94.             } elseif ($isGuidProperty && $isUuidQuery) {
  95.                 $expressions[] = $queryBuilder->expr()->eq(sprintf('%s.%s'$entityName$propertyName), ':query_for_uuids');
  96.                 $queryBuilder->setParameter('query_for_uuids'$dqlParameters['uuid_query'], 'uuid' === $propertyDataType 'uuid' null);
  97.             } elseif ($isUlidProperty && $isUlidQuery) {
  98.                 $expressions[] = $queryBuilder->expr()->eq(sprintf('%s.%s'$entityName$propertyName), ':query_for_uuids');
  99.                 $queryBuilder->setParameter('query_for_uuids'$dqlParameters['uuid_query'], 'ulid');
  100.             } elseif ($isTextProperty) {
  101.                 $expressions[] = $queryBuilder->expr()->like(sprintf('LOWER(%s.%s)'$entityName$propertyName), ':query_for_text');
  102.                 $queryBuilder->setParameter('query_for_text'$dqlParameters['text_query']);
  103.                 $expressions[] = $queryBuilder->expr()->in(sprintf('LOWER(%s.%s)'$entityName$propertyName), ':query_as_words');
  104.                 $queryBuilder->setParameter('query_as_words'$dqlParameters['words_query']);
  105.             }
  106.         }
  107.         $queryBuilder->andWhere($queryBuilder->expr()->orX(...$expressions));
  108.     }
  109. }