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?
- 🎯 Common Use Cases - Find scenarios that match your needs
- 🤯 Holy $#!% Moments - Mind-blowing possibilities
- 📚 Complete Guide - Master all features and patterns