Skip to content

AbstractSpecification

The AbstractSpecification class provides the base implementation of the SpecificationInterface with built-in support for composite operations and caching.

Namespace

php
DangerWayne\Specification\Specifications\AbstractSpecification

Class Definition

php
<?php

namespace DangerWayne\Specification\Specifications;

use DangerWayne\Specification\Contracts\SpecificationInterface;
use DangerWayne\Specification\Specifications\Composites\AndSpecification;
use DangerWayne\Specification\Specifications\Composites\NotSpecification;
use DangerWayne\Specification\Specifications\Composites\OrSpecification;
use Illuminate\Database\Eloquent\Builder;

abstract class AbstractSpecification implements SpecificationInterface
{
    abstract public function isSatisfiedBy(mixed $candidate): bool;
    abstract public function toQuery(Builder $query): Builder;
    
    public function and(SpecificationInterface $specification): SpecificationInterface
    {
        return new AndSpecification($this, $specification);
    }
    
    public function or(SpecificationInterface $specification): SpecificationInterface
    {
        return new OrSpecification($this, $specification);
    }
    
    public function not(): SpecificationInterface
    {
        return new NotSpecification($this);
    }
    
    public function getCacheKey(): string
    {
        return md5(static::class . serialize($this->getParameters()));
    }
    
    protected function getParameters(): array
    {
        return [];
    }
}

Abstract Methods

Classes extending AbstractSpecification must implement these methods:

isSatisfiedBy()

php
abstract public function isSatisfiedBy(mixed $candidate): bool;

Implement your business logic to determine if a candidate satisfies the specification.

toQuery()

php
abstract public function toQuery(Builder $query): Builder;

Translate your specification into Eloquent query constraints.

Provided Methods

Composite Operations

The abstract class automatically provides implementations for composite operations:

and()

php
$spec = $activeSpec->and($premiumSpec);

Creates an AndSpecification combining both specifications.

or()

php
$spec = $adminSpec->or($moderatorSpec);

Creates an OrSpecification allowing either specification.

not()

php
$spec = $activeSpec->not();

Creates a NotSpecification negating the current specification.

Cache Key Generation

getCacheKey()

php
public function getCacheKey(): string

Generates a unique cache key based on the class name and parameters.

getParameters()

php
protected function getParameters(): array

Override this method to provide specification parameters for cache key generation:

php
protected function getParameters(): array
{
    return [
        'status' => $this->status,
        'age' => $this->minAge,
    ];
}

Implementation Examples

Basic Specification

php
<?php

namespace App\Specifications\User;

use App\Models\User;
use DangerWayne\Specification\Specifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;

class ActiveUserSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate instanceof User 
            && $candidate->status === 'active';
    }
    
    public function toQuery(Builder $query): Builder
    {
        return $query->where('status', 'active');
    }
}

Specification with Parameters

php
<?php

namespace App\Specifications\Product;

use App\Models\Product;
use DangerWayne\Specification\Specifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;

class PriceRangeSpecification extends AbstractSpecification
{
    public function __construct(
        private float $minPrice,
        private float $maxPrice
    ) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof Product) {
            return false;
        }
        
        $price = $candidate->sale_price ?? $candidate->price;
        
        return $price >= $this->minPrice && $price <= $this->maxPrice;
    }
    
    public function toQuery(Builder $query): Builder
    {
        return $query->where(function($q) {
            $q->whereBetween('price', [$this->minPrice, $this->maxPrice])
              ->orWhereBetween('sale_price', [$this->minPrice, $this->maxPrice]);
        });
    }
    
    protected function getParameters(): array
    {
        return [
            'min_price' => $this->minPrice,
            'max_price' => $this->maxPrice,
        ];
    }
}

Complex Specification with Relationships

php
<?php

namespace App\Specifications\Order;

use App\Models\Order;
use DangerWayne\Specification\Specifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;

class HighValueCustomerOrderSpecification extends AbstractSpecification
{
    public function __construct(
        private float $orderThreshold = 1000,
        private int $customerOrderCount = 5
    ) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof Order) {
            return false;
        }
        
        return $candidate->total_amount >= $this->orderThreshold
            && $candidate->customer->orders_count >= $this->customerOrderCount;
    }
    
    public function toQuery(Builder $query): Builder
    {
        return $query->where('total_amount', '>=', $this->orderThreshold)
                     ->whereHas('customer', function($q) {
                         $q->where('orders_count', '>=', $this->customerOrderCount);
                     });
    }
    
    protected function getParameters(): array
    {
        return [
            'order_threshold' => $this->orderThreshold,
            'customer_order_count' => $this->customerOrderCount,
        ];
    }
}

Using Composite Operations

The power of AbstractSpecification comes from its built-in composite operations:

php
// Create individual specifications
$activeSpec = new ActiveUserSpecification();
$premiumSpec = new PremiumUserSpecification();
$verifiedSpec = new EmailVerifiedSpecification();

// Combine with AND
$activePremiumSpec = $activeSpec->and($premiumSpec);

// Combine with OR
$staffSpec = $adminSpec->or($moderatorSpec);

// Use NOT
$inactiveSpec = $activeSpec->not();

// Complex combinations
$complexSpec = $activeSpec
    ->and($premiumSpec)
    ->or($adminSpec)
    ->and($verifiedSpec->not());

// Apply to query
$users = User::whereSpecification($complexSpec)->get();

Best Practices

1. Single Responsibility

Each specification should represent a single business rule:

php
// Good: Single responsibility
class ActiveUserSpecification extends AbstractSpecification { }
class PremiumUserSpecification extends AbstractSpecification { }

// Bad: Multiple responsibilities
class ActivePremiumUserSpecification extends AbstractSpecification { }

2. Immutability

Make specifications immutable by using constructor parameters:

php
class AgeRangeSpecification extends AbstractSpecification
{
    public function __construct(
        private readonly int $minAge,
        private readonly int $maxAge
    ) {}
}

3. Type Safety

Always check the candidate type in isSatisfiedBy():

php
public function isSatisfiedBy(mixed $candidate): bool
{
    if (!$candidate instanceof User) {
        return false;
    }
    
    // Your logic here
}

4. Efficient Queries

Optimize toQuery() for database performance:

php
public function toQuery(Builder $query): Builder
{
    // Use indexes
    return $query->where('indexed_column', $value);
    
    // Avoid N+1 with eager loading
    return $query->with('relationship')
                 ->whereHas('relationship', function($q) {
                     // conditions
                 });
}

Extending AbstractSpecification

You can create your own base specifications:

php
abstract class TenantAwareSpecification extends AbstractSpecification
{
    protected function applyTenantScope(Builder $query): Builder
    {
        return $query->where('tenant_id', auth()->user()->tenant_id);
    }
    
    public function toQuery(Builder $query): Builder
    {
        return $this->applyTenantScope($query);
    }
}

Performance Tips

  1. Cache Key Optimization: Include only parameters that affect the result
  2. Query Optimization: Use appropriate indexes
  3. Lazy Evaluation: Don't execute queries in isSatisfiedBy()
  4. Composition Over Inheritance: Use composite specifications instead of complex inheritance

See Also

Released under the MIT License.