Skip to content

Commit 61dda89

Browse files
committed
improvements
1 parent 743247c commit 61dda89

File tree

1 file changed

+193
-6
lines changed

1 file changed

+193
-6
lines changed

core/filters.md

Lines changed: 193 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,108 @@ class Book
135135
```
136136

137137
This configuration creates a dynamic parameter. API clients can now filter on any of the properties configured in the `SearchFilter` (in this case, `title` and `description`) by using a URL like `/books?search[title]=Ring` or `/books?search[description]=journey`.
138+
139+
When using the `:property` placeholder, API Platform automatically populates the parameter's `extraProperties` with a `_properties` array containing all the available properties for the filter. Your filter can access this information:
140+
141+
```php
142+
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
143+
{
144+
$parameter = $context['parameter'] ?? null;
145+
$properties = $parameter?->getExtraProperties()['_properties'] ?? [];
146+
147+
// $properties contains: ['title' => 'title', 'description' => 'description']
148+
// This allows your filter to know which properties are available for filtering
149+
}
150+
```
151+
152+
### Restricting Properties with `:property` Placeholders
153+
154+
There are two different approaches to property restriction depending on your filter design:
155+
156+
#### 1. Legacy Filters (SearchFilter, etc.) - Not Recommended
157+
158+
> [!WARNING]
159+
> Filters that extend `AbstractFilter` with pre-configured properties are considered legacy. They don't support property restriction via parameters and may be deprecated in future versions. Consider using per-parameter filters instead for better flexibility and performance.
160+
161+
For existing filters that extend `AbstractFilter` and have pre-configured properties, the parameter's `properties` does **not** restrict the filter's behavior. These filters use their own internal property configuration:
162+
163+
```php
164+
<?php
165+
// This does NOT restrict SearchFilter - it processes all its configured properties
166+
'search[:property]' => new QueryParameter(
167+
properties: ['title', 'author'], // Only affects _properties, doesn't restrict filter
168+
filter: new SearchFilter(properties: ['title' => 'partial', 'description' => 'partial'])
169+
)
170+
171+
// To restrict legacy filters, configure them with only the desired properties:
172+
'search[:property]' => new QueryParameter(
173+
filter: new SearchFilter(properties: ['title' => 'partial', 'author' => 'exact'])
174+
)
175+
```
176+
177+
#### 2. Per-Parameter Filters (Recommended)
178+
179+
> [!NOTE]
180+
> Per-parameter filters are the modern approach. They provide better performance (only process requested properties), cleaner code, and full support for parameter-based property restriction.
181+
182+
Modern filters that work on a per-parameter basis can be effectively restricted using the parameter's `properties`:
183+
184+
```php
185+
<?php
186+
// src/Filter/PartialSearchFilter.php
187+
use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
188+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
189+
use ApiPlatform\Metadata\Operation;
190+
use Doctrine\ORM\QueryBuilder;
191+
192+
final class PartialSearchFilter implements FilterInterface
193+
{
194+
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
195+
{
196+
$parameter = $context['parameter'];
197+
$value = $parameter->getValue();
198+
199+
// Get the property for this specific parameter
200+
$property = $parameter->getProperty();
201+
$alias = $queryBuilder->getRootAliases()[0];
202+
$field = $alias.'.'.$property;
203+
204+
$parameterName = $queryNameGenerator->generateParameterName($property);
205+
206+
$queryBuilder
207+
->andWhere($queryBuilder->expr()->like('LOWER('.$field.')', ':'.$parameterName))
208+
->setParameter($parameterName, '%'.strtolower($value).'%');
209+
}
210+
}
211+
```
212+
213+
```php
214+
<?php
215+
// api/src/Resource/Book.php
216+
#[ApiResource(operations: [
217+
new GetCollection(
218+
parameters: [
219+
// This WILL restrict to only title and author properties
220+
'search[:property]' => new QueryParameter(
221+
properties: ['title', 'author'], // Only these properties get parameters created
222+
filter: new PartialSearchFilter()
223+
)
224+
]
225+
)
226+
])]
227+
class Book {
228+
// ...
229+
}
230+
```
231+
232+
**How it works:**
233+
1. API Platform creates individual parameters: `search[title]` and `search[author]` only
234+
2. URLs like `/books?search[description]=foo` are ignored (no parameter exists)
235+
3. Each parameter calls the filter with its specific property via `$parameter->getProperty()`
236+
4. The filter processes only that one property
237+
238+
This approach is recommended for new filters as it's more flexible and allows true property restriction via the parameter configuration.
239+
138240
Note that invalid values are usually ignored by our filters, use [validation](#parameter-validation) to trigger errors for wrong parameter values.
139241

140242
## OpenAPI and JSON Schema
@@ -291,7 +393,7 @@ Parameter Providers are powerful services that can inspect, transform, or provid
291393

292394
### `IriConverterParameterProvider`
293395

294-
This built-in provider takes an IRI string (e.g., `/users/1`) and converts it into the corresponding Doctrine entity object.
396+
This built-in provider takes an IRI string (e.g., `/users/1`) and converts it into the corresponding Doctrine entity object. It supports both single IRIs and arrays of IRIs.
295397

296398
```php
297399
<?php
@@ -307,6 +409,10 @@ use ApiPlatform\State\ParameterProvider\IriConverterParameterProvider;
307409
uriTemplate: '/with_parameters_iris',
308410
parameters: [
309411
'dummy' => new QueryParameter(provider: IriConverterParameterProvider::class),
412+
'related' => new QueryParameter(
413+
provider: IriConverterParameterProvider::class,
414+
extraProperties: ['fetch_data' => true] // Forces fetching the entity data
415+
),
310416
],
311417
provider: [self::class, 'provideDummyFromParameter'],
312418
)
@@ -316,11 +422,22 @@ class WithParameter
316422
public static function provideDummyFromParameter(Operation $operation, array $uriVariables = [], array $context = []): object|array
317423
{
318424
// The value has been transformed from an IRI to an entity by the provider.
319-
return $operation->getParameters()->get('dummy')->getValue();
425+
$dummy = $operation->getParameters()->get('dummy')->getValue();
426+
427+
// If multiple IRIs were provided as an array, this will be an array of entities
428+
$related = $operation->getParameters()->get('related')->getValue();
429+
430+
return $dummy;
320431
}
321432
}
322433
```
323434

435+
#### Configuration Options
436+
437+
The `IriConverterParameterProvider` supports the following options in `extraProperties`:
438+
439+
- **`fetch_data`**: Boolean (default: `false`) - When `true`, forces the IRI converter to fetch the actual entity data instead of just creating a reference.
440+
324441
### `ReadLinkParameterProvider`
325442

326443
This provider fetches a linked resource from a given identifier. This is useful when you need to load a related entity to use later, for example in your own state provider.
@@ -370,14 +487,35 @@ The provider will:
370487
- Optionally use the `uri_template` from `extraProperties` to construct the proper operation for loading the resource
371488
- Return the loaded entity, making it available in your state provider
372489

373-
You can also control error handling by setting `throw_not_found` to `false` in the `extraProperties` to prevent exceptions when the linked resource is not found:
490+
#### Array Support
491+
492+
Both `IriConverterParameterProvider` and `ReadLinkParameterProvider` support processing arrays of values. When you pass an array of identifiers or IRIs, they will return an array of resolved entities:
493+
494+
```php
495+
// For IRI converter: ?related[]=/dummies/1&related[]=/dummies/2
496+
// For ReadLink provider: ?dummies[]=uuid1&dummies[]=uuid2
497+
'items' => new QueryParameter(
498+
provider: ReadLinkParameterProvider::class,
499+
extraProperties: ['resource_class' => Dummy::class]
500+
)
501+
```
502+
503+
#### Configuration Options
504+
505+
You can control the behavior of `ReadLinkParameterProvider` with these `extraProperties`:
506+
507+
- **`resource_class`**: The class of the resource to load
508+
- **`uri_template`**: Optional URI template for the linked resource operation
509+
- **`uri_variable`**: Name of the URI variable to use when building URI variables array
510+
- **`throw_not_found`**: Boolean (default: `true`) - Whether to throw `NotFoundHttpException` when resource is not found
374511

375512
```php
376513
'dummy' => new QueryParameter(
377514
provider: ReadLinkParameterProvider::class,
378515
extraProperties: [
379516
'resource_class' => Dummy::class,
380-
'throw_not_found' => false // Won't throw NotFoundHttpException if resource is missing
517+
'throw_not_found' => false, // Won't throw NotFoundHttpException if resource is missing
518+
'uri_variable' => 'customId' // Use 'customId' as the URI variable name
381519
]
382520
)
383521
```
@@ -445,7 +583,8 @@ final class RegexpFilter implements FilterInterface
445583
{
446584
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
447585
{
448-
$value = $context['parameter']?->getValue();
586+
$parameter = $context['parameter'] ?? null;
587+
$value = $parameter?->getValue();
449588

450589
// The parameter may not be present.
451590
// It's recommended to add validation (e.g., `required: true`) on the Parameter attribute
@@ -456,9 +595,15 @@ final class RegexpFilter implements FilterInterface
456595

457596
$alias = $queryBuilder->getRootAliases()[0];
458597
$parameterName = $queryNameGenerator->generateParameterName('regexp_name');
598+
599+
// Access the parameter's property or use the parameter key as fallback
600+
$property = $parameter->getProperty() ?? $parameter->getKey() ?? 'name';
601+
602+
// You can also access filter context if the parameter provides it
603+
$filterContext = $parameter->getFilterContext() ?? null;
459604

460605
$queryBuilder
461-
->andWhere(sprintf('REGEXP(%s.name, :%s) = 1', $alias, $parameterName))
606+
->andWhere(sprintf('REGEXP(%s.%s, :%s) = 1', $alias, $property, $parameterName))
462607
->setParameter($parameterName, $value);
463608
}
464609

@@ -568,3 +713,45 @@ class LogEntry {}
568713
| `hydra` | Hide the parameter from Hydra documentation (`false`). |
569714
| `security` | A [Symfony expression](https://symfony.com/doc/current/security/expressions.html) to control access to the parameter. |
570715

716+
## Parameter Security
717+
718+
You can secure individual parameters using Symfony expression language. When a security expression evaluates to `false`, the parameter will be ignored and treated as if it wasn't provided.
719+
720+
```php
721+
<?php
722+
// api/src/Resource/SecureResource.php
723+
use ApiPlatform\Metadata\ApiResource;
724+
use ApiPlatform\Metadata\GetCollection;
725+
use ApiPlatform\Metadata\HeaderParameter;
726+
use ApiPlatform\Metadata\QueryParameter;
727+
728+
#[ApiResource(operations: [
729+
new GetCollection(
730+
uriTemplate: '/secure_resources',
731+
parameters: [
732+
'name' => new QueryParameter(
733+
security: 'is_granted("ROLE_ADMIN")'
734+
),
735+
'auth' => new HeaderParameter(
736+
security: '"secured" == auth',
737+
description: 'Only accessible when auth header equals "secured"'
738+
),
739+
'secret' => new QueryParameter(
740+
security: '"secured" == secret',
741+
description: 'Only accessible when secret parameter equals "secured"'
742+
)
743+
]
744+
)
745+
])]
746+
class SecureResource
747+
{
748+
// ...
749+
}
750+
```
751+
752+
In the security expressions, you have access to:
753+
- Parameter values by their key name (e.g., `auth`, `secret`)
754+
- Standard security functions like `is_granted()`
755+
- The current user via `user`
756+
- Request object via `request`
757+

0 commit comments

Comments
 (0)