Skip to content

Commit 2e727ed

Browse files
committed
feat: use doctrine metadata for for construction autofilters
1 parent 4245598 commit 2e727ed

File tree

2 files changed

+81
-121
lines changed

2 files changed

+81
-121
lines changed

src/Extension/FilterExtension.php

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@
3131

3232
use Doctrine\Common\Annotations\AnnotationException;
3333
use Doctrine\ORM\EntityManagerInterface;
34+
use Doctrine\ORM\Mapping\FieldMapping;
35+
use Doctrine\ORM\Mapping\ManyToManyAssociationMapping;
36+
use Doctrine\ORM\Mapping\ManyToOneAssociationMapping;
37+
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
38+
use Doctrine\ORM\Mapping\ToManyAssociationMapping;
39+
use Doctrine\ORM\Mapping\ToOneAssociationMapping;
3440
use Doctrine\ORM\QueryBuilder;
3541
use Psr\Log\LoggerInterface;
3642
use Symfony\Component\HttpFoundation\RequestStack;
@@ -172,24 +178,26 @@ public function addFiltersAutomatically(Table $table, ?callable $labelCallable =
172178
}
173179

174180
if ($table->getDataLoader() instanceof DoctrineDataLoader) {
181+
if ($propertyNames === null) {
182+
$propertyNames = [];
183+
}
175184
/** @var QueryBuilder $queryBuilder */
176185
$queryBuilder = $table->getDataLoader()->getOption(DoctrineDataLoader::OPT_QUERY_BUILDER);
177186
$entityClass = $queryBuilder->getRootEntities()[0];
178187

179-
$reflectionClass = new \ReflectionClass($entityClass);
180188
$labelCallable = \is_callable($labelCallable) ? $labelCallable : [$this, 'labelCallable'];
181189
$jsonSearchCallable = \is_callable($jsonSearchCallable) ? $jsonSearchCallable : [$this, 'jsonSearchCallable'];
182190

183-
$properties = $propertyNames ? array_map([$reflectionClass, 'getProperty'], $propertyNames) : $reflectionClass->getProperties();
191+
$properties = $this->getMetaDataPropertyMapping($entityClass, $propertyNames);
184192

185-
foreach ($properties as $property) {
186-
if ($this->getOption(self::OPT_ADD_ALL) && in_array($property->getName(), $this->getOption(self::OPT_EXCLUDE_FIELDS), true)) {
193+
foreach ($properties as $propertyName => $mapping) {
194+
if ($this->getOption(self::OPT_ADD_ALL) && in_array($propertyName, $this->getOption(self::OPT_EXCLUDE_FIELDS), true)) {
187195
continue;
188196
}
189-
if (! $this->getOption(self::OPT_ADD_ALL) && ! in_array($property->getName(), $this->getOption(self::OPT_INCLUDE_FIELDS), true)) {
197+
if (! $this->getOption(self::OPT_ADD_ALL) && ! in_array($propertyName, $this->getOption(self::OPT_INCLUDE_FIELDS), true)) {
190198
continue;
191199
}
192-
$this->addFilterAutomatically($table, $queryBuilder, $labelCallable, $jsonSearchCallable, $property, $reflectionClass->getNamespaceName());
200+
$this->addFilterAutomatically($propertyName, $table, $queryBuilder, $labelCallable, $jsonSearchCallable, $mapping);
193201
}
194202
}
195203
}
@@ -320,19 +328,26 @@ private static function jsonSearchCallable(string $entityClass)
320328
*
321329
* @throws AnnotationException
322330
*/
323-
private function addFilterAutomatically(Table $table, QueryBuilder $queryBuilder, callable $labelCallable, callable $jsonSearchCallable, \ReflectionProperty $property, string $namespace)
331+
private function addFilterAutomatically(
332+
string $propertyName,
333+
Table $table,
334+
QueryBuilder $queryBuilder,
335+
callable $labelCallable,
336+
callable $jsonSearchCallable,
337+
array $mapping,
338+
)
324339
{
325-
$acronymNoSuffix = $property->getName();
326-
$acronym = '_' . $property->getName();
327-
$label = \call_user_func($labelCallable, $table, $property->getName());
340+
$acronymNoSuffix = $propertyName;
341+
$acronym = '_' . $propertyName;
342+
$label = \call_user_func($labelCallable, $table, $propertyName);
328343
$allAliases = $queryBuilder->getAllAliases();
329344
$isPropertySelected = \in_array($acronym, $allAliases, true);
330345
$accessor = sprintf('%s.%s', $allAliases[0], $acronymNoSuffix);
331346
$joins = ! $isPropertySelected ? [
332347
$acronym => $accessor,
333348
] : [];
334349
try {
335-
$filterType = $this->filterGuesser->getFilterType($property, $accessor, $acronym, $jsonSearchCallable, $joins, $namespace);
350+
$filterType = $this->filterGuesser->getFilterType($accessor, $acronym, $jsonSearchCallable, $joins, $mapping);
336351
if ($filterType) {
337352
$this->addFilter($acronymNoSuffix, $label, $filterType);
338353

@@ -374,4 +389,33 @@ private function getFromRequest(string $param, bool $withPredefined = true)
374389

375390
return $value;
376391
}
392+
393+
394+
395+
private function getMetaDataPropertyMapping(string $entityClass, array $propertyNames = []): array
396+
{
397+
$metadata = $this->entityManager->getClassMetadata($entityClass);
398+
399+
$properties = [];
400+
401+
foreach ($metadata->getFieldNames() as $fieldName) {
402+
if (!empty($propertyNames) && !in_array($fieldName, $propertyNames, true)) {
403+
continue;
404+
}
405+
if (!array_key_exists($fieldName, $properties)) {
406+
$properties[$fieldName] = $metadata->getFieldMapping($fieldName);
407+
}
408+
}
409+
foreach ($metadata->getAssociationNames() as $associationName) {
410+
if (!empty($propertyNames) && !in_array($associationName, $propertyNames, true)) {
411+
continue;
412+
}
413+
if (!array_key_exists($associationName, $properties)) {
414+
$properties[$associationName] = $metadata->getAssociationMapping($associationName);
415+
}
416+
}
417+
418+
return $properties;
419+
}
420+
377421
}

src/Filter/FilterGuesser.php

Lines changed: 26 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@
2929

3030
namespace whatwedo\TableBundle\Filter;
3131

32-
use Doctrine\Common\Annotations\AnnotationReader;
3332
use Doctrine\ORM\EntityManagerInterface;
34-
use Doctrine\ORM\Mapping\Column;
35-
use Doctrine\ORM\Mapping\ManyToMany;
36-
use Doctrine\ORM\Mapping\ManyToOne;
37-
use Doctrine\ORM\Mapping\OneToMany;
33+
use Doctrine\ORM\Mapping\ClassMetadataInfo;
34+
use Doctrine\ORM\Mapping\FieldMapping;
35+
use Doctrine\ORM\Mapping\ManyToManyAssociationMapping;
36+
use Doctrine\ORM\Mapping\ManyToManyInverseSideMapping;
37+
use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping;
38+
use Doctrine\ORM\Mapping\ManyToOneAssociationMapping;
39+
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
40+
use Doctrine\ORM\Mapping\ToManyAssociationMapping;
41+
use Doctrine\ORM\Mapping\ToOneAssociationMapping;
3842
use whatwedo\TableBundle\Filter\Type\AjaxManyToManyFilterType;
3943
use whatwedo\TableBundle\Filter\Type\AjaxOneToManyFilterType;
4044
use whatwedo\TableBundle\Filter\Type\AjaxRelationFilterType;
@@ -58,39 +62,29 @@ class FilterGuesser
5862
'boolean' => BooleanFilterType::class,
5963
];
6064

61-
private const RELATION_TYPE = [
62-
OneToMany::class => AjaxOneToManyFilterType::class,
63-
ManyToOne::class => AjaxRelationFilterType::class,
64-
ManyToMany::class => AjaxManyToManyFilterType::class,
65-
];
66-
6765
public function __construct(
6866
protected EntityManagerInterface $entityManager
6967
) {
7068
}
7169

7270
public function getFilterType(
73-
\ReflectionProperty $property,
7471
string $accessor,
7572
string $acronym,
7673
callable $jsonSearchCallable,
7774
array $joins,
78-
string $namespace
79-
): ?FilterTypeInterface {
80-
$all = $this->getAnnotationsAndAttributes($property);
81-
foreach ($all as $holder) {
82-
$class = $this->getClass($holder);
83-
$type = $this->getType($holder, $property);
84-
$targetEntity = $this->getTargetEntity($holder, $property);
85-
$result = match ($class) {
86-
Column::class => $this->newColumnFilter($type, $accessor),
87-
ManyToMany::class => $this->newManyToManyFilter($class, $acronym, $targetEntity, $jsonSearchCallable, $joins),
88-
OneToMany::class, ManyToOne::class => $this->newRelationFilter($class, $acronym, $targetEntity, $namespace, $jsonSearchCallable, $joins),
89-
default => null,
90-
};
91-
if ($result) {
92-
return $result;
93-
}
75+
array $mapping,
76+
): ?FilterTypeInterface
77+
{
78+
$result = match (true) {
79+
is_string($mapping['type']) => $this->newColumnFilter($mapping['type'], $accessor),
80+
$mapping['type'] === ClassMetadataInfo::MANY_TO_MANY => $this->newManyToManyFilter($acronym, $mapping['targetEntity'], $jsonSearchCallable, $joins),
81+
$mapping['type'] === ClassMetadataInfo::ONE_TO_MANY => $this->newRelationFilter(AjaxOneToManyFilterType::class, $acronym, $mapping['targetEntity'], $jsonSearchCallable, $joins),
82+
$mapping['type'] === ClassMetadataInfo::MANY_TO_ONE => $this->newRelationFilter(AjaxRelationFilterType::class, $acronym, $mapping['targetEntity'], $jsonSearchCallable, $joins),
83+
default => null,
84+
};
85+
86+
if ($result) {
87+
return $result;
9488
}
9589

9690
return null;
@@ -105,13 +99,9 @@ private function newColumnFilter(string $type, string $accessor): ?FilterTypeInt
10599
return new (self::SCALAR_TYPES[$type])($accessor);
106100
}
107101

108-
private function newManyToManyFilter(string $class, string $acronym, string $targetEntity, callable $jsonSearchCallable, array $joins): ?FilterTypeInterface
102+
private function newManyToManyFilter(string $acronym, string $targetEntity, callable $jsonSearchCallable, array $joins): FilterTypeInterface
109103
{
110-
if (! isset(self::RELATION_TYPE[$class])) {
111-
return null;
112-
}
113-
114-
return new (self::RELATION_TYPE[$class])(
104+
return new AjaxManyToManyFilterType(
115105
$acronym,
116106
$targetEntity,
117107
$this->entityManager,
@@ -120,88 +110,14 @@ private function newManyToManyFilter(string $class, string $acronym, string $tar
120110
);
121111
}
122112

123-
private function newRelationFilter(string $class, string $acronym, string $targetEntity, string $namespace, callable $jsonSearchCallable, array $joins): ?FilterTypeInterface
113+
private function newRelationFilter(string $filterClass, string $acronym, string $targetEntity, callable $jsonSearchCallable, array $joins): FilterTypeInterface
124114
{
125-
if (! isset(self::RELATION_TYPE[$class])) {
126-
return null;
127-
}
128-
129-
if (mb_strpos($targetEntity, '\\') === false) {
130-
$targetEntity = $namespace . '\\' . $targetEntity;
131-
}
132-
133-
return new (self::RELATION_TYPE[$class])(
115+
return new ($filterClass)(
134116
$acronym,
135117
$targetEntity,
136118
$this->entityManager,
137119
$jsonSearchCallable($targetEntity),
138120
$joins
139121
);
140122
}
141-
142-
private function getAnnotationsAndAttributes(\ReflectionProperty $property): ?array
143-
{
144-
$annotations = (new AnnotationReader())->getPropertyAnnotations($property);
145-
$attributes = $property->getAttributes();
146-
147-
return [...$annotations, ...$attributes];
148-
}
149-
150-
private function getFieldMapping(\ReflectionProperty $property): array
151-
{
152-
$meta = $this->entityManager->getClassMetadata($property->getDeclaringClass()->getName());
153-
$mappings = array_merge($meta->fieldMappings, $meta->associationMappings);
154-
if (! isset($mappings[$property->getName()])) {
155-
return [];
156-
}
157-
158-
return $mappings[$property->getName()];
159-
}
160-
161-
private function isAttribute(mixed $x): bool
162-
{
163-
return $x instanceof \ReflectionAttribute;
164-
}
165-
166-
private function getClass(mixed $x): ?string
167-
{
168-
if ($this->isAttribute($x)) {
169-
return $x->getName();
170-
}
171-
172-
return get_class($x);
173-
}
174-
175-
private function getType(mixed $x, \ReflectionProperty $property): null|string|int
176-
{
177-
return $this->getXYZ($x, $property, 'type');
178-
}
179-
180-
private function getTargetEntity(mixed $x, \ReflectionProperty $property): null|string|int
181-
{
182-
return $this->getXYZ($x, $property, 'targetEntity');
183-
}
184-
185-
private function getXYZ(mixed $x, \ReflectionProperty $property, string $what): null|string|int
186-
{
187-
if ($this->isAttribute($x)) {
188-
$arguments = $x->getArguments();
189-
if (isset($arguments[$what])) {
190-
return $arguments[$what];
191-
}
192-
}
193-
194-
if (! $this->isAttribute($x)) {
195-
if (property_exists($x, $what)) {
196-
return $x->{$what};
197-
}
198-
}
199-
200-
$fieldMappings = $this->getFieldMapping($property);
201-
if (isset($fieldMappings[$what])) {
202-
return $fieldMappings[$what];
203-
}
204-
205-
return null;
206-
}
207123
}

0 commit comments

Comments
 (0)