Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guides/computed-field.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q

// Extract the desired sort direction ('asc' or 'desc') from the parameter's value.
// IMPORTANT: 'totalQuantity' here MUST match the alias defined in Cart::handleLinks.
$queryBuilder->addOrderBy('totalQuantity', $context['parameter']->getValue()['totalQuantity'] ?? 'ASC');
$queryBuilder->addOrderBy('totalQuantity', $context['parameter']->getValue() ?? 'ASC');
}

/**
Expand Down
116 changes: 49 additions & 67 deletions docs/guides/create-a-custom-doctrine-filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,97 +7,80 @@
// tags: doctrine, expert
// ---

// Custom filters can be written by implementing the `ApiPlatform\Metadata\FilterInterface` interface.
// Custom filters allow you to execute specific logic directly on the Doctrine QueryBuilder.
//
// API Platform provides a convenient way to create Doctrine ORM and MongoDB ODM filters. If you use [custom state providers](/docs/guide/state-providers), you can still create filters by implementing the previously mentioned interface, but - as API Platform isn't aware of your persistence system's internals - you have to create the filtering logic by yourself.
// While API Platform provides many built-in filters (Search, Date, Range...), you often need to implement custom business logic. The recommended way is to implement the `ApiPlatform\Metadata\FilterInterface` and link it to a `QueryParameter`.
//
// Doctrine ORM filters have access to the context created from the HTTP request and to the `QueryBuilder` instance used to retrieve data from the database. They are only applied to collections. If you want to deal with the DQL query generated to retrieve items, [extensions](/docs/core/extensions/) are the way to go.
// A Doctrine ORM filter has access to the `QueryBuilder` and the `QueryParameter` context.
//
// A Doctrine ORM filter is basically a class implementing the `ApiPlatform\Doctrine\Orm\Filter\FilterInterface`. API Platform includes a convenient abstract class implementing this interface and providing utility methods: `ApiPlatform\Doctrine\Orm\Filter\AbstractFilter`.
//
// Note: Doctrine MongoDB ODM filters have access to the context created from the HTTP request and to the [aggregation builder](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/reference/aggregation-builder.html) instance used to retrieve data from the database and to execute [complex operations on data](https://docs.mongodb.com/manual/aggregation/). They are only applied to collections. If you want to deal with the aggregation pipeline generated to retrieve items, [extensions](/docs/core/extensions/) are the way to go.
//
// A Doctrine MongoDB ODM filter is basically a class implementing the `ApiPlatform\Doctrine\Odm\Filter\FilterInterface`. API Platform includes a convenient abstract class implementing this interface and providing utility methods: `ApiPlatform\Doctrine\Odm\Filter\AbstractFilter`.
//
// In this example, we create a class to filter a collection by applying a regular expression to a property. The `REGEXP` DQL function used in this example can be found in the [DoctrineExtensions](https://github.com/beberlei/DoctrineExtensions) library. This library must be properly installed and registered to use this example (works only with MySQL).
// In this example, we create a `MinLengthFilter` that filters resources where the length of a property is greater than or equal to a specific value. We map this filter to specific API parameters using the `#[QueryParameter]` attribute on our resource.

namespace App\Filter {
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;

final class RegexpFilter extends AbstractFilter
final class MinLengthFilter implements FilterInterface
{
/*
* Filtered properties is accessible through getProperties() method: property => strategy
*/
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
//The `apply` method is where the filtering logic happens.
//We retrieve the parameter definition and its value from the context.
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
/*
* Otherwise this filter is applied to order and page as well.
*/
if (
!$this->isPropertyEnabled($property, $resourceClass)
|| !$this->isPropertyMapped($property, $resourceClass)
) {
$parameter = $context['parameter'] ?? null;
$value = $parameter?->getValue();

//If the value is missing or invalid, we skip the filter.
if (!$value) {
return;
}

// We determine which property to filter on.
// The `QueryParameter` attribute provides the property name (explicitly or inferred).
$property = $parameter->getProperty();
if (!$property) {
return;
}

/*
* Generate a unique parameter name to avoid collisions with other filters.
*/
// Generate a unique parameter name to avoid collisions in the DQL.
$parameterName = $queryNameGenerator->generateParameterName($property);
$alias = $queryBuilder->getRootAliases()[0];

$queryBuilder
->andWhere(sprintf('REGEXP(o.%s, :%s) = 1', $property, $parameterName))
->andWhere(sprintf('LENGTH(%s.%s) >= :%s', $alias, $property, $parameterName))
->setParameter($parameterName, $value);
}

/*
* This function is only used to hook in documentation generators (supported by Swagger and Hydra).
*/
// Note: The `getDescription` method is no longer needed when using `QueryParameter`
// because the documentation is handled by the attribute itself.
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}

$description = [];
foreach ($this->properties as $property => $strategy) {
$description["regexp_$property"] = [
'property' => $property,
'type' => 'string',
'required' => false,
'description' => 'Filter using a regex. This will appear in the OpenAPI documentation!',
'openapi' => [
'example' => 'Custom example that will be in the documentation and be the default value of the sandbox',
/*
* If true, query parameters will be not percent-encoded
*/
'allowReserved' => false,
'allowEmptyValue' => true,
/*
* To be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product[]=blue&product[]=green
*/
'explode' => false,
],
];
}

return $description;
return [];
}
}
}

namespace App\Entity {
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use App\Filter\RegexpFilter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
use App\Filter\MinLengthFilter;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource]
#[ApiFilter(RegexpFilter::class, properties: ['title'])]
#[ApiResource(
operations: [
new GetCollection(
parameters: [
// We define a parameter 'min_length' that filters on the `title` and the `author` property using our custom logic.
'min_length[:property]' => new QueryParameter(
filter: MinLengthFilter::class,
properties: ['title', 'author'],
),
]
)
]
)]
class Book
{
#[ORM\Column(type: 'integer')]
Expand All @@ -109,7 +92,6 @@ class Book
public string $title;

#[ORM\Column]
#[ApiFilter(RegexpFilter::class)]
public string $author;
}
}
Expand All @@ -119,7 +101,7 @@ class Book

function request(): Request
{
return Request::create('/books.jsonld?regexp_title=^[Found]', 'GET');
return Request::create('/books.jsonld?min_length[title]=10', 'GET');
}
}

Expand Down Expand Up @@ -147,25 +129,25 @@ final class BookTest extends ApiTestCase

public function testAsAnonymousICanAccessTheDocumentation(): void
{
static::createClient()->request('GET', '/books.jsonld?regexp_title=^[Found]');
static::createClient()->request('GET', '/books.jsonld?min_length[title]=10');

$this->assertResponseIsSuccessful();
$this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection');
$this->assertJsonContains([
'search' => [
'@type' => 'IriTemplate',
'template' => '/books.jsonld{?regexp_title,regexp_author}',
'template' => '/books.jsonld{?min_length[title],min_length[author]}',
'variableRepresentation' => 'BasicRepresentation',
'mapping' => [
[
'@type' => 'IriTemplateMapping',
'variable' => 'regexp_title',
'variable' => 'min_length[title]',
'property' => 'title',
'required' => false,
],
[
'@type' => 'IriTemplateMapping',
'variable' => 'regexp_author',
'variable' => 'min_length[author]',
'property' => 'author',
'required' => false,
],
Expand Down
Loading