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
- Named Arguments
- Match Expression
- Nullsafe Operator (?->)
- Constructor Property Promotion
- Type System Evolution (Union, mixed, static)
- Attributes / Annotations
- New String Functions
- Weak Maps
- Throw Expression and Error Handling
- JIT Compiler and Performance
- Module and Core Changes
- Syntax Tweaks (::class, trailing commas)
- Saner String to Number Comparisons
- Backward Incompatible Changes (Migration Notes)
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'],
) {}
}
finalis 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
- PHP parses attributes at compile time and associates them with the reflection structure.
- At runtime you obtain a
ReflectionClass,ReflectionMethod,ReflectionProperty, etc. - Call
getAttributes()to getReflectionAttributeinstances, thennewInstance()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()returnsCurlHandle. -
GD:
imagecreate()returnsGdImage. -
Sockets:
socket_create()returnsSocket. -
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
matchis now a reserved keyword.mixedis now a reserved word (cannot be used for class/interface/trait names and is prohibited in namespaces).__autoload()has been removed. Usespl_autoload_register()instead.create_function()has been removed. Use anonymous functions / closures instead.each()has been removed. UseforeachorArrayIterator.- 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_errorsini directive has been removed (sophp_errormsgis no longer available; useerror_get_last()instead). - The
@operator no longer silences fatal errors. Error handlers should not rely onerror_reporting() == 0to detect suppression. - The default
error_reportinglevel is nowE_ALL(includesE_NOTICEandE_DEPRECATED). display_startup_errorsis enabled by default.- Many warnings became
Errorexceptions (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 (useisset()orproperty_exists()).- Invalid key types for
array_key_exists()are handled more consistently and may throwTypeError. call_user_func_array()array keys are now interpreted as parameter names (can change behavior if you pass associative arrays unintentionally).debug_backtrace()andException::getTrace()no longer provide references to arguments.
OOP edge cases
- Using
parentinside 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()andgmmktime()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; useexif_read_data(). - Filter:
FILTER_FLAG_SCHEME_REQUIREDandFILTER_FLAG_HOST_REQUIREDwere removed (scheme/host are always required forFILTER_VALIDATE_URL).INPUT_REQUESTandINPUT_SESSIONwere removed fromfilter_input()sources.
- mbstring:
mbstring.func_overloadwas removed (and relatedMB_OVERLOAD_*constants /mb_get_info()entries).mb_parse_str()can no longer be used without specifying a result array.- The
emodifier formb_ereg_replace()was removed; usemb_ereg_replace_callback().
- OpenSSL:
openssl_seal()andopenssl_open()now require themethodargument (the old default"RC4"is considered insecure). - PCRE (Regular Expressions): invalid escape sequences are no longer interpreted as literals; the
Xmodifier is now ignored. - PDO:
- Default error mode changed from silent to exceptions.
- Signatures of some methods changed (notably
PDO::query()andPDOStatement::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(), andReflectionMethod::invoke()switched to variadics (...$args).Reflection*::export()methods were removed (cast reflection objects to string instead).
- SPL:
SplFixedArrayis nowIteratorAggregate(notIterator); several iteration methods were removed in favor ofgetIterator().spl_autoload_register()now always throwsTypeErroron invalid arguments (thedo_throwparameter is effectively ignored).
- Standard library:
assert()no longer evaluates string arguments (useassert($a == $b), notassert('$a == $b'));assert.quiet_eval/ASSERT_QUIET_EVALwere removed.parse_str()can no longer be used without specifying a result array.- The
'salt'option ofpassword_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.