Skip to content

🎯 DRY, KISS, and YAGNI in Laravel

Three essential pragmatic principles that keep your code maintainable, understandable, and focused. These complement SOLID principles and guide day-to-day development decisions.

🔄 DRY — Don't Repeat Yourself

"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."

Duplication is the enemy of maintainability. Extract repeated logic into reusable components.

✅ Good Example

php
<?php

declare(strict_types=1);

namespace App\Actions\Users;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;

/**
 * Reusable action for creating users
 */
class CreateUser
{
    public function execute(array $data): User
    {
        // Password hashing logic in ONE place
        $data['password'] = Hash::make($data['password']);
        $data['email'] = strtolower(trim($data['email']));
        
        $user = User::create($data);
        
        // Logging logic in ONE place
        Log::info('User created', [
            'user_id' => $user->id,
            'email' => $user->email,
        ]);
        
        return $user;
    }
}

/**
 * API Controller - reuses action
 */
class ApiUserController extends Controller
{
    public function store(CreateUserRequest $request, CreateUser $action): JsonResponse
    {
        $user = $action->execute($request->validated());
        return response()->json(new UserResource($user), 201);
    }
}

/**
 * Admin Controller - reuses same action
 */
class AdminUserController extends Controller
{
    public function store(CreateUserRequest $request, CreateUser $action): JsonResponse
    {
        $user = $action->execute($request->validated());
        return redirect()->route('admin.users.index')
            ->with('success', 'User created successfully');
    }
}

/**
 * CLI Command - reuses same action
 */
class CreateUserCommand extends Command
{
    public function handle(CreateUser $action): int
    {
        $user = $action->execute([
            'name' => $this->ask('Name'),
            'email' => $this->ask('Email'),
            'password' => $this->secret('Password'),
        ]);
        
        $this->info("User {$user->email} created successfully");
        return 0;
    }
}

❌ Bad Example

php
<?php

// Duplicated logic across multiple controllers
class ApiUserController
{
    public function store(Request $request)
    {
        // Password logic duplicated
        $data = $request->all();
        $data['password'] = Hash::make($data['password']);
        $data['email'] = strtolower(trim($data['email']));
        
        $user = User::create($data);
        
        // Logging duplicated
        Log::info('User created', ['user_id' => $user->id]);
        
        return response()->json($user, 201);
    }
}

class AdminUserController
{
    public function store(Request $request)
    {
        // Same password logic duplicated
        $data = $request->all();
        $data['password'] = Hash::make($data['password']);
        $data['email'] = strtolower(trim($data['email']));
        
        $user = User::create($data);
        
        // Same logging duplicated
        Log::info('User created', ['user_id' => $user->id]);
        
        return redirect()->back();
    }
}

// If we need to change password logic, we have to change it in multiple places!

Using Traits for Shared Behavior

✅ Good Example

php
<?php

declare(strict_types=1);

namespace App\Traits;

trait Searchable
{
    /**
     * Scope a query to search multiple columns
     */
    public function scopeSearch($query, string $term)
    {
        $searchColumns = $this->searchable ?? ['name'];
        
        return $query->where(function ($query) use ($searchColumns, $term) {
            foreach ($searchColumns as $column) {
                $query->orWhere($column, 'LIKE', "%{$term}%");
            }
        });
    }
}

// Now any model can be searchable
class User extends Model
{
    use Searchable;
    
    protected array $searchable = ['name', 'email'];
}

class Product extends Model
{
    use Searchable;
    
    protected array $searchable = ['name', 'description', 'sku'];
}

class Post extends Model
{
    use Searchable;
    
    protected array $searchable = ['title', 'content'];
}

// Usage is consistent across all models
$users = User::search($term)->get();
$products = Product::search($term)->get();
$posts = Post::search($term)->get();

KISS — Keep It Simple, Stupid

"Simplicity is the ultimate sophistication."

Prefer clear, straightforward solutions over clever, complex ones. Code is read far more often than it's written.

✅ Good Example

php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\User;
use App\Models\Project;

