Skip to content

5-Minute Transformations

Step-by-step makeovers that transform messy code into elegant specifications. Each transformation takes 5 minutes or less to implement and delivers immediate benefits.

🔄 Quick Win #1: Search Filter Chaos

Time to Transform: 3 minutes ⏱️

Step 1: Identify the Problem (30 seconds)

You have a controller method like this:

php
class ProductController extends Controller
{
    public function search(Request $request)
    {
        $query = Product::query();
        
        if ($request->category) {
            $query->where('category_id', $request->category);
        }
        
        if ($request->min_price) {
            $query->where('price', '>=', $request->min_price);
        }
        
        if ($request->max_price) {
            $query->where('price', '<=', $request->max_price);
        }
        
        if ($request->in_stock) {
            $query->where('stock_quantity', '>', 0);
        }
        
        return $query->get();
    }
}

Step 2: Generate the Specification (1 minute)

bash
php artisan make:specification Product/ProductSearchSpecification --model=Product

Step 3: Implement the Logic (90 seconds)

php
<?php

namespace App\Specifications\Product;

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

class ProductSearchSpecification extends AbstractSpecification
{
    public function __construct(
        private ?int $category = null,
        private ?float $minPrice = null,
        private ?float $maxPrice = null,
        private bool $inStockOnly = false
    ) {}
    
    public static function fromRequest($request): self
    {
        return new self(
            category: $request->category,
            minPrice: $request->min_price,
            maxPrice: $request->max_price,
            inStockOnly: $request->boolean('in_stock')
        );
    }
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof Product) {
            return false;
        }
        
        return $this->buildSearchSpec()->isSatisfiedBy($candidate);
    }
    
    public function toQuery(Builder $query): Builder
    {
        return $this->buildSearchSpec()->toQuery($query);
    }
    
    private function buildSearchSpec(): AbstractSpecification
    {
        $builder = Specification::create();
        
        if ($this->category) {
            $builder->where('category_id', $this->category);
        }
        
        if ($this->minPrice) {
            $builder->where('price', '>=', $this->minPrice);
        }
        
        if ($this->maxPrice) {
            $builder->where('price', '<=', $this->maxPrice);
        }
        
        if ($this->inStockOnly) {
            $builder->where('stock_quantity', '>', 0);
        }
        
        return $builder->build();
    }
}

Step 4: Transform the Controller (30 seconds)

php
class ProductController extends Controller
{
    public function search(Request $request)
    {
        $searchSpec = ProductSearchSpecification::fromRequest($request);
        
        return Product::whereSpecification($searchSpec)->get();
    }
}

✨ Immediate Benefits

  • 50% less code in the controller
  • 100% testable business logic
  • Reusable across your application
  • Self-documenting search criteria

🔄 Quick Win #2: User Permission Hell

Time to Transform: 4 minutes ⏱️

Step 1: Spot the Mess (30 seconds)

php
class PostController extends Controller
{
    public function show(Post $post)
    {
        $user = auth()->user();
        
        // Admin can see everything
        if ($user->role === 'admin') {
            return view('posts.show', compact('post'));
        }
        
        // Author can see their own posts
        if ($post->user_id === $user->id) {
            return view('posts.show', compact('post'));
        }
        
        // Published posts are public
        if ($post->status === 'published' && $post->published_at <= now()) {
            return view('posts.show', compact('post'));
        }
        
        // Premium posts for premium users
        if ($post->is_premium && $user->subscription === 'premium') {
            return view('posts.show', compact('post'));
        }
        
        abort(403);
    }
}

Step 2: Generate the Specification (30 seconds)

bash
php artisan make:specification Post/PostViewableSpecification --model=Post

Step 3: Implement Clean Logic (2 minutes)

php
<?php

namespace App\Specifications\Post;

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

class PostViewableSpecification extends AbstractSpecification
{
    public function __construct(private User $user) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof Post) {
            return false;
        }
        
        return $this->adminAccess()
            ->or($this->authorAccess($candidate))
            ->or($this->publicAccess($candidate))
            ->or($this->premiumAccess($candidate))
            ->isSatisfiedBy($candidate);
    }
    
    public function toQuery(Builder $query): Builder
    {
        return $query->where(function($q) {
            $q->where('user_id', $this->user->id) // Author access
              ->orWhere(function($subQ) { // Public access
                  $subQ->where('status', 'published')
                       ->where('published_at', '<=', now());
              })
              ->when($this->user->subscription === 'premium', function($subQ) {
                  $subQ->orWhere('is_premium', true); // Premium access
              })
              ->when($this->user->role === 'admin', function($subQ) {
                  $subQ->orWhereRaw('1 = 1'); // Admin sees all
              });
        });
    }
    
    private function adminAccess(): AbstractSpecification
    {
        return $this->user->role === 'admin' 
            ? new AlwaysTrueSpecification()
            : new AlwaysFalseSpecification();
    }
    
    private function authorAccess(Post $post): AbstractSpecification
    {
        return new WhereSpecification('user_id', $this->user->id);
    }
    
    private function publicAccess(Post $post): AbstractSpecification
    {
        return (new WhereSpecification('status', 'published'))
            ->and(new WhereSpecification('published_at', '<=', now()));
    }
    
    private function premiumAccess(Post $post): AbstractSpecification
    {
        return $this->user->subscription === 'premium'
            ? new WhereSpecification('is_premium', true)
            : new AlwaysFalseSpecification();
    }
}

