Skip to content

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:

php
// 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:

php
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:

php
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:

php
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:

php
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:

php
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:

php
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

php
// 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

php
// 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:

php
// 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

php
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

php
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:

php
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:

php
// 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

php
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:


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.

Next: Query Integration →

Released under the MIT License.