silpo-tech / filter-bundle
Common Filter bundle
Package info
github.com/silpo-tech/FilterBundle
Type:symfony-bundle
pkg:composer/silpo-tech/filter-bundle
Requires
- php: ^8.3
- silpo-tech/exception-handler-bundle: ^2.0
- silpo-tech/mapper-bundle: ^1.0
- silpo-tech/rest-bundle: ^2.0
- symfony/config: ^7.0
- symfony/dependency-injection: ^7.0
- symfony/framework-bundle: ^7.2
- symfony/orm-pack: ^1.0
- symfony/property-access: ^7.0
- symfony/validator: ^7.0
- symfony/yaml: ^7.2
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.4
This package is auto-updated.
Last update: 2026-05-27 13:53:41 UTC
README
About
The Filter Bundle contains mapping and criteria builder
Installation
Require the bundle and its dependencies with composer:
$ composer require silpo-tech/filter-bundle
Register the bundle:
// app/AppKernel.php public function registerBundles() { $bundles = array( new FilterBundle\FilterBundle(), ); }
Usage
- Action:
namespace App\Controller; use App\DTO\Request\Location\LocationFilter; use App\DTO\Request\Location\LocationOrder; use FilterBundle\Request\FilterValueResolver; use PaginatorBundle\Paginator\OffsetPaginator; use PermissionBundle\Configuration\Permissions; use RestBundle\Controller\RestController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use FilterBundle\Annotation\FilterMapper; class ListAction { public function __construct(private readonly LocationRepository $repository) { } #[Route(path: 'v1/action/entities', name: ListAction::class, methods: [Request::METHOD_GET])] public function all( #[FilterMapper] LocationFilter $filter, #[FilterMapper] LocationSort $order, OffsetPaginator $offsetPaginator ): Response { return $this->createPaginatedResponse( $this->repository->findWithConditions($filter, $order), $offsetPaginator, LocalizedLocationDto::class ); } }
- Repository method
namespace App\Repository; use App\DTO\Request\Location\LocationFilter; use App\DTO\Request\Location\LocationOrder; use App\Entity\Location; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use FilterBundle\Bridge\Doctrine\Orm\Util\QueryNameGenerator; use FilterBundle\Service\ConditionBuilder; use Hubber\LazyLib\EntityCollection; class EntityRepository extends ServiceEntityRepository { public const ALIAS = 'entity'; public function __construct(ManagerRegistry $registry, private ConditionBuilder $conditionBuilder) { parent::__construct($registry, Entity::class); } public function findWithConditions(EntityDTOFilter $filter): EntityCollection { $queryNameGenerator = new QueryNameGenerator(); $qb = $this->createQueryBuilder(self::ALIAS); $this->conditionBuilder->applyFilters($qb, $queryNameGenerator, $this->getClassName(), $filter); return new EntityCollection($qb); } }
Filter DTO example:
namespace App\Dto\Request\Entity; use FilterBundle\Annotation\ApiFilter; use FilterBundle\Bridge\Doctrine\Orm\BooleanFilter;use FilterBundle\Bridge\Doctrine\Orm\SearchFilter; use FilterBundle\Bridge\Doctrine\Orm\LocaleFilter; use FilterBundle\Bridge\Doctrine\Orm\OrderFilter; use FilterBundle\Validator\Constraints\ValidDateRange; use FilterBundle\Validator\Constraints\DateRangeBeforeGreaterThanAfter; use FilterBundle\Validator\Constraints\DateRangeBeforeNotEqualsAfter; use Symfony\Component\Validator\Constraints as Assert; #[ApiFilter(LocaleFilter::class, property: 'translations.locale')] class EntityDTOFilter { #[ApiFilter(SearchFilter::class, property: 'parents.id', strategy: SearchFilter::STRATEGY_EXACT)] #[Assert\Uuid] /** @var string */ public $parentId; #[Assert\Sequentially(constraints: [ new Assert\Type(type: 'string'), new Assert\Uuid(versions: [Assert\Uuid::V6_SORTABLE]), ])] #[ApiFilter(SearchFilter::class, property: 'nullableId', strategy: 'exact')] #[ApiFilter(NullFilter::class, property: 'nullableId')] /** @var string|null */ public $nullableId = null; #[ApiFilter(BooleanFilter::class)] /** @var boolean */ public $active = true; #[Assert\Sequentially( constraints: [ new Assert\NotBlank(), new Assert\Choice(callback: [EntityStatus::class, 'getChoices'], multiple: true) ] )] #[ApiFilter(SearchFilter::class, property: 'status', strategy: SearchFilter::STRATEGY_EXACT)] /** @var array */ public $status = []; #[ApiFilter(DateFilter::class, property: 'createdAt')] #[ValidDateRange(format: 'Y-m')] #[DateRangeBeforeGreaterThanAfter] #[DateRangeBeforeNotEqualsAfter] /** @var array */ public $createdAt = []; #[Assert\Type('array')] #[ApiSort( filterClass: OrderFilter::class, map: ['id' => 'id', 'status' => 'status', 'createdAt' => 'createdAt', 'updatedAt' => 'updatedAt'] )] /** @var string[] */ public $sort = ['-id']; }
Search Filter
If Doctrine ORM or MongoDB ODM support is enabled, adding filters is as easy as registering a filter service in the
api/config/services.yaml file and adding an attribute to your resource configuration.
The search filter supports exact, partial, start, end, and word_start matching strategies:
exactstrategy usesIN (...)to search for fields that containvalue1, value2, ..., valueN.partialstrategy usesLIKE %text%to search for fields that containtext.startstrategy usesLIKE text%to search for fields that start withtext.endstrategy usesLIKE %textto search for fields that end withtext.word_startstrategy usesLIKE text% OR LIKE % text%to search for fields that contain words starting withtext.
Prepend the letter i to the filter if you want it to be case insensitive. For example ipartial or iexact. Note
that
this will use the LOWER function and will impact
performance if there is no proper index.
Case insensitivity may already be enforced at the database level depending on
the collation
used. If you are using MySQL, note that the commonly used utf8_unicode_ci collation (and its
sibling utf8mb4_unicode_ci)
are already case-insensitive, as indicated by the _ci part in their names.
You can dynamically change the strategy to filters from the client, for this behavior your dto must implement
StrategyInterface:
?filter[title:istart]=Ukrainian
Example syntax for exact strategy:
?filter[status][0]=new&filter[status][1]=completed
Date Filter
Usage syntax: ?filter[createdAt][from]=2022-05&filter[createdAt][to]=2022-06
Managing null Values
The date filter is able to deal with date properties having null values.
Four behaviors are available at the property level of the filter via the arguments parameter:
| Description | Strategy to set |
|---|---|
| Use the default behavior of the DBMS | null (default) |
| Exclude items | exclude_null |
| Consider items as oldest | include_null_before |
| Consider items as youngest | include_null_after |
| Always include items | include_null_before_and_after |
To configure null management for a date property, pass the nullManagement key in the arguments array of the #[ApiFilter] attribute:
use FilterBundle\Annotation\ApiFilter; use FilterBundle\Bridge\Doctrine\Orm\DateFilter; class EntityDTOFilter { #[ApiFilter(DateFilter::class, property: 'deletedAt', arguments: ['nullManagement' => 'exclude_null'])] /** @var array */ public $deletedAt = []; #[ApiFilter(DateFilter::class, property: 'archivedAt', arguments: ['nullManagement' => 'include_null_before_and_after'])] /** @var array */ public $archivedAt = []; }
Behavior details:
exclude_null— addsIS NOT NULLcondition, completely excluding records where the date field isnull.include_null_before— when filtering withto/strictly_to, records withnullare included in the results (treated as oldest).include_null_after— when filtering withfrom/strictly_from, records withnullare included in the results (treated as youngest).include_null_before_and_after— records withnullare always included regardless of the filter direction.
You can also combine nullManagement with other DateFilter arguments like compareDateToDateTime or convertToTz:
#[ApiFilter(DateFilter::class, property: 'createdAt', arguments: [ 'nullManagement' => 'include_null_after', 'convertToTz' => 'UTC', ])] public $createdAt = [];
Order Filter (Sorting)
The order filter allows to sort a collection against the given properties.
Syntax: ?filter[sort][0]=-createdAt&filter[sort][0]=updatedAt
By default, whenever the query does not specify the direction explicitly (e.g.: ?filter[sort][0]=-createdAt), filters
will not be applied unless you configure a default order direction to use.
Constraints
ValidDateRange
Validates that given array value is a valid date range, e.g. property is an array with two keys: from, to; array values should date string of valid format, default format is Y-m-d
Basic usage
use FilterBundle\Validator\Constraints\DateRange; class FilterDTO { #[DateRange(format: DateTimeInterface::ATOM)] /** @var string[] */ public $createdAt = []; }
use FilterBundle\Validator\Constraints\DateRange; class FilterDTO { #[DateRange( format: DateTimeInterface::ATOM, min: '+1 sec', max: '+23 day 4 hour', )] /** @var string[] */ public $createdAt = []; }
Options
format
Defines date string format
invalidDateTimeMessage
Message that will be shown if from or to values are not valid date string
invalidDateRangeMessage
Message that will be shown if from is greater than to
min
If provided additional check will be performed to check to is greater than from for at least min
value. Value should be valid DateInterval string with leading plus sign, e.g. +1 sec, +2 hour 3 min.
max
If provided additional check will be performed to check to is greater than from for at most max
value. Value should be valid DateInterval string with leading plus sign, e.g. +1 sec, +2 hour 3 min.
minMessage
Message that will be shown if to is not greater than from for at least min value
maxMessage
Message that will be shown if to is greater than from for more than max value