AbstractSpecification
The AbstractSpecification
class provides the base implementation of the SpecificationInterface
with built-in support for composite operations and caching.
Namespace
DangerWayne\Specification\Specifications\AbstractSpecification
Class Definition
<?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()
abstract public function isSatisfiedBy(mixed $candidate): bool;
Implement your business logic to determine if a candidate satisfies the specification.
toQuery()
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()
$spec = $activeSpec->and($premiumSpec);
Creates an AndSpecification
combining both specifications.
or()
$spec = $adminSpec->or($moderatorSpec);
Creates an OrSpecification
allowing either specification.
not()
$spec = $activeSpec->not();
Creates a NotSpecification
negating the current specification.
Cache Key Generation
getCacheKey()
public function getCacheKey(): string
Generates a unique cache key based on the class name and parameters.
getParameters()
protected function getParameters(): array
Override this method to provide specification parameters for cache key generation:
protected function getParameters(): array
{
return [
'status' => $this->status,
'age' => $this->minAge,
];
}
Implementation Examples
Basic Specification
<?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
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
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:
// 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:
// 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:
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()
:
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof User) {
return false;
}
// Your logic here
}
4. Efficient Queries
Optimize toQuery()
for database performance:
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:
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
- Cache Key Optimization: Include only parameters that affect the result
- Query Optimization: Use appropriate indexes
- Lazy Evaluation: Don't execute queries in
isSatisfiedBy()
- Composition Over Inheritance: Use composite specifications instead of complex inheritance
See Also
- SpecificationInterface - The interface contract
- AndSpecification - AND composite operation
- OrSpecification - OR composite operation
- NotSpecification - NOT composite operation
- CacheableSpecification - Caching trait