PHPackages                             shlinkio/doctrine-specification - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. shlinkio/doctrine-specification

ActiveLibrary[Database &amp; ORM](/categories/database)

shlinkio/doctrine-specification
===============================

Specification Pattern for your Doctrine repositories. Forked from https://github.com/Happyr/Doctrine-Specification

v2.3.0(6mo ago)031.6k↓23.8%1MITPHPPHP &gt;=8.3CI passing

Since Feb 16Pushed 6mo agoCompare

[ Source](https://github.com/shlinkio/Doctrine-Specification)[ Packagist](https://packagist.org/packages/shlinkio/doctrine-specification)[ Docs](http://developer.happyr.com/)[ RSS](/packages/shlinkio-doctrine-specification/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (4)Versions (5)Used By (1)

Happyr Doctrine Specification
=============================

[](#happyr-doctrine-specification)

[![Build Status Travis (.org)](https://camo.githubusercontent.com/4fb370f4ca3c3a5607c4808cdcb65d709d7d0039955aadc62299b02e274026b9/68747470733a2f2f696d672e736869656c64732e696f2f7472617669732f6861707079722f646f637472696e652d73706563696669636174696f6e2e737667)](https://travis-ci.org/Happyr/Doctrine-Specification)[![Latest Stable Version Packagist](https://camo.githubusercontent.com/bc002d75a9f4f15b25e8a8cca377c9d1ab83794f592fbe0a6a19cce2520715f5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6861707079722f646f637472696e652d73706563696669636174696f6e2e737667)](https://packagist.org/packages/happyr/doctrine-specification)[![Monthly Downloads Packagist](https://camo.githubusercontent.com/c1f47f080fa5fc9495eec508e4444f60c4b326fb1125842b09a471b0bdc67af3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646d2f6861707079722f646f637472696e652d73706563696669636174696f6e2e737667)](https://packagist.org/packages/happyr/doctrine-specification)[![Total Downloads Packagist](https://camo.githubusercontent.com/8b705a3432f3190978ac4fcc8a2a7999e8bf43c8340dcdeb35cf24e8853d746e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6861707079722f646f637472696e652d73706563696669636174696f6e2e737667)](https://packagist.org/packages/happyr/doctrine-specification)[![Quality Score](https://camo.githubusercontent.com/c34b8a52dce3a620b2ad735f60e8ebd8056a9eeaf8b35d79f238749572ae08a1/68747470733a2f2f696d672e736869656c64732e696f2f7363727574696e697a65722f672f6861707079722f646f637472696e652d73706563696669636174696f6e2e737667)](https://scrutinizer-ci.com/g/happyr/doctrine-specification)

This library gives you a new way for writing queries. Using the [Specification pattern](http://en.wikipedia.org/wiki/Specification_pattern) you will get small Specification classes that are highly reusable.

The problem with writing Doctrine queries is that it soon will be messy. When your application grows you will have 20+ function in your Doctrine repositories. All with long and complicated QueryBuilder calls. You will also find that you are using a lot of parameters to the same method to accommodate different use cases.

After a discussion with Kacper Gunia on [Sound of Symfony podcast](http://www.soundofsymfony.com/episode/episode-2/) about how to test your Doctrine repositories properly, we (Kacper and Tobias) decided to create this library. We have been inspired by Benjamin Eberlei's thoughts in his [blog post](http://www.whitewashing.de/2013/03/04/doctrine_repositories.html).

### Table of contents

[](#table-of-contents)

1. [Motivation](#why-do-we-need-this-lib) and [basic understanding](#the-practical-differences) (this page)
2. [Usage examples](docs/0-usage.md)
3. [Create your own spec](docs/1-creatingSpecs.md)
4. [Contributing to the library](CONTRIBUTING.md)

Why do we need this lib?
------------------------

[](#why-do-we-need-this-lib)

You are probably wondering why we created this library. Your entity repositories are working just fine as they are, right?

But if your friend open one of your repository classes he/she would probably find that the code is not as perfect as you thought. Entity repositories have a tendency to get messy. Problems may include:

- Too many functions (`findActiveUser`, `findActiveUserWithPicture`, `findUserToEmail`, etc)
- Some functions have too many arguments
- Code duplication
- Difficult to test

Requirements of the solution
----------------------------

[](#requirements-of-the-solution)

The solution should have the following features:

- Easy to test
- Easy to extend, store and run
- Re-usable code
- Single responsibility principle
- Hides the implementation details of the ORM. (This might seen like nitpicking, however it leads to bloated client code doing the query builder work over and over again.)

The practical differences
-------------------------

[](#the-practical-differences)

This is an example of how you use the lib. Say that you want to fetch some Adverts and close them. We should select all Adverts that have their `endDate` in the past. If `endDate` is null make it 4 weeks after the `startDate`.

```
// Not using the lib
$qb = $this->em->getRepository('HappyrRecruitmentBundle:Advert')
    ->createQueryBuilder('r');

return $qb->where('r.ended = 0')
    ->andWhere(
        $qb->expr()->orX(
            'r.endDate < :now',
            $qb->expr()->andX(
                'r.endDate IS NULL',
                'r.startDate < :timeLimit'
            )
        )
    )
    ->setParameter('now', new \DateTime())
    ->setParameter('timeLimit', new \DateTime('-4weeks'))
    ->getQuery()
    ->getResult();
```

```
// Using the lib
$spec = Spec::andX(
    Spec::eq('ended', 0),
    Spec::orX(
        Spec::lt('endDate', new \DateTime()),
        Spec::andX(
            Spec::isNull('endDate'),
            Spec::lt('startDate', new \DateTime('-4weeks'))
        )
    )
);

return $this->em->getRepository('HappyrRecruitmentBundle:Advert')->match($spec);
```

Yes, it looks pretty much the same. But the later is reusable. Say you want another query to fetch Adverts that we should close but only for a specific company.

#### Doctrine Specification

[](#doctrine-specification)

```
class AdvertsWeShouldClose extends BaseSpecification
{
    public function getSpec()
    {
        return Spec::andX(
            Spec::eq('ended', 0),
            Spec::orX(
                Spec::lt('endDate', new \DateTime()),
                Spec::andX(
                    Spec::isNull('endDate'),
                    Spec::lt('startDate', new \DateTime('-4weeks'))
                )
            )
        );
    }
}

class OwnedByCompany extends BaseSpecification
{
    private $companyId;

    public function __construct(Company $company, ?string $context = null)
    {
        parent::__construct($context);
        $this->companyId = $company->getId();
    }

    public function getSpec()
    {
        return Spec::andX(
            Spec::join('company', 'c'),
            Spec::eq('id', $this->companyId, 'c')
        );
    }
}

class SomeService
{
    /**
     * Fetch Adverts that we should close but only for a specific company
     */
    public function myQuery(Company $company)
    {
        $spec = Spec::andX(
            new AdvertsWeShouldClose(),
            new OwnedByCompany($company)
        );

        return $this->em->getRepository('HappyrRecruitmentBundle:Advert')->match($spec);
    }
}
```

#### QueryBuilder

[](#querybuilder)

If you were about to do the same thing with only the QueryBuilder it would look like this:

```
class AdvertRepository extends EntityRepository
{
    protected function filterAdvertsWeShouldClose(QueryBuilder $qb)
    {
        $qb
            ->andWhere('r.ended = 0')
            ->andWhere(
                $qb->expr()->orX(
                    'r.endDate < :now',
                    $qb->expr()->andX('r.endDate IS NULL', 'r.startDate < :timeLimit')
                )
            )
            ->setParameter('now', new \DateTime())
            ->setParameter('timeLimit', new \DateTime('-4weeks'))
        ;
    }

    protected function filterOwnedByCompany(QueryBuilder $qb, Company $company)
    {
        $qb
            ->join('company', 'c')
            ->andWhere('c.id = :company_id')
            ->setParameter('company_id', $company->getId())
        ;
    }

    public function myQuery(Company $company)
    {
        $qb = $this->em->getRepository('HappyrRecruitmentBundle:Advert')->createQueryBuilder('r');
        $this->filterAdvertsWeShouldClose($qb);
        $this->filterOwnedByCompany($qb, $company);

        return $qb->getQuery()->getResult();
    }
}
```

The issues with the QueryBuilder implementation are:

- You may only use the filters `filterOwnedByCompany` and `filterAdvertsWeShouldClose` inside AdvertRepository.
- You can not build a tree with And/Or/Not. Say that you want every Advert but not those owned by $company. There is no way to reuse `filterOwnedByCompany()` in that case.
- Different parts of the QueryBuilder filtering cannot be composed together, because of the way the API is created. Assume we have a filterGroupsForApi() call, there is no way to combine it with another call filterGroupsForPermissions(). Instead reusing this code will lead to a third method filterGroupsForApiAndPermissions().

Check single entity
-------------------

[](#check-single-entity)

You can apply specifications to validate specific entities or dataset.

```
$highRankFemalesSpec = Spec::andX(
    Spec::eq('gender', 'F'),
    Spec::gt('points', 9000)
);

// an array of arrays
$playersArr = [
    ['pseudo' => 'Joe',   'gender' => 'M', 'points' => 2500],
    ['pseudo' => 'Moe',   'gender' => 'M', 'points' => 1230],
    ['pseudo' => 'Alice', 'gender' => 'F', 'points' => 9001],
];

// or an array of objects
$playersObj = [
    new Player('Joe',   'M', 40, 2500),
    new Player('Moe',   'M', 55, 1230),
    new Player('Alice', 'F', 27, 9001),
];

foreach ($playersArr as $playerArr) {
    if ($highRankFemalesSpec->isSatisfiedBy($playerArr)) {
        // do something
    }
}

foreach ($playersObj as $playerObj) {
    if ($highRankFemalesSpec->isSatisfiedBy($playerObj)) {
        // do something
    }
}
```

Filter collection
-----------------

[](#filter-collection)

You can apply specifications to filter collection of entities or datasets.

```
$highRankFemalesSpec = Spec::andX(
    Spec::eq('gender', 'F'),
    Spec::gt('points', 9000)
);

// an array of arrays
$playersArr = [
    ['pseudo' => 'Joe',   'gender' => 'M', 'points' => 2500],
    ['pseudo' => 'Moe',   'gender' => 'M', 'points' => 1230],
    ['pseudo' => 'Alice', 'gender' => 'F', 'points' => 9001],
];

// or an array of objects
$playersObj = [
    new Player('Joe',   'M', 40, 2500),
    new Player('Moe',   'M', 55, 1230),
    new Player('Alice', 'F', 27, 9001),
];

$highRankFemales = $highRankFemalesSpec->filterCollection($playersArr);
$highRankFemales = $highRankFemalesSpec->filterCollection($playersObj);
$highRankFemales = $this->em->getRepository(Player::class)->match($highRankFemalesSpec);
```

Continue reading
----------------

[](#continue-reading)

You may want to take a look at some [usage examples](docs/0-usage.md) or find out how to [create your own spec](docs/1-creatingSpecs.md).

###  Health Score

46

—

FairBetter than 93% of packages

Maintenance68

Regular maintenance activity

Popularity28

Limited adoption so far

Community21

Small or concentrated contributor base

Maturity59

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 64.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~212 days

Total

4

Last Release

186d ago

PHP version history (2 changes)v2.1.0PHP &gt;=8.2

v2.3.0PHP &gt;=8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/73480af83259e096d154a8c4333e550c186b28ccb7a0d11f537b9aa57ad35392?d=identicon)[acelaya](/maintainers/acelaya)

---

Top Contributors

[![peter-gribanov](https://avatars.githubusercontent.com/u/1954436?v=4)](https://github.com/peter-gribanov "peter-gribanov (415 commits)")[![Nyholm](https://avatars.githubusercontent.com/u/1275206?v=4)](https://github.com/Nyholm "Nyholm (164 commits)")[![acelaya](https://avatars.githubusercontent.com/u/2719332?v=4)](https://github.com/acelaya "acelaya (13 commits)")[![cakper](https://avatars.githubusercontent.com/u/1022346?v=4)](https://github.com/cakper "cakper (11 commits)")[![cordoval](https://avatars.githubusercontent.com/u/328359?v=4)](https://github.com/cordoval "cordoval (11 commits)")[![KDederichs](https://avatars.githubusercontent.com/u/24696606?v=4)](https://github.com/KDederichs "KDederichs (5 commits)")[![XWB](https://avatars.githubusercontent.com/u/1032281?v=4)](https://github.com/XWB "XWB (3 commits)")[![adamquaile](https://avatars.githubusercontent.com/u/69929?v=4)](https://github.com/adamquaile "adamquaile (2 commits)")[![AP-Hunt](https://avatars.githubusercontent.com/u/1747386?v=4)](https://github.com/AP-Hunt "AP-Hunt (2 commits)")[![Strontium-90](https://avatars.githubusercontent.com/u/1295249?v=4)](https://github.com/Strontium-90 "Strontium-90 (2 commits)")[![thundo](https://avatars.githubusercontent.com/u/703181?v=4)](https://github.com/thundo "thundo (2 commits)")[![timroberson](https://avatars.githubusercontent.com/u/913294?v=4)](https://github.com/timroberson "timroberson (2 commits)")[![vudaltsov](https://avatars.githubusercontent.com/u/2552865?v=4)](https://github.com/vudaltsov "vudaltsov (1 commits)")[![edefimov](https://avatars.githubusercontent.com/u/12027442?v=4)](https://github.com/edefimov "edefimov (1 commits)")[![theviniciusmartins](https://avatars.githubusercontent.com/u/17742735?v=4)](https://github.com/theviniciusmartins "theviniciusmartins (1 commits)")[![drewclauson](https://avatars.githubusercontent.com/u/10655807?v=4)](https://github.com/drewclauson "drewclauson (1 commits)")[![kgilden](https://avatars.githubusercontent.com/u/918599?v=4)](https://github.com/kgilden "kgilden (1 commits)")[![kix](https://avatars.githubusercontent.com/u/345754?v=4)](https://github.com/kix "kix (1 commits)")[![dragosprotung](https://avatars.githubusercontent.com/u/1081073?v=4)](https://github.com/dragosprotung "dragosprotung (1 commits)")

---

Tags

specificationdoctrinerepository

### Embed Badge

![Health badge](/badges/shlinkio-doctrine-specification/health.svg)

```
[![Health](https://phpackages.com/badges/shlinkio-doctrine-specification/health.svg)](https://phpackages.com/packages/shlinkio-doctrine-specification)
```

###  Alternatives

[happyr/doctrine-specification

Specification Pattern for your Doctrine repositories

452915.0k8](/packages/happyr-doctrine-specification)[scienta/doctrine-json-functions

A set of extensions to Doctrine that add support for json query functions.

58723.9M36](/packages/scienta-doctrine-json-functions)[kphoen/rulerz

Powerful implementation of the Specification pattern

8831.3M6](/packages/kphoen-rulerz)[rikbruil/doctrine-specification

Doctrine Specification pattern for building queries dynamically and with re-usable classes for composition.

50251.6k2](/packages/rikbruil-doctrine-specification)[mediagone/doctrine-specifications

Doctrine implementation of repository Specifications pattern

353.8k3](/packages/mediagone-doctrine-specifications)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
