Composition Techniques
The true power of specifications lies in their ability to compose together like building blocks. Master the art of combining specifications to express complex business logic with clarity and precision.
The Composition Foundation
Specifications support three fundamental operations from Boolean algebra:
// AND - Both specifications must be satisfied
$bothSpec = $spec1->and($spec2);
// OR - Either specification must be satisfied
$eitherSpec = $spec1->or($spec2);
// NOT - Invert the specification result
$invertedSpec = $spec->not();
These simple operations create infinite possibilities for expressing business logic.
Basic Composition Patterns
The AND Pattern: Required Conditions
Use and()
when all conditions must be met:
class EligibleForDiscountSpecification extends AbstractSpecification
{
public function __construct(
private readonly ActiveUserSpecification $activeSpec,
private readonly VerifiedUserSpecification $verifiedSpec,
private readonly MinimumPurchaseSpecification $purchaseSpec
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->activeSpec
->and($this->verifiedSpec)
->and($this->purchaseSpec)
->isSatisfiedBy($candidate);
}
public function toQuery($query)
{
return $this->activeSpec
->and($this->verifiedSpec)
->and($this->purchaseSpec)
->toQuery($query);
}
}
// Usage
$eligibleUsers = User::whereSpecification(
new EligibleForDiscountSpecification(
new ActiveUserSpecification(),
new VerifiedUserSpecification(),
new MinimumPurchaseSpecification(100)
)
)->get();
The OR Pattern: Alternative Conditions
Use or()
when any condition can be satisfied:
class CanAccessPremiumContentSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
$premiumSubscriber = new PremiumSubscriberSpecification();
$staffMember = new StaffMemberSpecification();
$betaTester = new BetaTesterSpecification();
return $premiumSubscriber
->or($staffMember)
->or($betaTester)
->isSatisfiedBy($candidate);
}
}
// Cleaner with helper method
class CanAccessPremiumContentSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->buildCompositeSpec()->isSatisfiedBy($candidate);
}
public function toQuery($query)
{
return $this->buildCompositeSpec()->toQuery($query);
}
private function buildCompositeSpec(): SpecificationInterface
{
return (new PremiumSubscriberSpecification())
->or(new StaffMemberSpecification())
->or(new BetaTesterSpecification());
}
}
The NOT Pattern: Exclusion Logic
Use not()
to invert conditions:
class NonTestUserSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
$testUserSpec = new TestUserSpecification();
return $testUserSpec->not()->isSatisfiedBy($candidate);
}
public function toQuery($query)
{
return (new TestUserSpecification())->not()->toQuery($query);
}
}
// More complex exclusion
class ProductionReadyUserSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
$activeSpec = new ActiveUserSpecification();
$testSpec = new TestUserSpecification();
$bannedSpec = new BannedUserSpecification();
return $activeSpec
->and($testSpec->not())
->and($bannedSpec->not())
->isSatisfiedBy($candidate);
}
}
Advanced Composition Patterns
Fluent Builder Pattern
Create specifications with readable, chainable methods:
class UserSearchSpecificationBuilder
{
private SpecificationInterface $spec;
public function __construct()
{
$this->spec = new AlwaysTrueSpecification();
}
public function active(): self
{
$this->spec = $this->spec->and(new ActiveUserSpecification());
return $this;
}
public function verified(): self
{
$this->spec = $this->spec->and(new VerifiedUserSpecification());
return $this;
}
public function registeredAfter(Carbon $date): self
{
$this->spec = $this->spec->and(new UserRegisteredAfterSpecification($date));
return $this;
}
public function withRole(string $role): self
{
$this->spec = $this->spec->and(new UserRoleSpecification($role));
return $this;
}
public function orPremium(): self
{
$this->spec = $this->spec->or(new PremiumUserSpecification());
return $this;
}
public function build(): SpecificationInterface
{
return $this->spec;
}
}
// Usage with beautiful fluent interface
$spec = (new UserSearchSpecificationBuilder())
->active()
->verified()
->registeredAfter(now()->subYear())
->withRole('customer')
->orPremium()
->build();
$users = User::whereSpecification($spec)->get();
Composite Factory Pattern
Create complex specifications from simple building blocks:
class ECommerceSpecificationFactory
{
public function createCustomerEligibilitySpec(array $criteria): SpecificationInterface
{
$spec = new AlwaysTrueSpecification();
if ($criteria['active'] ?? true) {
$spec = $spec->and(new ActiveCustomerSpecification());
}
if ($criteria['verified'] ?? true) {
$spec = $spec->and(new VerifiedCustomerSpecification());
}
if (isset($criteria['min_orders'])) {
$spec = $spec->and(new MinimumOrdersSpecification($criteria['min_orders']));
}
if (isset($criteria['min_spent'])) {
$spec = $spec->and(new MinimumSpentSpecification($criteria['min_spent']));
}
if ($criteria['loyalty_member'] ?? false) {
$spec = $spec->and(new LoyaltyMemberSpecification());
}
return $spec;
}
public function createProductVisibilitySpec(User $user): SpecificationInterface
{
$baseSpec = (new PublishedProductSpecification())
->and(new InStockProductSpecification());
if ($user->isPremium()) {
// Premium users see early access products
return $baseSpec->or(new EarlyAccessProductSpecification());
}
if ($user->isStaff()) {
// Staff see everything including drafts
return $baseSpec->or(new DraftProductSpecification());
}
return $baseSpec;
}
}
// Usage
$factory = new ECommerceSpecificationFactory();
$customerSpec = $factory->createCustomerEligibilitySpec([
'active' => true,
'verified' => true,
'min_orders' => 5,
'min_spent' => 500,
]);
$eligibleCustomers = Customer::whereSpecification($customerSpec)->get();
Conditional Composition
Build specifications dynamically based on runtime conditions:
class DynamicReportSpecification extends AbstractSpecification
{
private SpecificationInterface $spec;
public function __construct(array $filters)
{
$this->spec = $this->buildFromFilters($filters);
}
private function buildFromFilters(array $filters): SpecificationInterface
{
$specs = [];
// Date range filter
if (!empty($filters['start_date']) || !empty($filters['end_date'])) {
$specs[] = new DateRangeSpecification(
$filters['start_date'] ?? null,
$filters['end_date'] ?? null
);
}
// Status filter
if (!empty($filters['status'])) {
if (is_array($filters['status'])) {
$statusSpecs = array_map(
fn($status) => new StatusSpecification($status),
$filters['status']
);
$specs[] = $this->orCombine($statusSpecs);
} else {
$specs[] = new StatusSpecification($filters['status']);
}
}
// Category filter
if (!empty($filters['categories'])) {
$specs[] = new CategorySpecification($filters['categories']);
}
// User filter
if (!empty($filters['user_types'])) {
$userSpecs = [];
foreach ($filters['user_types'] as $type) {
match($type) {
'premium' => $userSpecs[] = new PremiumUserSpecification(),
'trial' => $userSpecs[] = new TrialUserSpecification(),
'free' => $userSpecs[] = new FreeUserSpecification(),
};
}
if ($userSpecs) {
$specs[] = $this->orCombine($userSpecs);
}
}
return $this->andCombine($specs);
}
private function andCombine(array $specs): SpecificationInterface
{
if (empty($specs)) {
return new AlwaysTrueSpecification();
}
return array_reduce(
array_slice($specs, 1),
fn($carry, $spec) => $carry->and($spec),
$specs[0]
);
}
private function orCombine(array $specs): SpecificationInterface
{
if (empty($specs)) {
return new AlwaysFalseSpecification();
}
return array_reduce(
array_slice($specs, 1),
fn($carry, $spec) => $carry->or($spec),
$specs[0]
);
}
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->spec->isSatisfiedBy($candidate);
}
public function toQuery($query)
{
return $this->spec->toQuery($query);
}
}
// Usage with dynamic filters from request
public function generateReport(Request $request)
{
$spec = new DynamicReportSpecification($request->only([
'start_date', 'end_date', 'status', 'categories', 'user_types'
]));
return ReportModel::whereSpecification($spec)->get();
}
Logical Equivalencies
Understanding Boolean algebra helps you write cleaner compositions:
De Morgan's Laws
// NOT (A AND B) = (NOT A) OR (NOT B)
$notBoth = $specA->and($specB)->not();
$equivalent = $specA->not()->or($specB->not());
// NOT (A OR B) = (NOT A) AND (NOT B)
$notEither = $specA->or($specB)->not();
$equivalent = $specA->not()->and($specB->not());
Distributive Laws
// A AND (B OR C) = (A AND B) OR (A AND C)
$distributed = $specA->and($specB->or($specC));
$equivalent = $specA->and($specB)->or($specA->and($specC));
// A OR (B AND C) = (A OR B) AND (A OR C)
$distributed = $specA->or($specB->and($specC));
$equivalent = $specA->or($specB)->and($specA->or($specC));
Practical Optimization
Use these laws to optimize complex specifications:
// Instead of this complex composition:
$inefficient = $premiumSpec
->and($activeSpec->or($trialSpec))
->and($verifiedSpec->or($adminSpec));
// Optimize to:
$optimized = $premiumSpec
->and($activeSpec->or($trialSpec))
->and($verifiedSpec->or($adminSpec));
// Or even better, factor out common parts:
$baseEligibility = $premiumSpec->and($verifiedSpec->or($adminSpec));
$final = $baseEligibility->and($activeSpec->or($trialSpec));
Complex Real-World Examples
E-commerce Product Recommendation
class ProductRecommendationSpecification extends AbstractSpecification
{
public function __construct(
private readonly User $user,
private readonly ?Category $category = null
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->buildRecommendationSpec()->isSatisfiedBy($candidate);
}
public function toQuery($query)
{
return $this->buildRecommendationSpec()->toQuery($query);
}
private function buildRecommendationSpec(): SpecificationInterface
{
// Base requirements: published and in stock
$baseSpec = (new PublishedProductSpecification())
->and(new InStockProductSpecification());
// User-specific specs
$userSpecs = [];
// If user has purchase history, recommend similar products
if ($this->user->orders()->exists()) {
$userSpecs[] = new SimilarToPreviousPurchasesSpecification($this->user);
}
// If user has viewed products, recommend from same categories
if ($this->user->productViews()->exists()) {
$userSpecs[] = new SameCategoryAsViewedSpecification($this->user);
}
// Category-specific recommendations
if ($this->category) {
$userSpecs[] = (new CategorySpecification($this->category))
->or(new RelatedCategorySpecification($this->category));
}
// Trending products as fallback
$userSpecs[] = new TrendingProductSpecification();
// Combine user specs with OR (any match is good)
$userRelevantSpec = array_reduce(
array_slice($userSpecs, 1),
fn($carry, $spec) => $carry->or($spec),
$userSpecs[0]
);
return $baseSpec->and($userRelevantSpec);
}
}
Content Access Control
class ContentAccessSpecification extends AbstractSpecification
{
public function __construct(
private readonly User $user,
private readonly Content $content
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->buildAccessSpec()->isSatisfiedBy($candidate);
}
private function buildAccessSpec(): SpecificationInterface
{
// Public content is always accessible
if ($this->content->is_public) {
return new AlwaysTrueSpecification();
}
// Premium content access rules
$premiumAccess = (new PremiumSubscriberSpecification())
->or(new StaffMemberSpecification())
->or(new ContentOwnerSpecification($this->content->author_id));
// Age-restricted content
if ($this->content->age_restricted) {
$ageVerification = (new AgeVerifiedUserSpecification())
->and(new MinimumAgeSpecification(18));
$premiumAccess = $premiumAccess->and($ageVerification);
}
// Geographic restrictions
if ($this->content->geo_restricted) {
$geoAccess = new AllowedRegionSpecification($this->content->allowed_regions);
$premiumAccess = $premiumAccess->and($geoAccess);
}
// Time-based access (early access, embargo)
if ($this->content->early_access_until) {
$earlyAccessSpec = new EarlyAccessSubscriberSpecification();
$publicAccessSpec = new AfterDateSpecification($this->content->early_access_until);
$timeBasedAccess = $earlyAccessSpec->or($publicAccessSpec);
$premiumAccess = $premiumAccess->and($timeBasedAccess);
}
return $premiumAccess;
}
}
Testing Composed Specifications
Test each level of composition:
class ComposedSpecificationTest extends TestCase
{
public function test_individual_specifications()
{
$user = User::factory()->active()->verified()->create();
$activeSpec = new ActiveUserSpecification();
$verifiedSpec = new VerifiedUserSpecification();
$this->assertTrue($activeSpec->isSatisfiedBy($user));
$this->assertTrue($verifiedSpec->isSatisfiedBy($user));
}
public function test_and_composition()
{
$activeVerifiedUser = User::factory()->active()->verified()->create();
$activeUnverifiedUser = User::factory()->active()->unverified()->create();
$composedSpec = (new ActiveUserSpecification())
->and(new VerifiedUserSpecification());
$this->assertTrue($composedSpec->isSatisfiedBy($activeVerifiedUser));
$this->assertFalse($composedSpec->isSatisfiedBy($activeUnverifiedUser));
}
public function test_complex_composition()
{
$spec = (new ActiveUserSpecification())
->and(new VerifiedUserSpecification())
->or(new AdminUserSpecification());
// Test all cases
$activeVerifiedUser = User::factory()->active()->verified()->create();
$inactiveAdmin = User::factory()->inactive()->admin()->create();
$inactiveUnverified = User::factory()->inactive()->unverified()->create();
$this->assertTrue($spec->isSatisfiedBy($activeVerifiedUser));
$this->assertTrue($spec->isSatisfiedBy($inactiveAdmin));
$this->assertFalse($spec->isSatisfiedBy($inactiveUnverified));
}
public function test_query_composition()
{
$spec = (new ActiveUserSpecification())
->and(new VerifiedUserSpecification());
$query = User::whereSpecification($spec);
$expectedSql = 'select * from "users" where "status" = ? and "email_verified_at" is not null';
$this->assertEquals($expectedSql, $query->toSql());
$this->assertEquals(['active'], $query->getBindings());
}
}
Performance Optimization
Specification Order Matters
Put the most selective specifications first:
// Good: Most selective first
$optimized = $rareConditionSpec // Eliminates 95% of candidates
->and($commonConditionSpec) // Only evaluates remaining 5%
->and($expensiveConditionSpec); // Only evaluates filtered results
// Bad: Expensive operations first
$inefficient = $expensiveConditionSpec // Evaluates all candidates
->and($commonConditionSpec) // Still many candidates
->and($rareConditionSpec); // Finally filters down
Caching Composed Specifications
class CachedCompositionSpecification extends AbstractSpecification
{
private ?SpecificationInterface $cachedSpec = null;
public function getComposedSpec(): SpecificationInterface
{
if ($this->cachedSpec === null) {
$this->cachedSpec = $this->buildComplexComposition();
}
return $this->cachedSpec;
}
private function buildComplexComposition(): SpecificationInterface
{
// Expensive composition logic here
return (new ActiveUserSpecification())
->and(new PremiumUserSpecification())
->and(new LocationBasedSpecification())
->and(new TimeBasedSpecification());
}
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->getComposedSpec()->isSatisfiedBy($candidate);
}
}
Next Steps
Master specification composition opens the door to advanced techniques:
- Query Integration → - Optimize database queries with specifications
- Testing Strategies → - Test complex composed specifications
- Performance & Caching → - Optimize specification performance
Composition Mastery
The art of specification composition lies in expressing complex business logic through simple, testable building blocks. Think in terms of Boolean algebra and build specifications that read like business requirements.