Doctrine Specification Pattern или ваш реюзабельный QueryBuilder

в 13:56, , рубрики: design patterns, doctrine, Doctrine ORM, php, symfony

Я постараюсь максимально коротко рассказать о том, как можно использовать этот паттерн с нашей любимой Doctrine на примерах и почему так делать — true.

Давайте представим себе базовый кейс:
1. У нас есть: сущность «Дом», сущность «Квартира в доме», сущность «Застройщик», сущность «Регион».
2. У нас есть задача: иметь возможность получить всех застройщиков, иметь возможность получить все занятые регионы застройщиком, уметь возможность получить все дома, которые принадлежат застройщику и все доступные регионы вообще в принципе, где ведутся продажи домов.
3. У нас есть правила от бизнеса:

Валидный застройщик — это тот, которого мы подтвердили через админку, т.е. у которого $verifed = true.
«Но правда мы не знаем, как оно будет потом, может быть условие валидности вскоре поменяется — хз, ребята».

Валидный дом — это тот дом, у которого уже заполнились координаты и есть хотя бы какое-то описание.
«А еще чтобы была привязанна хотя бы одна квартира, у нас пока непонятно, мы думаем дома без квартир не показывать никак нигде!1! Но тут опять же, мы можем изменить понятие валидности — пока пусть будет так. Это ведь не долго потом поправить?!!! А, кстати да!!1 Если застройщик без $verifed = true, мы должны не показывать эти дома ващеее!!! Недолго же поправить?».

«А еще мы хотим чтобы показывались только те регионы, в которых есть хотя бы 1 валидный дом!!!1 И кстати такую фильтрацию надо проворачивать как на главной странице, так и на странице отдельного застройщика!!! Ты же помнишь что такое валидный дом у нас?? А??? Ты жив??»

Итак, как раньше выглядили бы мои репозитории:

RegionRepository:

class RegionRepository extends DoctrineORMEntityRepository
{
    public function findAvailableRegions()
    {
        $qb = $this->createQueryBuilder('r');

        return $qb
            ->join('r.houses', 'h')
            ->join('h.developer', 'd')

            #Куча бесполезного дерьма start
            ->innerJoin('h.apartments', 'a') //Нам же только корректные дома нужны
            ->where('h.longitude IS NOT NULL') //и тут фильтруем
            ->andWhere('h.latitude IS NOT NULL') //блин, и тут
            ->andWhere('h.description IS NOT NULL') //бл*ть... это же регион.. нахера я думаю про дом здесь..
            #Куча бесполезного дерьма end

            #Куча бесполезного дерьма start
            ->andWhere('d.verified') //мне кажется я что-то делаю не так...
            #Куча бесполезного дерьма end

            ->getQuery()
            ->getResult();
    }

    public function findAvailableRegionsByDeveloper(DeveloperCompany $developerCompany)
    {
        $qb = $this->createQueryBuilder('r');

        return $qb
                ->join('r.houses', 'h')
                ->join('h.developer', 'd')
            #Куча бесполезного дерьма start
                ->innerJoin('h.apartments', 'a') //Нам же только корректные дома нужны
                ->where('h.longitude IS NOT NULL') //и тут фильтруем
                ->andWhere('h.latitude IS NOT NULL') //блин, и тут
                ->andWhere('h.description IS NOT NULL') //бл*ть... это же регион.. нахера я думаю про дом здесь..
            #Куча бесполезного дерьма end
                ->andWhere('d.id = :developer_id')
                ->setParameter('developer_id', $developerCompany->getId())
                ->getQuery()
                ->getResult();
    }
}

HouseRepository:

class HouseRepository extends DoctrineORMEntityRepository
{
    public function findAvailableHouses()
    {
        $qb = $this->createQueryBuilder('h');

        return $qb
            ->join('h.developer', 'd')
            ->innerJoin('h.apartments', 'a') //фильтруем дома без квартир
            ->where('h.longitude IS NOT NULL') //без
            ->andWhere('h.latitude IS NOT NULL') //координат
            ->andWhere('h.description IS NOT NULL') //без описания
        #опасна!!!
            ->where('d.verified') //черт, я ж в доме. нахера я думаю про застройщика тут...
            ->getQuery()
            ->getResult();
    }
}

DeveloperCompanyRepository:
class DeveloperCompanyRepository extends DoctrineORMEntityRepository
{
    public function findAvailableDevelopers()
    {
        return $this->createQueryBuilder('d')
            ->where('d.verified') //Дежавю........
            ->getQuery()
            ->getResult();
    }
}

Итак, мы 100 раз задублировали проверку валидности застройщика по verified = true.
Сто раз задублировки проверку валидности дома по координатам, описанию и так далее.
Сто раз задублировали одновременно эти оба условия.