class ProjectAccessService
{
    /**
     * Simple, readable access check
     */
    public function canAccess(User $user, Project $project): bool
    {
        // Clear early returns
        if ($user->isAdmin()) {
            return true;
        }
        
        if ($project->isPublic()) {
            return true;
        }
        
        if ($project->members->contains($user->id)) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Simple discount calculation
     */
    public function calculateDiscount(int $subtotal, ?string $couponCode = null): int
    {
        if (!$couponCode) {
            return 0;
        }
        
        $coupon = Coupon::where('code', $couponCode)->first();
        
        if (!$coupon || !$coupon->isValid()) {
            return 0;
        }
        
        if ($coupon->type === 'percentage') {
            return (int) ($subtotal * $coupon->value / 100);
        }
        
        return $coupon->value;
    }
    
    /**
     * Simple status check
     */
    public function getOrderStatus(Order $order): string
    {
        if ($order->cancelled_at) {
            return 'cancelled';
        }
        
        if ($order->delivered_at) {
            return 'delivered';
        }
        
        if ($order->shipped_at) {
            return 'shipped';
        }
        
        if ($order->paid_at) {
            return 'paid';
        }
        
        return 'pending';
    }
}

❌ Bad Example

php
<?php

// Overly clever, hard to understand
class ProjectAccessService
{
    public function canAccess(User $user, Project $project): bool
    {
        // Nested ternary - hard to read
        return $user->isAdmin() ? true : ($project->isPublic() ? true : 
            ($project->members->contains($user->id) ? true : false));
    }
    
    public function calculateDiscount(int $subtotal, ?string $couponCode = null): int
    {
        // Complex one-liner - clever but hard to debug
        return ($coupon = $couponCode ? Coupon::where('code', $couponCode)->first() : null) 
            ? ($coupon->isValid() ? ($coupon->type === 'percentage' 
                ? (int) ($subtotal * $coupon->value / 100) 
                : $coupon->value) 
            : 0) 
            : 0;
    }
    
    public function getOrderStatus(Order $order): string
    {
        // Overly complex array reduce
        return array_reduce(
            ['cancelled_at', 'delivered_at', 'shipped_at', 'paid_at'],
            fn($carry, $field) => $carry ?: ($order->$field ? explode('_', $field)[0] : null),
            null
        ) ?? 'pending';
    }
}

Keep Methods Small and Focused

✅ Good Example

php
<?php

class OrderService
{
    public function processOrder(Order $order): bool
    {
        $this->validateOrder($order);
        $this->calculateTotals($order);
        $this->processPayment($order);
        $this->sendConfirmation($order);
        
        return true;
    }
    
    private function validateOrder(Order $order): void
    {
        if ($order->items->isEmpty()) {
            throw new InvalidOrderException('Order has no items');
        }
    }
    
    private function calculateTotals(Order $order): void
    {
        $order->subtotal = $order->items->sum('price');
        $order->tax = $order->subtotal * 0.15;
        $order->total = $order->subtotal + $order->tax;
        $order->save();
    }
    
    private function processPayment(Order $order): void
    {
        $this->paymentGateway->charge($order->total);
    }
    
