Recognizing Legacy PHP: Red Flags in Inherited Codebases

Recognize the telltale signs of PHP 5-era code — the mysql_* extension, magic quotes, register_globals, old-style constructors, and other removed or deprecated patterns — and explain precisely why each one is dangerous or broken on a modern PHP runtime.

Sooner or later, almost every working PHP developer inherits a codebase that is older than they are comfortable with — a client's ten-year-old shopping cart, an internal tool nobody has touched since PHP 5.3, a WordPress plugin last updated during the Obama administration. These codebases don't just look old; many of them are actively insecure or will refuse to run at all on a current PHP version, because PHP's maintainers have, over multiple major releases, deliberately removed entire APIs and behaviors that were once mainstream. This lesson is a field guide: a tour of the specific patterns that mark code as dangerously old, why each one was removed or deprecated, and what it tells you about the rest of the codebase you're about to work in. Treat every item here as a smoke detector — finding one is rarely the only problem; it's usually a sign the whole file (or the whole application) was written to standards nobody would defend today.

The mysql_* Extension: Removed, and for Good Reason

For roughly the first fifteen years of PHP's popularity, the standard way to talk to a MySQL database was the mysql_* family of functions: mysql_connect(), mysql_query(), mysql_fetch_array(), and friends. If you see any of these function names in a codebase, you are looking at code written for PHP 5.4 or earlier at the latest — the extension was formally deprecated in PHP 5.5 (2013), and completely removed in PHP 7.0 (2015). A script that still calls mysql_connect() will not merely run slowly or emit warnings on PHP 7+; it will fatally error with 'Call to undefined function', because the function no longer exists in the engine at all.

The bigger problem with mysql_* was never really that it was old — it's that it made SQL injection the path of least resistance. The extension had no first-class support for prepared statements with bound parameters; the idiomatic way to build a query was to concatenate variables directly into a SQL string (mysql_query("SELECT * FROM users WHERE name = '" . $_GET['name'] . "'")), relying entirely on the developer remembering to manually call mysql_real_escape_string() on every single piece of untrusted input, every single time, with zero enforcement from the API itself. In a large enough codebase, someone always forgets — and unlike mysqli or PDO, where prepared statements make the safe path also the easy path, mysql_* made the dangerous path the default one.

Its replacements — mysqli (the 'improved' MySQL extension, object-oriented or procedural) and PDO (PHP Data Objects, database-agnostic) — both support real prepared statements: the query text and the data are sent to the database server separately, so user input can never be interpreted as SQL syntax no matter what characters it contains. If you're modernizing a codebase and find mysql_* calls, the fix is never a mechanical find-and-replace to mysqli_* with the same string-concatenation logic left in place — that just swaps one SQL-injection-prone API for another. Every mysql_query() call built from concatenated user input needs to be rewritten as a parameterized query.

• mysql_* deprecated in PHP 5.5, fully removed in PHP 7.0 — calling it on PHP 7+ is a fatal 'undefined function' error, not a warning. • mysql_* had no built-in prepared-statement binding, so string concatenation of user input into SQL was the normal, expected usage pattern. • Safety depended entirely on developers remembering mysql_real_escape_string() everywhere, with no enforcement. • Replacements: mysqli and PDO, both with real parameterized queries that separate SQL structure from data. • Migrating mysql_* code means rewriting queries as prepared statements, not just renaming function calls.

Magic Quotes: Automatic Escaping That Made Everything Worse

'Magic quotes' was a php.ini feature — controlled by magic_quotes_gpc (for GET/POST/Cookie data) and related settings — that, when enabled, automatically ran every incoming request value through something like addslashes() before your script ever saw it, escaping quotes and backslashes. The intent was to make SQL injection harder by default for developers who forgot to escape input manually. In practice it was a disaster: it applied uniformly to every request value regardless of where that value was later used (SQL query, HTML output, a file path, a shell command — contexts each needing *different* escaping rules, not one blanket rule), it varied by server configuration so the same code behaved differently across hosting environments, and it trained a generation of developers to just double up on addslashes() calls on top of the already-escaped data, or to strip the magic-quotes slashes back out with stripslashes() and then escape properly themselves — extra, error-prone bookkeeping that existed purely to work around the feature.

