Category: PHP

  • Extract Cookie Domain from URL — Don’t Hardcode It

    Extract Cookie Domain from URL — Don’t Hardcode It

    Sending cookies to an API? Don’t hardcode the domain. Extract it from the URL instead.

    The Problem

    // ❌ Hardcoded domain — breaks when URL changes
    $cookieJar->setCookie(new SetCookie([
        'Name' => 'session',
        'Value' => $token,
        'Domain' => 'api.example.com',
    ]));

    Hardcoded domains break the moment someone changes the base URL in config, or you switch between staging and production environments.

    The Fix

    // ✅ Extract domain dynamically
    $baseUrl = config('services.api.base_url');
    $domain = parse_url($baseUrl, PHP_URL_HOST);
    
    $cookieJar->setCookie(new SetCookie([
        'Name' => 'session',
        'Value' => $token,
        'Domain' => $domain,
    ]));

    parse_url() with PHP_URL_HOST gives you just the hostname — no protocol, no path, no port. Clean and environment-agnostic.

    Takeaway

    Any time you need a domain, host, or path from a URL — use parse_url(). It handles edge cases (ports, trailing slashes, query strings) that string manipulation misses.

  • UUID v1 for Sessions, UUID v4 for Requests

    UUID v1 for Sessions, UUID v4 for Requests

    Not all UUIDs are created equal. When you need to replicate how a browser or external system generates identifiers, the version matters.

    UUID v1 vs v4

    UUID v4 is random — great for request IDs where uniqueness is all you need:

    use Ramsey\Uuid\Uuid;
    
    // Each request gets a unique random ID
    $requestId = Uuid::uuid4()->toString();
    // e.g., "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"

    UUID v1 is time-based — useful for session IDs where sortability and temporal ordering matter:

    // Session ID that encodes when it was created
    $sessionId = Uuid::uuid1()->toString();
    // e.g., "6ba7b810-9dad-11d1-80b4-00c04fd430c8"

    When to Use Which

    • v4 (random): Request IDs, correlation IDs, idempotency keys — anything where uniqueness matters but order doesn’t
    • v1 (time-based): Session IDs, event IDs, audit logs — anything where you want to sort by creation time or match sequential behavior

    Takeaway

    Match the UUID version to the lifecycle. Random for one-off requests, time-based for persistent sessions. It’s a small detail that makes debugging much easier when you’re tracing requests through logs.

  • Don’t Hardcode Cache TTL — Use What the API Tells You

    Don’t Hardcode Cache TTL — Use What the API Tells You

    Working with an API that returns authentication tokens? Don’t hardcode the cache TTL. The API already tells you when the token expires — use it.

    The Common Mistake

    // ❌ Hardcoded — what if the API changes expiry?
    Cache::put('api_token', $token, 3600);

    Hardcoding means your cache could expire before the token does (wasting API calls) or after it does (causing auth failures).

    The Fix

    // ✅ Dynamic — uses what the API tells you
    $response = Http::post('https://api.example.com/auth', [
        'client_id' => config('services.api.client_id'),
        'client_secret' => config('services.api.client_secret'),
    ]);
    
    $data = $response->json();
    $token = $data['access_token'];
    $expiresIn = $data['expires_in']; // seconds
    
    // Cache with a small buffer (expire 60s early)
    Cache::put('api_token', $token, $expiresIn - 60);

    The expires_in field is there for a reason. Subtract a small buffer (30-60 seconds) to avoid edge cases where your cache and the token expire at the same instant.

    Takeaway

    Let the API dictate your cache duration. It’s one less magic number in your codebase, and it automatically adapts if the provider changes their token lifetime.

  • Regex Lookaheads: Check Multiple Words Without Verbose Permutations

    Regex Lookaheads: Check Multiple Words Without Verbose Permutations

    Need to validate that a string contains multiple words in any order? Don’t write 6 permutations of the same regex. Use positive lookaheads instead.

    The Problem

    You want to check if a string contains “cat” AND “dog” AND “bird” in any order. The naive approach is a mess of permutations — 6 for 3 words, 24 for 4 words. No thanks.

    The Fix: Chained Positive Lookaheads

    $pattern = '/^(?=.*cat)(?=.*dog)(?=.*bird)/';
    $text = 'I saw a bird, then a cat, then a dog';
    
    if (preg_match($pattern, $text)) {
        echo 'All three words found!';
    }

    Each (?=.*word) is a separate assertion. All must pass. Order doesn’t matter. Add a fourth word? Add one more lookahead. Clean and scalable.

    Takeaway

    Positive lookaheads let you check multiple conditions without caring about order. Use (?=.*word) for each required word. Works in PHP, JavaScript, Python — basically everywhere.

  • Stop Swallowing Exceptions in PHP

    Stop Swallowing Exceptions in PHP

    You inherit a codebase. Every API call wrapped in try-catch. Every exception swallowed. Every error returns an empty collection.

    And nobody knows when things break.

    The Anti-Pattern

    Here’s what I found in a legacy integration:

    public function getProducts(): Collection
    {
        try {
            $response = Http::get($this->apiUrl . '/products');
            return collect($response->json('data'));
        } catch (Exception $e) {
            // "Handle" the error by pretending it didn't happen
            return collect([]);
        }
    }

    What happens when the API is down? Nothing. The method returns empty. The caller thinks there are no products. The error disappears into the void.

    Weeks later, you’re debugging why users see blank pages. The real error? A 500 from the API three weeks ago that nobody noticed because the logs were clean.

    Let It Fail

    The fix is brutal: delete the try-catch.

    public function getProducts(): Collection
    {
        $response = Http::get($this->apiUrl . '/products');
        return collect($response->json('data'));
    }

    Now when the API fails, the exception bubbles up. Your error tracking (Sentry, Bugsnag, whatever) catches it. You get alerted. You fix it.

    But What About Graceful Degradation?

    If you genuinely want to fail gracefully, make it explicit:

    public function getProducts(): Collection
    {
        try {
            $response = Http::timeout(5)->get($this->apiUrl . '/products');
            return collect($response->json('data'));
        } catch (RequestException $e) {
            // Log it so you know it happened
            Log::warning('Product API failed', [
                'error' => $e->getMessage(),
                'url' => $this->apiUrl
            ]);
            
            // Return cached/stale data as fallback
            return Cache::get('products.fallback', collect([]));
        }
    }

    Now you’re:

    • Logging the failure (visibility)
    • Using a fallback strategy (resilience)
    • Not silently lying to callers (honesty)

    When to Catch

    Catch exceptions when you have a recovery strategy:

    • Retry logic: API hiccup? Try again.
    • Fallback data: Cache, defaults, partial results.
    • User-facing context: Transform technical errors into user messages.

    If you’re just catching to return empty, you’re hiding problems.

    Laravel’s Global Exception Handler

    Laravel already has a system for this. Exceptions bubble to App\Exceptions\Handler, where you can:

    // app/Exceptions/Handler.php
    public function register()
    {
        $this->reportable(function (RequestException $e) {
            // Log to Sentry/Slack/etc
        });
        
        $this->renderable(function (RequestException $e) {
            return response()->json([
                'error' => 'External service unavailable'
            ], 503);
        });
    }

    Now all API failures follow the same pattern. No more scattered try-catch blocks pretending everything’s fine.

    The Takeaway

    Empty catch blocks are lies. If you can’t handle the error meaningfully, let it bubble. Your future self — the one debugging at 2 AM — will thank you.

  • Before/After API Testing: Compare Bytes, Not Just Objects

    Before/After API Testing: Compare Bytes, Not Just Objects

    When refactoring code that talks to external APIs, how do you know you didn’t break something subtle?

    Compare both the parsed response AND the raw wire format:

    // Test old implementation
    $oldClient = new OldApiClient($config);
    $oldParsed = $oldClient->fetchData($params);
    $oldRaw = $oldClient->getLastRawResponse();
    
    // Test new implementation  
    $newClient = new NewApiClient($config);
    $newParsed = $newClient->fetchData($params);
    $newRaw = $newClient->getLastRawResponse();
    
    // Compare both
    assert($oldParsed == $newParsed);  // Functional behavior
    assert($oldRaw === $newRaw);       // Wire-level compatibility

    Why both? Because:

    • Parsed objects verify functional correctness
    • Raw responses catch encoding issues, header differences, whitespace handling
    • Some APIs are picky about request formatting even if the parsed result looks identical

    This approach caught cases where new code was adding HTTP headers the old code didn’t send, and another where namespace handling differed slightly. Both “worked” but matching exact wire format means safer deployment.

    When refactoring integrations, test the bytes on the wire, not just the objects in memory.

  • Keep DTOs Thin — Move Logic to Services

    Keep DTOs Thin — Move Logic to Services

    When building API integrations, it’s tempting to put helper methods on your DTOs. I learned this creates problems.

    Started with “fat” DTOs:

    class Product {
        public function getUniqueVariants() { ... }
        public function isVariantAvailable($type) { ... }
        public function getDisplayType() { ... }
    }
    

    Seemed convenient — call $product->getUniqueVariants() anywhere. But issues emerged:

    1. Serialization problems: DTOs with methods are harder to cache/serialize
    2. Testing complexity: need to mock entire DTO structures just to test one method
    3. Coupling: business logic tied to the external API’s data structure

    The fix — move all logic to a service class:

    class ApiClient {
        public function getUniqueVariants(Product $product) { ... }
        public function isVariantAvailable(Product $product, string $type) { ... }
    }
    

    Now DTOs are pure data containers — just public properties or readonly classes. Business logic lives in services where it belongs.

    Feels less convenient at first (passing DTOs as arguments), but far more maintainable. DTOs stay simple and serializable. Services become testable in isolation.

  • Auto-Generate DTOs from JSON API Responses

    Auto-Generate DTOs from JSON API Responses

    Working with third-party APIs means writing a lot of DTOs. So I built a quick code generator that turns JSON fixtures into typed PHP classes.

    The workflow:

    1. Capture fixtures: add HTTP middleware to dump API responses to JSON files
    2. Analyze schema: scan fixtures, merge similar structures, infer types
    3. Generate DTOs: output properly typed classes with serializer annotations
    php artisan generate:dtos \
      --output=app/Services/SDK/ThirdParty \
      --namespace="App\\Services\\SDK\\ThirdParty"
    

    The generator handles:

    • Type inference from JSON values (string/int/float/bool/null)
    • Nullability detection: if a field is missing in any fixture, it’s nullable
    • Nested objects: creates separate classes for complex types
    • Array types: uses Str::singular() to name item classes (products → Product)

    This saved hours of manual DTO writing. The key insight: API responses are the source of truth. Let the actual data structure define your types, not the other way around.

  • When Old Code Fixes Are Actually Bugs

    When Old Code Fixes Are Actually Bugs

    Sometimes the best fix is deleting old fixes.

    While consolidating API client code, I found this “normalization” logic in production:

    $normalized = str_replace(
        ['SOAP-ENV:', 'xmlns:SOAP-ENV', 'xmlns:ns1'],
        ['soapenv:', 'xmlns:soapenv', 'xmlns'],
        $request
    );

    It looked intentional. Maybe the API was picky about namespace prefixes? Nope. It was breaking everything.

    Removing the normalization fixed server errors that had been happening silently. The API worked fine with standard SOAP envelopes — the str_replace was stripping namespace declarations and causing “undeclared prefix” errors on the server side.

    This kind of code survives because: (1) it was probably copy-pasted from a StackOverflow answer, (2) maybe it worked once on a different API, (3) nobody questioned it because it was already there.

    When refactoring, actually test the old code path independently. Sometimes those “necessary” workarounds are just ancient bugs in disguise.

  • SoapClient Timeouts Don’t Work the Way You Think

    SoapClient Timeouts Don’t Work the Way You Think

    You’d think adding a timeout to PHP’s SoapClient would be straightforward. Maybe a constructor option like 'timeout' => 30?

    Nope. Welcome to PHP SOAP hell.

    The only way to set a request timeout on native SoapClient is ini_set():

    ini_set('default_socket_timeout', 30);
    $client = new SoapClient($wsdl, $options);

    The connection_timeout constructor option? That’s only for the initial TCP handshake, not the actual SOAP call. It won’t save you from slow API responses.

    But there’s a cleaner approach — extend SoapClient and override __doRequest() to use Guzzle:

    class GuzzleSoapClient extends SoapClient
    {
        private GuzzleClient $guzzle;
    
        public function __construct($wsdl, array $options = [])
        {
            parent::__construct($wsdl, $options);
            $this->guzzle = new GuzzleClient([
                'timeout' => $options['timeout'] ?? 30,
            ]);
        }
    
        public function __doRequest(
            $request, $location, $action, $version, $oneWay = 0
        ): ?string {
            $response = $this->guzzle->post($location, [
                'body' => $request,
                'headers' => ['SOAPAction' => $action],
            ]);
            return (string)$response->getBody();
        }
    }

    Drop-in replacement. Actual timeout support. No ini_set() hacks.