Skip to content

Basic Usage

Master the fundamentals of Laravel Specifications. This tutorial covers creating specifications, using them for validation and queries, and understanding the core concepts that make specifications powerful.

Your First Specification

Let's start with a simple example that demonstrates the power of specifications:

The Problem: Messy Conditional Logic

Imagine you have this scattered throughout your application:

php
// In controller
if ($user->status === 'active' && $user->email_verified_at !== null) {
    // Allow access
}

// In service  
if ($user->status === 'active' && $user->email_verified_at !== null) {
    // Send notification
}

// In job
if ($user->status === 'active' && $user->email_verified_at !== null) {
    // Process user
}

The Solution: A Simple Specification

php
<?php

namespace App\Specifications\User;

use DangerWayne\LaravelSpecifications\AbstractSpecification;

class ActiveVerifiedUserSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->status === 'active' 
            && $candidate->email_verified_at !== null;
    }
    
    public function toQuery($query)
    {
        return $query->where('status', 'active')
                    ->whereNotNull('email_verified_at');
    }
}

Now your code becomes:

php
$spec = new ActiveVerifiedUserSpecification();

// In controller
if ($spec->isSatisfiedBy($user)) {
    // Allow access
}

// In service
if ($spec->isSatisfiedBy($user)) {
    // Send notification  
}

// In query
$activeUsers = User::whereSpecification($spec)->get();

Core Concepts

1. The Specification Interface

Every specification implements these key methods:

php
interface SpecificationInterface
{
    // Check if a candidate satisfies the specification
    public function isSatisfiedBy(mixed $candidate): bool;
    
    // Convert to database query
    public function toQuery($query);
    
    // Combine with other specifications
    public function and(SpecificationInterface $spec): SpecificationInterface;
    public function or(SpecificationInterface $spec): SpecificationInterface;
    public function not(): SpecificationInterface;
}

2. Two Modes of Operation

Specifications work in two distinct modes:

Memory Mode: Object Validation

php
$user = User::find(1);
$spec = new ActiveVerifiedUserSpecification();

if ($spec->isSatisfiedBy($user)) {
    // User object satisfies the specification
}

Query Mode: Database Filtering

php
$activeUsers = User::whereSpecification(
    new ActiveVerifiedUserSpecification()
)->get();

// Generates: SELECT * FROM users WHERE status = 'active' AND email_verified_at IS NOT NULL

Creating Specifications

Method 1: Using Artisan Generator

The fastest way to create specifications:

bash
# Basic specification
php artisan make:specification User/ActiveUserSpecification

# With model binding
php artisan make:specification User/ActiveUserSpecification --model=User

# With caching support
php artisan make:specification User/ActiveUserSpecification --cacheable

Method 2: Manual Creation

Create the file manually:

php
<?php

namespace App\Specifications\User;

use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;

class PremiumUserSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->subscription_type === 'premium'
            && $candidate->subscription_expires_at > now();
    }
    
    public function toQuery($query): Builder
    {
        return $query->where('subscription_type', 'premium')
                    ->where('subscription_expires_at', '>', now());
    }
}

Practical Examples

Example 1: E-commerce Product Filtering

php
class InStockProductSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->stock_quantity > 0 
            && $candidate->status === 'published';
    }
    
    public function toQuery($query): Builder
    {
        return $query->where('stock_quantity', '>', 0)
                    ->where('status', 'published');
    }
}

// Usage
$inStockProducts = Product::whereSpecification(
    new InStockProductSpecification()
)->get();

// Or with a single product
$product = Product::find(1);
if ((new InStockProductSpecification())->isSatisfiedBy($product)) {
    // Product is available
}

Example 2: User Permissions

php
class CanAccessAdminSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->role === 'admin' 
            && $candidate->status === 'active'
            && $candidate->two_factor_enabled;
    }
    
    public function toQuery($query): Builder
    {
        return $query->where('role', 'admin')
                    ->where('status', 'active')
                    ->where('two_factor_enabled', true);
    }
}

// Usage in controller
public function dashboard(Request $request)
{
    $canAccess = new CanAccessAdminSpecification();
    
    if (!$canAccess->isSatisfiedBy($request->user())) {
        abort(403);
    }
    
    // Show admin dashboard
}

Example 3: Parameterized Specifications

Sometimes you need specifications that accept parameters:

