PHP 8 Features Every Developer Should Know
PHP 8 marked a monumental shift in the PHP ecosystem. Released on November 26, 2020, it introduced a wealth of features that dramatically improved developer experience, code quality, and performance. If you're still writing PHP 7 code or haven't explored all that PHP 8 offers, this guide will transform how you write PHP applications.
From the revolutionary JIT compiler to elegant syntax additions like named arguments and the match expression, PHP 8 represents the most significant update to the language in years. Let's explore every feature you need to know.
- JIT Compiler and its performance implications
- Named arguments for cleaner function calls
- Attributes (PHP's annotation system)
- Union types and mixed type
- Match expression vs switch
- Constructor property promotion
- Nullsafe operator
- New string and array functions
JIT Compilation: The Performance Revolution
The Just-In-Time (JIT) compiler is arguably the most significant addition in PHP 8. Unlike the traditional interpretation model where PHP code is compiled to opcodes at runtime, JIT compiles frequently-used code segments into native machine code.
JIT compiles hot code paths to native machine code for dramatic speedups
JIT provides the most benefit for CPU-bound operations like mathematical computations, image processing, and data transformations. For typical web applications (database queries, API calls), the performance gain is minimal since they're I/O-bound.
To enable JIT, add these settings to your php.ini:
opcache.enable=1
opcache.jit_buffer_size=256M
opcache.jit=1255
Named Arguments: Clarity in Function Calls
Named arguments allow you to pass arguments to a function based on the parameter name rather than position. This dramatically improves code readability, especially for functions with many optional parameters.
❌ PHP 7 (Positional Only)
// What do these mean?
htmlspecialchars(
$string,
ENT_COMPAT | ENT_HTML5,
'UTF-8',
false
);
// Skipping to a later parameter
setcookie(
'name',
'value',
0, // expires - not used
'', // path - not used
'', // domain - not used
false, // secure - not used
true // httponly - the one we want!
);
✓ PHP 8 (Named Arguments)
// Crystal clear intent
htmlspecialchars(
string: $string,
flags: ENT_COMPAT | ENT_HTML5,
encoding: 'UTF-8',
double_encode: false
);
// Skip right to what matters
setcookie(
name: 'name',
value: 'value',
httponly: true
);
Key Benefits
• Self-documenting code
• Skip optional parameters
• Order-independent arguments
• Combine with positional args
Rules to Remember
• Named args must come after positional
• Can't use same name twice
• Works with variadic functions
• Respects default values
Attributes: Native Annotations
Attributes (sometimes called annotations in other languages) provide a way to add structured metadata to classes, methods, properties, and parameters. They replace the need for docblock annotations that frameworks like Doctrine and Symfony relied on.
Here's how to define and use attributes:
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class Route
{
public function __construct(
public string $path,
public array $methods = ['GET'],
public ?string $name = null
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Validate
{
public function __construct(
public string $rule,
public ?string $message = null
) {}
}
class UserController
{
#[Route('/users', methods: ['GET'], name: 'user.list')]
public function index(): array
{
return User::all();
}
#[Route('/users/{id}', methods: ['GET', 'POST'])]
public function show(int $id): User
{
return User::findOrFail($id);
}
}
class User
{
#[Validate('email', message: 'Invalid email format')]
public string $email;
#[Validate('min:8', message: 'Password too short')]
public string $password;
}
$reflection = new ReflectionClass(UserController::class);
foreach ($reflection->getMethods() as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
echo "Path: {$route->path}, Methods: " . implode(',', $route->methods);
}
}
Union Types: Flexible Type Declarations
Union types allow you to declare that a parameter, return type, or property can accept multiple types. This provides type safety while maintaining the flexibility PHP is known for.
class Calculator
{
// Accept int or float, return int or float
public function add(int|float $a, int|float $b): int|float
{
return $a + $b;
}
}
class Repository
{
// Return entity or null
public function find(int $id): User|null
{
return User::find($id);
}
// Accept multiple input types
public function search(string|array $criteria): Collection
{
if (is_string($criteria)) {
$criteria = ['name' => $criteria];
}
return User::where($criteria)->get();
}
}
// Properties can have union types too
class ApiResponse
{
public string|array|null $data = null;
public int|string $statusCode = 200;
}
voidcannot be part of a union type- Use
Type|nullinstead of?Typefor clarity in unions falsecan be used as a standalone type in unions- Duplicate types (including via inheritance) are not allowed
Match Expression: The Better Switch
The match expression is a more concise and safer alternative to switch statements. It uses strict comparison, returns values, and doesn't require break statements.
❌ PHP 7 Switch
switch ($statusCode) {
case 200:
case 201:
$message = 'Success';
break;
case 400:
$message = 'Bad Request';
break;
case 404:
$message = 'Not Found';
break;
case 500:
$message = 'Server Error';
break;
default:
$message = 'Unknown';
}
echo $message;
✓ PHP 8 Match
$message = match($statusCode) {
200, 201 => 'Success',
400 => 'Bad Request',
404 => 'Not Found',
500 => 'Server Error',
default => 'Unknown',
};
echo $message;
| Feature | switch | match |
|---|---|---|
| Comparison type | Loose (==) | Strict (===) |
| Returns value | No | Yes |
| Fall-through | Yes (needs break) | No |
| Multiple conditions | Multiple case statements | Comma-separated |
| Unmatched value | Continues silently | Throws UnhandledMatchError |
// Using expressions in conditions
$result = match(true) {
$age < 13 => 'Child',
$age < 20 => 'Teenager',
$age < 65 => 'Adult',
default => 'Senior',
};
// With objects and methods
$action = match($request->getMethod()) {
'GET' => $this->index(),
'POST' => $this->store($request),
'PUT', 'PATCH' => $this->update($request),
'DELETE' => $this->destroy($request),
default => throw new MethodNotAllowedException(),
};
Constructor Property Promotion
Constructor property promotion dramatically reduces boilerplate code by allowing you to declare and initialize class properties directly in the constructor signature.
❌ PHP 7 (Verbose)
class User
{
private string $name;
private string $email;
private int $age;
private bool $active;
public function __construct(
string $name,
string $email,
int $age,
bool $active = true
) {
$this->name = $name;
$this->email = $email;
$this->age = $age;
$this->active = $active;
}
}
✓ PHP 8 (Promoted)
class User
{
public function __construct(
private string $name,
private string $email,
private int $age,
private bool $active = true,
) {}
}
// That's it! Properties are declared
// and assigned automatically.
Constructor property promotion works beautifully with attributes for validation and serialization:
class CreateUserDTO
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 50)]
public readonly string $name,
#[Assert\Email]
public readonly string $email,
#[Assert\PositiveOrZero]
public readonly int $age = 0,
) {}
}
Nullsafe Operator: Chain with Confidence
The nullsafe operator (?->) allows you to chain method calls and property accesses without worrying about null values. If any part of the chain returns null, the entire expression returns null.
❌ PHP 7 (Null Checks)
// Tedious null checking
$country = null;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->getCountry();
if ($country !== null) {
$country = $country->getName();
}
}
}
// Or with ternaries (still ugly)
$country = $user
? ($user->getAddress()
? ($user->getAddress()->getCountry()
? $user->getAddress()->getCountry()->getName()
: null)
: null)
: null;
✓ PHP 8 (Nullsafe)
// Clean and elegant
$country = $user
?->getAddress()
?->getCountry()
?->getName();
// If any part is null,
// the whole expression is null.
// No errors, no exceptions.
// API response handling
$userName = $response?->data?->user?->name ?? 'Guest';
// Config access
$timeout = $config?->getDatabase()?->getTimeout() ?? 30;
// Collection operations
$firstUserEmail = $users->first()?->email;
// Combined with null coalescing
$avatar = $user?->profile?->avatar ?? '/default-avatar.png';
New String and Array Functions
PHP 8 introduced several utility functions that eliminate the need for awkward workarounds:
str_contains(), str_starts_with(), str_ends_with()
// Before PHP 8
if (strpos($haystack, $needle) !== false) { ... }
if (strpos($haystack, $needle) === 0) { ... }
if (substr($haystack, -strlen($needle)) === $needle) { ... }
// PHP 8 - Clean and readable
if (str_contains($haystack, $needle)) { ... }
if (str_starts_with($url, 'https://')) { ... }
if (str_ends_with($filename, '.php')) { ... }
// Real-world examples
$isSecure = str_starts_with($url, 'https://');
$isImage = str_ends_with($file, '.jpg') || str_ends_with($file, '.png');
$hasKeyword = str_contains($content, 'php8');
array_is_list()
// Check if array is a list (sequential integer keys starting from 0)
$list = [1, 2, 3, 4, 5];
$assoc = ['a' => 1, 'b' => 2];
$mixed = [0 => 'a', 2 => 'b']; // Gap in keys
array_is_list($list); // true
array_is_list($assoc); // false
array_is_list($mixed); // false
array_is_list([]); // true (empty array is a list)
// Useful for JSON encoding decisions
if (array_is_list($data)) {
// Will encode as JSON array: [1, 2, 3]
} else {
// Will encode as JSON object: {"a": 1, "b": 2}
}
Throw Expression
In PHP 8, throw is an expression, not a statement. This means you can use it in arrow functions, null coalescing, and ternary operators:
// In arrow functions
$fn = fn() => throw new Exception('Error!');
// With null coalescing
$value = $array['key'] ?? throw new InvalidArgumentException('Key required');
// In ternary operators
$result = $condition
? $this->process()
: throw new LogicException('Condition not met');
// Short-circuit evaluation
$user = $this->findUser($id) ?? throw new UserNotFoundException();
// In match expressions
$message = match($code) {
200 => 'OK',
404 => 'Not Found',
default => throw new UnexpectedValueException("Unknown code: $code"),
};
Improved Error Handling
PHP 8 brings significant improvements to error handling with clearer error messages, new exception types, and stricter type checking:
TypeError for Internal Functions
Internal functions now throw TypeError on invalid argument types instead of emitting warnings.
ValueError
New ValueError exception for valid types but invalid values (e.g., negative array size).
Consistent Type Errors
Extension functions now behave like user-defined functions with strict typing.
Better Error Messages
Error messages now include more context: expected type, given type, and parameter name.
// PHP 7: Warning, returns null
strlen([]); // Warning: strlen() expects string, array given
// PHP 8: TypeError exception
strlen([]); // TypeError: strlen(): Argument #1 ($string) must be of type string, array given
// New ValueError
array_fill(-1, 5, 'x'); // ValueError: array_fill(): Argument #1 ($start_index) must be >= 0
// Better error messages
function greet(string $name): void {}
greet(123);
// PHP 8: TypeError: greet(): Argument #1 ($name) must be of type string, int given
Other Notable Features
Trailing Comma in Parameter Lists
You can now add trailing commas to function parameters and closure use lists:
function longFunctionName(
string $param1,
int $param2,
array $param3, // Trailing comma allowed!
) { }
$closure = function($a, $b,) use ($c, $d,) {
// ...
};
This makes version control diffs cleaner and rearranging parameters easier.
::class on Objects
You can now use ::class on objects instead of get_class():
$user = new User();
// PHP 7
$className = get_class($user);
// PHP 8
$className = $user::class;
Mixed Type
The mixed type represents any value and is equivalent to array|bool|callable|int|float|null|object|resource|string:
function processData(mixed $data): mixed
{
// Can accept and return any type
return $data;
}
// Useful for:
// - Legacy code migration
// - Truly polymorphic functions
// - External API responses
WeakMap
WeakMap allows you to store data associated with objects without preventing garbage collection:
$cache = new WeakMap();
$user = new User();
$cache[$user] = computeExpensiveData($user);
// When $user goes out of scope and has no other references,
// both $user AND its cached data are garbage collected.
unset($user);
// Cache entry is automatically removed!
Perfect for caching, memoization, and tracking object metadata.
Migration Checklist
Before Upgrading to PHP 8
- Run your test suite on PHP 8 and fix any failures
- Check for deprecated features removed in PHP 8
- Update third-party dependencies to PHP 8-compatible versions
- Review uses of
==that might behave differently with strict comparison - Test internal function calls that might now throw TypeError
- Update any code relying on specific warning/error behavior
- Check for
@error suppression on code that now throws exceptions - Review reflection usage for attribute API changes
Conclusion
PHP 8 represents a massive leap forward for the language. The combination of JIT compilation for performance-critical code, syntax improvements like named arguments and match expressions, and type system enhancements with union types and attributes makes PHP a more powerful and pleasant language to work with.
Whether you're building new applications or maintaining legacy code, these features can significantly improve your codebase's readability, maintainability, and performance. Start by adopting the features that provide immediate benefits—named arguments and constructor promotion for cleaner code, union types for better type safety—and gradually incorporate more advanced features like attributes as your codebase evolves.
Ready to dive deeper? Explore PHP 8.1 and 8.2 for even more features like enums, readonly properties, fibers for async programming, and the new random extension. The PHP ecosystem continues to evolve rapidly!
