A Specification
is a class that can filter the result (implements the Filter
interface) and modify the query
(implements the QueryModifier
interface). There are two important functions getFilter
and modify
respectively.
When you create a specification you should implement Happyr\DoctrineSpecification\Specification
.
You could choose to only implement Filter
if you know and are sure that you (or your child
specifications) are using the QueryModifier::modify
. Same goes the other way around for QueryModifier
. It is
however recommended to use the Specification
interface at all times.
The getFilter
function is where the action happens. You are most probably to return a $qb->expr()->xxx()
of some kind. Or you may
choose to return null. Consider this ImageIsNullFilter
public function getFilter(QueryBuilder $qb, string $context): string
{
if ($this->context !== null) {
$context = $this->context;
}
$field = ArgumentToOperandConverter::toField('image');
return (string) $qb->expr()->isNull($field->transform($qb, $context));
}
You will get a QueryBuilder
and a $context
as parameters. The $context
is (by default) the alias for the root
entity. You may use or change the alias as you like.
Implement this function if you want to make any changes to the query object. Say that you want to join an other table
or limit the result set. Consider this SmartContestCache
.
public function modify(AbstractQuery $query): void
{
if ($this->contest->isEnded()) {
$lifetime = 3600;
} elseif (!$this->contest->voteOptions()->isVotingEnabled()) {
$lifetime = 600;
} else {
$lifetime = 300;
}
$query->setResultCacheLifetime($lifetime);
}
You can write a rule with which you will filter the collection of entities and discard non-matching entities.
public function filterCollection(iterable $collection, ?string $context = null): iterable
{
$field = ArgumentToOperandConverter::toField($this->field);
$value = ArgumentToOperandConverter::toValue($this->value);
foreach ($collection as $candidate) {
if ($field->execute($candidate, $context) === $value->execute($candidate, $context))) {
yield $candidate;
}
}
}
You can check a specific entity against a specific rule.
public function isSatisfiedBy($candidate, ?string $context = null): bool
{
$field = ArgumentToOperandConverter::toField($this->field);
$value = ArgumentToOperandConverter::toValue($this->value);
return $field->execute($candidate, $context) >= $value->execute($candidate, $context);
}
To make your life easier you may use the Happyr\DoctrineSpecification\Specification\BaseSpecification
class. When you
extend this class you don't need to bother with getFilter
, modify
, filterCollection
or isSatisfiedBy
. You need
to do 2 things:
- If you implement a constructor, make sure to call the parent constructor with
$context
- Implement
getSpec
to return yourSpecifications
Consider the following example.
use Happyr\DoctrineSpecification\Specification\BaseSpecification;
use Happyr\DoctrineSpecification\Spec;
/**
* Matches every active user
*/
class IsActive extends BaseSpecification
{
/**
* @return Filter|QueryModifier
*/
public function getSpec()
{
return Spec::andX(
Spec::eq('banned', false),
Spec::gt('lastLogin', new \DateTime('-6months'))
);
}
}
You also don't need to worry about joins. The Happyr Doctrine Specification will do everything for you.
use Happyr\DoctrineSpecification\Specification\BaseSpecification;
use Happyr\DoctrineSpecification\Spec;
/**
* Matches every questionnaires of active user in active contests
*/
class PublishedQuestionnaires extends BaseSpecification
{
/**
* @return Filter|QueryModifier
*/
public function getSpec()
{
return Spec::andX(
Spec::eq('state', State::active()->value(), 'contestant.user'),
Spec::eq('enabled', true, 'contestant.contest')
);
}
}
The greatest benefit from specs is when you divide rules into small specs and compose them.
final class PublishedQuestionnaires extends BaseSpecification
{
/**
* @return Filter|QueryModifier
*/
protected function getSpec()
{
return new ContestantPublished('contestant');
}
}
final class ContestantPublished extends BaseSpecification
{
/**
* @return Filter|QueryModifier
*/
protected function getSpec()
{
return Spec::andX(
new JoinedContestant(),
new ContestantApproved()
);
}
}
final class JoinedContestant extends BaseSpecification
{
/**
* @return Filter|QueryModifier
*/
protected function getSpec()
{
return Spec::andX(
new UserActivated('user'),
new ContestPublished('contest')
);
}
}
final class UserActivated extends BaseSpecification
{
/**
* @return Filter|QueryModifier
*/
protected function getSpec()
{
return Spec::eq('state', State::active()->value());
}
}
final class ContestPublished extends BaseSpecification
{
/**
* @return Filter|QueryModifier
*/
protected function getSpec()
{
return Spec::eq('enabled', true);
}
}
final class ContestantApproved extends BaseSpecification
{
/**
* @return Filter|QueryModifier
*/
protected function getSpec()
{
return Spec::orX(
Spec::eq('permission', Permission::approved()->value()),
Spec::not(new ContestRequireModeration('contest'))
);
}
}
final class ContestRequireModeration extends BaseSpecification implements Satisfiable
{
/**
* @return Filter|QueryModifier
*/
protected function getSpec()
{
return Spec::eq('join_options.require_moderation', true);
}
}