frankdejonge / doctrine-query-specification
Specification based querying for Doctrine2
Installs: 25 919
Dependents: 0
Suggesters: 0
Security: 0
Stars: 75
Watchers: 6
Forks: 4
Open Issues: 1
Requires
- php: ^7.4|^8.0
- doctrine/orm: ^2|^3
Requires (Dev)
- doctrine/annotations: ^1.13
- doctrine/cache: ^1.1
- doctrine/data-fixtures: ^1.1
- phpunit/phpunit: ^9.5
README
This packages eases the translation of domain questions to things doctrine can understand.
Query specifications allow you to hook into three stages of the query building process.
- Applying constraints
- Modifying the query builder.
- Modifying the query.
This allows you to encapsulate query logic in bite-sized, small, object. When naming these objects you can take into account what purpose they fulfill.
Installation:
composer require frankdejonge/doctrine-query-specification
Effect
Turn code like this:
// Get the newest 5 articles, needed for front-page $qb = $articleRepository->createQueryBuilder('a'); $query = $qb ->where($qb->expr()->eq('a.published', true)) ->getQuery() ->getResult();
Into this:
$articles = $articleRepositoryFieldEquals->findBySpecification(new FrontPageArticles());
Examples
Specification aware repositories.
use FrankDeJonge\DoctrineQuerySpecification\SpecificationAwareEntityRepository; use FrankDeJonge\DoctrineQuerySpecification\SpecificationAwareRepository; use FrankDeJonge\DoctrineQuerySpecification\SpecificationAwareRepositoryTrait; class ArticleRepository extends SpecificationAwareEntityRepository { } // OR class ArticleRepository implements SpecificationAwareRepository { use SpecificationAwareRepositoryTrait; }
Query constraints.
<?php use Doctrine\ORM\QueryBuilder; use FrankDeJonge\DoctrineQuerySpecification\QueryConstraint; class IsPublished implements QueryConstraint { public function asQueryConstraint(QueryBuilder $builder, string $rootAlias): ?object { $expr = $builder->expr(); return $expr->eq("{$rootAlias}.published", true); } } $publishedArticles = $articleRepository->findBySpecification(new IsPublished);
Query constrains can also accept user-provided input in constructors. When doing so, use parameterized queries to protect yourself against SQL-injections.
<?php use Doctrine\ORM\QueryBuilder; use FrankDeJonge\DoctrineQuerySpecification\QueryConstraint; class ArticleHasNameLike implements QueryConstraint { /** @var string */ private $name; public function __construct(string $name) { $this->name = $name; } public function asQueryConstraint(QueryBuilder $builder, string $rootAlias): ?object { $expr = $builder->expr(); $builder->setParameter('name_search', $this->name); return $expr->like("{$rootAlias}.name", ':name_search'); } } $publishedArticles = $articleRepository->findBySpecification(new ArticleHasNameLike('Awesome Name'));
Query modifiers.
<?php use Doctrine\ORM\Query; use FrankDeJonge\DoctrineQuerySpecification\QueryModifier; class AsArray implements QueryModifier { public function modifyQuery(Query $query, string $rootAlias): void { $query->setHydrationMode(Query::HYDRATE_ARRAY); } } $publishedArticles = $articleRepository->findBySpecification(new AsArray);
QueryBuilder modifiers.
<?php use Doctrine\ORM\QueryBuilder; use FrankDeJonge\DoctrineQuerySpecification\QueryBuilderModifier; class InReverseOrder implements QueryBuilderModifier { public function modifyQueryBuilder(QueryBuilder $builder, string $rootAlias): void { $builder->orderBy("{$rootAlias}.id", "DESC"); } } $publishedArticles = $articleRepository->findBySpecification(new InReverseOrder);
Specification composition
There are three ways of building compositions. Firstly there are specification collections
which allow you to create andX
and orX
groups.
$andSpecification = SpecificationCollection::all( new IsPublished(), new InReversedOrder(), new WithAuthor(), ); $orSpecification = SpecificationCollection::any( new IsFeatured(), new IsPublishedToday(), );
The second way is to create new specification objects which encapsulate one of more other specifications.
<?php use Doctrine\ORM\QueryBuilder; use FrankDeJonge\DoctrineQuerySpecification\QueryConstraint; class FeaturedFromAuthor implements QueryConstraint { public function __construct(Author $author) { $this->author = $author; } public function asQueryConstraint(QueryBuilder $queryBuilder, string $rootAlias): ?object { $expr = $queryBuilder->expr(); return $expr->andX( (new FromAuthor($this->author))->asQueryConstraint($queryBuilder, $rootAlias), (new FeaturedArticle)->asQueryConstraint($queryBuilder, $rootAlias), ); } }
Lastly you can extend a generic collection:
<?php use FrankDeJonge\DoctrineQuerySpecification\SpecificationCollection\All; class FeaturedFromAuthor extends All { public function __construct(Author $author) { parent::__construct( new FromAuthor($author), new FeaturedArticle(), ); } }