Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Doctrine Specification Laravel Package

happyr/doctrine-specification

Reusable Doctrine query Specifications for PHP. Replace messy repositories and huge QueryBuilder methods with small, composable, testable spec classes. Reduce duplication, avoid methods with many arguments, and extend queries cleanly as your app grows.

View on GitHub
Deep Wiki
Context7

Installation and usage

Install this lib with composer.

// composer.json
{
    // ...
    require: {
        // ...
        "happyr/doctrine-specification": "dev-master@dev",
    }
}

Let your repositories extend Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository instead of Doctrine\ORM\EntityRepository, or use the EntitySpecificationRepositoryTrait in your repositories, like so:

<?php
declare(strict_types=1);

namespace Acme\DemoBundle\Repository;

use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;
use Happyr\DoctrineSpecification\EntitySpecificationRepositoryTrait;

class MyRepository extends ServiceEntityRepository
{
    // Add this line:
    use EntitySpecificationRepositoryTrait;

    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Tag::class);
    }

    // ...
}

Also make sure that the default repository is changed. If you haven't created a repository class in your source then you will have to tell $this->em->getRepository('xxx') to return a instance of Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository. See instructions for Laravel, Symfony, Zend 1, Zend 2, Zend 3.

Then you may start to create your specifications. Put them in Acme\DemoBundle\Entity\Spec. Lets start with a simple one:

<?php
declare(strict_types=1);

namespace Acme\DemoBundle\Entity\Spec;

use Happyr\DoctrineSpecification\Specification\BaseSpecification;
use Happyr\DoctrineSpecification\Spec;

/**
 * Check if a user is active.
 * An active user is not banned and has logged in within the last 6 months.
 */
class IsActive extends BaseSpecification
{
    public function getSpec()
    {
        return Spec::andX(
            Spec::eq('banned', false),
            Spec::gt('lastLogin', new \DateTime('-6months'))
        );
    }
}

I recommend you to write simple Specifications and combine them with Spec::andX and Spec::orX. To use the IsActive Specification, do like this:

<?php
declare(strict_types=1);

namespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Acme\DemoBundle\Entity\Spec\IsActive;

class DefaultController extends AbstractController
{
    public function someAction(): Response
    {
        $users = $this->getEntityManager()
            ->getRepository('AcmeDemoBundle:User')
            ->match(new IsActive())
        ;

        // Do whatever with your active users
    }
}

Syntactic sugar

There is a few different ways of using a Specification. You might use the Spec factory which probably is the most convenient one. (At least it reduces the number of imports.)

use Happyr\DoctrineSpecification\Spec;

// ...

$objects = $this->getEntityManager()
    ->getRepository('...')
    ->match(Spec::gt('age', 18));

You may of course use the specification classes directly.

use Happyr\DoctrineSpecification\Comparison\GreaterThan;

// ...

$objects = $this->getEntityManager()
    ->getRepository('...')
    ->match(new GreaterThan('age', 18))
;

Re-cap

  • Spec::gt('age', 18)
  • new GreaterThan('age', 18)

ResultModifier

When you call EntitySpecificationRepository::match with your specification as first parameter you may use a ResultModifier as second parameter to modify the result. An excellent use-case of this function is when you want to change the Hydration mode.

public function anyFunction()
{
    $repo = // EntitySpecificationRepository
    $spec = new MySpecification();

    $entitiesAsArray = $repo->match($spec, new AsArray());

    // Do whatever
}

You can use multiple ResultModifiers with the ResultModifierCollection.

public function anyFunction()
{
    $repo = // EntitySpecificationRepository
    $spec = new MySpecification();
    $resultModifiers = new ResultModifierCollection(
        new AsArray(),
        new Cache(60)
    );

    $entities = $repo->match($spec, $resultModifiers);

    // Do whatever
}

Comparison operands

All a comparison operations in the filters consist of the left operand, operator and the right operand. As default the left operand can be a entity field, and the right operand a value. This is a simple and effective mechanic to solve the standard tasks. But operands can be not only scalar values. You can use objects that implement the Happyr\DoctrineSpecification\Operand\Operand interface. For example, you can compare two fields:

// DQL: e.price_current < e.price_old
$spec = Spec::lt(Spec::field('price_current'), Spec::field('price_old'));

You can compare fields of different entities:

// DQL: a.email = u.email
$spec = Spec::eq(Spec::field('email', 'a'), Spec::field('email', 'u'));

