What PHP 8.0 Actually Changed for Developers

PHP 8.0 landed in November 2020 and marked the most significant shift in PHP's language design since PHP 7.0 introduced scalar type hints. Beyond performance improvements, this release brought a complete overhaul of the type system, new syntax features that reduce boilerplate, and the long-awaited Just-In-Time compiler. It also removed dozens of deprecated functions and patterns that had accumulated over years of backward compatibility. If you maintain or develop PHP applications, understanding these changes helps you decide when to upgrade and how to prepare your codebase for a smoother transition.

The JIT Compiler in PHP 8.0

The headline feature of PHP 8.0 is the Just-In-Time compiler. Unlike traditional PHP execution, which interprets opcodes line by line, JIT compiles frequently used code paths to native machine instructions at runtime. This eliminates the overhead of repeated interpretation for code that runs in loops or intensive calculations.

For typical web requests serving dynamic pages, the performance improvement is modest. Most applications see around 10-20% faster execution, which may not translate to dramatic user-facing changes. However, for long-running CLI scripts, data processing tasks, or mathematical computations, the JIT compiler can deliver substantial speed gains. Benchmarks on computation-heavy workloads have shown improvements of two to five times in some cases.

JIT configuration lives in your php.ini file. The default settings attempt to detect whether JIT is beneficial for your workload, but you can tune it for specific use cases.

opcache.enable=1
opcache.jit_buffer_size=100M
opcache.jit=tracking

If you are on shared hosting, JIT availability depends entirely on your provider. Many managed WordPress hosts disable JIT by default because it conflicts with certain opcache preloading setups and some third-party PHP extensions. VPS or dedicated server users have full control over these settings.

The JIT compiler does not magically speed up database queries, network requests, or file system operations. Those remain bound by external factors. What JIT accelerates is pure PHP computation, so measure your specific workload before assuming the JIT will solve performance problems elsewhere in your stack.

Named Arguments

Named arguments let you pass parameters to a function by specifying their names rather than relying on positional order. This sounds simple, but it significantly improves code readability when working with functions that have multiple optional parameters.

Consider the htmlspecialchars function, which historically required passing parameters in strict order even when you only wanted to change the last option.

// Before PHP 8.0: must specify all parameters in order
htmlspecialchars($string, ENT_COMPAT | ENT_HTML5, 'UTF-8', false);

// PHP 8.0: specify only the parameter you want to change
htmlspecialchars($string, double_encode: false);

Named arguments also make code more maintainable. If a function signature changes and new parameters are inserted, calls using named arguments continue working correctly as long as the named parameter exists.

// Set a cookie with only the options you need
setcookie(
    name: 'session_id',
    value: 'abc123',
    httponly: true,
    secure: true,
    samesite: 'Strict'
);

Named arguments pair well with configuration arrays, but they offer one advantage arrays lack: the PHP interpreter validates parameter names at compile time, catching typos immediately rather than silently ignoring them.

Attributes in PHP 8.0

Attributes provide a standardised way to attach structured metadata to classes, methods, functions, properties, and parameters. Before PHP 8.0, developers used docblock annotations like @Route("/api") or @NotNull, which were purely strings that frameworks parsed manually. Attributes replace this with proper PHP syntax that the interpreter understands.

use Symfony\Component\Validator\Constraints as Assert;

class User
{
    #[Assert\NotBlank]
    #[Assert\Email]
    public string $email;

    #[Assert\Positive]
    #[Assert\LessThan(120)]
    public int $age;
}

The framework validates these attributes at compile time or runtime, depending on the attribute definition. This means typos in attribute names produce errors rather than silent failures. Most modern PHP frameworks, including Symfony, Laravel, and Doctrine, have adopted attributes extensively since PHP 8.0.

Attributes also enable powerful metaprogramming patterns. You can inspect class definitions at runtime using the Reflection API and read attribute values to build dynamic behaviour without relying on string parsing or naming conventions.

Union Types

PHP 8.0 introduced native union types, allowing a parameter, return type, or class property to accept multiple data types. Previously, you could use nullable types with ?string, but custom unions required docblock annotations or external libraries.

// Accept either int or float, return the same type
function processQuantity(int|float $quantity): int|float
{
    return $quantity * 1.5;
}

// Nullable return: string or null
function getName(?string $name): string|null
{
    return $name ?? 'Anonymous';
}

The mixed pseudo-type serves as a shortcut for a union of all possible types. Using mixed instead of removing type hints entirely preserves type checking while allowing any input.

function processInput(mixed $input): mixed
{
    // Handle any type without losing type information
    return $input;
}

Union types catch more errors during development and make function signatures more expressive. Instead of documentation explaining acceptable types, the code itself declares them explicitly.

Match Expressions

The match expression replaces the traditional switch statement with a more concise alternative that returns a value directly. It also eliminates common switch pitfalls like accidental fallthrough.

// Traditional switch statement
switch ($status) {
    case 'draft':
        $label = 'Draft';
        break;
    case 'published':
        $label = 'Published';
        break;
    default:
        $label = 'Unknown';
}

// Match expression returns the value directly
$label = match ($status) {
    'draft' => 'Draft',
    'published' => 'Published',
    default => 'Unknown',
};

Match expressions support multiple conditions per arm by combining them with commas and use strict comparison by default, matching the behaviour of === rather than the loose comparison of switch.

$result = match ($httpCode) {
    200, 201, 204 => 'Success',
    400 => 'Bad Request',
    401, 403 => 'Authentication Error',
    404 => 'Not Found',
    default => 'Unknown Response',
};

Because match expressions return values, they work well in assignments, function returns, and expressions where switch would require temporary variables.

Constructor Property Promotion

Constructor property promotion reduces boilerplate in classes where the constructor simply assigns its parameters to properties. Instead of declaring properties separately and assigning them, you combine both steps in the parameter list.

// Traditional PHP 7.x approach
class Point {
    public float $x;
    public float $y;

    public function __construct(float $x, float $y) {
        $this->x = $x;
        $this->y = $y;
    }
}

// PHP 8.0 with constructor property promotion
class Point {
    public function __construct(
        public float $x,
        public float $y,
    ) {}
}

Property promotion works with any visibility modifier: public, protected, or private. The promoted parameter creates a typed property with the specified visibility and assigns the argument automatically.

class Config {
    public function __construct(
        public readonly string $apiKey,
        protected string $baseUrl,
        private ?int $timeout = 30,
    ) {}
}

This feature reduces repetitive code in value objects, data transfer objects, and dependency injection containers. Less code means fewer places for bugs to hide and easier maintenance over time.

The Nullsafe Operator

The nullsafe operator ?-> short-circuits method calls and property access when the left side evaluates to null. Instead of throwing an error or returning null unexpectedly, the entire expression returns null immediately.

// Before PHP 8.0: verbose nested null checks
$country = null;
if ($session !== null) {
    $user = $session->getUser();
    if ($user !== null) {
        $address = $user->getAddress();
        if ($address !== null) {
            $country = $address->getCountry();
        }
    }
}

// PHP 8.0: clean chained calls with nullsafe operator
$country = $session?->getUser()?->getAddress()?->getCountry();

The nullsafe operator works with method calls, property access, and array access. It does not work as a replacement for the null coalescing operator ??, which handles missing values differently. Use nullsafe for chained method calls, and keep coalescing for providing fallback values.

// Combine both operators for clean null handling
$city = $user?->getAddress()?->getCity() ?? 'Unknown City';

Breaking Changes in PHP 8.0

PHP 8.0 removed many features that were deprecated during the PHP 7.x cycle. If your application runs on PHP 7.4 without deprecation warnings, it is a good candidate for upgrading. Deprecation warnings indicate code that will break in PHP 8.0, so address those first.

Key removals affecting existing codebases include:

  • The real type cast and is_real() function: Use (float) and is_float() instead.
  • The each() function: Replaced by foreach loops, which have been the recommended approach since PHP 5.
  • create_function(): Anonymous functions using function() {} syntax replaced this years ago.
  • Dynamic static method calls: Calling $class::$method() where $class is a variable string no longer works. Use ($class)::$method() instead.
  • Several mbstring function aliases: The PHP manual lists the preferred function names for each removed alias.

Most of these removals affect legacy code written before PHP 7.0. Applications that follow modern PHP practices are unlikely to use any of these removed features.

How to Upgrade Your Application Safely

Upgrading PHP versions requires careful testing. Start by setting up a development environment running PHP 8.0 and running your full test suite against it. Unexpected errors at this stage reveal compatibility issues before they reach production.

Static analysis tools catch many issues automatically. Installing phpstan or phan with strict rules enabled identifies type mismatches, deprecated API usage, and potential runtime errors that tests might miss.

composer require --dev phpstan/phpstan
./vendor/bin/phpstan analyse src/ --level=max

For WordPress sites or plugins, check compatibility using tools designed for that ecosystem. The WordPress 5.7 PHP 8 compatibility guide covers the specific changes WordPress developers need to address before upgrading.

Update your continuous integration pipeline to test against PHP 8.0 alongside your current version. This catches regressions before code merges reach your main branch.

Many hosting providers offer PHP version switching through their control panels. This lets you test your application under PHP 8.0 on the same server configuration before committing to the upgrade across your entire deployment.

Why the Upgrade Is Worth It

PHP 8.0 delivers value beyond the JIT compiler. Union types and attributes make code more expressive and easier to maintain. Constructor property promotion reduces boilerplate in the classes you write most often. Match expressions and named arguments make code more readable at a glance.

The strict type system catches bugs earlier in development, reducing the time spent debugging mysterious behaviour in production. JIT compilation provides measurable improvements for computation-heavy tasks without any code changes.

For a broader view of the PHP 8 upgrade landscape, the PHP 8 upgrade guide covers the full series including PHP 8.1, 8.2, and 8.3 changes that follow.