PHP 8 removed more functions than any previous major release and deprecated a second group that
will disappear later. The removals are not gradual: a call to each() or
mysql_connect() is a fatal error the moment that code path runs, in production,
under load, on the one URL nobody tested. This is the list of what to look for and what to
write instead.
The short answer
Three function families were removed in PHP 8.0 and now raise a fatal error:
each(), create_function() and the mysql_* procedural
API. Two more were removed earlier and are simply gone: the ereg() family and the
magic quotes helpers. Separately, utf8_encode() and utf8_decode() are
deprecated since PHP 8.2 but still work, so they can be scheduled later.
The scanner reports all of these with the file and the line, because "this function is deprecated" is only half of the information you need. The other half is where it is called from.
Functions removed in PHP 8.0
each() — replaced by foreach
each() returned the current key and value of an array and advanced the internal
pointer. It was removed in PHP 8.0.
// before
while (list($key, $value) = each($items)) {
echo "$key => $value\n";
}
// after
foreach ($items as $key => $value) {
echo "$key => $value\n";
}
The one behaviour to check is that each() moved the array's internal pointer, so a
loop that also read the array with current() or reset() depended on
the side effect. A foreach loop works on a copy and does not touch the pointer.
create_function() — replaced by a closure
create_function() built a function from a string at runtime. It was removed in
PHP 8.0 because it was slow, unreadable and impossible to analyse statically.
// before
$callback = create_function('$a, $b', 'return $a + $b;');
// after
$callback = static fn ($a, $b) => $a + $b;
If the old body used use for captured variables, the arrow function captures them
by value automatically. For a body that needs to modify a captured variable, use a full closure
with use (&$var).
The mysql_* procedural family
mysql_connect(), mysql_query(), mysql_fetch_array() and
the rest of the mysql_* API were removed in PHP 7.0. A codebase that still contains
them predates PHP 7 and has not been touched in a long time, which means the surrounding
database layer usually needs attention too.
The replacement is PDO or MySQLi. PDO is the better default because it gives you prepared statements and a consistent interface across database drivers.
Functions deprecated but still working
These still run in PHP 8.3 and raise a deprecation notice. They will be removed in a future major release, so they belong on the migration list, but they do not break the upgrade on the day.
| Function | Deprecated | Replacement |
|---|---|---|
utf8_encode() | PHP 8.2 | mb_convert_encoding($v, 'UTF-8', 'ISO-8859-1') |
utf8_decode() | PHP 8.2 | mb_convert_encoding($v, 'ISO-8859-1', 'UTF-8') |
strftime() | PHP 8.1 | DateTimeImmutable::format() |
date_sunrise(), date_sunset() | PHP 8.1 | DateTimeImmutable with the sunrise/sunset calculation |
mhash() | PHP 8.1 | hash() |
key_exists() alias | PHP 7.4 | array_key_exists() |
Replacing utf8_encode() and utf8_decode()
This one surprises people, because the name suggests a UTF-8 encoder and it is not one.
utf8_encode() took an ISO-8859-1 string and returned its UTF-8 equivalent.
utf8_decode() went the other way.
// before
$utf8 = utf8_encode($latin1);
$latin1 = utf8_decode($utf8);
// after
$utf8 = mb_convert_encoding($latin1, 'UTF-8', 'ISO-8859-1');
$latin1 = mb_convert_encoding($utf8, 'ISO-8859-1', 'UTF-8');
If the input was already UTF-8 and the old code was used as a no-op fix, the correct migration
is different: validate the string instead of converting it, for example with
mb_check_encoding($value, 'UTF-8'), and decide what to do with the invalid bytes.
Converting a UTF-8 string as if it were Latin-1 corrupts it.
Moving from ereg() to PCRE
The ereg(), eregi(), ereg_replace() and
ereg_replace() functions were removed. Nothing in PCRE depends on them, so this is
a pattern translation rather than a rewrite of the matching logic.
| Old | New | Note |
|---|---|---|
ereg($p, $s) | preg_match($p, $s) | Pattern needs delimiters, for example '/pattern/' |
eregi($p, $s) | preg_match($pi, $s) | Add the i modifier |
ereg_replace($p, $r, $s) | preg_replace($p, $r, $s) | Same |
eregi_replace($p, $r, $s) | preg_replace($pi, $r, $s) | Add the i modifier |
The pattern syntax itself also changed. POSIX classes such as [:alpha:] work in
PCRE too, but backreferences are written \1 instead of \1 with a
different escaping rule, and anchors behave more like Perl than like POSIX. Test the rewritten
pattern against the old one with the same inputs before you trust it.
Replacing the mysql_* family
The mysql_* API is gone. If your code still uses it, the migration is not a
function rename:
// before
$conn = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('shop', $conn);
$res = mysql_query('SELECT id, name FROM products', $conn);
while ($row = mysql_fetch_assoc($res)) {
echo $row['name'];
}
// after
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->query('SELECT id, name FROM products');
while ($row = $stmt->fetch()) {
echo $row['name'];
}
Two things change beyond the API. First, mysql_query() returned
false on failure while PDO throws by default, so the error handling around every
query has to be revisited. Second, mysql_real_escape_string() is replaced by
prepared statements, which is strictly better: the escaping disappears and the injection risk
goes with it.
The migration checklist
- Search for
each(,create_function(,mysql_,ereg,utf8_encode(andutf8_decode(across the whole project, including templates and config files. - Run a compatibility scan to catch the calls that are hidden behind a variable function name, which a plain text search cannot find.
- Replace the removals first: they are fatal errors, so they are the ones that can take down production.
- Replace the deprecations next: they still work, so they can be scheduled without urgency, but they are the PHP 9 work list.
- Re-run the test suite and check that no new deprecation notice appeared. A notice you did not see before is a behaviour change waiting to become a bug.
php2shell reports each of these with the file, the line and the recommended replacement, and the fixer applies the mechanical ones as a reviewable diff.
Frequently asked questions
Which PHP functions were removed in PHP 8.0?
each(), create_function(), the mysql_* procedural family, the ereg() family and the magic quotes helpers were removed in PHP 8.0 or earlier and now raise a fatal error when they are called.
What replaces utf8_encode()?
utf8_encode() only handled ISO-8859-1 input, so the replacement is mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1'). The decode direction is mb_convert_encoding($value, 'ISO-8859-1', 'UTF-8').
Is it safe to run both regex families side by side?
Yes. preg_match(), preg_replace() and preg_split() are the only regex engine you need. The ereg() family was removed, but nothing in PCRE depends on it, so the migration is a pattern translation, not a rewrite.
Where to go next
- How to upgrade PHP 7.4 to 8.3 — the full step-by-step migration plan.
- 12 PHP 8 error messages — what the errors look like in the log and how to fix them.
- PHPCompatibility vs Rector — which tool covers which half of the migration.
- The FAQ — plans, credits, licences and how your code is handled.