krystal-sf / doctrine
Doctrine Middleware for Krystal Symfony Projects: Searchable Repositories & Tagged Filters
Requires
- php: ^8.1
- doctrine/collections: ^1.8|^2.0
- symfony/config: ^6.4|^7.4|^8.0
- symfony/dependency-injection: ^6.4|^7.4|^8.0
- symfony/http-kernel: ^6.4|^7.4|^8.0
- webmozart/assert: ^1.0
Requires (Dev)
- badpixxel/php-sdk: ^3.0
- doctrine/mongodb-odm: ^2.6
- doctrine/orm: ^2.15|^3.0
- krystal-sf/common: ^1.0
- phpunit/phpunit: ^10.0|^11.0
- symfony/debug-bundle: ^6.4|^7.4|^8.0
- symfony/framework-bundle: ^6.4|^7.4|^8.0
- symfony/monolog-bundle: ^3.0|^4.0
- symfony/stopwatch: ^6.4|^7.4|^8.0
- symfony/web-profiler-bundle: ^6.4|^7.4|^8.0
Suggests
- doctrine/mongodb-odm: Searchable Document Repositories & tagged ODM filters
- doctrine/orm: Searchable Entity Repositories & tagged ORM filters
This package is auto-updated.
Last update: 2026-08-10 16:19:29 UTC
README
Doctrine middleware for Symfony projects: filter any ORM Entity or
MongoDB ODM Document repository with a plain [key => value] array,
filters being simple tagged services.
Standalone bundle: no Krystal runtime dependency. It powers the
krystal-sf/ux-datatables Doctrine adapters, but is designed to be
used directly from any application code.
Installation
composer require krystal-sf/doctrine
Enable the bundle in config/bundles.php:
return [
// ...
Ksf\Core\Doctrine\KsfDoctrineBundle::class => ['all' => true],
];
Install doctrine/orm and / or doctrine/mongodb-odm depending on the
flavors you use: each side of the bundle only activates when its
Doctrine flavor is installed.
The Idea
Instead of hand-writing query methods for every filtering combination, a repository becomes searchable: it accepts a generic filters array and delegates each key to a dedicated, reusable filter service.
$users = $usersRepository->findByFilters([
'status' => 'active',
'country' => 'FR',
'query' => 'john', // conventional free text search key
], limit: 25);
$count = $usersRepository->countByFilters(['status' => 'active']);
Searchable Repositories (ORM)
Extend the base class (or use the trait in an existing repository):
use Ksf\Core\Doctrine\Model\ORM\SearchableEntityRepository;
/**
* @extends SearchableEntityRepository<User>
*/
class UserRepository extends SearchableEntityRepository
{
}
The SearchableEntityRepositoryInterface contract provides:
findByFilters(array $filters, ?int $limit, ?int $offset): arrayfindOneByFilters(array $filters): ?objectcountByFilters(array $filters): intapplyFilters(QueryBuilder $builder, array $filters): void- stack the tagged filters on a query builder you own
Tagged Filters
A filter is a service implementing EntityFilterInterface, registered
with the #[AsOrmFilter] attribute. It declares which classes it
supports & translates filter values into Doctrine Criteria:
use Doctrine\Common\Collections\Criteria;
use Ksf\Core\Doctrine\Attribute\AsOrmFilter;
use Ksf\Core\Doctrine\Interfaces\ORM\EntityFilterInterface;
#[AsOrmFilter]
class UserStatusFilter implements EntityFilterInterface
{
public function supports(string $className): bool
{
return is_a($className, User::class, true);
}
public static function apply(Criteria $criteria, ?array $values = null): void
{
if (is_string($values['status'] ?? null)) {
$criteria->andWhere(Criteria::expr()->eq('status', $values['status']));
}
}
}
All filters supporting the repository class are applied on every
findByFilters() call: each filter reads the keys it cares about &
ignores the others. Use the #[AsOrmFilter(priority: 10)] argument to
order applications.
By convention, the "query" key carries the free text search (this is
what the ux-datatables adapters feed with the global search input).
MongoDB ODM
Fully symmetric: SearchableDocumentRepository (or the trait),
DocumentFilterInterface & #[AsOdmFilter]. Documents cursors are
returned instead of arrays, and findScalarByFilters() adds a
projection variant:
$cursor = $ordersRepository->findByFilters(['status' => 'pending'], limit: 100);
$ids = $ordersRepository->findScalarByFilters(['status' => 'pending'], ['_id']);
Timestamps Traits (Bonus)
Drop-in create / update timestamps for entities & documents, with lifecycle callbacks wired:
use Ksf\Core\Doctrine\Model\ORM\Entity\TimestampAwareEntityTrait;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class Order
{
use TimestampAwareEntityTrait; // createdAt / updatedAt + prePersist / preUpdate
}
TimestampAwareDocumentTrait is the ODM counterpart.
How it Works
- Filters are tagged
ksf.doctrine.filter.orm/ksf.doctrine.filter.odm(via the attributes) & collected by theEntityFiltersResolver/DocumentFiltersResolverservices. - Resolvers are booted once by the bundle & accessed statically by the repositories - repositories stay plain Doctrine repositories, not services with injected dependencies.
Testing
make quality # lint + style + stan
make phpunit # test suites