Magic quotes was deprecated in PHP 5.3 and removed entirely in PHP 5.4 (2012). This created a nasty trap for old applications: any codebase that had come to depend on magic quotes silently escaping its $_GET/$_POST data for it — instead of using parameterized queries or explicit, context-appropriate escaping — became vulnerable to SQL injection and other injection attacks the moment it was deployed on PHP 5.4+, often with no code change and no announcement, just a silent loss of a safety net nobody had actually understood was load-bearing.

If you find code checking get_magic_quotes_gpc() or calling stripslashes() on superglobal data as a matter of course, that is a strong signal the application was written under the magic-quotes assumption. The correct modern approach has nothing to do with automatic input escaping at all: use parameterized queries (mysqli/PDO prepared statements) for SQL, and context-specific output escaping (htmlspecialchars() for HTML, escapeshellarg() for shell commands, and so on) at the point where data is actually used — never a single blanket escape applied to all incoming data regardless of destination.

• magic_quotes_gpc auto-escaped all GET/POST/Cookie data with addslashes()-style rules before your script ran. • One blanket escaping rule can't be correct for every destination (SQL, HTML, shell) — a core design flaw. • Deprecated in PHP 5.3, removed in PHP 5.4 — code depending on it silently lost its escaping when upgraded. • Look for get_magic_quotes_gpc() checks or reflexive stripslashes() calls as a tell-tale sign. • The correct fix is context-specific escaping/parameterization at point of use, never a global auto-escape.

register_globals: The Infamous Superglobal-Injection Setting

register_globals was a php.ini setting, on by default in very old PHP installs, that automatically created a top-level variable for every entry in $_GET, $_POST, $_COOKIE, $_SERVER, and other request sources. A request to page.php?is_admin=1 would, with register_globals on, cause a plain variable $is_admin to spring into existence in the script with the value "1" — with no $_GET['is_admin'] needed anywhere in the code. This was originally sold as a convenience: it let scripts read $username directly instead of $_POST['username']. It turned into one of the most notorious security failures in PHP's history.

The danger is that any variable a script *forgot* to initialize could be silently overwritten by an attacker simply by adding it to the query string. A classic real pattern: if ($authenticated) { showAdminPanel(); } where $authenticated was supposed to only be set after a successful login check earlier in the script — but if that earlier check was skipped for some code path, or the variable was never initialized to false up front, an attacker could just request ?authenticated=1 and register_globals would create $authenticated = "1" for them, sailing straight past the intended check. This class of bug, called a variable-injection or 'register_globals' vulnerability, was so widespread and so severe that it's a major reason PHP's own maintainers, and every serious security guide of the era, pushed hard for the setting to be disabled.

register_globals was deprecated in PHP 5.3 and removed entirely in PHP 5.4 (2012) — it is not merely off by default in modern PHP, the mechanism doesn't exist anymore at all. Any code that relies on it (reading $username instead of $_POST['username'], with no explicit assignment anywhere in between) will simply see undefined variables on PHP 5.4+, which usually breaks the application outright rather than leaving a silent security hole — arguably the better failure mode, since it forces someone to notice and fix it rather than staying quietly exploitable.

• register_globals auto-created top-level variables from $_GET/$_POST/$_COOKIE/$_SERVER contents. • Any uninitialized variable in the script could be attacker-controlled just by naming it in the query string. • This enabled trivial authentication/authorization bypasses (e.g. ?authenticated=1 overriding a login check). • Deprecated in PHP 5.3, fully removed in PHP 5.4 — the mechanism no longer exists at all in modern PHP. • Code depending on it now fails loudly with undefined variables, rather than staying silently exploitable.

Old-Style Constructors: Method Names Matching the Class

Before PHP 5, and supported for backward compatibility for a long time after, a class's constructor was simply a method with the exact same name as the class itself — a class named User had its constructor logic in a method called function User() { ... }, with no special __construct keyword at all. PHP 5 introduced __construct() as the standard, name-independent way to define a constructor, but kept honoring the old style (a same-named method) for any class that didn't define __construct(), purely so ancient code wouldn't instantly break.

This backward-compatibility shim was finally removed in PHP 8.0. As of PHP 8, a method that merely shares its class's name is no longer treated as a constructor at all — it's just an ordinary method that happens to have a confusing name, and it will not run automatically when you create a new instance with new ClassName(). If an inherited codebase has a class like class Database { function Database($host) { $this->host = $host; } } and no separate __construct(), that code silently stops initializing anything on PHP 8+: new Database('localhost') creates the object but never calls that method, leaving $this->host unset and any code that relied on it broken in ways that often don't fail immediately or obviously — they just produce wrong, uninitialized state.

This is a particularly sneaky migration hazard precisely because it doesn't throw a fatal error — PHP 8 doesn't complain that Database() looks like a constructor and isn't being called; it just quietly treats it as a regular method nobody happens to invoke. Any old-style constructor found during a modernization pass needs to be renamed to __construct() (keeping the same body and parameters) before the class can be trusted to initialize correctly on PHP 8+. Note also that as of PHP 8, defining a method with the same name as its class no longer even emits the old deprecation notice — the feature is gone, not just discouraged, so there's no runtime hint pointing you at the bug; only careful code review or a static analyzer will catch it.

• Old-style constructor: a method named identically to its class, honored as the constructor before PHP 8. • __construct() is the modern, name-independent way to define a constructor, introduced in PHP 5. • PHP 8.0 removed old-style-constructor support entirely — such a method becomes an ordinary, never-auto-called method. • The failure mode is silent: no fatal error, just uninitialized object state wherever the old constructor was relied on. • Fix: rename any old-style constructor method to __construct() before running that class on PHP 8+.

A Broader Field Guide to Removed and Deprecated Patterns

Beyond the headline examples above, a handful of other patterns reliably mark code as old and worth extra scrutiny. each() — a function that stepped through an array's internal pointer one key/value pair at a time, once commonly used to hand-roll array iteration before foreach covered every case cleanly — was deprecated in PHP 7.2 and removed in PHP 8.0; any while (list($k, $v) = each($arr)) loop needs to become a plain foreach ($arr as $k => $v). create_function(), which built a function from strings at runtime (effectively eval() in disguise, with the same code-injection risk if any part of the string came from user input), was deprecated in PHP 7.2 and removed in PHP 8.0; every use should become a real anonymous function or arrow function, which existed as an alternative for the entire time create_function() was being phased out.

PHP 8.2 introduced deprecation warnings for 'dynamic properties' — assigning to a property that was never declared in the class body, like $user->nickname = 'Ada'; on a class with no public $nickname; declared. This was extremely common in older, loosely-typed PHP code (and in code that mimicked JavaScript's or Python's freeform object style), but it defeats IDE autocompletion, static analysis, and typo detection (a mistyped property name silently creates a new, wrong one instead of erroring). As of PHP 8.2 it emits a deprecation notice, and classes that genuinely need dynamic properties must opt in explicitly with the #[AllowDynamicProperties] attribute — everything else should have its properties declared up front.

A few other things worth flagging on sight in an old codebase: raw mysql_real_escape_string() calls (tied to the removed mysql_* extension, see above); heavy use of the @ error-suppression operator to hide warnings instead of fixing their cause; short open tags (<? ... ?> without the php) which depend on a php.ini setting that's often off; ereg/eregi POSIX regex functions, removed in PHP 7.0 in favor of the PCRE preg_* family; and PHP files with no declare(strict_types=1) and no type declarations anywhere, which by itself isn't dangerous but strongly correlates with code that predates PHP's modern type system and hasn't been touched to take advantage of it. None of these single-handedly proves a codebase is unsafe, but finding several together is a reliable signal that the whole file — and often the whole application — needs a careful compatibility and security pass before it's trusted with real user data.

• each() — deprecated PHP 7.2, removed PHP 8.0; replace with foreach. • create_function() — effectively eval() from a string; deprecated PHP 7.2, removed PHP 8.0; use closures/arrow functions. • Dynamic (undeclared) properties — deprecated in PHP 8.2; declare properties explicitly or use #[AllowDynamicProperties]. • ereg/eregi POSIX regex functions — removed in PHP 7.0; use preg_* (PCRE) instead. • Heavy @ error suppression, short open tags, and a total absence of type declarations are all softer 'this code is old' signals worth noticing together.

