Override getAttribute() for Backward-Compatible Schema Migrations

πŸ“– 2 minutes read

Schema migrations are easy to plan and hard to deploy β€” especially when a column rename or consolidation touches dozens of call sites across the codebase. Here’s a trick I’ve used to make incremental migrations way less painful.

The Scenario

Imagine you have a model with numbered columns: price_1, price_2, all the way through price_60. A past design decision that seemed reasonable at the time but now just wastes space. You want to consolidate everything down to price_1.

The data migration is straightforward β€” copy the canonical value into price_1 for every row. But the scary part is all the existing code that reads $record->price_5 or $record->price_23 scattered across the app.

The Trick: Override getAttribute()

Instead of finding and updating every reference before deploying, override Eloquent’s getAttribute() to silently redirect old column access to the new consolidated column:

class PricingRecord extends Model
{
    public function getAttribute($key)
    {
        $consolidatedPrefixes = [
            'price_',
            'cost_',
            'fee_',
        ];

        foreach ($consolidatedPrefixes as $prefix) {
            if (preg_match('/^' . preg_quote($prefix) . '\d+/', $key)
                && $key !== $prefix . '1') {
                // Redirect any tier N access to tier 1
                return parent::getAttribute($prefix . '1');
            }
        }

        return parent::getAttribute($key);
    }
}

The Deployment Sequence

  1. Run the data migration β€” consolidate all values into the _1 columns
  2. Deploy the getAttribute() override β€” old code reading $record->price_5 transparently gets price_1
  3. Clean up call sites at your own pace β€” update references one PR at a time, no rush
  4. Remove the override once all references point to _1
  5. Drop the old columns in a final migration

No big-bang refactor. No merge conflicts from a 40-file PR. No “please don’t deploy anything until my branch lands.”

One Caveat

This adds a regex check on every attribute access for that model. For a model you load a few times per request, it’s negligible. But if you’re hydrating thousands of records in a loop, profile it first. The regex is cheap, but “cheap Γ— 10,000” can add up.

For most real-world use cases though, this buys you a safe, incremental migration path with zero downtime and zero broken features.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *