PHP 8.1: Major Features

Upgrade path from PHP 8.0.x: language additions, runtime tightening, and migration checklists for staging.

Table of Contents


Where 8.0 reset the platform, 8.1 fills in the language: Enums and readonly land in userland code, Fibers give libraries a concurrency primitive, and intersection/never round out the type story. Expect friction where the runtime gets stricter—$GLOBALS reassignment, MySQLi defaulting to exceptions, and a longer tail of deprecations that become hard errors in later 8.x releases.

Enums

Enums bring a first-class, type-safe way to represent a closed set of values.

  • Backed enums store a scalar value (string|int) that can be serialized/transported easily.
  • Pure enums are identity-only, great for domain state machines and exhaustive handling.
enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
}

function canEdit(Status $status): bool
{
    return $status !== Status::Archived;
}

$status = Status::from('draft');  // Status::Draft
$value  = $status->value;         // 'draft'

Practical guidance

  • Use enums instead of “stringly-typed” flags ('draft'|'published') in method signatures and DTOs.
  • Prefer backed enums for persistence and APIs; prefer pure enums for internal workflow states.
  • Combine with match for exhaustive handling:
function label(Status $status): string
{
    return match ($status) {
        Status::Draft => 'Draft',
        Status::Published => 'Published',
        Status::Archived => 'Archived',
    };
}

Readonly properties

Readonly properties can be assigned only once, typically in the constructor. This is a major win for immutable DTOs, value objects, and safer domain models.

final class UserProfile
{
    public function __construct(
        public readonly int $id,
        public readonly string $email,
    ) {}
}

What this changes architecturally

  • Encourages “construct fully, then don’t mutate” objects.
  • Reduces the need for private properties + getters for many DTO-like structures.
  • Plays nicely with constructor property promotion for concise immutable types.

First-class callable syntax

PHP 8.1 adds a convenient way to create closures from callables:

function normalize(string $s): string
{
    return strtolower(trim($s));
}

$fn = normalize(...);               // Closure
$out = array_map($fn, [' A ', 'B ']); // ['a', 'b']

This is equivalent to Closure::fromCallable('normalize'), but is shorter and reads better in pipelines.

Fibers

Fibers are a low-level primitive for cooperative concurrency. They do not magically make your code async, but they enable frameworks/libraries to build async runtimes, schedulers, and structured concurrency.

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('paused');
    echo "resumed with: {$value}\n";
});

echo $fiber->start() . "\n"; // paused
$fiber->resume('payload');   // resumed with: payload

When to care

  • If you use an async framework/event loop: Fibers may significantly simplify integration.
  • For most classic request/response apps: Fibers are mostly “infrastructure for libraries”.

Intersection types

Intersection types express “must satisfy all of these interfaces/classes”.

function export(iterable&Countable $items): array
{
    if (count($items) === 0) {
        return [];
    }

    return iterator_to_array($items, preserve_keys: true);
}

Note: Intersection types cannot be combined with union types.

never return type

The never return type indicates that a function does not return: it always throws or exits.

function fail(string $message): never
{
    throw new RuntimeException($message);
}

This improves static analysis and makes control flow explicit.

new in initializers

PHP 8.1 allows new expressions in more places: parameter defaults, static variables, constant initializers, and attribute arguments.

final class Clock
{
    public function now(): DateTimeImmutable
    {
        return new DateTimeImmutable('now');
    }
}

function handler(Clock $clock = new Clock()): DateTimeImmutable
{
    return $clock->now();
}

This removes a lot of “factory boilerplate” in simple cases. (Be mindful of service lifetimes if you’re in a DI container.)

Syntax & language tweaks (0o, array unpacking, named args after unpack)

Octal integer literal prefix: 0o / 0O

$mask = 0o755;

Array unpacking with string keys

$a = [1, 'a' => 'b'];
$b = [...$a, 'c' => 'd']; // [1, 'a' => 'b', 'c' => 'd']

Named argument after argument unpacking

