Category: Laravel

  • 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.

  • Temporarily Override Vendor Classes to Fix Beta Feature Bugs

    When working with beta features in Laravel, you might encounter bugs in framework code that block your progress. Instead of waiting for an official fix, you can temporarily override the problematic vendor class while maintaining upgrade compatibility.

    Copy the vendor class to your app directory with the same structure, extend the original class, and override only the specific method causing issues. For example, if you’re experiencing duplicate entries with a new resource feature:

    // app/Http/Resources/CustomResource.php
    namespace App\Http\Resources;
    
    use Illuminate\Http\Resources\VendorResource as BaseResource;
    
    class CustomResource extends BaseResource
    {
        protected function resolveItems($collection)
        {
            // Your fixed implementation
            $uniqueKey = fn($item) => $item->id . ':' . $item->type;
            return collect($collection)->unique($uniqueKey)->values();
        }
    }
    

    Then update your resources to extend your custom class instead of the vendor class:

    // app/Http/Resources/ProductResource.php
    namespace App\Http\Resources;
    
    class ProductResource extends CustomResource
    {
        public function toArray($request)
        {
            return [
                'id' => $this->id,
                'name' => $this->name,
                'price' => $this->price,
            ];
        }
    }
    

    This approach lets you fix blocking bugs immediately while keeping the door open for removing your override once the official fix lands. When Laravel releases a patch, you can simply delete your custom class and revert your resources to extend the framework class directly.

    The key is to extend the original class rather than copying its entire implementation. This way, you only override the broken method while inheriting all other functionality, making it easy to track what you’ve changed and why.

  • Feature Flag Implementation Pitfall: When Flags Always Return True

    Feature flags are supposed to give you control. Enable a feature for 10% of users, test in production safely, roll back instantly if something breaks.

    But what happens when your feature flag always returns the same value – no matter what’s in the database?

    The Bug

    I was debugging why a new feature was enabled for all records, even though we’d only flagged it for a handful. The model had a clean feature flag method:

    class ProductType extends Model
    {
        public function hasAdvancedPricing(): bool
        {
            // return $this->advanced_pricing_enabled;
            return true; // TODO: Enable flag check after testing
        }
    }

    See the problem?

    The actual database check was commented out. The method just returned true for everything. The TODO comment suggests this was temporary during development – but it made it to production.

    The Impact

    Everywhere the app checked if ($productType->hasAdvancedPricing()), it got true. The entire feature flag system was bypassed.

    • Records that should use the old pricing logic were using the new one
    • The database field advanced_pricing_enabled was ignored
    • Gradual rollout wasn’t possible – it was all-or-nothing

    The Fix

    class ProductType extends Model
    {
        public function hasAdvancedPricing(): bool
        {
            return (bool) $this->advanced_pricing_enabled;
        }
    }

    Now the flag actually checks the database. Revolutionary.

    How This Happens

    Development shortcut becomes permanent. Someone comments out the real check to force-enable a feature during testing. They forget to uncomment it before merging.

    No test coverage on flags. If you’re not testing hasAdvancedPricing() with both true and false database values, you won’t catch this.

    Silent failures. The app doesn’t crash. It just behaves incorrectly. Feature flags fail open instead of closed.

    Better Pattern: Explicit Database Check

    Make the database read explicit and obvious:

    class ProductType extends Model
    {
        protected $casts = [
            'advanced_pricing_enabled' => 'boolean',
        ];
    
        public function hasAdvancedPricing(): bool
        {
            // Explicit attribute access - harder to comment out by accident
            return $this->getAttribute('advanced_pricing_enabled');
        }
    }

    Or use an enum if your flag has multiple states:

    enum PricingMode: string
    {
        case LEGACY = 'legacy';
        case ADVANCED = 'advanced';
    }
    
    class ProductType extends Model
    {
        protected $casts = [
            'pricing_mode' => PricingMode::class,
        ];
    
        public function hasAdvancedPricing(): bool
        {
            return $this->pricing_mode === PricingMode::ADVANCED;
        }
    }

    Now it’s impossible to return a hardcoded value without explicitly ignoring the enum.

    Test Your Flags

    Write tests that verify both states:

    /** @test */
    public function feature_flag_respects_database_value()
    {
        $enabled = ProductType::factory()->create(['advanced_pricing_enabled' => true]);
        $disabled = ProductType::factory()->create(['advanced_pricing_enabled' => false]);
    
        $this->assertTrue($enabled->hasAdvancedPricing());
        $this->assertFalse($disabled->hasAdvancedPricing());
    }

    If this test fails, your flag is broken.

    The Lesson

    Feature flags are only useful if they actually read the database. Always check that your flag methods aren’t returning hardcoded values. And test both states – enabled and disabled – to catch this bug before it ships.

    Otherwise, you’re not feature-flagging. You’re just writing complicated if (true) statements.