Step 4: Clean Up the Controller (1 minute)

php
class PostController extends Controller
{
    public function show(Post $post)
    {
        $viewableSpec = new PostViewableSpecification(auth()->user());
        
        if (!$viewableSpec->isSatisfiedBy($post)) {
            abort(403);
        }
        
        return view('posts.show', compact('post'));
    }
    
    public function index()
    {
        $viewableSpec = new PostViewableSpecification(auth()->user());
        $posts = Post::whereSpecification($viewableSpec)->paginate(10);
        
        return view('posts.index', compact('posts'));
    }
}

✨ Immediate Benefits

  • Logic reuse across multiple methods
  • Clear separation of concerns
  • Easy to test each access rule
  • Policy integration ready

🔄 Quick Win #3: Validation Spaghetti

Time to Transform: 5 minutes ⏱️

Step 1: Identify the Chaos (30 seconds)

php
class UserController extends Controller
{
    public function store(Request $request)
    {
        $errors = [];
        
        // Email validation
        if (!$request->email) {
            $errors['email'] = 'Email is required';
        } elseif (!filter_var($request->email, FILTER_VALIDATE_EMAIL)) {
            $errors['email'] = 'Invalid email format';
        } elseif (User::where('email', $request->email)->exists()) {
            $errors['email'] = 'Email already taken';
        }
        
        // Age-based validation for user type
        if ($request->user_type === 'premium') {
            if (!$request->age || $request->age < 18) {
                $errors['age'] = 'Premium users must be 18+';
            }
        } elseif ($request->user_type === 'business') {
            if (!$request->age || $request->age < 21) {
                $errors['age'] = 'Business users must be 21+';
            }
            if (!$request->company) {
                $errors['company'] = 'Company name required for business users';
            }
        }
        
        if ($errors) {
            return response()->json(['errors' => $errors], 422);
        }
        
        User::create($request->validated());
        
        return response()->json(['message' => 'User created']);
    }
}

Step 2: Generate Validation Specifications (1 minute)

bash
php artisan make:specification User/Validation/UserValidationSpecification --composite
php artisan make:specification User/Validation/EmailValidationSpecification
php artisan make:specification User/Validation/AgeValidationSpecification

Step 3: Implement Validation Logic (2 minutes)

php
<?php

namespace App\Specifications\User\Validation;

use DangerWayne\Specification\Specifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;

class UserValidationSpecification extends AbstractSpecification
{
    private array $errors = [];
    
    public function __construct(private array $data) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        $this->errors = [];
        
        // Validate email
        $emailSpec = new EmailValidationSpecification($this->data);
        if (!$emailSpec->isSatisfiedBy($this->data)) {
            $this->errors['email'] = $emailSpec->getError();
        }
        
        // Validate age based on user type
        $ageSpec = new AgeValidationSpecification($this->data);
        if (!$ageSpec->isSatisfiedBy($this->data)) {
            $this->errors = array_merge($this->errors, $ageSpec->getErrors());
        }
        
        return empty($this->errors);
    }
    
    public function getErrors(): array
    {
        return $this->errors;
    }
    
    public function toQuery(Builder $query): Builder
    {
        return $query; // Not applicable for validation
    }
}
php
class EmailValidationSpecification extends AbstractSpecification
{
    private ?string $error = null;
    
    public function __construct(private array $data) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        $email = $this->data['email'] ?? null;
        
        if (!$email) {
            $this->error = 'Email is required';
            return false;
        }
        
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $this->error = 'Invalid email format';
            return false;
        }
        
        if (User::where('email', $email)->exists()) {
            $this->error = 'Email already taken';
            return false;
        }
        
        return true;
    }
    
    public function getError(): ?string
    {
        return $this->error;
    }
    
    public function toQuery(Builder $query): Builder
    {
        return $query;
    }
}

Step 4: Transform the Controller (1.5 minutes)

php
class UserController extends Controller
{
    public function store(Request $request)
    {
        $validationSpec = new UserValidationSpecification($request->all());
        
        if (!$validationSpec->isSatisfiedBy($request->all())) {
            return response()->json([
                'errors' => $validationSpec->getErrors()
            ], 422);
        }
        
        User::create($request->validated());
        
        return response()->json(['message' => 'User created']);
    }
}

✨ Immediate Benefits

  • Modular validation rules
  • Easy to unit test each rule
  • Reusable across update/create operations
  • Clear error handling

🔄 Quick Win #4: Query Builder Monster

Time to Transform: 4 minutes ⏱️

Step 1: Recognize the Monster (30 seconds)

