As a programmer, surely each of us is not unfamiliar with the concept of Design Pattern. They are standard design patterns, templates for common problems in software design. In this article, I will introduce a popular design pattern – Specification and how to implement it in Laravel.
1. What is the Specification Design Pattern?
Specification pattern is a design pattern whereby business rules can be recombined by chaining. Each specification has a rule that must be followed. Specification allows us to encapsulate some business information into a single unit of code and re-use them in different places, making our code more reusable and easy to read and understand. more maintenance.
This approach not only eliminates business duplication, it also allows for combining multiple businesses by multiple Specifications. Makes it easy to set up complex search conditions and check data. There are three main use cases for the Specification Pattern:
- Search data in DB. Search for records that satisfy the condition.
- Check objects in memory. In other words, check if an object matches the condition
- Create a new instance that satisfies the condition.
Suppose we have a problem that retrieves invoices and sends them to a collection agency if the following three conditions are met: (1) the invoice is past due, (2) the overdue notice has been sent, (3) the invoice has not been sent to the collection agency. This example is intended to show the end result of how the business logic is ‘chained’ together. It is possible to write a method to check the condition that sends an invoice to a collection agency in the Invoice model class. However, such writing violates the Single Responsibility principle in SOLID, furthermore it is not possible to reuse those business logic.
Solution: We have an OverdueSpecification class that is satisfied when the invoice due date is 30 days or more, a NoticeSentSpecification class that is satisfied when the notification has been sent to the customer, and an InCollectionSpecification class that is satisfied when the invoice is has been sent to the collection agency. Using these three specification classes, we create a new specification class called SendToCollection , which will be satisfied when the invoice is past due, when the notification has been sent to the customer and has not been sent to the collection agency.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var OverDue = new OverDueSpecification(); var NoticeSent = new NoticeSentSpecification(); var InCollection = new InCollectionSpecification(); // example of specification pattern logic chaining var SendToCollection = OverDue.And(NoticeSent).And(InCollection.Not()); var InvoiceCollection = Service.GetInvoices(); foreach (var currentInvoice in InvoiceCollection) { if (SendToCollection.IsSatisfiedBy(currentInvoice)) { currentInvoice.SendToCollection(); } } |
2. Implement Specification Pattern in Laravel
When querying the database, we often have to combine different query conditions (where, orWhere, whereIn…). For example, get jobs with the corresponding company_id, get a model with the corresponding id, orderBy by column, eager loading relationships… For each condition we will create a corresponding Specification class (like CompanyIdSpecification, IdSpecification, OrderBySpecification, WithRelationsSpecification ), reusable in many places in the code.
Interface SpecificationInterface
1 2 3 4 5 6 7 8 9 10 11 | <span class="token php language-php"><span class="token delimiter important"><?php</span> <span class="token keyword">namespace</span> <span class="token package">App Repositories</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">Illuminate Database Eloquent Builder</span> <span class="token punctuation">;</span> <span class="token keyword">interface</span> <span class="token class-name-definition class-name">SpecificationInterface</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">apply</span> <span class="token punctuation">(</span> <span class="token class-name type-declaration">Builder</span> <span class="token variable">$query</span> <span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">Builder</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> </span> |
AndSpecification class to chain Specifications logically and
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <span class="token php language-php"><span class="token delimiter important"><?php</span> <span class="token keyword">namespace</span> <span class="token package">App Repositories Specification</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">App Repositories SpecificationInterface</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">Illuminate Database Eloquent Builder</span> <span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name-definition class-name">AndSpecification</span> <span class="token keyword">implements</span> <span class="token class-name">SpecificationInterface</span> <span class="token punctuation">{</span> <span class="token comment">/** * @var SpecificationInterface[] */</span> <span class="token keyword">private</span> <span class="token keyword type-declaration">array</span> <span class="token variable">$listSpecs</span> <span class="token punctuation">;</span> <span class="token comment">/** * @param SpecificationInterface[] $listSpecs */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">__construct</span> <span class="token punctuation">(</span> <span class="token keyword type-hint">array</span> <span class="token variable">$listSpecs</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">listSpecs</span> <span class="token operator">=</span> <span class="token variable">$listSpecs</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/** * @param Builder $query * @return Builder */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">apply</span> <span class="token punctuation">(</span> <span class="token class-name type-declaration">Builder</span> <span class="token variable">$query</span> <span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">Builder</span> <span class="token punctuation">{</span> <span class="token keyword">foreach</span> <span class="token punctuation">(</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">listSpecs</span> <span class="token keyword">as</span> <span class="token variable">$specification</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$specification</span> <span class="token operator">-></span> <span class="token function">apply</span> <span class="token punctuation">(</span> <span class="token variable">$query</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token variable">$query</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> </span> |
Class OrSpecification to chain Specifications logically or
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <span class="token php language-php"><span class="token delimiter important"><?php</span> <span class="token keyword">namespace</span> <span class="token package">App Repositories Specification</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">App Repositories SpecificationInterface</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">Illuminate Database Eloquent Builder</span> <span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name-definition class-name">OrSpecification</span> <span class="token keyword">implements</span> <span class="token class-name">SpecificationInterface</span> <span class="token punctuation">{</span> <span class="token comment">/** * @var SpecificationInterface[] */</span> <span class="token keyword">private</span> <span class="token keyword type-declaration">array</span> <span class="token variable">$listSpecs</span> <span class="token punctuation">;</span> <span class="token comment">/** * @param SpecificationInterface[] $listSpecs */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">__construct</span> <span class="token punctuation">(</span> <span class="token keyword type-hint">array</span> <span class="token variable">$listSpecs</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">listSpecs</span> <span class="token operator">=</span> <span class="token variable">$listSpecs</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/** * @param Builder $query * @return Builder */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">apply</span> <span class="token punctuation">(</span> <span class="token class-name type-declaration">Builder</span> <span class="token variable">$query</span> <span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">Builder</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token variable">$query</span> <span class="token operator">-></span> <span class="token function">where</span> <span class="token punctuation">(</span> <span class="token keyword">function</span> <span class="token punctuation">(</span> <span class="token variable">$q1</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">foreach</span> <span class="token punctuation">(</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">listSpecs</span> <span class="token keyword">as</span> <span class="token variable">$specification</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$q1</span> <span class="token operator">-></span> <span class="token function">orWhere</span> <span class="token punctuation">(</span> <span class="token keyword">function</span> <span class="token punctuation">(</span> <span class="token variable">$q2</span> <span class="token punctuation">)</span> <span class="token keyword">use</span> <span class="token punctuation">(</span> <span class="token variable">$specification</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$specification</span> <span class="token operator">-></span> <span class="token function">apply</span> <span class="token punctuation">(</span> <span class="token variable">$q2</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> </span> |
Class NoneSpecification
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <span class="token php language-php"><span class="token delimiter important"><?php</span> <span class="token keyword">namespace</span> <span class="token package">App Repositories Specification</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">App Repositories SpecificationInterface</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">Illuminate Database Eloquent Builder</span> <span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name-definition class-name">NoneSpecification</span> <span class="token keyword">implements</span> <span class="token class-name">SpecificationInterface</span> <span class="token punctuation">{</span> <span class="token comment">/** * @param Builder $query * @return Builder */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">apply</span> <span class="token punctuation">(</span> <span class="token class-name type-declaration">Builder</span> <span class="token variable">$query</span> <span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">Builder</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token variable">$query</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> </span> |
Class CompanyIdSpecification
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <span class="token php language-php"><span class="token delimiter important"><?php</span> <span class="token keyword">namespace</span> <span class="token package">App Repositories Specification Impl</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">App Repositories SpecificationInterface</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">Illuminate Database Eloquent Builder</span> <span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name-definition class-name">CompanyIdSpecification</span> <span class="token keyword">implements</span> <span class="token class-name">SpecificationInterface</span> <span class="token punctuation">{</span> <span class="token comment">/** * @var int */</span> <span class="token keyword">private</span> <span class="token keyword type-declaration">int</span> <span class="token variable">$id</span> <span class="token punctuation">;</span> <span class="token comment">/** * @param int $id */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">__construct</span> <span class="token punctuation">(</span> <span class="token keyword type-hint">int</span> <span class="token variable">$id</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">id</span> <span class="token operator">=</span> <span class="token variable">$id</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/** * @param Builder $query * @return Builder */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">apply</span> <span class="token punctuation">(</span> <span class="token class-name type-declaration">Builder</span> <span class="token variable">$query</span> <span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">Builder</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token variable">$query</span> <span class="token operator">-></span> <span class="token function">where</span> <span class="token punctuation">(</span> <span class="token string single-quoted-string">'company_id'</span> <span class="token punctuation">,</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">id</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> </span> |
Class IdSpecification
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <span class="token php language-php"><span class="token delimiter important"><?php</span> <span class="token keyword">namespace</span> <span class="token package">App Repositories Specification Impl</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">App Repositories SpecificationInterface</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">Illuminate Database Eloquent Builder</span> <span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name-definition class-name">IdSpecification</span> <span class="token keyword">implements</span> <span class="token class-name">SpecificationInterface</span> <span class="token punctuation">{</span> <span class="token comment">/** * @var int */</span> <span class="token keyword">private</span> <span class="token keyword type-declaration">int</span> <span class="token variable">$id</span> <span class="token punctuation">;</span> <span class="token comment">/** * @param int $id */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">__construct</span> <span class="token punctuation">(</span> <span class="token keyword type-hint">int</span> <span class="token variable">$id</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">id</span> <span class="token operator">=</span> <span class="token variable">$id</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/** * @param Builder $query * @return Builder */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">apply</span> <span class="token punctuation">(</span> <span class="token class-name type-declaration">Builder</span> <span class="token variable">$query</span> <span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">Builder</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token variable">$query</span> <span class="token operator">-></span> <span class="token function">where</span> <span class="token punctuation">(</span> <span class="token string single-quoted-string">'id'</span> <span class="token punctuation">,</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">id</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> </span> |
Class OrderBySpecification
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <span class="token php language-php"><span class="token delimiter important"><?php</span> <span class="token keyword">namespace</span> <span class="token package">App Repositories Specification Impl</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">App Repositories SpecificationInterface</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">Illuminate Database Eloquent Builder</span> <span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name-definition class-name">OrderBySpecification</span> <span class="token keyword">implements</span> <span class="token class-name">SpecificationInterface</span> <span class="token punctuation">{</span> <span class="token comment">/** * @var string */</span> <span class="token keyword">private</span> <span class="token keyword type-declaration">string</span> <span class="token variable">$column</span> <span class="token punctuation">;</span> <span class="token comment">/** * @var string */</span> <span class="token keyword">private</span> <span class="token keyword type-declaration">string</span> <span class="token variable">$order</span> <span class="token punctuation">;</span> <span class="token comment">/** * @param string $column * @param string $order */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">__construct</span> <span class="token punctuation">(</span> <span class="token keyword type-hint">string</span> <span class="token variable">$column</span> <span class="token punctuation">,</span> <span class="token keyword type-hint">string</span> <span class="token variable">$order</span> <span class="token operator">=</span> <span class="token string single-quoted-string">'DESC'</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">column</span> <span class="token operator">=</span> <span class="token variable">$column</span> <span class="token punctuation">;</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">order</span> <span class="token operator">=</span> <span class="token variable">$order</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/** * @param Builder $query * @return Builder */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">apply</span> <span class="token punctuation">(</span> <span class="token class-name type-declaration">Builder</span> <span class="token variable">$query</span> <span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">Builder</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token variable">$query</span> <span class="token operator">-></span> <span class="token function">orderBy</span> <span class="token punctuation">(</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">column</span> <span class="token punctuation">,</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">order</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> </span> |
Class WithRelationsSpecification
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <span class="token php language-php"><span class="token delimiter important"><?php</span> <span class="token keyword">namespace</span> <span class="token package">App Repositories Specification Impl</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">App Repositories SpecificationInterface</span> <span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">Illuminate Database Eloquent Builder</span> <span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name-definition class-name">WithRelationsSpecification</span> <span class="token keyword">implements</span> <span class="token class-name">SpecificationInterface</span> <span class="token punctuation">{</span> <span class="token comment">/** * @var array */</span> <span class="token keyword">private</span> <span class="token keyword type-declaration">array</span> <span class="token variable">$with</span> <span class="token punctuation">;</span> <span class="token comment">/** * @param array $with */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">__construct</span> <span class="token punctuation">(</span> <span class="token keyword type-hint">array</span> <span class="token variable">$with</span> <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">with</span> <span class="token operator">=</span> <span class="token variable">$with</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/** * @param Builder $query * @return Builder */</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">apply</span> <span class="token punctuation">(</span> <span class="token class-name type-declaration">Builder</span> <span class="token variable">$query</span> <span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">Builder</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token variable">$query</span> <span class="token operator">-></span> <span class="token function">with</span> <span class="token punctuation">(</span> <span class="token variable">$this</span> <span class="token operator">-></span> <span class="token property">with</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> </span> |
Using specification in controller, service or repository class: Example retrieves jobs with company_id = 15, eager loading candidates for each job and sorting jobs by creation time
1 2 3 4 5 6 7 8 | <span class="token variable">$specs</span> <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token keyword">new</span> <span class="token class-name">CompanyIdSpecification</span> <span class="token punctuation">(</span> <span class="token number">15</span> <span class="token punctuation">)</span> <span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">WithRelationsSpecification</span> <span class="token punctuation">(</span> <span class="token punctuation">[</span> <span class="token string single-quoted-string">'candidates'</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">OrderBySpecification</span> <span class="token punctuation">(</span> <span class="token string single-quoted-string">'created_at'</span> <span class="token punctuation">,</span> <span class="token string single-quoted-string">'DESC'</span> <span class="token punctuation">)</span> <span class="token punctuation">,</span> <span class="token punctuation">]</span> <span class="token punctuation">;</span> <span class="token variable">$specification</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AndSpecification</span> <span class="token punctuation">(</span> <span class="token variable">$specs</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token variable">$jobs</span> <span class="token operator">=</span> <span class="token variable">$specification</span> <span class="token operator">-></span> <span class="token function">apply</span> <span class="token punctuation">(</span> <span class="token class-name static-context">Job</span> <span class="token operator">::</span> <span class="token function">query</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token function">get</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> |