Lesson 24 / 25

Legacy PHP Versus Modern PHP

Recognise outdated practices and modernise PHP code step by step.

Leaving bad habits behind

Much PHP advice online predates modern PHP. Practices to avoid: the removed mysql_* functions and SQL built by string concatenation; md5 or sha1 for passwords; extract() and variable variables; global state and global keywords; mixing HTML, SQL and business logic in one file; suppressing errors with @; relying on loose == comparisons; untyped functions and arrays of arrays everywhere; and include chains instead of autoloading. Modern equivalents: PDO or an ORM with prepared statements, password_hash, typed classes and value objects, dependency injection, templates with auto-escaping, strict_types, enums, readonly classes, Composer autoloading, PSR interfaces, static analysis and automated tests. Modernise legacy code incrementally: add Composer and autoloading, add tests around critical paths (characterisation tests), raise PHPStan levels with a baseline, introduce types, use Rector to automate syntax upgrades, and replace hand-written plumbing with framework components one area at a time, the strangler approach applied inside a codebase.

Legacy code and its modern rewrite

Same behaviour: fetch a user's orders.

<?php
// legacy (do not do this)
// $id = $_GET['id'];
// $res = mysqli_query($conn, "SELECT * FROM orders WHERE user_id = $id");   // SQL injection
// while ($row = mysqli_fetch_assoc($res)) { echo "<li>" . $row['title'] . "</li>"; }   // XSS

// modern
declare(strict_types=1);

final readonly class OrderSummary
{
    public function __construct(public string $id, public string $title, public OrderStatus $status) {}
}

final class OrderQueries
{
    public function __construct(private readonly PDO $pdo) {}

    /** @return list<OrderSummary> */
    public function forUser(int $userId): array
    {
        $stmt = $this->pdo->prepare('SELECT id, title, status FROM orders WHERE user_id = :uid ORDER BY created_at DESC');
        $stmt->execute(['uid' => $userId]);
        return array_map(
            fn (array $r): OrderSummary => new OrderSummary($r['id'], $r['title'], OrderStatus::from($r['status'])),
            $stmt->fetchAll(PDO::FETCH_ASSOC),
        );
    }
}
// template: <li><?= e($order->title) ?></li>   (escaped output)

Upgrade in small, tested steps

Big-bang rewrites of legacy PHP applications often fail. Add tests around a feature, modernise it, ship it, and repeat. Rector and PHPStan make each step mechanical and safe.

Quick check: Which practice belongs to modern PHP?

  • Prepared statements with PDO and strict types
  • Using mysql_* functions
  • Hashing passwords with md5
  • Suppressing errors with @
Answer

Prepared statements with PDO and strict types — Prepared statements and strict types are core modern practices; the others are outdated and unsafe.