krystal-sf/doctrine

Doctrine Middleware for Krystal Symfony Projects: Searchable Repositories & Tagged Filters

Maintainers

Package info

gitlab.com/krystal-sf/doctrine

Homepage

Issues

Type:symfony-bundle

pkg:composer/krystal-sf/doctrine

Transparency log

Statistics

Installs: 18

Dependents: 2

Suggesters: 0

Stars: 0

1.0.x-dev 2026-08-10 16:19 UTC

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.

PHP Version Symfony

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): array
  • findOneByFilters(array $filters): ?object
  • countByFilters(array $filters): int
  • applyFilters(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 the EntityFiltersResolver / DocumentFiltersResolver services.
  • 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