NightFox

NightFox

Modernizing Legacy PHP Without a Rewrite

Old PHP doesn't scare me. I started out modifying PHP-Nuke modules, after all. What scares me is the urge to rewrite it all from scratch. A full rewrite is usually the riskiest way to improve an old app. Modernizing it piece by piece keeps what works working while making future changes cheaper.

Inventory before changing

  • Supported PHP and database versions
  • Entry points, cron jobs, webhooks, and background scripts
  • External integrations and undocumented file exchanges
  • Authentication and permission paths
  • Tables written by multiple modules
  • Files containing credentials or environment-specific settings
  • Production-only behavior and manual operator steps

Create a safety net

Start with characterization tests around the important stuff. They don't say the current behavior is right, they just pin down what it does today, so you notice if a refactor changes it by accident. Sometimes the "bug" turns out to be something a customer depends on.

<?php
declare(strict_types=1);

public function testLegacyQuoteCalculationRemainsCompatible(): void
{
    $result = legacy_calculate_quote(
        guestHours: 4.0,
        deadheadHours: 1.5,
        dayType: 'saturday'
    );

    self::assertSame(100500, $result['total_cents']);
}

Introduce seams around old code

Start by wrapping the risky dependencies: direct mail() calls, payment APIs, that global database connection, filesystem access. Put each behind a small interface. Then move one feature at a time into a proper service. Slow and boring wins here.

Replace global state gradually

<?php
// Before
function find_customer($id) {
    global $db;
    return $db->query("SELECT * FROM customers WHERE id = " . (int) $id)->fetch();
}

// After
final class CustomerRepository
{
    public function __construct(private PDO $pdo) {}

    public function find(int $id): ?array
    {
        $stmt = $this->pdo->prepare('SELECT id, name, status FROM customers WHERE id = :id');
        $stmt->execute(['id' => $id]);
        return $stmt->fetch() ?: null;
    }
}

Database modernization

  • Add primary keys and foreign keys where data permits.
  • Convert implicit text dates to real date/time columns.
  • Move money to integer cents or exact decimal types.
  • Add indexes for actual production queries.
  • Replace string-concatenated SQL with prepared statements.
  • Create repeatable migrations before changing schema manually.

A low-risk migration plan

  1. Stabilize the runtime and back up the application and database.
  2. Add logging around important workflows.
  3. Upgrade dependencies and PHP in supported increments.
  4. Introduce Composer autoloading without moving every file at once.
  5. Route one feature through a new controller and service.
  6. Move its SQL into a repository and add tests.
  7. Repeat feature by feature.
  8. Remove old entry points only after traffic and logs prove they are unused.
Why I'm wary of rewrites: an old app is full of requirements nobody wrote down, buried in years of bug fixes and workarounds. That weird if statement exists because something broke once. A rewrite throws all of that away unless you go dig it out first.

Last Modified: October 12, 2026, 11:46 am