Category: PHP

  • Pass Dynamic Values as Exception Context, Not in the Message

    Pass Dynamic Values as Exception Context, Not in the Message

    When throwing exceptions, avoid including dynamic values (like database IDs, user IDs, or timestamps) directly in the exception message. Instead, pass them as context data. This allows error monitoring tools like Sentry to properly group similar errors together instead of treating each unique ID as a separate issue.

    Why It Matters

    Error monitoring tools use the exception message as the primary grouping key. If your message includes dynamic values, every occurrence creates a new issue, making it impossible to see patterns and track frequency.

    Bad Approach

    throw new Exception("Order {$orderId} could not be processed");
    // Sentry creates separate issues for:
    // "Order 123 could not be processed"
    // "Order 456 could not be processed"
    // "Order 789 could not be processed"
    

    Good Approach

    throw new InvalidOrderException(
        "Order could not be processed",
        ['order_id' => $orderId]
    );
    // All occurrences group under one issue:
    // "Order could not be processed" (123 events)
    

    For HTTP Client Exceptions

    When working with Guzzle or other HTTP clients, pass context as the second parameter:

    try {
        return $this->httpClient->request($method, $url, $requestBody);
    } catch (RequestException $exception) {
        throw ApiClientException::fromGuzzleException(
            $exception,
            [
                'request_data' => json_encode($data),
                'request_body' => json_encode($body),
                'query_string' => json_encode($query)
            ]
        );
    }
    

    Benefits

    • Proper error grouping and frequency tracking
    • Cleaner error reports
    • Easier to identify recurring vs one-off issues
    • Context data is still logged and searchable

    This applies to any exception in your Laravel application—database exceptions, API errors, validation failures, etc.

  • Defensive Coding for Configuration Arrays in PHP 8+

    Defensive Coding for Payment Configuration Arrays in PHP 8+

    PHP 8 and later versions throw errors when you try to access undefined array keys. This stricter behavior is great for catching bugs, but it can cause production issues when dealing with configuration arrays that might be incomplete—especially when those configurations come from databases or are managed by users.

    The Problem

    Consider a payment gateway configuration class that expects both production and testing credentials:

    class PaymentConfig
    {
        private string $apiKey;
        private string $testApiKey;
        private string $webhookSecret;
        private string $testWebhookSecret;
    
        public function __construct(array $config)
        {
            $this->apiKey = $config['api_key'];
            $this->testApiKey = $config['test']['api_key']; // ⚠️ Undefined key!
            
            $this->webhookSecret = $config['webhook_secret'];
            $this->testWebhookSecret = $config['test']['webhook_secret'];
        }
    }
    

    If the $config['test'] array is missing the api_key field, PHP 8 will throw an ErrorException: Undefined array key "api_key" error in production.

    Why This Happens

    Configuration arrays stored in databases or managed through admin UIs can become incomplete over time:

    • Older records might predate new required fields
    • Users might skip optional fields when setting up integrations
    • Different payment providers might require different credential sets
    • Migration scripts might not populate every nested key

    The Solution

    Use the null coalescing operator (??) to provide safe defaults:

    class PaymentConfig
    {
        private string $apiKey;
        private ?string $testApiKey;
        private string $webhookSecret;
        private ?string $testWebhookSecret;
    
        public function __construct(array $config)
        {
            $testConfig = $config['test'] ?? [];
            
            $this->apiKey = $config['api_key'];
            $this->testApiKey = $testConfig['api_key'] ?? null;
            
            $this->webhookSecret = $config['webhook_secret'];
            $this->testWebhookSecret = $testConfig['webhook_secret'] ?? null;
        }
        
        public function getApiKey(): string
        {
            return $this->isTestMode() && $this->testApiKey 
                ? $this->testApiKey 
                : $this->apiKey;
        }
    }
    

    Alternative: Check Before Accessing

    For more explicit control, use array_key_exists() or isset():

    public function __construct(array $config)
    {
        $this->apiKey = $config['api_key'];
        
        if (isset($config['test']['api_key'])) {
            $this->testApiKey = $config['test']['api_key'];
        } else {
            $this->testApiKey = null;
        }
    }
    

    When to Use This Pattern

    • Configuration loaded from databases or external APIs
    • User-managed settings that might be incomplete
    • Optional integration credentials
    • Migrated data that predates schema changes
    • Multi-tenant applications where features vary per tenant

    Production Safety Checklist

    1. Never assume keys exist in user-provided or database-stored configuration
    2. Use nullable types (?string) for optional config fields
    3. Provide sensible defaults with ?? operator
    4. Fail gracefully when required keys are missing
    5. Log warnings for missing expected keys (helps identify configuration drift)

    The Takeaway

    PHP 8’s stricter error handling is a feature, not a bug—but it requires defensive coding practices when dealing with external data. Always use the null coalescing operator or explicit checks when accessing configuration arrays that might be incomplete. Your production errors will thank you.

  • Defensive Error Message Extraction from API Responses

    When consuming external APIs, error responses come in all shapes and sizes. Some vendors nest errors deeply (error.response.data.error.message), others put them at the root (error.message), and some just throw generic HTTP status text.

    If you’re not defensive about extracting these messages, your users will see unhelpful errors like “undefined” or “Cannot read property ‘message’ of undefined” instead of the actual problem.

    The Pattern: Fallback Chains with Existence Checks

    Build a chain of fallback attempts, each guarded by existence checks:

    // Frontend API call (Vue.js, React, plain JS)
    try {
      const response = await axios.get('/api/products/123')
      this.product = response.data
    } catch (error) {
      const message = (
        error.response?.data?.error?.message ||  // Try nested first
        error.response?.data?.message ||         // Then less nested
        error.message ||                         // Generic error message
        'Failed to load product data'            // Hardcoded fallback
      )
      
      this.showError(message)
    }
    

    The same pattern works server-side in PHP/Laravel:

    use Illuminate\Support\Facades\Http;
    
    try {
        $response = Http::get('https://api.vendor.com/products/123');
        return $response->json();
    } catch (\Exception $e) {
        $message = $e->response['error']['message'] 
            ?? $e->response['message']
            ?? $e->getMessage()
            ?? 'Failed to fetch product from external API';
        
        Log::error('External API error', ['message' => $message]);
        throw new \RuntimeException($message);
    }
    

    Why This Matters

    This pattern:

    • Prevents runtime errors: Each level is optional-chained (?. or ??), so missing properties don’t crash
    • Provides useful fallbacks: Users see the most specific error available, never “undefined”
    • Works across vendors: Different API error formats are handled gracefully
    • Debuggable: Hardcoded fallback tells you “we got an error but couldn’t extract a message”

    Common API Error Formats

    Here are the patterns you’ll encounter:

    // Format 1: Deeply nested (Stripe-style)
    {
      "error": {
        "type": "invalid_request_error",
        "message": "No such customer: cus_xxxxx"
      }
    }
    
    // Format 2: Root-level message (Laravel-style)
    {
      "message": "The given data was invalid.",
      "errors": { ... }
    }
    
    // Format 3: HTTP status text only
    // (no JSON body, just 500 Internal Server Error)
    

    Your fallback chain handles all three:

    1. Try error.response.data.error.message → catches Format 1
    2. Try error.response.data.message → catches Format 2
    3. Try error.message → catches Format 3 (Axios wraps HTTP status text here)
    4. Use hardcoded fallback → catches unexpected formats

    Laravel HTTP Client Version

    Laravel’s HTTP client throws different exceptions, so adjust your chain:

    use Illuminate\Http\Client\RequestException;
    
    try {
        $response = Http::timeout(10)->get('https://api.vendor.com/data');
        return $response->throw()->json();
    } catch (RequestException $e) {
        // Laravel wraps the response
        $message = data_get($e->response, 'error.message')
            ?? data_get($e->response, 'message')
            ?? $e->getMessage()
            ?? 'API request failed';
        
        throw new \RuntimeException($message);
    }
    

    Using data_get() is cleaner than ?? chains for deeply nested arrays.

    When to Use This

    • Any external API integration (payment gateways, shipping APIs, third-party services)
    • Frontend API calls where you display errors to users
    • Background jobs that need to log meaningful error messages
    • Webhook handlers that receive errors from external systems

    Remember: Never assume error responses will match the API docs. Build fallback chains so your users (and logs) always see a meaningful message, even when the API returns something unexpected.

  • Debugging Cannot assign null to property in PHP 8.4 Response Objects

    Debugging “Cannot assign null to property” in PHP 8.4 Response Objects

    Recently ran into a TypeError that PHP 8.4’s strict typing loves to throw: “Cannot assign null to property OrderResponse::$shippingCost of type float”.

    The Setup

    You’re consuming a third-party API. Their response sometimes returns null for optional fields. Your response DTO uses strict typed properties:

    class OrderResponse
    {
        public array $items;
        public float $shippingCost;  // ❌ Problem: not nullable
    }

    When the API returns {"items": [...], "shippingCost": null}, your JSON deserializer (JMS Serializer, Symfony Serializer, etc.) tries to assign null to a float property. PHP 8.4 says no.

    The Fix

    Make it nullable:

    class OrderResponse
    {
        public array $items;
        public ?float $shippingCost;  // ✅ Allows null
    }

    Don’t Forget Usage Checks

    After making a property nullable, audit everywhere you access it:

    // Old code (assumes always has value)
    $total = $response->basePrice + $response->shippingCost;
    
    // Updated (handles null)
    $total = $response->basePrice + ($response->shippingCost ?? 0);

    When NOT to Use Nullable

    If null isn’t semantically valid (like an exchange rate that defaults to 1.0), set a default value in your deserializer config instead of making the property nullable. Nullable should mean “this value is optional and absence is meaningful,” not “the API is inconsistent.”

    Key takeaway: PHP 8.4 strict typing is your friend, but APIs don’t always play nice. Nullable types are the escape hatch – use them intentionally, and always validate assumptions in your code.

  • PHP Concatenation Pitfall: Initialize Before Using .=

    PHP’s concatenation assignment operator (.=) will throw an ‘Undefined variable’ error if you use it on an uninitialized variable. Always initialize string variables before using .= in loops. This is especially common when conditionally building strings within nested loops where the variable might not exist when the condition first matches.

    // ❌ WRONG: Undefined variable error
    foreach ($records as $record) {
        if ($record->type === 'important') {
            foreach ($record->items as $item) {
                $summary .= $item->text;  // Error!
            }
        }
    }
    
    // ✅ CORRECT: Initialize first
    $summary = '';  // Initialize empty string
    
    foreach ($records as $record) {
        if ($record->type === 'important') {
            foreach ($record->items as $item) {
                $summary .= $item->text;  // Works!
            }
        }
    }
  • Closure Variable Binding with use Keyword

    When passing closures to Laravel collection methods like whenNotEmpty(), each(), or filter(), variables from the outer scope aren’t automatically available inside the closure. You must explicitly bind them using the use keyword.

    Wrong:

    $service = app(ReportGenerator::class);
    $items->whenNotEmpty(function () {
        $service->generate(); // Error: Undefined variable $service
    });
    

    Correct:

    $service = app(ReportGenerator::class);
    $items->whenNotEmpty(function () use ($service) {
        $service->generate(); // Works!
    });
    

    Multiple variables:

    $project->tasks->whenNotEmpty(function () use ($taskService, $project) {
        $taskService->processForProject($project);
    });
    

    This is a fundamental PHP closure behavior that catches many developers coming from JavaScript where closures automatically capture outer scope.

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

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