diff --git a/src/Extension/FilterExtension.php b/src/Extension/FilterExtension.php index d13d3b19..6beaeb95 100644 --- a/src/Extension/FilterExtension.php +++ b/src/Extension/FilterExtension.php @@ -31,6 +31,12 @@ use Doctrine\Common\Annotations\AnnotationException; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\FieldMapping; +use Doctrine\ORM\Mapping\ManyToManyAssociationMapping; +use Doctrine\ORM\Mapping\ManyToOneAssociationMapping; +use Doctrine\ORM\Mapping\OneToManyAssociationMapping; +use Doctrine\ORM\Mapping\ToManyAssociationMapping; +use Doctrine\ORM\Mapping\ToOneAssociationMapping; use Doctrine\ORM\QueryBuilder; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -172,24 +178,26 @@ public function addFiltersAutomatically(Table $table, ?callable $labelCallable = } if ($table->getDataLoader() instanceof DoctrineDataLoader) { + if ($propertyNames === null) { + $propertyNames = []; + } /** @var QueryBuilder $queryBuilder */ $queryBuilder = $table->getDataLoader()->getOption(DoctrineDataLoader::OPT_QUERY_BUILDER); $entityClass = $queryBuilder->getRootEntities()[0]; - $reflectionClass = new \ReflectionClass($entityClass); $labelCallable = \is_callable($labelCallable) ? $labelCallable : [$this, 'labelCallable']; $jsonSearchCallable = \is_callable($jsonSearchCallable) ? $jsonSearchCallable : [$this, 'jsonSearchCallable']; - $properties = $propertyNames ? array_map([$reflectionClass, 'getProperty'], $propertyNames) : $reflectionClass->getProperties(); + $properties = $this->getMetaDataPropertyMapping($entityClass, $propertyNames); - foreach ($properties as $property) { - if ($this->getOption(self::OPT_ADD_ALL) && in_array($property->getName(), $this->getOption(self::OPT_EXCLUDE_FIELDS), true)) { + foreach ($properties as $propertyName => $mapping) { + if ($this->getOption(self::OPT_ADD_ALL) && in_array($propertyName, $this->getOption(self::OPT_EXCLUDE_FIELDS), true)) { continue; } - if (! $this->getOption(self::OPT_ADD_ALL) && ! in_array($property->getName(), $this->getOption(self::OPT_INCLUDE_FIELDS), true)) { + if (! $this->getOption(self::OPT_ADD_ALL) && ! in_array($propertyName, $this->getOption(self::OPT_INCLUDE_FIELDS), true)) { continue; } - $this->addFilterAutomatically($table, $queryBuilder, $labelCallable, $jsonSearchCallable, $property, $reflectionClass->getNamespaceName()); + $this->addFilterAutomatically($propertyName, $table, $queryBuilder, $labelCallable, $jsonSearchCallable, $mapping); } } } @@ -320,11 +328,18 @@ private static function jsonSearchCallable(string $entityClass) * * @throws AnnotationException */ - private function addFilterAutomatically(Table $table, QueryBuilder $queryBuilder, callable $labelCallable, callable $jsonSearchCallable, \ReflectionProperty $property, string $namespace) + private function addFilterAutomatically( + string $propertyName, + Table $table, + QueryBuilder $queryBuilder, + callable $labelCallable, + callable $jsonSearchCallable, + array $mapping, + ) { - $acronymNoSuffix = $property->getName(); - $acronym = '_' . $property->getName(); - $label = \call_user_func($labelCallable, $table, $property->getName()); + $acronymNoSuffix = $propertyName; + $acronym = '_' . $propertyName; + $label = \call_user_func($labelCallable, $table, $propertyName); $allAliases = $queryBuilder->getAllAliases(); $isPropertySelected = \in_array($acronym, $allAliases, true); $accessor = sprintf('%s.%s', $allAliases[0], $acronymNoSuffix); @@ -332,7 +347,7 @@ private function addFilterAutomatically(Table $table, QueryBuilder $queryBuilder $acronym => $accessor, ] : []; try { - $filterType = $this->filterGuesser->getFilterType($property, $accessor, $acronym, $jsonSearchCallable, $joins, $namespace); + $filterType = $this->filterGuesser->getFilterType($accessor, $acronym, $jsonSearchCallable, $joins, $mapping); if ($filterType) { $this->addFilter($acronymNoSuffix, $label, $filterType); @@ -374,4 +389,33 @@ private function getFromRequest(string $param, bool $withPredefined = true) return $value; } + + + + private function getMetaDataPropertyMapping(string $entityClass, array $propertyNames = []): array + { + $metadata = $this->entityManager->getClassMetadata($entityClass); + + $properties = []; + + foreach ($metadata->getFieldNames() as $fieldName) { + if (!empty($propertyNames) && !in_array($fieldName, $propertyNames, true)) { + continue; + } + if (!array_key_exists($fieldName, $properties)) { + $properties[$fieldName] = $metadata->getFieldMapping($fieldName); + } + } + foreach ($metadata->getAssociationNames() as $associationName) { + if (!empty($propertyNames) && !in_array($associationName, $propertyNames, true)) { + continue; + } + if (!array_key_exists($associationName, $properties)) { + $properties[$associationName] = $metadata->getAssociationMapping($associationName); + } + } + + return $properties; + } + } diff --git a/src/Filter/FilterGuesser.php b/src/Filter/FilterGuesser.php index 4d23c2c9..b20b7ef1 100644 --- a/src/Filter/FilterGuesser.php +++ b/src/Filter/FilterGuesser.php @@ -29,12 +29,16 @@ namespace whatwedo\TableBundle\Filter; -use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\Column; -use Doctrine\ORM\Mapping\ManyToMany; -use Doctrine\ORM\Mapping\ManyToOne; -use Doctrine\ORM\Mapping\OneToMany; +use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\FieldMapping; +use Doctrine\ORM\Mapping\ManyToManyAssociationMapping; +use Doctrine\ORM\Mapping\ManyToManyInverseSideMapping; +use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; +use Doctrine\ORM\Mapping\ManyToOneAssociationMapping; +use Doctrine\ORM\Mapping\OneToManyAssociationMapping; +use Doctrine\ORM\Mapping\ToManyAssociationMapping; +use Doctrine\ORM\Mapping\ToOneAssociationMapping; use whatwedo\TableBundle\Filter\Type\AjaxManyToManyFilterType; use whatwedo\TableBundle\Filter\Type\AjaxOneToManyFilterType; use whatwedo\TableBundle\Filter\Type\AjaxRelationFilterType; @@ -58,39 +62,29 @@ class FilterGuesser 'boolean' => BooleanFilterType::class, ]; - private const RELATION_TYPE = [ - OneToMany::class => AjaxOneToManyFilterType::class, - ManyToOne::class => AjaxRelationFilterType::class, - ManyToMany::class => AjaxManyToManyFilterType::class, - ]; - public function __construct( protected EntityManagerInterface $entityManager ) { } public function getFilterType( - \ReflectionProperty $property, string $accessor, string $acronym, callable $jsonSearchCallable, array $joins, - string $namespace - ): ?FilterTypeInterface { - $all = $this->getAnnotationsAndAttributes($property); - foreach ($all as $holder) { - $class = $this->getClass($holder); - $type = $this->getType($holder, $property); - $targetEntity = $this->getTargetEntity($holder, $property); - $result = match ($class) { - Column::class => $this->newColumnFilter($type, $accessor), - ManyToMany::class => $this->newManyToManyFilter($class, $acronym, $targetEntity, $jsonSearchCallable, $joins), - OneToMany::class, ManyToOne::class => $this->newRelationFilter($class, $acronym, $targetEntity, $namespace, $jsonSearchCallable, $joins), - default => null, - }; - if ($result) { - return $result; - } + array $mapping, + ): ?FilterTypeInterface + { + $result = match (true) { + is_string($mapping['type']) => $this->newColumnFilter($mapping['type'], $accessor), + $mapping['type'] === ClassMetadataInfo::MANY_TO_MANY => $this->newManyToManyFilter($acronym, $mapping['targetEntity'], $jsonSearchCallable, $joins), + $mapping['type'] === ClassMetadataInfo::ONE_TO_MANY => $this->newRelationFilter(AjaxOneToManyFilterType::class, $acronym, $mapping['targetEntity'], $jsonSearchCallable, $joins), + $mapping['type'] === ClassMetadataInfo::MANY_TO_ONE => $this->newRelationFilter(AjaxRelationFilterType::class, $acronym, $mapping['targetEntity'], $jsonSearchCallable, $joins), + default => null, + }; + + if ($result) { + return $result; } return null; @@ -105,13 +99,9 @@ private function newColumnFilter(string $type, string $accessor): ?FilterTypeInt return new (self::SCALAR_TYPES[$type])($accessor); } - private function newManyToManyFilter(string $class, string $acronym, string $targetEntity, callable $jsonSearchCallable, array $joins): ?FilterTypeInterface + private function newManyToManyFilter(string $acronym, string $targetEntity, callable $jsonSearchCallable, array $joins): FilterTypeInterface { - if (! isset(self::RELATION_TYPE[$class])) { - return null; - } - - return new (self::RELATION_TYPE[$class])( + return new AjaxManyToManyFilterType( $acronym, $targetEntity, $this->entityManager, @@ -120,17 +110,9 @@ private function newManyToManyFilter(string $class, string $acronym, string $tar ); } - private function newRelationFilter(string $class, string $acronym, string $targetEntity, string $namespace, callable $jsonSearchCallable, array $joins): ?FilterTypeInterface + private function newRelationFilter(string $filterClass, string $acronym, string $targetEntity, callable $jsonSearchCallable, array $joins): FilterTypeInterface { - if (! isset(self::RELATION_TYPE[$class])) { - return null; - } - - if (mb_strpos($targetEntity, '\\') === false) { - $targetEntity = $namespace . '\\' . $targetEntity; - } - - return new (self::RELATION_TYPE[$class])( + return new ($filterClass)( $acronym, $targetEntity, $this->entityManager, @@ -138,70 +120,4 @@ private function newRelationFilter(string $class, string $acronym, string $targe $joins ); } - - private function getAnnotationsAndAttributes(\ReflectionProperty $property): ?array - { - $annotations = (new AnnotationReader())->getPropertyAnnotations($property); - $attributes = $property->getAttributes(); - - return [...$annotations, ...$attributes]; - } - - private function getFieldMapping(\ReflectionProperty $property): array - { - $meta = $this->entityManager->getClassMetadata($property->getDeclaringClass()->getName()); - $mappings = array_merge($meta->fieldMappings, $meta->associationMappings); - if (! isset($mappings[$property->getName()])) { - return []; - } - - return $mappings[$property->getName()]; - } - - private function isAttribute(mixed $x): bool - { - return $x instanceof \ReflectionAttribute; - } - - private function getClass(mixed $x): ?string - { - if ($this->isAttribute($x)) { - return $x->getName(); - } - - return get_class($x); - } - - private function getType(mixed $x, \ReflectionProperty $property): null|string|int - { - return $this->getXYZ($x, $property, 'type'); - } - - private function getTargetEntity(mixed $x, \ReflectionProperty $property): null|string|int - { - return $this->getXYZ($x, $property, 'targetEntity'); - } - - private function getXYZ(mixed $x, \ReflectionProperty $property, string $what): null|string|int - { - if ($this->isAttribute($x)) { - $arguments = $x->getArguments(); - if (isset($arguments[$what])) { - return $arguments[$what]; - } - } - - if (! $this->isAttribute($x)) { - if (property_exists($x, $what)) { - return $x->{$what}; - } - } - - $fieldMappings = $this->getFieldMapping($property); - if (isset($fieldMappings[$what])) { - return $fieldMappings[$what]; - } - - return null; - } }