Изначально ты просто чешешься, временами не можешь уснуть, вспоминая об этой части твоего кода и у тебя никогда не появляется желания идти на работу с улыбкой. Но это только пока.., вот когда приходят менеджеры и меняют одно из этих условий «валидности» или ты забываешь где-то при выборке по JOIN указать тот или иной фактор валидности той или иной сущности, потому что вчера Петя вывел еще один важный момент, почему какие-то дома не должны показываться, но исправлять в 10000 репозиториях не стал и у нас теперь на странице застройщика показываются регионы, где есть невалидные дома, потому что Петя не знал о существовании подобной валидации, ибо это делал Вася — вот тогда тут уже начинает болеть в районе поясницы.

И тут, прекрасным солнечным утром ты почему-то пораньше приезжаешь на работу, когда никого нет, кофемашина готова для многократного использования — и при этом никакой очереди на нее, в офисе свежий воздух, еще не прожженый кулерами компьютеров твоих коллег и полная тишина.
В такие моменты ты становишься ближе к паттернам, и именно в такой момент ко мне пришел Specification Pattern.

Первый шаг — очистить свой разум, дабы спокойнее принять тот факт, что вам придется избавляться от $this->createQueryBuilder('alias'), воспринимать это не как какую-то революцию, а как путь в неизвестное, но в любом случае светлое будущее.
Второй шаг — composer require happyr/doctrine-specification
Третий шаг — прими факт того, что ты достоин лучшего и создай следующие класссы:

Специфика выборки валидных застройщиков.

CorrectDeveloperSpecification
use HappyrDoctrineSpecificationBaseSpecification;
use HappyrDoctrineSpecificationSpec;

class CorrectDeveloperSpecification extends BaseSpecification
{
    public function getSpec()
    {
        return Spec::eq('verified', true);
    }
}

Специфика выборки валидных домов.

CorrectHouseSpecification

use HappyrDoctrineSpecificationBaseSpecification;
use HappyrDoctrineSpecificationSpec;

class CorrectHouseSpecification extends BaseSpecification
{
    public function getSpec()
    {
        Spec::andX(
            Spec::innerJoin('apartments', 'a'),
            Spec::innerJoin('developer', 'd'),

            Spec::isNotNull('description'),
            Spec::isNotNull('longitude'),
            Spec::isNotNull('latitude'),

            new CorrectDeveloperSpecification('d')
        );
    }
}

Специфика выборки валидных регионов.

CorrectRegionSpecification

use HappyrDoctrineSpecificationBaseSpecification;
use HappyrDoctrineSpecificationSpec;

class CorrectRegionSpecification extends BaseSpecification
{
    public function getSpec()
    {
        return Spec::andX(
            Spec::innerJoin('houses', 'h'),

            new CorrectHouseSpecification('h')
        );
    }
}

Cпецифика выборки валидных по застройщику:

CorrectOccupiedRegionByDeveloperSpecification

use AppBundleEntityDeveloperCompany;
use HappyrDoctrineSpecificationBaseSpecification;
use HappyrDoctrineSpecificationSpec;

class CorrectOccupiedRegionByDeveloperSpecification extends BaseSpecification
{
    /** @var DeveloperCompany */
    private $developer;

    public function __construct(DeveloperCompany $developerCompany, $dqlAlias = null)
    {
        parent::__construct($dqlAlias);

        $this->developer = $developerCompany;
    }

    public function getSpec()
    {
        return Spec::andX(
            new CorrectRegionSpecification(),

            Spec::join('developer', 'd', 'h'),
            Spec::eq('d.id', $this->developer->getId())
        );
    }
}

Теперь самое приятное — уничтожаем, расщепляем, сжигаем говнокод из репозиториев!
Прежде чем посмотреть под спойлеры, убедитесь, что вы ничем не отвлечены и готовы полностью вкусить тот факт, насколько же проще и божественней стал код…

RegionRepository

class RegionRepository extends EntitySpecificationRepository
{
    public function findAvailableRegions()
    {
        return $this->match(
            new CorrectRegionSpecification()
        );
    }

    public function findAvailableRegionsByDeveloper(DeveloperCompany $developerCompany)
    {
        return $this->match(
            new CorrectOccupiedRegionByDeveloperSpecification($developerCompany)
        );
    }
}

HouseRepository

class HouseRepository extends EntitySpecificationRepository
{
    public function findAvailableHouses()
    {
        return $this->match(
            new CorrectHouseSpecification()
        );
    }
}

DeveloperCompanyRepository

class DeveloperCompanyRepository extends EntitySpecificationRepository
{
    public function findAvailableDevelopers()
    {
        return $this->match(
            new CorrectDeveloperSpecification()
        );
    }
}

Ну разве не конфета?

Ссылка на бундл — тут полно описания, как можно нужно использовать спецификации.

Всем приятных часов кодинга, паттернов, солнечного утра и тишины в вашем Open Space.

Автор: Андреев Данил

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js