Why This Matters Even If You Never Touch Old Code

It's tempting to treat this as trivia about a language's past, relevant only if you happen to land a legacy-maintenance job. In practice, recognizing these patterns matters for three concrete reasons even in a greenfield career. First, PHP powers an enormous share of the existing web — a large fraction of all PHP code running today was written years ago and has simply never been touched since, meaning the odds of encountering at least one of these patterns professionally are high, especially in agencies, freelance work, WordPress/Drupal ecosystems, or any company whose PHP application predates its current engineering team. Second, these patterns are excellent teaching tools for understanding *why* modern PHP's replacements are designed the way they are — prepared statements, __construct(), explicit property declarations, and PCRE regex all exist specifically because their predecessors caused real, well-documented harm at scale, and understanding the failure mode makes the fix feel motivated rather than arbitrary.

Third, and most practically: knowing these red flags lets you triage an unfamiliar codebase fast. Opening a client's PHP files and immediately spotting mysql_connect() calls, string-concatenated SQL, and a total absence of declare(strict_types=1) tells you, within minutes, that you're dealing with code from roughly the PHP 5.2–5.5 era, that it almost certainly has SQL injection vulnerabilities somewhere, that it will not run unmodified on any currently-supported PHP version, and that a security review needs to happen before any modernization work — all conclusions you can draw before reading a single line of business logic. That fast, confident triage is exactly what the next two lessons in this module build on: once you can recognize legacy code, you need a disciplined process for actually bringing it forward safely.

• A large share of PHP running in production today predates current engineers on the team maintaining it. • These removed features explain, by contrast, why modern PHP's replacements are designed the way they are. • Spotting a cluster of these red flags lets you triage an unfamiliar codebase's age and risk level in minutes. • Recognizing legacy code is the prerequisite skill for the migration process covered in the next lesson.

Code Examples

