Author: Daryle De Silva

  • Make Shell Scripts Username-Portable with sed

    You write a shell script that works perfectly on your machine. You share it with the team. It breaks immediately because your username is hardcoded in every path.

    #!/bin/bash
    source /home/jake/.config/app/settings.sh
    cp /home/jake/templates/nginx.conf /etc/nginx/sites-available/

    Classic. The fix isn’t “use variables from the start” (though you should). The fix for right now is a one-liner that makes any script portable after the fact.

    The sed One-Liner

    sed -i "s|/home/jake|/home/$(whoami)|g" setup.sh

    That’s it. Every instance of /home/jake becomes /home/<current_user>. The | delimiter avoids escaping the forward slashes in paths (using / as a delimiter with paths containing / is a nightmare).

    Make It Part of Your Install

    If you distribute scripts that reference paths, add a self-patching step at the top:

    #!/bin/bash
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    CURRENT_USER="$(whoami)"
    
    # Patch all config files for the current user
    for f in "$SCRIPT_DIR"/configs/*.conf; do
        sed -i "s|/home/[a-zA-Z0-9_-]*/|/home/$CURRENT_USER/|g" "$f"
    done

    The regex /home/[a-zA-Z0-9_-]*/ matches any username in a home path, not just one specific name. Way more robust than hardcoding the original username.

    The Better Long-Term Fix

    Obviously, the real solution is to never hardcode paths in the first place:

    #!/bin/bash
    HOME_DIR="${HOME:-/home/$(whoami)}"
    CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME_DIR/.config}"
    
    source "$CONFIG_DIR/app/settings.sh"

    Use $HOME, $USER, and $XDG_CONFIG_HOME from the start. But when you’re retrofitting an existing script or inheriting someone else’s work, sed with a regex pattern gets you portable in seconds.

  • Read Replica Lag Breaks Laravel Queue Jobs Before handle() Runs

    You dispatch a queue job right after saving a model. The job picks it up in milliseconds. And then — ModelNotFoundException.

    The model definitely exists. You just created it. You can query it manually. But the queue worker says otherwise.

    The Culprit: Read Replicas

    If your database uses read replicas (and most production setups do), there’s a lag between the primary and the replicas. Usually milliseconds, sometimes longer under load.

    Laravel’s SerializesModels trait only stores the model’s ID when the job is serialized. When the worker deserializes it, it runs a fresh query — against the read replica. If the replica hasn’t caught up yet, the model doesn’t exist from the worker’s perspective.

    The cruel part: this happens before your handle() method runs. Your retry logic never fires because the job fails during deserialization.

    The Fix: afterCommit()

    Laravel has a built-in solution. Add afterCommit to your job:

    class GenerateInvoice implements ShouldQueue
    {
        use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
        public $afterCommit = true;
    
        public function handle(): void
        {
            // This now runs after the transaction commits
            // and the replica has had time to sync
        }
    }

    Or dispatch with the method:

    GenerateInvoice::dispatch($invoice)->afterCommit();

    Other Options

    If afterCommit isn’t enough (replicas can still lag after commit), you have two more tools:

    // Option 1: Add a small delay
    GenerateInvoice::dispatch($invoice)->delay(now()->addSeconds(5));
    
    // Option 2: Skip missing models instead of failing
    public $deleteWhenMissingModels = true;

    Option 2 is a silent skip — the job just disappears. Use it only when the job is truly optional (like sending a notification for a model that might get deleted).

    The Lesson

    If you’re dispatching queue jobs immediately after writes and seeing phantom ModelNotFoundException errors, check your database topology. Read replicas + SerializesModels + fast workers = a race condition that only shows up under load. afterCommit() is the cleanest fix.

  • PHP 8 Strictness: Don’t Forget to Initialize Your Strings

    PHP 8 is stricter about uninitialized variables. Using the concatenation assignment operator (.=) on a variable that hasn’t been declared will now throw an ErrorException in most modern framework environments.

    
    // Throws ErrorException in PHP 8 if $content is undefined
    foreach ($items as $item) {
        $content .= $item->name;
    }
    
    // Correct
    $content = '';
    foreach ($items as $item) {
        $content .= $item->name;
    }
    

    Always initialize your string variables before entering a loop where you append data. This ensures forward compatibility and makes your code’s intent clearer.

  • Translation Placeholders: Enable Word Order Flexibility for Localizers

    When defining translation strings in Laravel, always use named placeholders (like :count or :percent) instead of positional ones or manual string concatenation.

    
    // Bad
    __('You have ' . $count . ' notifications');
    
    // Good
    __('You have :count notifications', ['count' => $count]);
    

    Different languages have varying word orders; named placeholders allow translators to move variables within the sentence without breaking the logic or requiring code changes for each locale.

  • Frontend/Backend Parity: Treat Your UI Logic as the Specification

    When duplicating complex UI validation or calculation logic on the backend, treat the frontend component as the source of truth. Copy the logic line-by-line into your PHP services, and include comments with the original file and line numbers.

    
    // Replicating logic from ProductForm.vue:142
    if ($data['price'] > 0) {
        // ...
    }
    

    This makes future parity checks easier and ensures your backend handles edge cases exactly as the user experience defines them. It also serves as great documentation for why certain backend checks exist.

  • Stop Passing App::getLocale() to Laravel’s __() Helper

    I’ve seen this pattern in multiple Laravel codebases — a translation helper that manually fetches the locale before passing it to the translation function:

    // Don't do this
    $locale = App::getLocale();
    $label = __('orders.status_label', [], $locale);

    That third parameter is unnecessary. The __() helper already calls App::getLocale() internally when no locale is provided.

    How __() Actually Works

    Under the hood, __() delegates to the Translator’s get() method:

    // Illuminate\Translation\Translator::get()
    public function get($key, array $replace = [], $locale = null, $fallback = true)
    {
        $locale = $locale ?: $this->locale;
        // ...
    }

    When $locale is null (the default), it uses $this->locale — which is the application locale set by App::setLocale(). It’s the same value App::getLocale() returns.

    So the clean version is just:

    // Do this instead
    $label = __('orders.status_label');

    When You DO Need the Locale Parameter

    The third parameter exists for a reason — when you need a translation in a specific locale that differs from the current one:

    // Sending an email in the user's preferred language
    $subject = __('emails.welcome_subject', [], $user->preferred_locale);
    
    // Generating a PDF in a specific language regardless of current request
    $label = __('invoice.total', [], 'ja');

    These are legitimate uses. The anti-pattern is fetching the current locale just to pass it right back.

    The Compound Version

    This gets worse when the manual locale fetch spreads across a method:

    // This entire method is doing unnecessary work
    public function getLabels()
    {
        $locale = App::getLocale();
        
        return [
            'name'    => __('fields.name', [], $locale),
            'email'   => __('fields.email', [], $locale),
            'phone'   => __('fields.phone', [], $locale),
            'address' => __('fields.address', [], $locale),
        ];
    }

    Every single $locale parameter is redundant. This should be:

    public function getLabels()
    {
        return [
            'name'    => __('fields.name'),
            'email'   => __('fields.email'),
            'phone'   => __('fields.phone'),
            'address' => __('fields.address'),
        ];
    }

    Same output, less noise, fewer places to introduce bugs. The framework already handles locale resolution — let it do its job.

  • The Hidden public/hot File in Laravel Mix HMR

    You run npm run hot and Laravel Mix starts a webpack dev server with Hot Module Replacement. Your browser auto-refreshes when you edit Vue components or CSS. Magic. But have you ever wondered how Laravel knows to serve assets from the dev server instead of the compiled files in public/?

    The answer is a tiny file you’ve probably never noticed: public/hot.

    What public/hot Does

    When you run npm run hot, Laravel Mix creates a file at public/hot. It contains the dev server URL — typically http://localhost:8080.

    Laravel’s mix() helper checks for this file on every request:

    // Simplified version of what mix() does internally
    if (file_exists(public_path('hot'))) {
        $devServerUrl = rtrim(file_get_contents(public_path('hot')));
        return $devServerUrl . $path;
    }
    
    // No hot file? Serve from mix-manifest.json as normal
    return $manifest[$path];

    So when public/hot exists, mix('js/app.js') returns http://localhost:8080/js/app.js instead of /js/app.js?id=abc123.

    When This Bites You

    The classic gotcha: you kill the dev server with Ctrl+C, but the public/hot file doesn’t get cleaned up. Now your app is trying to load assets from a server that doesn’t exist.

    # Symptoms: blank page, console full of ERR_CONNECTION_REFUSED
    # Fix:
    rm public/hot

    Add it to your troubleshooting checklist. If assets suddenly stop loading after you were running HMR, check if public/hot is still hanging around.

    Why This Matters for Teams

    The public/hot file should always be in your .gitignore. If someone accidentally commits it, everyone else’s app will try to load assets from localhost:8080 — which won’t be running on their machines.

    # .gitignore
    public/hot

    Most Laravel projects already have this, but if you bootstrapped your project a long time ago or generated your .gitignore manually, double-check.

    It’s a tiny file with a simple job, but understanding it saves you 20 minutes of confused debugging when HMR stops working or your assets vanish after killing the dev server.

  • Code Archaeology: How to Reverse-Engineer a Complex Operation

    Code Archaeology: How to Reverse-Engineer a Complex Operation

    You join a project mid-flight. There’s a complex operation that creates records, updates statuses, sends notifications, and touches three different services. You need to build the reverse of it. Nobody wrote docs.

    Welcome to code archaeology.

    The Approach That Actually Works

    Don’t start by reading the code top-to-bottom. Start by finding the entry point and tracing outward.

    # Find where the operation starts
    grep -rn "createOrder\|placeOrder\|submitOrder" app/ --include="*.php" -l
    
    # Find what events it fires
    grep -rn "event(\|dispatch(" app/Services/OrderService.php
    
    # Find what listeners react
    grep -rn "OrderCreated\|OrderPlaced" app/Listeners/ -l

    Build a map as you go. I literally open a scratch file and write:

    OrderService::create()
      -> validates input
      -> creates DB record
      -> fires OrderCreated event
         -> SendConfirmationEmail (listener)
         -> UpdateInventory (listener)
         -> NotifyWarehouse (listener)
      -> returns response

    Repository Pattern Makes This Harder

    If the codebase uses the repository pattern, the actual logic might be buried two or three layers deep. The controller calls the service, the service calls the repository, the repository has the Eloquent query. Grep is your best friend here.

    # When you can't find where the actual DB write happens
    grep -rn "->save()\|->create(\|->insert(" app/Repositories/ --include="*.php"

    The Undo Operation

    Once you have the map, building the reverse is mechanical. Each step in the forward operation needs a corresponding undo step, executed in reverse order. The hard part was never the coding. It was understanding what the original code actually does.

    Next time you’re staring at a method that calls six other methods across four files, resist the urge to “just figure it out” in your head. Write the map. It takes five minutes and saves five hours.

  • Per-Step Try/Catch: Don’t Let One Bad Record Kill Your Entire Batch

    Per-Step Try/Catch: Don’t Let One Bad Record Kill Your Entire Batch

    Last week I had an Artisan command that processed about 2,000 records. The first version used a transaction wrapper — if any single record failed, the whole batch rolled back. Clean, right?

    Except when record #1,847 hit an edge case, all 1,846 successful records got nuked. That’s not clean. That’s a landmine.

    The Fix: Per-Step Try/Catch

    Instead of wrapping the entire loop in one big try/catch, wrap each iteration individually:

    $records->each(function ($record) {
        try {
            $this->processRecord($record);
            $this->info("✅ Processed #{$record->id}");
        } catch (\Throwable $e) {
            $this->error("❌ Failed #{$record->id}: {$e->getMessage()}");
            Log::error("Batch process failed", [
                'record_id' => $record->id,
                'error' => $e->getMessage(),
            ]);
        }
    });

    Why This Matters

    The all-or-nothing approach feels safer because it’s “atomic.” But for batch operations where each record is independent, it’s actually worse. One bad record shouldn’t hold 1,999 good ones hostage.

    The status symbols (✅/❌) aren’t just cute either. When you’re watching a command chug through thousands of records, that visual feedback tells you instantly if something’s going sideways without reading log files.

    When to Use Which

    Use transactions (all-or-nothing) when records depend on each other. Think: transferring money between accounts, or creating a parent record with its children.

    Use per-step try/catch when each record is independent. Think: sending notification emails, syncing external data, or migrating legacy records.

    The pattern is simple but I’ve seen teams default to transactions for everything. Sometimes the safest thing is to let the failures fail and keep the successes.

  • PR Descriptions: Describe the Final State, Not the Journey

    PR Descriptions: Describe the Final State, Not the Journey

    Stop writing PR descriptions that read like diary entries.

    The Problem

    Most PR descriptions describe the journey. “First I tried X, then I realized Y, then I refactored Z, and finally I settled on W.” That’s useful for a blog post. It’s terrible for a code review.

    The reviewer doesn’t need your autobiography. They need to understand what the code does right now and why.

    What to Write Instead

    A good PR description answers three questions:

    1. What does this change? “Replaces the CSV export with a streaming download that handles 100K+ rows without timing out.”
    2. Why? “Users with large datasets were hitting the 30s gateway timeout.”
    3. Anything non-obvious? “The chunked response means we can’t set Content-Length upfront, so download progress bars won’t show a percentage.”

    That’s it. Three short sections. The reviewer knows exactly what to look for.

    But What About the Investigation?

    That’s what commit history is for. Your commits capture the evolution: “try batch approach,” “switch to streaming,” “fix memory leak in chunk callback.” Anyone who wants the full story can read the log.

    The PR description is the summary. The commits are the chapters. Don’t put the chapters in the summary.

    The Litmus Test

    Read your PR description six months from now. Will you understand the change in 30 seconds? If you have to re-read your own journey narrative to figure out what the code actually does, you wrote the wrong thing.