Implement API Platform filters for search, date ranges, boolean, and custom filtering with proper indexing
This skill inherits all available tools. When active, it can use any tool Claude has access to.
<?php
// src/Entity/Product.php
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: [
'name' => 'partial', // LIKE %value%
'description' => 'partial',
'sku' => 'exact', // = value
'category.name' => 'exact', // Related entity
])]
class Product
{
// ...
}
Search strategies:
exact: Exact match (=)partial: Contains (LIKE %value%)start: Starts with (LIKE value%)end: Ends with (LIKE %value)word_start: Word starts withUsage:
GET /api/products?name=phone
GET /api/products?category.name=electronics
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
#[ApiFilter(DateFilter::class, properties: ['createdAt', 'updatedAt'])]
class Product
{
#[ORM\Column]
private \DateTimeImmutable $createdAt;
}
Usage:
GET /api/products?createdAt[after]=2024-01-01
GET /api/products?createdAt[before]=2024-12-31
GET /api/products?createdAt[strictly_after]=2024-01-01
GET /api/products?createdAt[strictly_before]=2024-12-31
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
#[ApiFilter(RangeFilter::class, properties: ['price', 'stock'])]
class Product
{
#[ORM\Column]
private int $price;
#[ORM\Column]
private int $stock;
}
Usage:
GET /api/products?price[gte]=1000&price[lte]=5000
GET /api/products?stock[gt]=0
Operators: lt, lte, gt, gte, between
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
#[ApiFilter(BooleanFilter::class, properties: ['isActive', 'isFeatured'])]
class Product
{
#[ORM\Column]
private bool $isActive = true;
}
Usage:
GET /api/products?isActive=true
GET /api/products?isFeatured=1
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
#[ApiFilter(OrderFilter::class, properties: [
'name' => 'ASC',
'price',
'createdAt',
])]
class Product
{
// ...
}
Usage:
GET /api/products?order[price]=desc
GET /api/products?order[createdAt]=asc&order[name]=asc
use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter;
#[ApiFilter(ExistsFilter::class, properties: ['deletedAt', 'description'])]
class Product
{
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $deletedAt = null;
}
Usage:
GET /api/products?exists[deletedAt]=false # Not deleted
GET /api/products?exists[description]=true # Has description
<?php
// src/Filter/ActiveProductFilter.php
namespace App\Filter;
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
final class ActiveProductFilter extends AbstractFilter
{
protected function filterProperty(
string $property,
mixed $value,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = null,
array $context = []
): void {
if ($property !== 'active') {
return;
}
$alias = $queryBuilder->getRootAliases()[0];
$paramName = $queryNameGenerator->generateParameterName('active');
$queryBuilder
->andWhere(sprintf('%s.isActive = :%s', $alias, $paramName))
->andWhere(sprintf('%s.deletedAt IS NULL', $alias))
->setParameter($paramName, filter_var($value, FILTER_VALIDATE_BOOLEAN));
}
public function getDescription(string $resourceClass): array
{
return [
'active' => [
'property' => 'active',
'type' => 'bool',
'required' => false,
'description' => 'Filter active products (not deleted)',
'openapi' => [
'example' => 'true',
],
],
];
}
}
Usage:
#[ApiResource]
#[ApiFilter(ActiveProductFilter::class)]
class Product { /* ... */ }
<?php
// src/Filter/PriceRangeFilter.php
namespace App\Filter;
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
final class PriceRangeFilter extends AbstractFilter
{
protected function filterProperty(
string $property,
mixed $value,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = null,
array $context = []
): void {
if ($property !== 'priceRange') {
return;
}
$alias = $queryBuilder->getRootAliases()[0];
$ranges = [
'budget' => [0, 5000],
'mid' => [5000, 20000],
'premium' => [20000, 50000],
'luxury' => [50000, null],
];
if (!isset($ranges[$value])) {
return;
}
[$min, $max] = $ranges[$value];
$minParam = $queryNameGenerator->generateParameterName('minPrice');
$queryBuilder
->andWhere(sprintf('%s.price >= :%s', $alias, $minParam))
->setParameter($minParam, $min);
if ($max !== null) {
$maxParam = $queryNameGenerator->generateParameterName('maxPrice');
$queryBuilder
->andWhere(sprintf('%s.price < :%s', $alias, $maxParam))
->setParameter($maxParam, $max);
}
}
public function getDescription(string $resourceClass): array
{
return [
'priceRange' => [
'property' => 'priceRange',
'type' => 'string',
'required' => false,
'description' => 'Filter by price range',
'openapi' => [
'enum' => ['budget', 'mid', 'premium', 'luxury'],
],
],
];
}
}
Apply multiple filters per operation:
#[ApiResource(
operations: [
new GetCollection(
filters: [
SearchFilter::class,
OrderFilter::class,
ActiveProductFilter::class,
]
),
]
)]
class Product { /* ... */ }
Always index filtered columns:
#[ORM\Entity]
#[ORM\Index(columns: ['name'], name: 'idx_product_name')]
#[ORM\Index(columns: ['price'], name: 'idx_product_price')]
#[ORM\Index(columns: ['created_at'], name: 'idx_product_created')]
#[ORM\Index(columns: ['is_active', 'deleted_at'], name: 'idx_product_active')]
class Product
{
// ...
}