diff --git a/src/Dto/EntityDto.php b/src/Dto/EntityDto.php index 1850059045..74e1777d5a 100644 --- a/src/Dto/EntityDto.php +++ b/src/Dto/EntityDto.php @@ -192,6 +192,22 @@ public function isToManyAssociation(string $propertyName): bool return \in_array($associationType, [ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::MANY_TO_MANY], true); } + public function getEmbeddedTargetClassName(string $propertyName): bool + { + return $this->getEmbeddedPropertyMetadata($propertyName)['class']; + } + + public function getEmbeddedPropertyMetadata(string $propertyName): array + { + if (!$this->isEmbeddedClassProperty($propertyName)) { + throw new \LogicException(sprintf('The property "%s" is not an embedded class property of class "%s".', $propertyName, $this->getFqcn())); + } + + $propertyNameParts = explode('.', $propertyName, 2); + + return $this->metadata->embeddedClasses[$propertyNameParts[0]]; + } + public function isEmbeddedClassProperty(string $propertyName): bool { $propertyNameParts = explode('.', $propertyName, 2); diff --git a/src/Factory/FilterFactory.php b/src/Factory/FilterFactory.php index 99052fe6ce..ab787e87ba 100644 --- a/src/Factory/FilterFactory.php +++ b/src/Factory/FilterFactory.php @@ -24,6 +24,7 @@ final class FilterFactory { private $adminContextProvider; + private $entityFactory; private $filterConfigurators; private static $doctrineTypeToFilterClass = [ 'json_array' => ArrayFilter::class, @@ -52,17 +53,28 @@ final class FilterFactory Types::TEXT => TextFilter::class, ]; - public function __construct(AdminContextProvider $adminContextProvider, iterable $filterConfigurators) + public function __construct(AdminContextProvider $adminContextProvider, EntityFactory $entityFactory, iterable $filterConfigurators) { $this->adminContextProvider = $adminContextProvider; + $this->entityFactory = $entityFactory; $this->filterConfigurators = $filterConfigurators; } public function create(FilterConfigDto $filterConfig, FieldCollection $fields, EntityDto $entityDto): FilterCollection { $builtFilters = []; + $filters = $filterConfig->all(); + + /** @var FilterInterface|string|array $filter */ + foreach ($filters as $key => $filter) { + if (\is_array($filter)) { + $filters = array_merge($filters, $this->normalizeEmbeddedFilters($key, $filter)); + unset($filters[$key]); + } + } + /** @var FilterInterface|string $filter */ - foreach ($filterConfig->all() as $property => $filter) { + foreach ($filters as $property => $filter) { if (\is_string($filter)) { $guessedFilterClass = $this->guessFilterClass($entityDto, $property); /** @var FilterInterface $filter */ @@ -86,12 +98,26 @@ public function create(FilterConfigDto $filterConfig, FieldCollection $fields, E return FilterCollection::new($builtFilters); } - private function guessFilterClass(EntityDto $entityDto, string $propertyName): string + private function guessFilterClass(EntityDto $entityDto, string $propertyName, array $context = []): string { if ($entityDto->isAssociation($propertyName)) { return EntityFilter::class; } + if ($entityDto->isEmbeddedClassProperty($propertyName)) { + $properties = explode('.', $propertyName, 2); + $context['root_entity'] = $context['root_entity'] ?? $entityDto; + $context['root_property'] = $context['root_property'] ?? $propertyName; + $embeddedEntity = $this->entityFactory->create($entityDto->getEmbeddedTargetClassName($propertyName)); + $embeddedProperty = $properties[1] ?? null; + + if (!$embeddedProperty) { + throw new \LogicException(sprintf('Missing embedded property name for the property "%s" in entity class "%s".', $context['root_property'], $context['root_entity']->getFqcn())); + } + + return $this->guessFilterClass($embeddedEntity, $properties, $context); + } + $metadata = $entityDto->getPropertyMetadata($propertyName); if ($metadata->isEmpty()) { @@ -100,4 +126,22 @@ private function guessFilterClass(EntityDto $entityDto, string $propertyName): s return self::$doctrineTypeToFilterClass[$metadata->get('type')] ?? TextFilter::class; } + + private function normalizeEmbeddedFilters(string $rootPropertyName, array $embeddedFilters = []): array + { + $filters = []; + + foreach ($embeddedFilters as $propertyName => $embeddedFilter) { + if (\is_array($embeddedFilter)) { + $filters = array_merge($filters, $this->normalizeEmbeddedFilters("$rootPropertyName.$propertyName", $embeddedFilter)); + + continue; + } + + $embeddedFilter->getAsDto()->setProperty("$rootPropertyName.$propertyName"); + $filters["$rootPropertyName.$propertyName"] = $embeddedFilter; + } + + return $filters; + } } diff --git a/src/Factory/FormFactory.php b/src/Factory/FormFactory.php index 8ba6934494..a31cc555c3 100644 --- a/src/Factory/FormFactory.php +++ b/src/Factory/FormFactory.php @@ -60,6 +60,28 @@ public function createNewForm(EntityDto $entityDto, KeyValueStore $formOptions, public function createFiltersForm(FilterCollection $filters, Request $request): FormInterface { + // To avoid errors on embedded class property fields + foreach ($filters as $filter) { + if (false !== strpos($filter->getProperty(), '.')) { + $normalizedFilterName = str_replace('.', '_', $filter->getProperty()); + $propertyPath = $filter->getFormTypeOption('property_path'); + + if (!$propertyPath) { + // The property accessor sets values on array. + // So we must replace object path to array path. + $paths = explode('.', $filter->getProperty()); + foreach ($paths as $key => $path) { + $paths[$key] = "[$path]"; + } + + // We set the property path as form option + $filter->setFormTypeOption('property_path', implode('', $paths)); + } + + $filter->setProperty($normalizedFilterName); + } + } + $filtersForm = $this->symfonyFormFactory->createNamed('filters', FiltersFormType::class, null, [ 'method' => 'GET', 'action' => $request->query->get(EA::REFERRER, ''), diff --git a/src/Orm/EntityRepository.php b/src/Orm/EntityRepository.php index 11dc2515fa..49214433ad 100644 --- a/src/Orm/EntityRepository.php +++ b/src/Orm/EntityRepository.php @@ -9,6 +9,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Contracts\Orm\EntityRepositoryInterface; use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto; use EasyCorp\Bundle\EasyAdminBundle\Dto\FilterDataDto; +use EasyCorp\Bundle\EasyAdminBundle\Dto\FilterDto; use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto; use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory; use EasyCorp\Bundle\EasyAdminBundle\Factory\FormFactory; @@ -186,7 +187,7 @@ private function addFilterClause(QueryBuilder $queryBuilder, SearchDto $searchDt foreach ($filtersForm as $filterForm) { $propertyName = $filterForm->getName(); - $filter = $configuredFilters->get($propertyName); + $filter = $this->resolveFilterDto($configuredFilters, $propertyName); // this filter is not defined or not applied if (null === $filter || !isset($appliedFilters[$propertyName])) { continue; @@ -211,4 +212,21 @@ private function addFilterClause(QueryBuilder $queryBuilder, SearchDto $searchDt ++$i; } } + + private function resolveFilterDto(FilterCollection $configuredFilters, string $propertyName): ?FilterDto + { + $filter = $configuredFilters->get($propertyName); + + if (!$filter) { + foreach ($configuredFilters as $filteredPropertyName => $filter) { + if ($propertyName === $filter->getProperty()) { + $filter->setProperty($filteredPropertyName); + + return $filter; + } + } + } + + return null; + } } diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index e463fec2db..6e4b568c7b 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -257,7 +257,8 @@ ->set(FilterFactory::class) ->arg(0, new Reference(AdminContextProvider::class)) - ->arg(1, \function_exists('tagged') + ->arg(1, new Reference(EntityFactory::class)) + ->arg(2, \function_exists('tagged') ? tagged(EasyAdminExtension::TAG_FILTER_CONFIGURATOR) : tagged_iterator(EasyAdminExtension::TAG_FILTER_CONFIGURATOR))