PHP 8.0: Major Features

From PHP 7.4: benchmark-driven notes on JIT, types, and the breaking changes that surface in real apps.

Table of Contents


PHP 8.0 is the first 8.x line: it ships JIT in core, expands the type system (unions, mixed, static), and adds everyday syntax wins—named arguments, match, the nullsafe operator, and attributes—alongside stricter errors and fewer “silent” failures. This page focuses on what changes in real codebases (frameworks, legacy apps, extensions), not just headline features.

Named Arguments

Passing arguments to a function by name eliminates the need to remember their order and allows skipping optional parameters. This makes the code self-documenting.

// Before: you had to specify all parameters in order
setcookie('test', '', time() + 60 * 60 * 2, '/', '', false, true);

// PHP 8.0: specify only what is required
setcookie(
    name: 'test',
    expires: time() + 60 * 60 * 2,
    httponly: true
);

Match Expression

A stricter and more concise alternative to the switch statement. It returns a value and uses strict comparison (===), eliminating unexpected bugs with type coercion.


$statusCode = 200;

$statusMessage = match ($statusCode) {
    200, 300 => 'Success or Redirect',
    400, 404 => 'Client Error',
    500 => 'Server Error',
    default => 'Unknown status',
};

Nullsafe Operator (?->)

Allows reading properties and calling methods in a chain. If one of the elements is null, the entire chain returns null without throwing a fatal error.


// PHP 7.4
$country = null;
if ($session !== null) {
    $user = $session->user;
    if ($user !== null) {
        $country = $user->getAddress()->country;
    }
}

// PHP 8.0
$country = $session?->user?->getAddress()?->country;

Constructor Property Promotion

Previously, creating simple DTOs (Data Transfer Objects) or Value Objects required writing a lot of boilerplate code: declaring properties, passing them to the constructor, and assigning them. PHP 8.0 combines all three steps into one.

// PHP 7.4: Classic (and verbose) approach
class UserDTO 
{
    public string $name;
    public string $email;
    protected int $age;

    public function __construct(string $name, string $email, int $age) 
    {
        $this->name = $name;
        $this->email = $email;
        $this->age = $age;
    }
}

// PHP 8.0: Elegant and concise
class UserDTO 
{
    public function __construct(
        public string $name,
        public string $email,
        protected int $age,
    ) {}
}

Type System Evolution

In PHP 8.0, the type system has become significantly stricter and more expressive.

Union Types

Before PHP 8.0, if a variable could accept multiple data types (e.g., int or float), we relied on PHPDoc annotations. Now, PHP supports this natively.


class Calculator 
{
    private int|float $number;

    public function setNumber(int|float $number): void 
    {
        $this->number = $number;
    }
}

The mixed Pseudo-type

The new mixed type solves legacy code issues. It is equivalent to array|bool|callable|int|float|null|object|resource|string. Note: mixed already includes null, so writing ?mixed or mixed|null is not allowed and will trigger a fatal error.

The static Return Type

To implement patterns like "Late Static Binding" and "Fluent Interfaces", the static return type has been added (previously, only self was available).


class BaseFactory {
    public function create(): static {
        return new static();
    }
}

The Stringable Interface

If a class implements the __toString() magic method, PHP 8.0 automatically (implicitly) assigns it the Stringable interface. This allows using string|Stringable in type hints.

Attributes / Annotations (Deep Dive)

Attributes (introduced in PHP 8.0) are structured metadata you attach to classes, methods, properties, parameters, and constants. The engine stores them in bytecode; you read them with Reflection. They are not magic: nothing happens unless your framework, router, or tool inspects them—same idea as Java/C# annotations, but native to PHP.

Attributes vs PHPDoc

