Author: Daryle De Silva

  • Use Git History to Understand Legacy Code Evolution

    When you inherit a codebase, understanding WHY code exists is often more valuable than WHAT it does. Git history is your time machine.

    The Basic Timeline

    # Get simple commit history for a file
    git log --follow --all --pretty=format:"%h|%ai|%an|%s" -- app/Services/ReportGenerator.php

    Output:

    f4bc663|2018-05-09 17:46:43 +0800|John Smith|TASK-707 Add report tracking
    201105b|2019-03-27 10:02:08 +0800|Jane Doe|Refactor - add dependency injection

    The Full Story (Patches + Stats)

    # See actual code changes with statistics
    git log --follow --all --stat --patch -- app/Services/ReportGenerator.php

    What You Learn

    • Original author: Who to ask if you have questions
    • Creation date: How old this code is (affects modernization priority)
    • Commit message: Why it was added (often references a ticket/task)
    • Evolution pattern: Was it written once and forgotten, or actively maintained?
    • Refactoring history: What patterns were replaced (helps avoid repeating mistakes)

    Reading the Story

    The example above tells us:

    • Created in May 2018 for admin dashboard tracking
    • Refactored in March 2019 to add dependency injection
    • One year gap between changes suggests low-priority maintenance code
    • Two different authors = knowledge might be spread across team

    Pro Tips

    • Use --follow: Tracks files even if renamed (critical!)
    • Filter by date: --since="2020-01-01" to see recent changes only
    • Check for deletions: If no commits since 2020, might be deprecated
    • Look for patterns: Frequent refactors = evolving requirements

    When to Use This

    • Before refactoring legacy code (understand why it’s weird)
    • When debugging mysterious behavior (was it always like this?)
    • During code review (does this change make sense given the history?)
    • When deciding whether to delete code (has it been touched recently?)

    Next time you see weird legacy code, don’t guess — check the git history. It often explains everything.

  • Refactoring Legacy Middleware: From Facades to Dependency Injection

    Legacy Laravel code often relies heavily on facades. While facades are convenient, they can make code harder to test and reason about. Here’s a pattern for refactoring middleware to use dependency injection instead.

    The Legacy Pattern (Bad)

    <?php
    
    namespace App\Http\Middleware;
    
    use Closure;
    use App\Models\ActivityLog;
    
    class TrackActivityStats
    {
        public function handle($request, Closure $next)
        {
            try {
                $route = \Route::current();
                ActivityLog::create([
                    'method' => $request->method(),
                    'uri' => $route->uri(),
                    'route_name' => $route->getName(),
                    'user_id' => \Auth::id()
                ]);
            } catch (\Throwable $e) {}
            return $next($request);
        }
    }

    The Modern Pattern (Good)

    <?php
    
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Psr\Log\LogLevel;
    use App\Models\ActivityLog;
    
    class TrackActivityStats
    {
        private $logger;
    
        public function __construct(ActivityLog $logger)
        {
            $this->logger = $logger;
        }
    
        public function handle(Request $request, Closure $next): Response
        {
            $this->logRequest($request);
            return $next($request);
        }
    
        private function logRequest(Request $request)
        {
            try {
                $this->logger->log(LogLevel::INFO, 'Activity tracked', [
                    'method' => $request->method(),
                    'uri' => $request->route()->uri(),
                    'route_name' => $request->route()->getName(),
                    'user_id' => $request->user()->id ?? null
                ]);
            } catch (\Throwable $e) {
                // Silent failure for non-critical tracking
            }
        }
    }

    Why This Is Better

    • Testable: Easy to mock the logger dependency in tests
    • Type-safe: Return type hints catch errors early
    • Explicit dependencies: Constructor shows what the middleware needs
    • No global state: Uses request object instead of facades
    • Separation of concerns: Logic extracted into private method
    • Better IDE support: Type hints enable autocomplete

    Key Changes

    • \Route::current()$request->route()
    • \Auth::id()$request->user()->id
    • Direct model call → Injected logger dependency
    • No type hints → Full type declarations

    Next time you touch old middleware, consider this refactoring pattern. Your future self (and your test suite) will thank you.

  • Run External CLI Commands from Laravel with Process::path()

    Laravel’s Process facade makes it easy to run external command-line tools from your PHP application. The path() method is especially useful when you need to execute commands in a specific working directory.

    Basic Usage

    use Illuminate\Support\Facades\Process;
    
    // Run a script from a specific directory
    Process::path('/opt/scripts')
        ->run(['./backup.sh', '--full'])
        ->throw();

    The path() method changes the working directory before executing the command. This is crucial when scripts depend on relative paths or expect to run from a specific location.

    Real-World Example: Integration Scripts

    Imagine you’re integrating with an external CLI tool installed in a separate directory:

    class DataSyncService
    {
        public function syncUser(User $user): void
        {
            $command = [
                'bin/sync-tool',
                'user',
                'update',
                $user->email,
                '--name=' . $user->name,
                '--active=' . ($user->is_active ? 'true' : 'false'),
            ];
    
            Process::path(config('services.sync.install_path'))
                ->run($command)
                ->throw();  // Throws ProcessFailedException on non-zero exit
        }
    }

    Key Features

    • path($directory): Set working directory
    • run($command): Execute and return result
    • throw(): Fail loudly on errors
    • timeout($seconds): Prevent hanging

    With Timeout Protection

    $result = Process::path('/var/tools')
        ->timeout(30)  // Kill after 30 seconds
        ->run(['./long-task.sh'])
        ->throw();
    
    echo $result->output();

    Handling Output

    $result = Process::path('/opt/reports')
        ->run(['./generate-report.sh', '--format=json']);
    
    if ($result->successful()) {
        $data = json_decode($result->output(), true);
        // Process the data...
    } else {
        Log::error('Report generation failed: ' . $result->errorOutput());
    }

    This pattern is especially useful when integrating third-party tools, running system utilities, or coordinating with non-PHP services that expose CLI interfaces.

  • Auto-Eager Load Common Relationships with Protected $with in Laravel Models

    When certain relationships are always needed with a model, you can eager load them by default using the protected $with property instead of adding with() to every query.

    The Pattern

    Add the $with property to your model:

    class User extends Model
    {
        protected $with = ['profile'];
    
        public function profile(): BelongsTo
        {
            return $this->belongsTo(Profile::class);
        }
    }

    Now every query automatically eager loads the profile:

    // These all include profile automatically:
    User::find(1);
    User::all();
    User::where('active', true)->get();
    
    // No N+1 queries! Profile is always loaded.

    When to Use This

    Perfect for:

    • API resources that always serialize certain relations
    • Computed attributes that depend on relationships
    • Models with logical “parent” relationships

    Example use case: A Comment model where you always need the author:

    class Comment extends Model
    {
        protected $with = ['author'];
    
        public function author(): BelongsTo
        {
            return $this->belongsTo(User::class, 'user_id');
        }
    }

    Disable When Needed

    You can still opt out of auto-eager loading in specific queries:

    // Skip the default eager load:
    User::without('profile')->get();

    Trade-offs

    Pros: Cleaner code, prevents forgotten eager loads, consistent behavior

    Cons: Always eager loads, even when not needed—can add overhead if overused

    Best practice: Combine protected $with for “always needed” relations with whenLoaded() in resources for “sometimes needed” ones.

  • Laravel API Resources: Use whenLoaded() to Conditionally Include Relationships

    When building Laravel API resources, accessing relationships directly can trigger N+1 queries if those relationships aren’t eager loaded. The whenLoaded() method provides an elegant solution.

    The Problem

    Consider this resource implementation:

    class UserResource extends JsonResource
    {
        public function toArray($request)
        {
            return [
                'id' => $this->id,
                'name' => $this->profile->display_name,  // ❌ May trigger query
                'email' => $this->profile->email,        // ❌ May trigger query
            ];
        }
    }

    If profile isn’t eager loaded, every resource instance will execute a separate database query, creating the classic N+1 problem.

    The Solution

    Use whenLoaded() to check if the relationship exists before accessing it:

    class UserResource extends JsonResource
    {
        public function toArray($request)
        {
            return [
                'id' => $this->id,
                'name' => $this->whenLoaded('profile', fn() => $this->profile->display_name),
                'email' => $this->whenLoaded('profile', fn() => $this->profile->email),
                'post_count' => $this->whenLoaded('posts', fn() => $this->posts->count()),
            ];
        }
    }

    Now the resource only accesses profile data when it’s already loaded:

    // Without eager loading - fields are omitted
    UserResource::collection(User::all());
    
    // With eager loading - fields are included
    UserResource::collection(User::with('profile', 'posts')->get());

    Why This Matters

    • Performance: Prevents accidental N+1 queries when relationships aren’t needed
    • Flexibility: Same resource works with or without eager loading
    • Clean errors: No cryptic “trying to get property of null” errors when relations are missing

    This pattern is especially valuable in API endpoints where clients can request different levels of detail via query parameters.

  • Laravel Route Order: Custom Routes Before Resource Routes

    When mixing custom routes with apiResource, order matters. Specific routes must come before parameterized ones.

    The Problem

    // ❌ WRONG: Custom route comes AFTER resource
    Route::apiResource('notifications', NotificationController::class);
    Route::get('/notifications/stats', [NotificationController::class, 'stats']);
    

    Result:

    GET /api/notifications/stats
    # Laravel matches this to show($id='stats') ❌
    # Returns 404: Notification not found
    

    The Solution

    // ✅ CORRECT: Custom routes BEFORE resource
    Route::get('/notifications/stats', [NotificationController::class, 'stats'])
        ->name('notifications.stats');
    
    Route::post('/notifications/actions/mark-all-read', [NotificationController::class, 'markAllAsRead'])
        ->name('notifications.mark-all-read');
    
    Route::apiResource('notifications', NotificationController::class)
        ->only(['index', 'show', 'update', 'destroy']);
    

    Route Matching Order

    Laravel matches routes top to bottom:

    1. GET /notifications/stats → matches first route ✓
    2. POST /notifications/actions/mark-all-read → matches second route ✓
    3. GET /notifications/abc123 → matches apiResource show route ✓

    Naming Convention for Custom Routes

    Use descriptive paths for clarity:

    // Collection-level actions
    Route::get('/users/export', ...);        // GET /api/users/export
    Route::post('/users/import', ...);       // POST /api/users/import
    
    // Named action routes
    Route::post('/orders/actions/bulk-cancel', ...);
    Route::get('/products/stats', ...);
    
    // Avoid generic prefixes that might conflict
    Route::get('/notifications/meta', ...);  // ✓ Good: 'meta' won't be a UUID
    Route::get('/notifications/all', ...);   // ⚠️ Could conflict if 'all' is valid ID
    

    Debug Routes

    Use php artisan route:list to verify order:

    php artisan route:list --path=notifications
    
    # Output shows registration order:
    GET     api/notifications/stats          notifications.stats
    POST    api/notifications/actions/...    notifications.mark-all-read  
    GET     api/notifications                notifications.index
    GET     api/notifications/{notification} notifications.show
    

    Remember: Specific beats generic. Always define custom routes before resource routes.

  • Consolidate Resource Controllers with Flexible Filters

    Instead of creating separate controllers for nested resources (like PostCommentController, UserCommentController), build one controller with flexible filtering using Spatie Query Builder.

    Before: Multiple Controllers

    // app/Http/Controllers/PostCommentController.php
    public function index(Post $post)
    {
        return CommentResource::collection(
            $post->comments()->paginate()
        );
    }
    
    // app/Http/Controllers/UserCommentController.php
    public function index(User $user)
    {
        return CommentResource::collection(
            $user->comments()->paginate()
        );
    }
    

    After: Single Controller with Filters

    // app/Http/Controllers/CommentController.php
    public function index(Request $request)
    {
        $query = QueryBuilder::for(Comment::query())
            ->allowedFilters([
                AllowedFilter::callback('post_id', 
                    fn ($q, $value) => $q->where('post_id', $value)
                ),
                AllowedFilter::callback('user_id', 
                    fn ($q, $value) => $q->where('user_id', $value)
                ),
                AllowedFilter::exact('status'),
            ])
            ->defaultSort('-created_at');
    
        return CommentResource::collection($query->jsonPaginate());
    }
    

    Usage

    # Get comments for a post
    GET /api/comments?filter[post_id]=123
    
    # Get comments by a user
    GET /api/comments?filter[user_id]=456
    
    # Combine filters
    GET /api/comments?filter[post_id]=123&filter[status]=approved
    

    Benefits

    • Single source of truth for filtering logic
    • Easier to maintain and test
    • More flexible API for consumers
    • Reduces route bloat
  • The Right Way to Implement Laravel Sanctum Mobile Authentication

    Laravel’s Sanctum documentation shows the exact pattern for mobile app authentication. Here’s the production-ready implementation:

    The Login Endpoint

    use App\Models\Account;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Hash;
    use Illuminate\Validation\ValidationException;
    
    Route::post('/auth/token', function (Request $request) {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required',
            'device_name' => 'required',
        ]);
    
        $account = Account::where('email', $request->email)->first();
    
        if (! $account || ! Hash::check($request->password, $account->password)) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }
    
        return $account->createToken($request->device_name)->plainTextToken;
    });
    

    Why This Pattern Works

    1. Single query with Hash::check() – Prevents timing attacks by checking both user existence and password in one validation
    2. ValidationException for API errors – Returns proper JSON:API error format automatically
    3. Device name tracking – The device_name field lets users manage their active sessions (“iPhone 14”, “Work Laptop”, etc.)
    4. plainTextToken – Critical: This is the ONLY time you can retrieve the token. Store it in the mobile app immediately.

    Protected Routes

    Route::get('/profile', function (Request $request) {
        return $request->user();
    })->middleware('auth:sanctum');
    

    Mobile client sends:

    Authorization: Bearer {plainTextToken}
    

    Logout Implementation

    Route::post('/auth/logout', function (Request $request) {
        // Revoke current device token only
        $request->user()->currentAccessToken()->delete();
        
        return response()->json(['message' => 'Logged out successfully']);
    })->middleware('auth:sanctum');
    
  • Build Conversational AI Agents With Laravel’s AI SDK

    Laravel 12’s AI SDK lets you build agents that maintain conversation context and use tools. This is perfect for multi-step decision-making workflows where the agent needs to query your database or call APIs based on previous responses.

    Define an agent with conversation memory and custom tools:

    use Laravel\Ai\Agent;
    use Laravel\Ai\Conversations\Conversational;
    use Laravel\Ai\Conversations\RemembersConversations;
    
    class DataValidator extends Agent implements Conversational
    {
        use RemembersConversations;
        
        protected function instructions(): string
        {
            return 'Check if data already exists. Use canonical values from database when found.';
        }
        
        protected function tools(): array
        {
            return [new SearchDatabase()];
        }
        
        protected function timeout(): int
        {
            return 60;
        }
    }
    

    Invoke the agent with conversation context:

    $response = app(DataValidator::class)
        ->forUser($conversationId)
        ->withSchema(DataSchema::schema())
        ->invoke($inputData);
    
    $validated = $response->output();
    

    The agent remembers previous interactions within the same conversation ID, letting it build up context across multiple invocations. This is powerful for workflows like data enrichment pipelines where each step informs the next.

    For example, a data import pipeline could use an agent to validate and deduplicate records. The first invocation establishes canonical values, and subsequent invocations reference those decisions:

    // First record - agent searches database and establishes canonical form
    $result1 = $agent->forUser('import-batch-123')
        ->invoke(['name' => 'John Doe', 'email' => '[email protected]']);
    
    // Second record - agent remembers the previous decision
    $result2 = $agent->forUser('import-batch-123')
        ->invoke(['name' => 'J. Doe', 'email' => '[email protected]']);
    // Agent recognizes this as the same person and reuses the canonical form
    

    The Conversational interface and RemembersConversations trait handle the complexity of managing conversation history, while your tools provide the agent with the capabilities it needs to make informed decisions.

  • Parse Dot-Notation Query Params Into Nested Relationship Arrays

    API query parameters like ?include=author,comments.user.profile need to be converted into nested arrays for eager loading. Here’s a minimal recursive approach that handles unlimited nesting depth.

    public function parseIncludes(Request $request): array
    {
        $includes = explode(',', $request->input('include', ''));
        
        $buildTree = function($relations) use (&$buildTree) {
            $tree = [];
            foreach ($relations as $relation) {
                $parts = explode('.', $relation, 2);
                $key = $parts[0];
                
                if (isset($parts[1])) {
                    $tree[$key][] = $parts[1];
                } else {
                    $tree[$key] = [];
                }
            }
            
            foreach ($tree as $key => $nested) {
                if (!empty($nested)) {
                    $tree[$key] = $buildTree($nested);
                }
            }
            
            return $tree;
        };
        
        return $buildTree(array_filter($includes));
    }
    

    This converts comments.user.profile into ['comments' => ['user' => ['profile' => []]]], ready to pass to your eager loading logic:

    public function index(Request $request)
    {
        $relations = $this->parseIncludes($request);
        
        return Post::query()
            ->when($relations, fn($q) => $q->load($relations))
            ->paginate();
    }
    

    The recursive pattern keeps the code under 30 lines while handling any nesting depth. Each iteration splits on the first dot, creating a tree structure where the first level becomes the array key and everything after the dot gets recursively processed.

    This is particularly useful for JSON:API implementations or GraphQL-like query patterns where clients specify exactly which relationships they need. Instead of always eager loading everything or writing manual if/else chains, you can dynamically build the relationship tree from user input.