foo(...$args, named: $arg);

full_path for file uploads

$_FILES entries now include a full_path key (intended for webkitdirectory uploads).

Practical recipes

Backed enum from untrusted input (tryFrom)

$status = Status::tryFrom($row['status']) ?? Status::Draft;

Pipelines with first-class callables

$normalize = static fn(string $s): string => strtolower(trim($s));
$mapper = $normalize(...);
$lines = array_map($mapper, $raw);

Backward incompatible changes (migration notes)

This section is intentionally pragmatic: it focuses on issues that commonly break applications during the upgrade.

PHP core / language

  • $GLOBALS write restrictions: writing to the whole $GLOBALS array is no longer supported (e.g. array_pop($GLOBALS) errors). Element access like $GLOBALS['x'] still works.
  • Static variables in inherited methods: inherited (non-overridden) methods now share static variables with the parent method.
  • Optional before required parameters: optional parameters declared before required ones are treated as required; in PHP 8.1 this can now raise ArgumentCountError at call time (even with named arguments).
  • Return type compatibility with internal classes: overriding internal methods without compatible return types triggers deprecations; use #[ReturnTypeWillChange] where needed for cross-version support.
  • New keywords:
    • readonly is now a keyword (still allowed as a function name).
    • never is now a reserved word (cannot be used for class/interface/trait names or in namespaces).

Resource to object migration (more of it)

Several extensions migrated resources to objects (update is_resource() checks to false checks or to object types):

  • FileInfo: uses finfo objects instead of fileinfo resources.
  • FTP: uses FTP\Connection objects.
  • IMAP: uses IMAP\Connection objects.
  • LDAP: uses LDAP\Connection, LDAP\Result, LDAP\ResultEntry objects.
  • PgSQL: uses PgSql\Connection, PgSql\Result, PgSql\Lob objects.
  • PSpell: uses PSpell\Dictionary and PSpell\Config objects.

PDO / MySQLi behavior changes

  • MySQLi: default error reporting mode changed from “silent” to “exceptions”. To restore old behavior: mysqli_report(MYSQLI_REPORT_OFF);
  • PDO: PDO::ATTR_STRINGIFY_FETCHES behavior changed for booleans; SQLite/MySQL drivers now return ints/floats as native PHP types more consistently.

Standard library behavior changes

  • version_compare() no longer accepts undocumented operator abbreviations.
  • HTML escaping functions now default to ENT_QUOTES | ENT_SUBSTITUTE (quotes are escaped; malformed UTF-8 replaced).

Deprecations (what to fix early)

Deprecations in 8.1 are “future errors”. Fixing them early pays off when you later move to PHP 8.2+.

Core deprecations

  • Passing null to non-nullable parameters of built-in functions is deprecated.
  • Implicit lossy float-to-int conversions are deprecated (array keys, int type declarations in coercive mode, int operators).
  • Calling a static element on a trait is deprecated.
  • Autovivification from false is deprecated ($x = false; $x[] = 1;).

Notable extension deprecations

  • Date: strftime() / gmstrftime() and strptime() deprecated (prefer date() / date_parse_from_format() / IntlDateFormatter).
  • Filter: FILTER_SANITIZE_STRING and FILTER_SANITIZE_STRIPPED deprecated.
  • Hash: mhash*() functions deprecated (use hash_*()).
  • PDO: PDO::FETCH_SERIALIZE deprecated.

Extensions: notable changes

This section collects “good to know” changes that are not necessarily breaking but impact behavior and operations.

  • Reflection: ReflectionProperty::setAccessible() / ReflectionMethod::setAccessible() no longer have an effect (always accessible via Reflection).
  • Phar: SHA256 is used by default for signatures; OpenSSL_SHA256/OpenSSL_SHA512 signatures supported.
  • OpenSSL: OpenSSL 3.0 is supported; cipher availability and parameter validation are stricter.
  • CLI: php -a without readline now errors (instead of a misleading non-interactive mode).
  • PHPDBG: remote functionality removed.