php
class OrderController extends Controller
{
    public function dashboard()
    {
        $orders = Order::where('status', '!=', 'cancelled')
            ->where(function($q) {
                $q->where('payment_status', 'paid')
                  ->orWhere('payment_method', 'cash_on_delivery');
            })
            ->whereHas('customer', function($q) {
                $q->where('status', 'active')
                  ->whereNotNull('email_verified_at');
            })
            ->where('created_at', '>=', now()->subDays(30))
            ->whereDoesntHave('refunds')
            ->get();
            
        return view('dashboard', compact('orders'));
    }
}

Step 2: Generate Dashboard Specification (30 seconds)

bash
php artisan make:specification Order/DashboardOrdersSpecification --model=Order

Step 3: Break Down Complex Logic (2.5 minutes)

php
<?php

namespace App\Specifications\Order;

use App\Models\Order;
use DangerWayne\Specification\Specifications\AbstractSpecification;
use DangerWayne\Specification\Specifications\Common\{WhereSpecification, WhereHasSpecification};
use Illuminate\Database\Eloquent\Builder;

class DashboardOrdersSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof Order) {
            return false;
        }
        
        return $this->buildCriteria()->isSatisfiedBy($candidate);
    }
    
    public function toQuery(Builder $query): Builder
    {
        return $this->buildCriteria()->toQuery($query);
    }
    
    private function buildCriteria(): AbstractSpecification
    {
        return $this->notCancelledOrders()
            ->and($this->validPayment())
            ->and($this->activeCustomers())
            ->and($this->recentOrders())
            ->and($this->noRefunds());
    }
    
    private function notCancelledOrders(): AbstractSpecification
    {
        return new WhereSpecification('status', '!=', 'cancelled');
    }
    
    private function validPayment(): AbstractSpecification
    {
        $paidOrders = new WhereSpecification('payment_status', 'paid');
        $codOrders = new WhereSpecification('payment_method', 'cash_on_delivery');
        
        return $paidOrders->or($codOrders);
    }
    
    private function activeCustomers(): AbstractSpecification
    {
        $activeCustomer = new WhereSpecification('status', 'active');
        $verifiedEmail = new WhereNullSpecification('email_verified_at', false);
        $customerCriteria = $activeCustomer->and($verifiedEmail);
        
        return new WhereHasSpecification('customer', $customerCriteria);
    }
    
    private function recentOrders(): AbstractSpecification
    {
        return new WhereSpecification('created_at', '>=', now()->subDays(30));
    }
    
    private function noRefunds(): AbstractSpecification
    {
        return new WhereDoesntHaveSpecification('refunds');
    }
}

Step 4: Simplify the Controller (30 seconds)

php
class OrderController extends Controller
{
    public function dashboard()
    {
        $dashboardSpec = new DashboardOrdersSpecification();
        $orders = Order::whereSpecification($dashboardSpec)->get();
        
        return view('dashboard', compact('orders'));
    }
}

✨ Immediate Benefits

  • Self-documenting business logic
  • Individual testing of each criteria
  • Easy maintenance and modifications
  • Composable for other use cases

🏆 Transformation Checklist

After each 5-minute transformation, you should have:

✅ Code Quality

  • [ ] Reduced complexity in controllers
  • [ ] Clear business intent in specification names
  • [ ] Separated concerns (UI logic vs business logic)
  • [ ] Self-documenting code that explains the "why"

✅ Testability

  • [ ] Unit tests for each specification
  • [ ] Independent testing of business rules
  • [ ] Mock-friendly design
  • [ ] Isolated logic with no side effects

✅ Reusability

  • [ ] Multiple usage contexts (controllers, policies, jobs)
  • [ ] Composable specifications for complex scenarios
  • [ ] Configurable parameters for flexibility
  • [ ] Domain organization for easy discovery

✅ Maintainability

  • [ ] Single responsibility for each specification
  • [ ] Clear naming conventions
  • [ ] Consistent patterns across the codebase
  • [ ] Documentation through code structure

🔥 Pro Tips for Fast Transformations

1. Start Small

Pick the messiest, most conditional controller method first. The dramatic improvement will motivate you to continue.

2. Use the Artisan Generator

bash
# Generate multiple specifications quickly
php artisan make:specification User/ActiveUserSpec --model=User --test
php artisan make:specification Product/AvailableSpec --model=Product --test

3. Test As You Go

php
// Write tests immediately after creating specifications
$spec = new ActiveUserSpecification();
$this->assertTrue($spec->isSatisfiedBy($activeUser));
$this->assertFalse($spec->isSatisfiedBy($inactiveUser));

4. Compose Incrementally

php
// Start simple, then compose
$activeSpec = new ActiveUserSpecification();
$verifiedSpec = new VerifiedEmailSpecification();
$eligibleSpec = $activeSpec->and($verifiedSpec);

5. Document with Examples

php
/**
 * Identifies orders eligible for express shipping
 * 
 * Example: Orders > $100, paid, domestic, no hazmat
 */
class ExpressShippingEligibleSpecification extends AbstractSpecification
{
    // Implementation...
}

Ready for More?

You've mastered 5-minute transformations! Ready to explore specific use cases that match your domain?


Explore Common Use Cases →

Released under the MIT License.