You can also customize data type values:

// DQL: e.day > :day
$spec = Spec::gt('day', Spec::value($day, Type::DATE));
// DQL: e.day IN (:days)
$spec = Spec::in('day', Spec::values($days, Type::DATE));

Arithmetic operands

You can use arithmetic operations in specifications such as -, +, *, /, %. For example, select users with a score greater than $user_score:

// DQL: e.posts_count + e.likes_count > :user_score
$spec = Spec::gt(
    Spec::add(Spec::field('posts_count'), Spec::field('likes_count')),
    $user_score
);

You can put arithmetic operations in each other:

// DQL: ((e.price_old - e.price_current) / (e.price_current / 100)) > :discount
$spec = Spec::gt(
    Spec::div(
        Spec::sub(Spec::field('price_old'), Spec::field('price_current')),
        Spec::div(Spec::field('price_current'), Spec::value(100))
    ),
    Spec::value($discount)
);

Functions

// DQL: SIZE(e.products) > 2
Spec::gt(Spec::SIZE('products'), 2);
// or
Spec::gt(Spec::fun('SIZE', 'products'), 2);
// or
Spec::gt(Spec::fun('SIZE', Spec::field('products')), 2);

Nested functions:

// DQL: TRIM(LOWER(e.email)) = :email
Spec::eq(Spec::TRIM(Spec::LOWER('email')), trim(strtolower($email)));
// or
Spec::eq(
    Spec::fun('TRIM', Spec::fun('LOWER', Spec::field('email'))),
    trim(strtolower($email))
);

Without arguments:

Spec::CURRENT_DATE();
Spec::fun('CURRENT_DATE');

With one argument:

Spec::LENGTH('email');
Spec::fun('LENGTH', 'email');

With several arguments:

Spec::DATE_DIFF('create_at', $date);
Spec::fun('DATE_DIFF', 'create_at', $date);

Customize selection

Sometimes we need to customize the selection. To do this, we can use select and addSelect query modifiers. Example of selection single field:

// DQL: SELECT e.email FROM ...
Spec::select('email')
// or
Spec::select(Spec::field('email'))

Add single field in the selected set:

// DQL: SELECT e, u.email FROM ...
Spec::addSelect(Spec::field('email', $context))

Add one more custom fields in the selected set:

// DQL: SELECT e.title, e.cover, u.name, u.avatar FROM ...
Spec::andX(
    Spec::select('title', 'cover'),
    Spec::addSelect(Spec::field('name', $context), Spec::field('avatar', $context))
)

Add single entry in the selected set:

// DQL: SELECT e, u FROM ...
Spec::addSelect(Spec::selectEntity($context))

Use aliases for selection fields:

// DQL: SELECT e.name AS author FROM ...
Spec::select(Spec::selectAs(Spec::field('name'), 'author'))

Add single hidden field in the selected set:

// DQL: SELECT e, u.name AS HIDDEN author FROM ...
Spec::addSelect(Spec::selectHiddenAs(Spec::field('email', $context), 'author')))

Use expression in selection for add product discount to the result:

// DQL: SELECT (e.price_old is not null and e.price_current < e.price_old) AS discount FROM ...
Spec::select(Spec::selectAs(
    Spec::andX(
        Spec::isNotNull('price_old'),
        Spec::lt(Spec::field('price_current'), Spec::field('price_old'))
    ),
    'discount'
))

Use aliases in conditions to search a cheap products:

// DQL: SELECT e.price_current AS price FROM ... WHERE price < :low_cost_limit
Spec::andX(
    Spec::select(Spec::selectAs('price_current', 'price')),
    Spec::lt(Spec::alias('price'), $low_cost_limit)
)
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
daikazu/eloquent-salesforce-objects
unseen-codes/chat
romalytar/yammi-jobs-monitoring-laravel
kisame76/filament-db-table-state
nqxcode/laravel-lucene-search
dpfx/laravel-livewire-wizards
workos/workos-php-laravel
sofa/laravel-global-scope
nawasara/auth-primitives
adhocrat-io/arkhe-main
make-dev/orca-harpoon
itsemon245/lamet
baks-dev/dashboard
amoifr/pickle-panther-bundle
make-dev/orca
dmstr/symfony-system-resources-bundle
dmstr/symfony-job-queue-bundle
dmstr/openapi-json-schema-bundle
dmstr/keycloak-security-bundle
dmstr/doctrine-audit-log-bundle