    private function sendConfirmation(Order $order): void
    {
        $order->user->notify(new OrderConfirmed($order));
    }
}

🚫 YAGNI — You Aren't Gonna Need It

"Always implement things when you actually need them, never when you just foresee that you might need them."

Don't build features or abstractions for hypothetical future needs. Build what you need now.

✅ Good Example

php
<?php

declare(strict_types=1);

namespace App\Services;

/**
 * Start simple - only what we need NOW
 */
class ReportExporter
{
    /**
     * Currently only CSV is required
     */
    public function exportToCsv(Collection $data): string
    {
        $handle = fopen('php://temp', 'r+');
        
        // Add headers
        fputcsv($handle, array_keys($data->first()->toArray()));
        
        // Add data
        foreach ($data as $row) {
            fputcsv($handle, $row->toArray());
        }
        
        rewind($handle);
        $csv = stream_get_contents($handle);
        fclose($handle);
        
        return $csv;
    }
}

// When PDF is actually needed, THEN add it
// When JSON is actually needed, THEN add it
// Don't build them "just in case"

❌ Bad Example

php
<?php

// Over-engineered for hypothetical future needs
interface ExportStrategyInterface
{
    public function export(Collection $data): string;
}

class CsvExportStrategy implements ExportStrategyInterface
{
    public function export(Collection $data): string
    {
        // CSV implementation
    }
}

class PdfExportStrategy implements ExportStrategyInterface
{
    public function export(Collection $data): string
    {
        // Not needed yet!
    }
}

class JsonExportStrategy implements ExportStrategyInterface
{
    public function export(Collection $data): string
    {
        // Not needed yet!
    }
}

class XmlExportStrategy implements ExportStrategyInterface
{
    public function export(Collection $data): string
    {
        // Not needed yet!
    }
}

class ExcelExportStrategy implements ExportStrategyInterface
{
    public function export(Collection $data): string
    {
        // Not needed yet!
    }
}

class ReportExporterFactory
{
    public function create(string $format): ExportStrategyInterface
    {
        return match($format) {
            'csv' => new CsvExportStrategy(),
            'pdf' => new PdfExportStrategy(),
            'json' => new JsonExportStrategy(),
            'xml' => new XmlExportStrategy(),
            'excel' => new ExcelExportStrategy(),
        };
    }
}

// Complex architecture for a feature that only needs CSV!

When to Add Abstraction

✅ Good Example - Evolving Code

php
<?php

// Step 1: Start simple (only CSV needed)
class ReportService
{
    public function exportCsv(Collection $data): string
    {
        // Simple CSV export
    }
}

// Step 2: Second format requested (PDF), duplication appears
class ReportService
{
    public function exportCsv(Collection $data): string
    {
        // CSV export
    }
    
    public function exportPdf(Collection $data): string
    {
        // PDF export (duplication noticed)
    }
}

// Step 3: Third format requested (Excel), NOW abstract
interface ExportStrategyInterface
{
    public function export(Collection $data): string;
}

class ReportService
{
    public function export(Collection $data, ExportStrategyInterface $strategy): string
    {
        return $strategy->export($data);
    }
}

// Now the abstraction is justified by actual need!

📋 Best Practices Summary

✅ Do's

  • DRY: Extract duplication after 2-3 occurrences (Rule of Three)
  • DRY: Use actions, traits, and services for shared logic
  • DRY: Create reusable components when patterns emerge
  • KISS: Write clear, simple code that others can understand
  • KISS: Use early returns to reduce nesting
  • KISS: Break complex methods into smaller ones
  • KISS: Favor readability over cleverness
  • YAGNI: Build features when actually needed
  • YAGNI: Start simple, add complexity when justified
  • YAGNI: Refactor when duplication appears, not before

❌ Don'ts

  • DRY: Don't extract after first occurrence (might be coincidence)
  • DRY: Don't force abstraction when logic is actually different
  • KISS: Don't use clever tricks that sacrifice readability
  • KISS: Don't nest conditions more than 2-3 levels deep
  • KISS: Don't write methods longer than one screen
  • YAGNI: Don't build "just in case" features
  • YAGNI: Don't create abstractions for single use cases
  • YAGNI: Don't over-engineer for hypothetical futures

🎯 Practical Decision Flow

Need to add functionality?
  ├─> Is it actually needed NOW? (YAGNI)
  │   ├─> No → Don't build it
  │   └─> Yes → Continue

  ├─> Does similar code exist elsewhere? (DRY)
  │   ├─> No → Write it simply (KISS)
  │   └─> Yes → Extract to shared component

  └─> Is the solution simple and clear? (KISS)
      ├─> No → Simplify
      └─> Yes → Ship it!

🔄 The Rule of Three

Extract duplication only after the third occurrence:

  1. First time: Write it
  2. Second time: Wince at duplication, but resist
  3. Third time: Now refactor and extract

This prevents premature abstraction while catching real patterns.


📝 Balance is Key: These principles sometimes conflict. Use judgment to find the right balance for your specific situation. Start simple (YAGNI), keep it readable (KISS), and refactor when patterns emerge (DRY).

Built with VitePress