📜 ⬆️ ⬇️

Doctrine Specification Pattern or your reusable QueryBuilder

I will try to tell you as briefly as possible how to use this pattern with our beloved Doctrine with examples and why this is true.

Let's imagine the base case:

1. We have: the essence of “House”, the essence of “Apartment in the house”, the essence of “Developer”, the essence of “Region”.
2. We have a task: to be able to get all the developers, to be able to get all the occupied regions by the developer, to be able to get all the houses that belong to the developer, and all the available regions in general, where homes are sold.
3. We have rules for business:

The valid developer is the one we confirmed through the admin panel, i.e. whose $ verifed = true .
')
"But the truth is, we don’t know how it will be later, maybe the condition of validity will soon change - xs, guys."

A valid house is one that already has its coordinates filled and there is at least some description .

“And so that at least one apartment is attached, it’s still unclear for us, we’re thinking of not showing it anywhere else at home ! 1! But here again, we can change the concept of validity - for now let it be so. This is not a long time to fix it then !!! And, by the way, yes !! 1 If a developer without $ verifed = true, we must not show these houses !!! Not for long to fix?

“And we want to show only those regions in which there is at least 1 valid house !!! 1 And by the way, such filtering should be turned both on the main page and on the page of a separate developer !!! Do you remember what a valid house is like with us ?? BUT??? You are alive??"

So, as before my repositories would look like:

RegionRepository:
class RegionRepository extends \Doctrine\ORM\EntityRepository { 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 \Doctrine\ORM\EntityRepository { 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 \Doctrine\ORM\EntityRepository { public function findAvailableDevelopers() { return $this->createQueryBuilder('d') ->where('d.verified') //........ ->getQuery() ->getResult(); } } 


So, we duplicated the developer’s validity check by verified = true 100 times.
A hundred times duplications check the validity of the house by coordinates, description, and so on.
A hundred times these two conditions were duplicated simultaneously.

Initially, you simply scratch, sometimes you can not fall asleep, remembering this part of your code and you never have a desire to go to work with a smile. But this is only for the time being ..., when managers come and change one of these conditions of “validity” or you forget somewhere in the JOIN sampling to indicate one or another factor of validity of this or that entity, because yesterday Peter brought out another important point why some houses should not be shown, but did not fix in 10,000 repositories, and now we have the developer’s page showing regions where there are invalid houses, because Petya did not know about the existence of such validation, for Vasya did it - then is already starting to hurt in the area sing Nice.

And here, on a beautiful sunny morning, for some reason you come to work early when there is no one, the coffee machine is ready for repeated use - and there is no queue for it, in the office there is fresh air, not yet burned by your colleagues' computers coolers and complete silence.
At such moments you get closer to the patterns, and at that very moment the Specification Pattern came to me.

The first step is to clear your mind in order to more calmly accept the fact that you have to get rid of $ this-> createQueryBuilder ('alias'), take it not as some kind of revolution, but as a path to an unknown, but in any case a bright future. .
The second step is composer require happyr / doctrine-specification
The third step is to accept the fact that you deserve the best and create the following classes:

Specificity of the sample of valid developers.

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


Specificity of sampling valid homes.

CorrectHouseSpecification
 use Happyr\DoctrineSpecification\BaseSpecification; use Happyr\DoctrineSpecification\Spec; 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') ); } } 


Specificity of sampling valid regions.

CorrectRegionSpecification
 use Happyr\DoctrineSpecification\BaseSpecification; use Happyr\DoctrineSpecification\Spec; class CorrectRegionSpecification extends BaseSpecification { public function getSpec() { return Spec::andX( Spec::innerJoin('houses', 'h'), new CorrectHouseSpecification('h') ); } } 


Specific sample valid by developer:

CorrectOccupiedRegionByDeveloperSpecification
 use AppBundle\Entity\DeveloperCompany; use Happyr\DoctrineSpecification\BaseSpecification; use Happyr\DoctrineSpecification\Spec; 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()) ); } } 


Now the best part - we destroy, we split, we burn govnokod from repositories! Before looking under the spoilers, make sure that you are not distracted and are ready to fully taste the fact how much simpler and more divine the code has become ...

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() ); } } 


Isn't it candy?

Link to the Bundle - there is a full description of how you can use the specifications.

All the pleasant hours of coding, patterns, sunshine and silence in your Open Space.

Source: https://habr.com/ru/post/334404/


All Articles