| | PHPDoc (@route, @deprecated) | Attributes (#[Route]) | |---|----------------------------------|-------------------------| | Parsing | String in comments; needs custom parsers | First-class syntax; no regex over comments | | Typing | Informal; easy to drift from code | Constructor args are real PHP values | | Tooling | IDE support varies | Reflection API is stable and fast |

Use PHPDoc for human documentation; use attributes for behavior your code will actually read (routing, validation hints, serialization, codegen).

Declaring an attribute class

Any class can be used as an attribute if it is marked with the built-in #[\Attribute]. The optional bitmask limits where it may appear (Attribute::TARGET_*). Omit targets to default to all targets (usually you want to be explicit).

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final class Route
{
    public function __construct(
        public string $path,
        public array $methods = ['GET'],
    ) {}
}
  • final is common so the attribute class is not subclassed accidentally.
  • Constructor parameters become the arguments passed at the use site (#[Route('/x', methods: [...])]).

You can also mark an attribute as repeatable on the same element (e.g. multiple validators) with Attribute::IS_REPEATABLE in the bitmask—see the Attribute class in the manual.

Applying attributes (syntax)

Attributes sit before the declaration, one or more per line or grouped:

#[Route('/api/users', methods: ['GET', 'POST'])]
final class UserController {}

#[Route('/items')]
final class ItemController
{
    #[Route('/items/{id}', methods: ['GET'])]
    public function show(int $id): array
    {
        return ['id' => $id];
    }
}

Named arguments (PHP 8.0) read naturally: methods: ['GET'] maps to the constructor parameter.

How it works at runtime

  1. PHP parses attributes at compile time and associates them with the reflection structure.
  2. At runtime you obtain a ReflectionClass, ReflectionMethod, ReflectionProperty, etc.
  3. Call getAttributes() to get ReflectionAttribute instances, then newInstance() to instantiate your attribute class with the arguments from the source code.

Nothing runs “by itself”: your bootstrap, DI container, or router must call Reflection (or a library that wraps it).

Reading attributes with Reflection

$reflection = new ReflectionClass(UserController::class);

// Only attributes of a given class (recommended)
foreach ($reflection->getAttributes(Route::class) as $attr) {
    /** @var Route $route */
    $route = $attr->newInstance();
    // $route->path, $route->methods
}

// All attributes on the class (filter yourself)
foreach ($reflection->getAttributes() as $attr) {
    $name = $attr->getName();       // e.g. Route::class
    $args = $attr->getArguments(); // raw constructor arguments
}

Use ReflectionMethod::getAttributes() for method-level routes or middleware, ReflectionParameter::getAttributes() for parameter validation attributes, etc.

End-to-end example (minimal “router” idea)

#[Route('/api/users', methods: ['GET', 'POST'])]
class UserController {}

$rc = new ReflectionClass(UserController::class);
$routeAttrs = $rc->getAttributes(Route::class);
if ($routeAttrs === []) {
    throw new RuntimeException('No Route on ' . UserController::class);
}
$route = $routeAttrs[0]->newInstance();
// Register $route->path in your dispatcher...

Where attributes shine (practical uses)

  • HTTP routing / middleware — map paths and verbs to controllers (Laravel/Symfony-style metadata).
  • Validation & serialization — mark properties with rules for a hydrator or serializer.
  • Dependency injection — mark constructors or parameters for autowiring hints (often combined with other attributes).
  • Tests & tooling — group fixtures, tag code for static analysis, or carry library-specific deprecation markers (your own attribute classes that CI reads).

Avoid stuffing business rules into attributes alone: keep them as declarative metadata; keep complex logic in ordinary PHP classes.

Saner String to Number Comparisons

This is one of the trickiest backward-incompatible changes. Previously, when performing a loose comparison between a number and a string, PHP converted the string to a number. In PHP 8.0, if the string is not numeric, numbers are compared as strings.


// PHP 7.4
0 == 'foobar' // true [cite: 224]

// PHP 8.0
0 == 'foobar' // false [cite: 226]

Syntax Tweaks

Allowing ::class on objects

You can now get the class name directly from an object variable using ::class. Previously, the get_class() function was used for this.


$object = new \App\Models\User();
// PHP 7: get_class($object);
echo $object::class; // Outputs "App\Models\User"

Trailing commas

It is allowed to leave a trailing comma in parameter lists of functions, methods, and closures. This makes Git commits cleaner.


public function makeRequest(
    string $url,
    array $data,
    array $headers, // <-- trailing comma is now legal [cite: 286]
) { ... }

New String Functions

Instead of strpos() and checking for !== false, three new functions have been added that return a strict bool:


$str = "DevSense is awesome";

str_starts_with($str, "DevSense"); // true
str_contains($str, "awesome");     // true
str_ends_with($str, "awesome");    // true



Practical recipe: prefix/suffix guards without strpos

if (!str_starts_with($path, '/var/www/app/')) {
    throw new InvalidArgumentException('Path escapes allowed root');
}
if (str_ends_with($filename, '.php')) {
    // handle script uploads, etc.
}

The get_debug_type() function

The new get_debug_type() function returns a useful type of a variable (e.g., App\Models\User instead of just object, or int instead of integer). It is perfect for compiling clear error messages.

Weak Maps

An architectural breakthrough for ORMs and caching. WeakMap allows creating links to objects in a way that doesn't prevent the Garbage Collector from removing those objects from memory.


class Cache {
    private WeakMap $cache;
    
    public function __construct() {
        $this->cache = new WeakMap();
    }
    
    public function getMetadata(object $obj): array {
        if (!isset($this->cache[$obj])) {
            $this->cache[$obj] = $this->computeExpensiveData($obj);
        }
        return $this->cache[$obj];
    }
}

As soon as the $obj object is destroyed in the application, it automatically disappears from the WeakMap as well, freeing up memory.

Throw Expression and Error Handling

The throw operator has changed from a statement to an expression.


// You can now throw exceptions directly in ternary operators or with null coalescing
$user = $request->get('user') ?? throw new InvalidArgumentException('User is required');

$callable = fn() => throw new Exception('This should not run');

Global strictness change: In PHP 8.0, the @ error suppression operator no longer silences fatal errors. Additionally, most Engine Warnings have been converted to Error exceptions (for example, attempting to access a property of a non-object now crashes the script instead of just logging a Warning).

Non-capturing catches

If you need to catch an exception, but you don't need the exception object itself, the variable can now be omitted.


// Before: we were required to declare the $e variable
try {
    // code
} catch (Exception $e) {
    Log::error('Something went wrong');
}

// PHP 8.0:
try {
    // code
} catch (Exception) {
    Log::error('Something went wrong');
}

Type Errors for Internal Functions

Most internal PHP functions now throw strict TypeError or ValueError exceptions when passed invalid parameters, instead of emitting a Warning and returning null.


// PHP 7.4
strlen([]); // Warning: strlen() expects parameter 1 to be string, array given [cite: 231]

// PHP 8.0
strlen([]); // TypeError: strlen(): Argument #1 ($str) must be of type string, array given [cite: 237]
array_chunk([], -1); // ValueError: array_chunk(): Argument #2 ($length) must be greater than 0

JIT Compiler and Performance

The most fundamental architectural change under the hood of PHP 8.0 is the introduction of the JIT (Just-In-Time) compiler.

OPcache (Before PHP 8): The source code was parsed into OpCodes and the Zend virtual machine executed them line by line.

JIT (PHP 8.0+): Analyzes OpCodes and compiles "hot paths" directly into machine code for the CPU (x86/ARM).

For typical I/O-bound web applications, the performance gain is around 1-5%. But for computations (CPU Bound) — it is from 300% to 500%.

Module and Resource Changes

The cleanup of the core from opaque resource types continues. They are being replaced by objects:

cURL: curl_init() returns a CurlHandle class

GD: imagecreate() returns GdImage.

Sockets: socket_create() returns a Socket.

Memory cleanup: New objects are automatically destroyed by the garbage collector. Functions like curl_close() no longer carry any meaning.

Other extension changes:

JSON is now permanently built into the core (it cannot be disabled with the --disable-json flag).

XML-RPC has been moved from the core to PECL.

Added the mathematical function fdiv(), which allows division by zero (returns INF, -INF, or NAN instead of an error).

The cleanup of the core from opaque resource types continues. They are being replaced by objects:

  • cURL: curl_init() returns CurlHandle.

  • GD: imagecreate() returns GdImage.

  • Sockets: socket_create() returns Socket.

  • The OpenSSL, XMLWriter, and XML extensions have also been affected.

Backward Incompatible Changes (Migration Notes)

Even if you don't use new syntax, upgrading to PHP 8.0 can break applications due to stricter runtime behavior and removals. Below is a compact checklist of common gotchas worth explicitly validating in CI and staging.

Language / keywords / removals

  • match is now a reserved keyword.
  • mixed is now a reserved word (cannot be used for class/interface/trait names and is prohibited in namespaces).
  • __autoload() has been removed. Use spl_autoload_register() instead.
  • create_function() has been removed. Use anonymous functions / closures instead.
  • each() has been removed. Use foreach or ArrayIterator.
  • Case-insensitive constant definitions have been removed (define('FOO', 'bar', true) is no longer supported).
  • Methods with the same name as the class are no longer treated as constructors (use __construct()).
  • Calling non-static methods statically is no longer allowed (this also affects checks like is_callable() when using a class name).
  • (real) and (unset) casts have been removed.

Error handling & diagnostics (stricter runtime)

  • Assertion failures now throw by default (review assert.*, e.g. assert.exception).
  • The track_errors ini directive has been removed (so php_errormsg is no longer available; use error_get_last() instead).
  • The @ operator no longer silences fatal errors. Error handlers should not rely on error_reporting() == 0 to detect suppression.
  • The default error_reporting level is now E_ALL (includes E_NOTICE and E_DEPRECATED).
  • display_startup_errors is enabled by default.
  • Many warnings became Error exceptions (for example: writing to a property of a non-object, invalid array key/string offset types, unpacking a non-array/Traversable, accessing undefined unqualified constants).
  • Many notices became warnings (undefined variables/properties/array keys, array-to-string conversion, invalid string offsets, etc.).

Numeric strings & type coercion

  • Non-strict comparisons between numbers and non-numeric strings changed (covered above in “Saner String to Number Comparisons”).
  • “Saner numeric strings”: some operations that previously emitted warnings/notices now emit warnings or throw TypeError (notably arithmetic/bitwise operations on non-numeric strings; behavior is more strict and more consistent).
  • Float-to-string casting is now locale-independent.

Arrays / keys / call conventions / reflection

  • array_key_exists() no longer works with objects (use isset() or property_exists()).
  • Invalid key types for array_key_exists() are handled more consistently and may throw TypeError.
  • call_user_func_array() array keys are now interpreted as parameter names (can change behavior if you pass associative arrays unintentionally).
  • debug_backtrace() and Exception::getTrace() no longer provide references to arguments.

OOP edge cases

  • Using parent inside a class without a parent is now a fatal compile-time error.
  • Magic method signatures are now validated if declared (mismatches can cause failures).

Extensions: notable BC breaks

  • cURL: CURLOPT_POSTFIELDS (and other “array-accepting” options) no longer accepts objects as arrays. Use an explicit (array) cast if you relied on that behavior.
  • Date/Time: mktime() and gmmktime() now require at least one argument.
  • DOM: a number of unimplemented DOM classes and methods were removed (if you used them as placeholders/test stubs, code will fail).
  • Exif: read_exif_data() has been removed; use exif_read_data().
  • Filter:
    • FILTER_FLAG_SCHEME_REQUIRED and FILTER_FLAG_HOST_REQUIRED were removed (scheme/host are always required for FILTER_VALIDATE_URL).
    • INPUT_REQUEST and INPUT_SESSION were removed from filter_input() sources.
  • mbstring:
    • mbstring.func_overload was removed (and related MB_OVERLOAD_* constants / mb_get_info() entries).
    • mb_parse_str() can no longer be used without specifying a result array.
    • The e modifier for mb_ereg_replace() was removed; use mb_ereg_replace_callback().
  • OpenSSL: openssl_seal() and openssl_open() now require the method argument (the old default "RC4" is considered insecure).
  • PCRE (Regular Expressions): invalid escape sequences are no longer interpreted as literals; the X modifier is now ignored.
  • PDO:
    • Default error mode changed from silent to exceptions.
    • Signatures of some methods changed (notably PDO::query() and PDOStatement::setFetchMode()).
  • Phar: phar metadata is no longer automatically unserialized (security hardening; code relying on implicit unserialize side effects must change).
  • Reflection:
    • ReflectionClass::newInstance(), ReflectionFunction::invoke(), and ReflectionMethod::invoke() switched to variadics (...$args).
    • Reflection*::export() methods were removed (cast reflection objects to string instead).
  • SPL:
    • SplFixedArray is now IteratorAggregate (not Iterator); several iteration methods were removed in favor of getIterator().
    • spl_autoload_register() now always throws TypeError on invalid arguments (the do_throw parameter is effectively ignored).
  • Standard library:
    • assert() no longer evaluates string arguments (use assert($a == $b), not assert('$a == $b')); assert.quiet_eval / ASSERT_QUIET_EVAL were removed.
    • parse_str() can no longer be used without specifying a result array.
    • The 'salt' option of password_hash() is no longer supported (it is ignored with a warning).

Summary (Conclusion)

Treat PHP 8.0 as a platform reset: fewer surprises from weak comparisons, clearer failures from internal APIs, and a type system that finally matches how large PHP codebases are written. The payoff is fewer production-only bugs and a smoother path to 8.1+—especially if you clean up deprecations while you still have PHP 7.4 parity tests to compare against.