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.