vendor/easycorp/easyadmin-bundle/src/Orm/EntityRepository.php line 48

Open in your IDE?
  1. <?php
  2. namespace EasyCorp\Bundle\EasyAdminBundle\Orm;
  3. use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use Doctrine\ORM\Mapping\ClassMetadata;
  6. use Doctrine\ORM\Query\Expr\Orx;
  7. use Doctrine\ORM\QueryBuilder;
  8. use Doctrine\Persistence\ManagerRegistry;
  9. use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
  10. use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection;
  11. use EasyCorp\Bundle\EasyAdminBundle\Config\Option\SearchMode;
  12. use EasyCorp\Bundle\EasyAdminBundle\Contracts\Orm\EntityRepositoryInterface;
  13. use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
  14. use EasyCorp\Bundle\EasyAdminBundle\Dto\FilterDataDto;
  15. use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
  16. use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntitySearchEvent;
  17. use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory;
  18. use EasyCorp\Bundle\EasyAdminBundle\Factory\FormFactory;
  19. use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
  20. use EasyCorp\Bundle\EasyAdminBundle\Form\Type\ComparisonType;
  21. use EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider;
  22. use Symfony\Component\Uid\Ulid;
  23. use Symfony\Component\Uid\Uuid;
  24. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  25. /**
  26.  * @author Javier Eguiluz <javier.eguiluz@gmail.com>
  27.  */
  28. final class EntityRepository implements EntityRepositoryInterface
  29. {
  30.     private AdminContextProvider $adminContextProvider;
  31.     private ManagerRegistry $doctrine;
  32.     private EntityFactory $entityFactory;
  33.     private FormFactory $formFactory;
  34.     private EventDispatcherInterface $eventDispatcher;
  35.     public function __construct(AdminContextProvider $adminContextProviderManagerRegistry $doctrineEntityFactory $entityFactoryFormFactory $formFactoryEventDispatcherInterface $eventDispatcher)
  36.     {
  37.         $this->adminContextProvider $adminContextProvider;
  38.         $this->doctrine $doctrine;
  39.         $this->entityFactory $entityFactory;
  40.         $this->formFactory $formFactory;
  41.         $this->eventDispatcher $eventDispatcher;
  42.     }
  43.     public function createQueryBuilder(SearchDto $searchDtoEntityDto $entityDtoFieldCollection $fieldsFilterCollection $filters): QueryBuilder
  44.     {
  45.         /** @var EntityManagerInterface $entityManager */
  46.         $entityManager $this->doctrine->getManagerForClass($entityDto->getFqcn());
  47.         $queryBuilder $entityManager->createQueryBuilder()
  48.             ->select('entity')
  49.             ->from($entityDto->getFqcn(), 'entity')
  50.         ;
  51.         if ('' !== $searchDto->getQuery()) {
  52.             try {
  53.                 $databasePlatform $entityManager->getConnection()->getDatabasePlatform();
  54.             } catch (\Throwable) {
  55.                 $databasePlatform null;
  56.             }
  57.             $databasePlatformFqcn null !== $databasePlatform $databasePlatform::class : '';
  58.             $this->addSearchClause($queryBuilder$searchDto$entityDto$databasePlatformFqcn);
  59.         }
  60.         $appliedFilters $searchDto->getAppliedFilters();
  61.         if (null !== $appliedFilters && !== \count($appliedFilters)) {
  62.             $this->addFilterClause($queryBuilder$searchDto$entityDto$filters$fields);
  63.         }
  64.         $this->addOrderClause($queryBuilder$searchDto$entityDto$fields);
  65.         return $queryBuilder;
  66.     }
  67.     private function addSearchClause(QueryBuilder $queryBuilderSearchDto $searchDtoEntityDto $entityDtostring $databasePlatformFqcn): void
  68.     {
  69.         $isPostgreSql PostgreSQLPlatform::class === $databasePlatformFqcn || is_subclass_of($databasePlatformFqcnPostgreSQLPlatform::class);
  70.         $searchablePropertiesConfig $this->getSearchablePropertiesConfig($queryBuilder$searchDto$entityDto);
  71.         $queryTerms $searchDto->getQueryTerms();
  72.         $queryTermIndex 0;
  73.         foreach ($queryTerms as $queryTerm) {
  74.             ++$queryTermIndex;
  75.             $lowercaseQueryTerm mb_strtolower($queryTerm);
  76.             $isNumericQueryTerm is_numeric($queryTerm);
  77.             $isSmallIntegerQueryTerm ctype_digit($queryTerm) && $queryTerm >= -32768 && $queryTerm <= 32767;
  78.             $isIntegerQueryTerm ctype_digit($queryTerm) && $queryTerm >= -2147483648 && $queryTerm <= 2147483647;
  79.             $isUuidQueryTerm Uuid::isValid($queryTerm);
  80.             $isUlidQueryTerm Ulid::isValid($queryTerm);
  81.             $dqlParameters = [
  82.                 // adding '0' turns the string into a numeric value
  83.                 'numeric_query' => is_numeric($queryTerm) ? $queryTerm $queryTerm,
  84.                 'uuid_query' => $queryTerm,
  85.                 'text_query' => '%'.$lowercaseQueryTerm.'%',
  86.             ];
  87.             $queryTermConditions = new Orx();
  88.             foreach ($searchablePropertiesConfig as $propertyConfig) {
  89.                 $entityName $propertyConfig['entity_name'];
  90.                 // this complex condition is needed to avoid issues on PostgreSQL databases
  91.                 if (
  92.                     ($propertyConfig['is_small_integer'] && $isSmallIntegerQueryTerm)
  93.                     || ($propertyConfig['is_integer'] && $isIntegerQueryTerm)
  94.                     || ($propertyConfig['is_numeric'] && $isNumericQueryTerm)
  95.                 ) {
  96.                     $parameterName sprintf('query_for_numbers_%d'$queryTermIndex);
  97.                     $queryTermConditions->add(sprintf('%s.%s = :%s'$entityName$propertyConfig['property_name'], $parameterName));
  98.                     $queryBuilder->setParameter($parameterName$dqlParameters['numeric_query']);
  99.                 } elseif ($propertyConfig['is_guid'] && $isUuidQueryTerm) {
  100.                     $parameterName sprintf('query_for_uuids_%d'$queryTermIndex);
  101.                     $queryTermConditions->add(sprintf('%s.%s = :%s'$entityName$propertyConfig['property_name'], $parameterName));
  102.                     $queryBuilder->setParameter($parameterName$dqlParameters['uuid_query'], 'uuid' === $propertyConfig['property_data_type'] ? 'uuid' null);
  103.                 } elseif ($propertyConfig['is_ulid'] && $isUlidQueryTerm) {
  104.                     $parameterName sprintf('query_for_ulids_%d'$queryTermIndex);
  105.                     $queryTermConditions->add(sprintf('%s.%s = :%s'$entityName$propertyConfig['property_name'], $parameterName));
  106.                     $queryBuilder->setParameter($parameterName$dqlParameters['uuid_query'], 'ulid');
  107.                 } elseif ($propertyConfig['is_text']) {
  108.                     $parameterName sprintf('query_for_text_%d'$queryTermIndex);
  109.                     // concatenating an empty string is needed to avoid issues on PostgreSQL databases (https://github.com/EasyCorp/EasyAdminBundle/issues/6290)
  110.                     $queryTermConditions->add(sprintf('LOWER(CONCAT(%s.%s, \'\')) LIKE :%s'$entityName$propertyConfig['property_name'], $parameterName));
  111.                     $queryBuilder->setParameter($parameterName$dqlParameters['text_query']);
  112.                 } elseif ($propertyConfig['is_json'] && !$isPostgreSql) {
  113.                     // neither LOWER() nor LIKE() are supported for JSON columns by all PostgreSQL installations
  114.                     $parameterName sprintf('query_for_text_%d'$queryTermIndex);
  115.                     $queryTermConditions->add(sprintf('LOWER(%s.%s) LIKE :%s'$entityName$propertyConfig['property_name'], $parameterName));
  116.                     $queryBuilder->setParameter($parameterName$dqlParameters['text_query']);
  117.                 }
  118.             }
  119.             // When no fields are queried, the current condition must not yield any results
  120.             if (=== $queryTermConditions->count()) {
  121.                 $queryTermConditions->add('0 = 1');
  122.             }
  123.             if (SearchMode::ALL_TERMS === $searchDto->getSearchMode()) {
  124.                 $queryBuilder->andWhere($queryTermConditions);
  125.             } else {
  126.                 $queryBuilder->orWhere($queryTermConditions);
  127.             }
  128.         }
  129.         $this->eventDispatcher->dispatch(new AfterEntitySearchEvent($queryBuilder$searchDto$entityDto));
  130.     }
  131.     private function addOrderClause(QueryBuilder $queryBuilderSearchDto $searchDtoEntityDto $entityDtoFieldCollection $fields): void
  132.     {
  133.         foreach ($searchDto->getSort() as $sortProperty => $sortOrder) {
  134.             $aliases $queryBuilder->getAllAliases();
  135.             $sortFieldIsDoctrineAssociation $entityDto->isAssociation($sortProperty);
  136.             if ($sortFieldIsDoctrineAssociation) {
  137.                 $sortFieldParts explode('.'$sortProperty2);
  138.                 // check if join has been added once before.
  139.                 if (!\in_array($sortFieldParts[0], $aliasestrue)) {
  140.                     $queryBuilder->leftJoin('entity.'.$sortFieldParts[0], $sortFieldParts[0]);
  141.                 }
  142.                 if (=== \count($sortFieldParts)) {
  143.                     if ($entityDto->isToManyAssociation($sortProperty)) {
  144.                         $metadata $entityDto->getPropertyMetadata($sortProperty);
  145.                         /** @var EntityManagerInterface $entityManager */
  146.                         $entityManager $this->doctrine->getManagerForClass($entityDto->getFqcn());
  147.                         $countQueryBuilder $entityManager->createQueryBuilder();
  148.                         if (ClassMetadata::MANY_TO_MANY === $metadata->get('type')) {
  149.                             // many-to-many relation
  150.                             $countQueryBuilder
  151.                                 ->select($queryBuilder->expr()->count('subQueryEntity'))
  152.                                 ->from($entityDto->getFqcn(), 'subQueryEntity')
  153.                                 ->join(sprintf('subQueryEntity.%s'$sortProperty), 'relatedEntity')
  154.                                 ->where('subQueryEntity = entity');
  155.                         } else {
  156.                             // one-to-many relation
  157.                             $countQueryBuilder
  158.                                 ->select($queryBuilder->expr()->count('subQueryEntity'))
  159.                                 ->from($metadata->get('targetEntity'), 'subQueryEntity')
  160.                                 ->where(sprintf('subQueryEntity.%s = entity'$metadata->get('mappedBy')));
  161.                         }
  162.                         $queryBuilder->addSelect(sprintf('(%s) as HIDDEN sub_query_sort'$countQueryBuilder->getDQL()));
  163.                         $queryBuilder->addOrderBy('sub_query_sort'$sortOrder);
  164.                         $queryBuilder->addOrderBy('entity.'.$entityDto->getPrimaryKeyName(), $sortOrder);
  165.                     } else {
  166.                         $field $fields->getByProperty($sortProperty);
  167.                         $associationSortProperty $field?->getCustomOption(AssociationField::OPTION_SORT_PROPERTY);
  168.                         if (null === $associationSortProperty) {
  169.                             $queryBuilder->addOrderBy('entity.'.$sortProperty$sortOrder);
  170.                         } else {
  171.                             $queryBuilder->addOrderBy($sortProperty.'.'.$associationSortProperty$sortOrder);
  172.                         }
  173.                     }
  174.                 } else {
  175.                     $queryBuilder->addOrderBy($sortProperty$sortOrder);
  176.                 }
  177.             } else {
  178.                 $queryBuilder->addOrderBy('entity.'.$sortProperty$sortOrder);
  179.             }
  180.         }
  181.     }
  182.     private function addFilterClause(QueryBuilder $queryBuilderSearchDto $searchDtoEntityDto $entityDtoFilterCollection $configuredFiltersFieldCollection $fields): void
  183.     {
  184.         $filtersForm $this->formFactory->createFiltersForm($configuredFilters$this->adminContextProvider->getContext()->getRequest());
  185.         if (!$filtersForm->isSubmitted()) {
  186.             return;
  187.         }
  188.         $appliedFilters $searchDto->getAppliedFilters();
  189.         $i 0;
  190.         foreach ($filtersForm as $filterForm) {
  191.             $propertyName $filterForm->getName();
  192.             $filter $configuredFilters->get($propertyName);
  193.             // this filter is not defined or not applied
  194.             if (null === $filter || !isset($appliedFilters[$propertyName])) {
  195.                 continue;
  196.             }
  197.             // if the form filter is not valid then we should not apply the filter
  198.             if (!$filterForm->isValid()) {
  199.                 continue;
  200.             }
  201.             $submittedData $filterForm->getData();
  202.             if (!\is_array($submittedData)) {
  203.                 $submittedData = [
  204.                     'comparison' => ComparisonType::EQ,
  205.                     'value' => $submittedData,
  206.                 ];
  207.             }
  208.             $filterDataDto FilterDataDto::new($i$filtercurrent($queryBuilder->getRootAliases()), $submittedData);
  209.             $filter->apply($queryBuilder$filterDataDto$fields->getByProperty($propertyName), $entityDto);
  210.             ++$i;
  211.         }
  212.     }
  213.     private function getSearchablePropertiesConfig(QueryBuilder $queryBuilderSearchDto $searchDtoEntityDto $entityDto): array
  214.     {
  215.         $searchablePropertiesConfig = [];
  216.         $configuredSearchableProperties $searchDto->getSearchableProperties();
  217.         $searchableProperties = (null === $configuredSearchableProperties || === \count($configuredSearchableProperties)) ? $entityDto->getAllPropertyNames() : $configuredSearchableProperties;
  218.         $entitiesAlreadyJoined = [];
  219.         foreach ($searchableProperties as $propertyName) {
  220.             if ($entityDto->isAssociation($propertyName)) {
  221.                 // support arbitrarily nested associations (e.g. foo.bar.baz.qux)
  222.                 $associatedProperties explode('.'$propertyName);
  223.                 $numAssociatedProperties \count($associatedProperties);
  224.                 if (=== $numAssociatedProperties) {
  225.                     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));
  226.                 }
  227.                 $originalPropertyName $associatedProperties[0];
  228.                 $originalPropertyMetadata $entityDto->getPropertyMetadata($originalPropertyName);
  229.                 $associatedEntityDto $this->entityFactory->create($originalPropertyMetadata->get('targetEntity'));
  230.                 $associatedEntityAlias $associatedPropertyName '';
  231.                 for ($i 0$i $numAssociatedProperties 1; ++$i) {
  232.                     $associatedEntityName $associatedProperties[$i];
  233.                     $associatedEntityAlias $entitiesAlreadyJoined[$associatedEntityName] ?? Escaper::escapeDqlAlias($associatedEntityName).(=== $i '' $i);
  234.                     $associatedPropertyName $associatedProperties[$i 1];
  235.                     if (!\in_array($associatedEntityAlias$entitiesAlreadyJoinedtrue)) {
  236.                         $parentEntityName === $i 'entity' $entitiesAlreadyJoined[$associatedProperties[$i 1]];
  237.                         $queryBuilder->leftJoin(Escaper::escapeDqlAlias($parentEntityName).'.'.$associatedEntityName$associatedEntityAlias);
  238.                         $entitiesAlreadyJoined[$associatedEntityName] = $associatedEntityAlias;
  239.                     }
  240.                     if ($i $numAssociatedProperties 2) {
  241.                         $propertyMetadata $associatedEntityDto->getPropertyMetadata($associatedPropertyName);
  242.                         $targetEntity $propertyMetadata->get('targetEntity');
  243.                         $associatedEntityDto $this->entityFactory->create($targetEntity);
  244.                     }
  245.                 }
  246.                 $entityName $associatedEntityAlias;
  247.                 $propertyName $associatedPropertyName;
  248.                 $propertyDataType $associatedEntityDto->getPropertyDataType($propertyName);
  249.             } else {
  250.                 $entityName 'entity';
  251.                 $propertyDataType $entityDto->getPropertyDataType($propertyName);
  252.             }
  253.             $isBoolean 'boolean' === $propertyDataType;
  254.             $isSmallIntegerProperty 'smallint' === $propertyDataType;
  255.             $isIntegerProperty 'integer' === $propertyDataType;
  256.             $isNumericProperty \in_array($propertyDataType, ['number''bigint''decimal''float'], true);
  257.             // 'citext' is a PostgreSQL extension (https://github.com/EasyCorp/EasyAdminBundle/issues/2556)
  258.             $isTextProperty \in_array($propertyDataType, ['string''text''citext''array''simple_array'], true);
  259.             $isGuidProperty \in_array($propertyDataType, ['guid''uuid'], true);
  260.             $isUlidProperty 'ulid' === $propertyDataType;
  261.             $isJsonProperty 'json' === $propertyDataType;
  262.             if (!$isBoolean
  263.                 && !$isSmallIntegerProperty
  264.                 && !$isIntegerProperty
  265.                 && !$isNumericProperty
  266.                 && !$isTextProperty
  267.                 && !$isGuidProperty
  268.                 && !$isUlidProperty
  269.                 && !$isJsonProperty
  270.             ) {
  271.                 $entityFqcn 'entity' !== $entityName && isset($associatedEntityDto)
  272.                     ? $associatedEntityDto->getFqcn()
  273.                     : $entityDto->getFqcn()
  274.                 ;
  275.                 /** @var \ReflectionNamedType|\ReflectionUnionType|null $idClassType */
  276.                 $idClassType = (new \ReflectionProperty($entityFqcn$propertyName))->getType();
  277.                 if (null !== $idClassType) {
  278.                     $idClassName $idClassType->getName();
  279.                     if (class_exists($idClassName)) {
  280.                         $isUlidProperty = (new \ReflectionClass($idClassName))->isSubclassOf(Ulid::class);
  281.                         $isGuidProperty = (new \ReflectionClass($idClassName))->isSubclassOf(Uuid::class);
  282.                     }
  283.                 }
  284.             }
  285.             $searchablePropertiesConfig[] = [
  286.                 'entity_name' => $entityName,
  287.                 'property_data_type' => $propertyDataType,
  288.                 'property_name' => $propertyName,
  289.                 'is_boolean' => $isBoolean,
  290.                 'is_small_integer' => $isSmallIntegerProperty,
  291.                 'is_integer' => $isIntegerProperty,
  292.                 'is_numeric' => $isNumericProperty,
  293.                 'is_text' => $isTextProperty,
  294.                 'is_guid' => $isGuidProperty,
  295.                 'is_ulid' => $isUlidProperty,
  296.                 'is_json' => $isJsonProperty,
  297.             ];
  298.         }
  299.         return $searchablePropertiesConfig;
  300.     }
  301. }