|
15 | 15 |
|
16 | 16 | use ApiPlatform\Doctrine\Common\Filter\ExistsFilterInterface; |
17 | 17 | use ApiPlatform\Doctrine\Common\Filter\ExistsFilterTrait; |
| 18 | +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; |
| 19 | +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; |
| 20 | +use ApiPlatform\Doctrine\Common\Filter\NameConverterAwareInterface; |
| 21 | +use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface; |
18 | 22 | use ApiPlatform\Doctrine\Common\Filter\PropertyPlaceholderOpenApiParameterTrait; |
19 | 23 | use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; |
20 | 24 | use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; |
| 25 | +use ApiPlatform\Metadata\Exception\InvalidArgumentException; |
21 | 26 | use ApiPlatform\Metadata\JsonSchemaFilterInterface; |
22 | 27 | use ApiPlatform\Metadata\OpenApiParameterFilterInterface; |
23 | 28 | use ApiPlatform\Metadata\Operation; |
24 | 29 | use ApiPlatform\Metadata\Parameter; |
| 30 | +use Doctrine\ORM\EntityManagerInterface; |
25 | 31 | use Doctrine\ORM\Mapping\AssociationMapping; |
26 | 32 | use Doctrine\ORM\Mapping\ClassMetadata; |
27 | 33 | use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; |
28 | 34 | use Doctrine\ORM\Mapping\ToOneOwningSideMapping; |
29 | 35 | use Doctrine\ORM\Query\Expr\Join; |
30 | 36 | use Doctrine\ORM\QueryBuilder; |
31 | 37 | use Doctrine\Persistence\ManagerRegistry; |
| 38 | +use Doctrine\Persistence\Mapping\ClassMetadata as LegacyClassMetadata; |
32 | 39 | use Psr\Log\LoggerInterface; |
| 40 | +use Psr\Log\NullLogger; |
33 | 41 | use Symfony\Component\Serializer\NameConverter\NameConverterInterface; |
34 | 42 |
|
35 | 43 | /** |
|
116 | 124 | * Given that the collection endpoint is `/books`, you can filter books with the following query: `/books?exists[comment]=true`. |
117 | 125 | * |
118 | 126 | * @author Teoh Han Hui <teohhanhui@gmail.com> |
119 | | - * |
120 | | - * @deprecated since API Platform 4.4: extending {@see AbstractFilter} is deprecated. In 5.0 this filter is rewritten as a standalone filter (reading its value from the QueryParameter instead of the legacy `context['filters']` lookup) — same class name, same URL syntax, drop-in. Declare it through a QueryParameter to migrate. |
121 | 127 | */ |
122 | | -final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface |
| 128 | +final class ExistsFilter implements ExistsFilterInterface, FilterInterface, JsonSchemaFilterInterface, ManagerRegistryAwareInterface, NameConverterAwareInterface, OpenApiParameterFilterInterface, PropertyAwareFilterInterface |
123 | 129 | { |
124 | 130 | use ExistsFilterTrait; |
| 131 | + use ManagerRegistryAwareTrait; |
125 | 132 | use PropertyPlaceholderOpenApiParameterTrait; |
126 | 133 |
|
127 | | - public function __construct(?ManagerRegistry $managerRegistry = null, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null) |
128 | | - { |
129 | | - parent::__construct($managerRegistry, $logger, $properties, $nameConverter); |
| 134 | + private LoggerInterface $logger; |
| 135 | + |
| 136 | + /** |
| 137 | + * Resolved from the QueryBuilder in apply(); metadata is read from it so the active filter path |
| 138 | + * never touches the injected ManagerRegistry (kept only for the deprecated getDescription() and |
| 139 | + * for BC injection through ManagerRegistryAwareInterface). |
| 140 | + */ |
| 141 | + private ?EntityManagerInterface $entityManager = null; |
130 | 142 |
|
| 143 | + /** |
| 144 | + * @param array<string, mixed>|null $properties |
| 145 | + */ |
| 146 | + public function __construct(?ManagerRegistry $managerRegistry = null, ?LoggerInterface $logger = null, private ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, private ?NameConverterInterface $nameConverter = null) |
| 147 | + { |
| 148 | + $this->managerRegistry = $managerRegistry; |
| 149 | + $this->logger = $logger ?? new NullLogger(); |
131 | 150 | $this->existsParameterName = $existsParameterName; |
132 | 151 | } |
133 | 152 |
|
| 153 | + public function getProperties(): ?array |
| 154 | + { |
| 155 | + return $this->properties; |
| 156 | + } |
| 157 | + |
| 158 | + /** |
| 159 | + * @param array<string, mixed> $properties |
| 160 | + */ |
| 161 | + public function setProperties(array $properties): void |
| 162 | + { |
| 163 | + $this->properties = $properties; |
| 164 | + } |
| 165 | + |
| 166 | + public function hasNameConverter(): bool |
| 167 | + { |
| 168 | + return $this->nameConverter instanceof NameConverterInterface; |
| 169 | + } |
| 170 | + |
| 171 | + public function getNameConverter(): ?NameConverterInterface |
| 172 | + { |
| 173 | + return $this->nameConverter; |
| 174 | + } |
| 175 | + |
| 176 | + public function setNameConverter(NameConverterInterface $nameConverter): void |
| 177 | + { |
| 178 | + $this->nameConverter = $nameConverter; |
| 179 | + } |
| 180 | + |
| 181 | + protected function getLogger(): LoggerInterface |
| 182 | + { |
| 183 | + return $this->logger; |
| 184 | + } |
| 185 | + |
| 186 | + protected function isPropertyEnabled(string $property, string $resourceClass): bool |
| 187 | + { |
| 188 | + if (null === $this->properties) { |
| 189 | + // to ensure sanity, nested properties must still be explicitly enabled |
| 190 | + return !$this->isPropertyNested($property, $resourceClass); |
| 191 | + } |
| 192 | + |
| 193 | + return \array_key_exists($property, $this->properties); |
| 194 | + } |
| 195 | + |
| 196 | + protected function denormalizePropertyName(string|int $property): string |
| 197 | + { |
| 198 | + if (!$this->nameConverter instanceof NameConverterInterface) { |
| 199 | + return (string) $property; |
| 200 | + } |
| 201 | + |
| 202 | + return implode('.', array_map($this->nameConverter->denormalize(...), explode('.', (string) $property))); |
| 203 | + } |
| 204 | + |
| 205 | + protected function normalizePropertyName(string $property): string |
| 206 | + { |
| 207 | + if (!$this->nameConverter instanceof NameConverterInterface) { |
| 208 | + return $property; |
| 209 | + } |
| 210 | + |
| 211 | + return implode('.', array_map($this->nameConverter->normalize(...), explode('.', $property))); |
| 212 | + } |
| 213 | + |
| 214 | + protected function getClassMetadata(string $resourceClass): LegacyClassMetadata |
| 215 | + { |
| 216 | + if ($this->entityManager instanceof EntityManagerInterface) { |
| 217 | + return $this->entityManager->getClassMetadata($resourceClass); |
| 218 | + } |
| 219 | + |
| 220 | + // Legacy getDescription() runs without a QueryBuilder: fall back to the injected registry. |
| 221 | + if ($this->hasManagerRegistry() && ($manager = $this->getManagerRegistry()->getManagerForClass($resourceClass))) { |
| 222 | + return $manager->getClassMetadata($resourceClass); |
| 223 | + } |
| 224 | + |
| 225 | + return new ClassMetadata($resourceClass); |
| 226 | + } |
| 227 | + |
| 228 | + /** |
| 229 | + * @return array{0: string, 1: string, 2: string[]} |
| 230 | + */ |
| 231 | + protected function addJoinsForNestedProperty(string $property, string $rootAlias, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $joinType): array |
| 232 | + { |
| 233 | + $propertyParts = $this->splitPropertyParts($property, $resourceClass); |
| 234 | + $parentAlias = $rootAlias; |
| 235 | + $alias = null; |
| 236 | + |
| 237 | + foreach ($propertyParts['associations'] as $association) { |
| 238 | + $alias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $parentAlias, $association, $joinType); |
| 239 | + $parentAlias = $alias; |
| 240 | + } |
| 241 | + |
| 242 | + if (null === $alias) { |
| 243 | + throw new InvalidArgumentException(\sprintf('Cannot add joins for property "%s" - property is not nested.', $property)); |
| 244 | + } |
| 245 | + |
| 246 | + return [$alias, $propertyParts['field'], $propertyParts['associations']]; |
| 247 | + } |
| 248 | + |
134 | 249 | /** |
135 | 250 | * {@inheritdoc} |
136 | 251 | */ |
137 | 252 | public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void |
138 | 253 | { |
| 254 | + $this->entityManager = $queryBuilder->getEntityManager(); |
139 | 255 | $parameter = $context['parameter'] ?? null; |
140 | 256 | $propertyKey = $parameter?->getProperty(); |
141 | 257 |
|
|
0 commit comments