Blog

  • Use array_reduce to Build Dynamic Union Queries in Laravel

    When you need to union multiple query builders dynamically, array_reduce provides a clean alternative to chaining .union() calls manually. This is especially useful when the number of queries varies or comes from configuration.

    The Problem with Manual Chaining

    When building complex queries that combine multiple query builders with UNION, manual chaining becomes verbose:

    $query1 = Order::where('status', 'pending')->toBase();
    $query2 = Order::where('status', 'processing')->toBase();
    $query3 = Order::where('status', 'completed')->toBase();
    
    $combined = $query1->union($query2)->union($query3);
    

    This gets unwieldy when:
    – The number of queries changes
    – Queries come from configuration
    – You’re building queries conditionally

    Use array_reduce Instead

    array_reduce lets you build the union dynamically:

    $queries = [
        Order::where('status', 'pending'),
        Order::where('status', 'processing'),
        Order::where('status', 'completed'),
    ];
    
    $combined = array_reduce(
        $queries,
        fn($sub, $query) => $sub ? $sub->union($query->toBase()) : $query->toBase()
    );
    

    The closure handles the first iteration (when $sub is null) and subsequent iterations (when $sub contains the accumulated union).

    Combine with fromSub for Complex Queries

    This pattern shines when used with Eloquent’s fromSub():

    $data = Task::query()
        ->fromSub(
            array_reduce(
                [
                    Task::where('priority', 'high'),
                    Task::where('priority', 'urgent'),
                    Task::where('status', 'overdue'),
                ],
                fn($sub, $query) => $sub 
                    ? $sub->union($query->toBase()) 
                    : $query->toBase()
            ),
            'tasks'
        )
        ->with('project', 'assignee')
        ->get();
    

    This gives you a clean subquery with proper eager loading.

    Works with Any Number of Queries

    The real power is flexibility:

    // Configuration-driven queries
    $statusQueries = config('report.statuses')
        ->map(fn($status) => Report::where('status', $status));
    
    $results = Report::withTrashed()
        ->fromSub(
            array_reduce(
                $statusQueries->all(),
                fn($sub, $q) => $sub ? $sub->union($q->toBase()) : $q->toBase()
            ),
            'reports'
        )
        ->orderByDesc('created_at')
        ->get();
    
    // Conditional queries
    $queries = collect([
        $request->filled('active') ? Item::where('is_active', true) : null,
        $request->filled('pending') ? Item::where('status', 'pending') : null,
        $request->filled('archived') ? Item::onlyTrashed() : null,
    ])->filter();
    
    $items = Item::fromSub(
        array_reduce(
            $queries->all(),
            fn($sub, $q) => $sub ? $sub->union($q->toBase()) : $q->toBase()
        ),
        'items'
    )->paginate();
    

    The pattern keeps your code DRY when query sources change or grow.

  • Detect Code Smells During Refactoring: The 4-Parameter Rule

    Detect Code Smells During Refactoring: The 4-Parameter Rule

    When refactoring legacy code, it’s easy to get lost in the weeds. One simple heuristic I use: methods with 4+ parameters are a code smell.

    Why 4+ Parameters is a Red Flag

    Methods with many parameters suffer from:

    • Poor readability: Hard to remember parameter order
    • High coupling: Too many dependencies
    • Testing difficulty: Combinatorial explosion of test cases
    • Maintenance burden: Every new requirement = another parameter

    Example: Before Refactoring

    class InvoiceHandler
    {
        public function processInvoice(
            $invoiceId,
            $customerId, 
            $amount,
            $currency,
            $taxRate,
            $discountCode,
            $paymentMethod
        ) {
            // 7 parameters = code smell!
            // Logic here...
        }
    }

    This is hard to call:

    $handler->processInvoice(
        123,           // invoiceId
        456,           // customerId
        99.99,         // amount
        'USD',         // currency
        0.08,          // taxRate
        'SAVE10',      // discountCode
        'credit_card'  // paymentMethod
    );

    Positional parameters force you to count and remember order. Miss one? Runtime error.

    Solution 1: Introduce a Value Object

    class InvoiceData
    {
        public function __construct(
            public readonly int $invoiceId,
            public readonly int $customerId,
            public readonly float $amount,
            public readonly string $currency,
            public readonly float $taxRate,
            public readonly ?string $discountCode,
            public readonly string $paymentMethod
        ) {}
    }
    
    class InvoiceHandler
    {
        public function processInvoice(InvoiceData $data)
        {
            // Single parameter!
            // Access via $data->amount, $data->currency, etc.
        }
    }

    Now the call site is self-documenting:

    $data = new InvoiceData(
        invoiceId: 123,
        customerId: 456,
        amount: 99.99,
        currency: 'USD',
        taxRate: 0.08,
        discountCode: 'SAVE10',
        paymentMethod: 'credit_card'
    );
    
    $handler->processInvoice($data);

    Solution 2: Builder Pattern (for complex construction)

    class InvoiceBuilder
    {
        private int $invoiceId;
        private int $customerId;
        private float $amount;
        private string $currency = 'USD';
        private float $taxRate = 0.0;
        private ?string $discountCode = null;
        private string $paymentMethod = 'credit_card';
    
        public function forInvoice(int $id): self
        {
            $this->invoiceId = $id;
            return $this;
        }
    
        public function forCustomer(int $id): self
        {
            $this->customerId = $id;
            return $this;
        }
    
        public function withAmount(float $amount, string $currency = 'USD'): self
        {
            $this->amount = $amount;
            $this->currency = $currency;
            return $this;
        }
    
        public function withDiscount(string $code): self
        {
            $this->discountCode = $code;
            return $this;
        }
    
        public function build(): InvoiceData
        {
            return new InvoiceData(
                $this->invoiceId,
                $this->customerId,
                $this->amount,
                $this->currency,
                $this->taxRate,
                $this->discountCode,
                $this->paymentMethod
            );
        }
    }
    
    // Usage
    $data = (new InvoiceBuilder())
        ->forInvoice(123)
        ->forCustomer(456)
        ->withAmount(99.99)
        ->withDiscount('SAVE10')
        ->build();
    
    $handler->processInvoice($data);

    The Refactoring Audit

    During any refactoring session, run this audit:

    1. Find all methods with 4+ parameters
    2. Check if parameters are related (they usually are)
    3. Group related parameters into value objects
    4. Update call sites to use named parameters or builders

    This simple rule catches bloated methods early and guides you toward cleaner abstractions.

    The Rule

    4+ parameters = time to introduce a value object or builder.

    Your future self will thank you.

  • Repository vs Service: What Goes Where in Laravel

    Repository vs Service: What Goes Where in Laravel

    When refactoring Laravel applications, one common anti-pattern I see is services that directly perform database queries. This violates separation of concerns and makes code harder to test and maintain.

    The Problem: Bloated Service Classes

    Consider a ReportService that handles business logic for generating reports. Over time, it accumulates methods like:

    class ReportService
    {
        public function generateReport($projectId, $startDate, $endDate)
        {
            // Business logic here
            $data = $this->fetchReportData($projectId, $startDate, $endDate);
            // More business logic
        }
    
        private function fetchReportData($projectId, $startDate, $endDate)
        {
            // Direct database query
            return DB::table('reports')
                ->where('project_id', $projectId)
                ->whereBetween('created_at', [$startDate, $endDate])
                ->get();
        }
    
        private function getProjectSettings($projectId)
        {
            // Another direct database query
            return DB::table('project_settings')
                ->where('project_id', $projectId)
                ->first();
        }
    }

    This service is doing two jobs: orchestrating business logic AND querying the database.

    The Solution: Move Database Queries to Repositories

    Repositories should handle all database access. Services should orchestrate business logic by calling repositories.

    Create a Repository:

    class ReportRepository
    {
        public function findReportData($projectId, $startDate, $endDate)
        {
            return DB::table('reports')
                ->where('project_id', $projectId)
                ->whereBetween('created_at', [$startDate, $endDate])
                ->get();
        }
    
        public function getProjectSettings($projectId)
        {
            return DB::table('project_settings')
                ->where('project_id', $projectId)
                ->first();
        }
    }

    Clean up the Service:

    class ReportService
    {
        public function __construct(
            private ReportRepository $reportRepo
        ) {}
    
        public function generateReport($projectId, $startDate, $endDate)
        {
            // Pure business logic - no database queries
            $data = $this->reportRepo->findReportData($projectId, $startDate, $endDate);
            $settings = $this->reportRepo->getProjectSettings($projectId);
            
            // Apply business rules, transformations, etc.
            return $this->processReportData($data, $settings);
        }
    }

    The Rule

    Services orchestrate. Repositories query.

    If your service has DB::table() or Eloquent queries, move them to a repository. Your service should read like a business workflow, not a database script.

    Bonus: Testing Becomes Easier

    With this separation, you can mock the repository in tests:

    public function test_generates_report()
    {
        $mockRepo = Mockery::mock(ReportRepository::class);
        $mockRepo->shouldReceive('findReportData')->once()->andReturn(collect([...]));
        
        $service = new ReportService($mockRepo);
        $result = $service->generateReport(1, '2024-01-01', '2024-12-31');
        
        $this->assertInstanceOf(Report::class, $result);
    }

    Clean separation = easier testing + clearer code architecture.

  • Separate Display Names from System Identifiers

    When building systems with both user interfaces and internal logic, explicitly separating display names from system identifiers prevents cascading changes when marketing decides to rename a feature.

    The pattern: use stable identifiers (type, id, slug) for code/database/filenames, and store display names (name, label) separately in configuration:

    // config/features.php
    return [
        'available_reports' => [
            [
                'type' => 'sales_report',               // Stable system identifier
                'name' => 'Quarterly Sales Analysis',   // User-facing display name
                'permission' => 'view_sales_reports',
            ],
            [
                'type' => 'inventory_report',
                'name' => 'Stock Level Summary',
                'permission' => 'view_inventory',
            ],
        ],
    ];

    In your controllers:

    class ReportController
    {
        public function index()
        {
            $reports = collect(config('features.available_reports'))
                ->filter(fn ($report) => auth()->user()->can($report['permission'] ?? ''))
                ->map(fn ($report) => [
                    'id' => $report['type'],      // Internal ID for API/routes
                    'label' => $report['name'],   // Display name for UI
                ]);
                
            return view('reports.index', compact('reports'));
        }
        
        public function generate(string $reportType)
        {
            // Use 'type' for routing, job dispatch, filename generation
            $job = match ($reportType) {
                'sales_report' => new GenerateSalesReport(),
                'inventory_report' => new GenerateInventoryReport(),
                default => throw new InvalidArgumentException(),
            };
            
            dispatch($job);
        }
    }

    Job classes use the stable identifier:

    class GenerateSalesReport implements ShouldQueue
    {
        public function handle()
        {
            $filename = 'sales_report_' . now()->format('Y-m-d') . '.pdf';
            
            Storage::put("exports/{$filename}", $this->generatePdf());
            
            // Filename: 'sales_report_2024-03-05.pdf' — never changes
        }
    }

    Why this matters:

    • Marketing freedom: “Quarterly Sales Analysis” can become “Revenue Insights Dashboard” without touching code
    • Stability: Database queries, API endpoints, and filenames don’t break when display names change
    • A/B testing: Easily test different labels for the same feature
    • Internationalization: Display names can be translated while system identifiers stay English

    What NOT to do:

    // ❌ DON'T couple display names to class constants
    class GenerateSalesReport
    {
        public const DISPLAY_NAME = 'Quarterly Sales Analysis';
        
        public function getFilename()
        {
            return self::DISPLAY_NAME . '_' . now()->format('Y-m-d') . '.pdf';
            // Filename: 'Quarterly Sales Analysis_2024-03-05.pdf' — spaces, changes when label changes
        }
    }
    
    // ❌ DON'T hardcode display names in multiple places
    // Controller
    $reportName = 'Quarterly Sales Analysis';
    
    // Blade view
    

    Quarterly Sales Analysis

    // Email notification Mail::send(..., ['report' => 'Quarterly Sales Analysis']); // Now you have 3+ places to update when marketing changes the name

    The right approach:

    • Store display names in config/*.php or database tables where non-developers can update them
    • Use system identifiers everywhere in code (sales_report, not "Quarterly Sales Analysis")
    • Fetch display names at runtime from the centralized source

    Your codebase becomes more flexible, and non-technical stakeholders can update user-facing labels without opening a pull request.

  • Sanitize Filenames with Laravel’s String Helper Chain

    When generating filenames from dynamic input—user-provided names, database values, or API responses—a three-step Laravel string helper chain ensures clean, filesystem-safe output.

    The pattern combines Str::lower(), Str::slug(), and Str::title() to handle edge cases you might not think of:

    use Illuminate\Support\Str;
    
    class FileGenerator
    {
        public static function sanitizeFilename(string $name): string
        {
            return Str::title(Str::slug(Str::lower($name), '_'));
        }
    }
    
    // Examples
    FileGenerator::sanitizeFilename('Q4 Sales Report');
    // Returns: 'Q4_Sales_Report'
    
    FileGenerator::sanitizeFilename('User Analytics (2024)');
    // Returns: 'User_Analytics_2024'
    
    FileGenerator::sanitizeFilename('Employee List - HR Dept.');
    // Returns: 'Employee_List_Hr_Dept'
    
    FileGenerator::sanitizeFilename('Données françaises');
    // Returns: 'Donnees_Francaises'

    Why this three-step chain works:

    1. Str::lower() normalizes case to avoid filesystem issues on case-sensitive systems (Linux servers are case-sensitive, Windows/Mac are not)
    2. Str::slug() converts to URL-safe format, replacing spaces and special characters with your chosen separator (underscore here)
    3. Str::title() capitalizes words for readable filenames without breaking filesystem compatibility

    Real-world usage with timestamps:

    class ReportExporter
    {
        public function export(string $reportName, array $data): string
        {
            $filename = FileGenerator::sanitizeFilename($reportName) 
                        . '_' . now()->format('Y-m-d_His') 
                        . '.csv';
            
            Storage::put("exports/{$filename}", $this->toCsv($data));
            
            return $filename;
        }
    }
    
    // Output: 'Monthly_Revenue_2024-03-05_093045.csv'

    Why not just use Str::slug() alone?

    Plain Str::slug() would give you 'q4-sales-report' (all lowercase). The Str::title() step makes filenames more readable when users download them. Compare:

    • Without title case: employee_performance_report.csv
    • With title case: Employee_Performance_Report.csv

    The second is clearer at a glance in file explorers.

    Alternative separators:

    You can use hyphens instead of underscores by changing the second parameter to Str::slug():

    return Str::title(Str::slug(Str::lower($name), '-'));

    This is common for web-facing URLs. Underscores are traditional for downloaded files, but both work fine for filesystems.

  • Use Nullable Class Constants for Flexible Fallback Behavior

    When building class hierarchies where child classes may or may not override certain values, nullable class constants offer a clean pattern for fallback behavior.

    By setting a parent constant to null and using PHP’s null coalescence operator (??), you create flexible inheritance without forcing every child class to define the same constant:

    abstract class BaseReportJob
    {
        public const NAME = null;
        
        abstract public function reportType(): string;
        
        protected function getDisplayName(): string
        {
            // Falls back to reportType() if NAME is null
            return static::NAME ?? $this->reportType();
        }
    }
    
    class SalesReportJob extends BaseReportJob
    {
        public const NAME = 'Q4 Sales Report';
        
        public function reportType(): string
        {
            return 'sales_report';
        }
        // getDisplayName() returns 'Q4 Sales Report'
    }
    
    class DataExportJob extends BaseReportJob
    {
        // Inherits NAME = null
        
        public function reportType(): string
        {
            return 'data_export';
        }
        // getDisplayName() returns 'data_export'
    }

    Why this works:

    • Child classes can optionally override NAME for custom display values
    • Classes without a specific name fall back to their reportType()
    • No need for multiple conditionals or checking defined()
    • The static:: keyword ensures late static binding resolves the correct child constant

    This pattern is especially useful for systems where some entities need custom branding while others use generic identifiers. The null coalescence keeps the logic clean and makes the fallback behavior explicit.

    Alternative approaches:

    You could achieve similar behavior with abstract methods, but constants are better when:

    • The value is truly constant (won’t change at runtime)
    • You want to access it statically: SalesReportJob::NAME
    • Child classes don’t need complex logic to determine the value

    For dynamic values that depend on instance state, stick with methods. For static configuration that some children override and others skip, nullable constants are perfect.

  • Use HAVING for Aggregate Filters, Not WHERE

    Use HAVING for Aggregate Filters, Not WHERE

    When filtering on aggregated columns like COUNT or SUM, WHERE won’t work—you need HAVING. The difference tripped me up when I needed to find projects with zero active tasks.

    Here’s the wrong approach that throws a syntax error:

    -- This FAILS with syntax error
    SELECT 
        p.id, 
        p.title,
        COUNT(t.id) as task_count
    FROM projects p
    LEFT JOIN tasks t ON t.project_id = p.id 
    WHERE t.status = 'active'
      AND COUNT(t.id) = 0  -- ERROR: Invalid use of aggregate
    GROUP BY p.id, p.title

    MySQL will complain: “Invalid use of group function.” You can’t filter on aggregates in the WHERE clause because aggregation happens after the WHERE filter is applied.

    The correct approach uses HAVING:

    SELECT 
        p.id, 
        p.title,
        COUNT(t.id) as task_count
    FROM projects p
    LEFT JOIN tasks t ON t.project_id = p.id 
        AND t.status = 'active'
    GROUP BY p.id, p.title
    HAVING task_count = 0

    The key difference: WHERE filters rows before aggregation, HAVING filters groups after. Using WHERE on an aggregate throws a syntax error.

    In Laravel’s query builder, this translates to:

    DB::table('projects')
        ->leftJoin('tasks', function ($join) {
            $join->on('tasks.project_id', '=', 'projects.id')
                 ->where('tasks.status', '=', 'active');
        })
        ->select('projects.id', 'projects.title', DB::raw('COUNT(tasks.id) as task_count'))
        ->groupBy('projects.id', 'projects.title')
        ->havingRaw('task_count = 0')
        ->get();

    Or if you already aliased it in selectRaw, you can use the cleaner having() method:

    ->having('task_count', 0)

    Understanding this distinction prevents hours of debugging cryptic MySQL errors. Remember: filter rows with WHERE, filter aggregates with HAVING.

  • Subqueries in SELECT Clauses Are Performance Killers

    Subqueries in SELECT Clauses Are Performance Killers

    A query that took 40+ seconds to run had this pattern: each row triggered a separate subquery execution—terrible for large tables.

    The problematic query looked like this:

    SELECT 
        p.id,
        p.title,
        (SELECT COUNT(*) 
         FROM tasks t 
         WHERE t.project_id = p.id 
           AND t.status = 'active') as task_count
    FROM projects p
    WHERE p.created_at > '2024-01-01'

    The issue: for every row in the projects table, MySQL executes the subquery individually. With 10,000 projects, that’s 10,000 separate COUNT queries. Ouch.

    The fix is to refactor to a JOIN with aggregation:

    SELECT 
        p.id,
        p.title,
        COUNT(t.id) as task_count
    FROM projects p
    LEFT JOIN tasks t ON t.project_id = p.id 
        AND t.status = 'active'
    WHERE p.created_at > '2024-01-01'
    GROUP BY p.id, p.title

    Same result, but execution time dropped from 40+ seconds to under 200ms. The database can optimize JOINs far better than correlated subqueries.

    How to spot these: Use EXPLAIN to catch correlated subqueries before they hit production. Look for “DEPENDENT SUBQUERY” in the type column—that’s your red flag. Here’s what you’ll see:

    EXPLAIN SELECT ...
    
    +----+--------------------+-------+-------------------+
    | id | select_type        | table | type              |
    +----+--------------------+-------+-------------------+
    |  1 | PRIMARY            | p     | ALL               |
    |  2 | DEPENDENT SUBQUERY | t     | ref               |
    +----+--------------------+-------+-------------------+

    That “DEPENDENT SUBQUERY” line means the inner query depends on the outer query’s values and runs once per row. Refactor it to a JOIN with GROUP BY, and watch your query times drop dramatically.

  • MySQL Reserved Words Will Break Your Queries Silently

    MySQL Reserved Words Will Break Your Queries Silently

    When aliasing tables in SQL, avoid reserved keywords like if, select, where, etc. I recently debugged a query that failed with a cryptic syntax error, only to discover the alias if was the culprit. MySQL’s parser treats it as the IF() function, not your alias.

    Instead of this broken query:

    SELECT r.id, r.name
    FROM reports r
    LEFT JOIN report_data rd ON rd.report_id = r.id

    If you accidentally use a reserved word like if as an alias:

    -- This will fail with syntax error!
    SELECT r.id, r.name
    FROM reports r
    LEFT JOIN report_data if ON if.report_id = r.id

    Use something descriptive instead:

    -- Safe and readable
    SELECT r.id, r.name
    FROM reports r
    LEFT JOIN report_data rd ON rd.report_id = r.id

    The extra characters are worth the clarity and reliability. This is especially tricky because some reserved words work fine as column names but fail as aliases, making the behavior inconsistent. Always consult MySQL’s reserved words list when choosing aliases, or better yet, just use short descriptive abbreviations that are clearly not keywords.

    Pro tip: Modern IDEs will often highlight reserved words differently. Pay attention to that syntax coloring—it can save you debugging time.

  • DRY Your Laravel AI Agents with a Base Agent Class

    If you’re building multiple Laravel AI agents, you’ll notice a lot of repetitive setup code in every agent class:

    // ❌ Every agent repeats the same middleware setup
    class CustomerSupport implements Agent, HasMiddleware
    {
        use Promptable;
    
        public function middleware(): array
        {
            return [
                RateLimitMiddleware::class,
                LoggingMiddleware::class,
                RetryMiddleware::class,
            ];
        }
    }
    
    class ReportGenerator implements Agent, HasMiddleware
    {
        use Promptable;
    
        public function middleware(): array
        {
            return [
                RateLimitMiddleware::class,
                LoggingMiddleware::class,
                RetryMiddleware::class,
            ];
        }
    }

    DRY it up with a base agent:

    // app/Ai/Agents/BaseAgent.php
    abstract class BaseAgent implements Agent, HasMiddleware
    {
        use Promptable;
    
        public function middleware(): array
        {
            return [
                RateLimitMiddleware::class,
                LoggingMiddleware::class,
                RetryMiddleware::class,
            ];
        }
    }

    Now every agent just extends BaseAgent:

    // ✅ Clean and minimal
    class CustomerSupport extends BaseAgent
    {
        // Just your agent-specific logic
    }
    
    class ReportGenerator extends BaseAgent
    {
        // Just your agent-specific logic
    }

    Benefits:

    • Add/remove middleware in one place – affects all agents instantly
    • Onboarding a new agent? One line: extends BaseAgent
    • Override middleware for specific agents if needed (just override the middleware() method)

    Example override for special cases:

    // Most agents use BaseAgent defaults
    class BulkProcessor extends BaseAgent
    {
        // Different rate limit for bulk operations
        public function middleware(): array
        {
            return [
                BulkRateLimitMiddleware::class,
                ...parent::middleware(),
            ];
        }
    }

    Pattern applies beyond agents: Same concept works for controllers (BaseController), jobs (BaseJob), or any class hierarchy where you have shared setup.