Appearance
🎯 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:
- First time: Write it
- Second time: Wince at duplication, but resist
- 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).