The legacy version concatenates untrusted input directly into the SQL string, so any value containing SQL syntax (like a quote followed by OR '1'='1) changes the query's meaning. The mysqli version sends the query text and the parameter separately, so the database never interprets the parameter as part of the SQL statement no matter what characters it contains — and mysql_connect() itself would simply fatal-error as an undefined function on PHP 7+ regardless.

Common mistake: Renaming mysql_query() to mysqli_query() with the string-concatenated $name left in place 'fixes' the fatal error but leaves the SQL injection completely intact.

On PHP 8+, Logger() is just a regular method with a confusing name -- new Logger('APP') never calls it, so $prefix is never assigned, and the first attempt to read $this->prefix as a typed, uninitialized property throws an Error. LoggerFixed uses __construct(), which PHP always calls automatically on instantiation regardless of the class name. Expected result: [APP] starting up (only from LoggerFixed; Logger throws a fatal 'must not be accessed before initialization' error on PHP 8+)

<?php
// LEGACY: PHP 5-era, and fatal on PHP 7+ (mysql_connect no longer exists)
$link = mysql_connect('localhost', 'root', '');
mysql_select_db('shop', $link);
$name = $_GET['name']; // untrusted, straight from the query string
$result = mysql_query("SELECT * FROM users WHERE name = '$name'", $link);
// A request like ?name=' OR '1'='1 returns every row in the table.

// MODERN: mysqli with a real prepared statement
$mysqli = new mysqli('localhost', 'root', '', 'shop');
$stmt = $mysqli->prepare('SELECT * FROM users WHERE name = ?');
$stmt->bind_param('s', $_GET['name']);
$stmt->execute();
$result = $stmt->get_result();
<?php
// LEGACY: relies on the pre-PHP-8 old-style-constructor rule
class Logger {
    private string $prefix;

    // Same name as the class -- this WAS the constructor before PHP 8.
    function Logger(string $prefix) {
        $this->prefix = $prefix;
    }

    public function log(string $message): void {
        echo "[{$this->prefix}] {$message}" . PHP_EOL;
    }
}

$logger = new Logger('APP');
$logger->log('starting up'); // PHP 8+: fatal error, $this->prefix never set

// FIXED: rename to __construct()
class LoggerFixed {
    private string $prefix;

    public function __construct(string $prefix) {
        $this->prefix = $prefix;
    }

    public function log(string $message): void {
        echo "[{$this->prefix}] {$message}" . PHP_EOL;
    }
}

Why Agencies Budget Extra Time for 'Just a Small Update'

Web agencies that take over maintenance of an old client site routinely find that a request as simple as 'add a new field to the contact form' turns into a multi-day job, because the moment they open the codebase they find mysql_* calls, string-built SQL, and old-style constructors throughout -- meaning the safe path is to fix the surrounding danger before touching anything, not just bolt a new field onto code that's already one bad request away from a breach.

if (get_magic_quotes_gpc()) { $name = stripslashes($_POST['name']); } else { $name = $_POST['name']; }

$stmt = $pdo->prepare('INSERT INTO users (name) VALUES (?)'); $stmt->execute([$_POST['name']]);

get_magic_quotes_gpc() and the setting it checks were removed in PHP 5.4 -- this branch is dead code on any supported PHP version, and more importantly, escaping input for storage was never the right defense anyway. Parameterized queries make the escaping question moot entirely.

class Product { function Product($name) { $this->name = $name; } } class Product { public function __construct(private string $name) {} }

A method sharing the class's name stopped being recognized as a constructor in PHP 8.0. The fix uses __construct(), and additionally shows PHP 8's constructor property promotion, which declares and assigns $name in one line.

while (list($key, $value) = each($config)) { echo "$key => $value\n"; } foreach ($config as $key => $value) { echo "$key => $value\n"; } each() was deprecated in PHP 7.2 and removed entirely in PHP 8.0. foreach has covered every use case each() supported since PHP 4 and never depended on the array's internal pointer, so it's both the safer and the idiomatic replacement.

Why was the mysql_* extension especially dangerous compared to mysqli or PDO, beyond simply being old? A. It was slower at running queries B. It had no built-in support for prepared statements, making string-concatenated (and therefore injectable) SQL the normal way to use it C. It could only connect to MySQL 4.0 and earlier D. It required a paid license to use in production

B. It had no built-in support for prepared statements, making string-concatenated (and therefore injectable) SQL the normal way to use it — mysql_* offered no first-class parameter binding, so building queries by concatenating variables into SQL strings was the idiomatic usage pattern, making SQL injection the path of least resistance rather than something a developer had to go out of their way to introduce.

What made register_globals a severe security risk? A. It slowed down every request by parsing extra headers B. It automatically created top-level script variables from request data (GET/POST/COOKIE), letting an attacker set any uninitialized variable just by naming it in the URL C. It disabled all error reporting by default D. It forced every variable to be a global, breaking function scope entirely

B. It automatically created top-level script variables from request data (GET/POST/COOKIE), letting an attacker set any uninitialized variable just by naming it in the URL — With register_globals on, a request like ?authenticated=1 could create a top-level $authenticated variable with no corresponding line of code anywhere in the script, which could bypass authentication or authorization checks that assumed the variable was only ever set after a real login.

On PHP 8, what happens to a class method that has the exact same name as its class but no __construct() method defined? A. PHP still treats it as the constructor for backward compatibility B. PHP emits a deprecation warning but still calls it as the constructor C. It is treated as an ordinary method and is never automatically invoked when the object is created D. PHP refuses to parse the file and throws a syntax error

C. It is treated as an ordinary method and is never automatically invoked when the object is created — PHP 8.0 removed old-style-constructor support entirely. Such a method is now just a regular, oddly-named method that must be called explicitly -- it will not run automatically on 'new ClassName()', which often leaves object state silently uninitialized.

Which of these was deprecated in PHP 8.2 specifically because it undermines IDE autocompletion, static analysis, and typo detection? A. Using foreach to iterate an array B. Assigning to an undeclared ('dynamic') property on an object C. Declaring a class with a constructor D. Using declare(strict_types=1)

B. Assigning to an undeclared ('dynamic') property on an object — PHP 8.2 began emitting deprecation notices for dynamic properties -- assigning to a property never declared in the class body -- because it makes typos silently create new wrong properties instead of erroring, and defeats tooling that relies on knowing a class's declared shape.