Я постараюсь максимально коротко рассказать о том, как можно использовать этот паттерн с нашей любимой Doctrine на примерах и почему так делать — true.
Давайте представим себе базовый кейс:
1. У нас есть: сущность «Дом», сущность «Квартира в доме», сущность «Застройщик», сущность «Регион».
2. У нас есть задача: иметь возможность получить всех застройщиков, иметь возможность получить все занятые регионы застройщиком, уметь возможность получить все дома, которые принадлежат застройщику и все доступные регионы вообще в принципе, где ведутся продажи домов.
3. У нас есть правила от бизнеса:
Валидный застройщик — это тот, которого мы подтвердили через админку, т.е. у которого $verifed = true.
«Но правда мы не знаем, как оно будет потом, может быть условие валидности вскоре поменяется — хз, ребята».
Валидный дом — это тот дом, у которого уже заполнились координаты и есть хотя бы какое-то описание.
«А еще чтобы была привязанна хотя бы одна квартира, у нас пока непонятно, мы думаем дома без квартир не показывать никак нигде!1! Но тут опять же, мы можем изменить понятие валидности — пока пусть будет так. Это ведь не долго потом поправить?!!! А, кстати да!!1 Если застройщик без $verifed = true, мы должны не показывать эти дома ващеее!!! Недолго же поправить?».
«А еще мы хотим чтобы показывались только те регионы, в которых есть хотя бы 1 валидный дом!!!1 И кстати такую фильтрацию надо проворачивать как на главной странице, так и на странице отдельного застройщика!!! Ты же помнишь что такое валидный дом у нас?? А??? Ты жив??»
Итак, как раньше выглядили бы мои репозитории:
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();
}
}
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();
}
}
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
Третий шаг — прими факт того, что ты достоин лучшего и создай следующие класссы:
Специфика выборки валидных застройщиков.
use HappyrDoctrineSpecificationBaseSpecification;
use HappyrDoctrineSpecificationSpec;
class CorrectDeveloperSpecification extends BaseSpecification
{
public function getSpec()
{
return Spec::eq('verified', true);
}
}
Специфика выборки валидных домов.
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')
);
}
}
Специфика выборки валидных регионов.
use HappyrDoctrineSpecificationBaseSpecification;
use HappyrDoctrineSpecificationSpec;
class CorrectRegionSpecification extends BaseSpecification
{
public function getSpec()
{
return Spec::andX(
Spec::innerJoin('houses', 'h'),
new CorrectHouseSpecification('h')
);
}
}
Cпецифика выборки валидных по застройщику:
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())
);
}
}
Теперь самое приятное — уничтожаем, расщепляем, сжигаем говнокод из репозиториев!
Прежде чем посмотреть под спойлеры, убедитесь, что вы ничем не отвлечены и готовы полностью вкусить тот факт, насколько же проще и божественней стал код…
class RegionRepository extends EntitySpecificationRepository
{
public function findAvailableRegions()
{
return $this->match(
new CorrectRegionSpecification()
);
}
public function findAvailableRegionsByDeveloper(DeveloperCompany $developerCompany)
{
return $this->match(
new CorrectOccupiedRegionByDeveloperSpecification($developerCompany)
);
}
}
class HouseRepository extends EntitySpecificationRepository
{
public function findAvailableHouses()
{
return $this->match(
new CorrectHouseSpecification()
);
}
}
class DeveloperCompanyRepository extends EntitySpecificationRepository
{
public function findAvailableDevelopers()
{
return $this->match(
new CorrectDeveloperSpecification()
);
}
}
Ну разве не конфета?
Ссылка на бундл — тут полно описания, как можно нужно использовать спецификации.
Всем приятных часов кодинга, паттернов, солнечного утра и тишины в вашем Open Space.
Автор: Андреев Данил