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.
composer require frankdejonge/doctrine-query-specification
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());
use FrankDeJonge\DoctrineQuerySpecification\SpecificationAwareEntityRepository;
use FrankDeJonge\DoctrineQuerySpecification\SpecificationAwareRepository;
use FrankDeJonge\DoctrineQuerySpecification\SpecificationAwareRepositoryTrait;
class ArticleRepository extends SpecificationAwareEntityRepository
{
}
// OR
class ArticleRepository implements SpecificationAwareRepository
{
use SpecificationAwareRepositoryTrait;
}
<?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'));
<?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);
<?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);
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(),
);
}
}