php
class UserRegisteredAfterSpecification extends AbstractSpecification
{
    public function __construct(
        private readonly Carbon $date
    ) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->created_at > $this->date;
    }
    
    public function toQuery($query): Builder
    {
        return $query->where('created_at', '>', $this->date);
    }
}

// Usage
$newUsers = User::whereSpecification(
    new UserRegisteredAfterSpecification(now()->subMonth())
)->get();

Working with Collections

Specifications work seamlessly with Laravel collections:

php
$users = User::all();
$spec = new ActiveVerifiedUserSpecification();

// Filter collection
$activeUsers = $users->filter(
    fn($user) => $spec->isSatisfiedBy($user)
);

// Check if any user satisfies spec
$hasActiveUsers = $users->contains(
    fn($user) => $spec->isSatisfiedBy($user)
);

// Get first user matching spec
$firstActive = $users->first(
    fn($user) => $spec->isSatisfiedBy($user)
);

Error Handling

Invalid Candidates

Always validate your input:

php
class UserSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof User) {
            throw new InvalidArgumentException(
                'Candidate must be a User instance'
            );
        }
        
        return $candidate->status === 'active';
    }
}

Query Errors

Handle database-specific errors:

php
class SafeUserSpecification extends AbstractSpecification
{
    public function toQuery($query): Builder
    {
        try {
            return $query->where('status', 'active')
                        ->whereNotNull('email_verified_at');
        } catch (QueryException $e) {
            // Log error and return safe fallback
            Log::error('Specification query failed', ['error' => $e]);
            return $query->whereRaw('1 = 0'); // Return empty result
        }
    }
}

Testing Your Specifications

Basic Tests

php
use Tests\TestCase;
use App\Models\User;
use App\Specifications\User\ActiveVerifiedUserSpecification;

class ActiveVerifiedUserSpecificationTest extends TestCase
{
    public function test_satisfied_by_active_verified_user()
    {
        $user = User::factory()->create([
            'status' => 'active',
            'email_verified_at' => now(),
        ]);
        
        $spec = new ActiveVerifiedUserSpecification();
        
        $this->assertTrue($spec->isSatisfiedBy($user));
    }
    
    public function test_not_satisfied_by_inactive_user()
    {
        $user = User::factory()->create([
            'status' => 'inactive',
            'email_verified_at' => now(),
        ]);
        
        $spec = new ActiveVerifiedUserSpecification();
        
        $this->assertFalse($spec->isSatisfiedBy($user));
    }
    
    public function test_generates_correct_query()
    {
        $spec = new ActiveVerifiedUserSpecification();
        $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());
    }
}

Common Patterns

1. Status-Based Specifications

php
class PublishedContentSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->status === 'published'
            && $candidate->published_at <= now();
    }
}

2. Date-Based Specifications

php
class RecentActivitySpecification extends AbstractSpecification
{
    public function __construct(
        private readonly int $days = 30
    ) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->last_activity_at >= now()->subDays($this->days);
    }
}

3. Relationship-Based Specifications

php
class UserWithOrdersSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->orders()->exists();
    }
    
    public function toQuery($query): Builder
    {
        return $query->whereHas('orders');
    }
}

Performance Considerations

1. N+1 Query Prevention

php
class UserWithOrdersSpecification extends AbstractSpecification
{
    public function toQuery($query): Builder
    {
        // Use whereHas instead of loading relationships
        return $query->whereHas('orders');
    }
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        // Only check if relationship is loaded
        if ($candidate->relationLoaded('orders')) {
            return $candidate->orders->isNotEmpty();
        }
        
        // Fallback to database check
        return $candidate->orders()->exists();
    }
}

2. Index Optimization

Ensure your query methods use indexed columns:

php
class OptimizedUserSpecification extends AbstractSpecification
{
    public function toQuery($query): Builder
    {
        // Ensure 'status' and 'created_at' are indexed
        return $query->where('status', 'active')
                    ->where('created_at', '>', now()->subYear());
    }
}

Next Steps

You now understand the fundamentals of Laravel Specifications! Next, learn how to combine them into powerful compositions:


Key Takeaways

  • Specifications encapsulate business rules in testable objects
  • They work both for object validation and database queries
  • Always implement both isSatisfiedBy() and toQuery() methods
  • Use the Artisan generator to create specifications quickly

Next: Composition Techniques →

Released under the MIT License.