PHP 8
Backend

PHP 8 Features Every Developer Should Know

Mayur Dabhi
Mayur Dabhi
March 7, 2026
18 min read

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.

What You'll Master
  • 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.

PHP 7 Execution PHP Source Lexer/ Parser OPcache Bytecode interpret PHP 8 with JIT OPcache Bytecode JIT Compiler Native Machine Code Web Requests: Minimal improvement (I/O bound operations) CPU-intensive: Up to 3x faster! (Math, image processing, ML)

JIT compiles hot code paths to native machine code for dramatic speedups

When JIT Shines

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:

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.

PHP 7: Docblock Annotations /** * @Route("/api/users") * @Method("GET") */ Parsed as comments • No IDE support PHP 8: Native Attributes #[Route('/api/users')] #[Method('GET')] public function list() {} Native syntax • Full IDE support • Reflection API

Here's how to define and use attributes:

Defining an Attribute
#[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
    ) {}
}
Using Attributes
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;
}
Reading Attributes via Reflection
$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.

Union Types in Action
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;
}
Important Considerations
  • void cannot be part of a union type
  • Use Type|null instead of ?Type for clarity in unions
  • false can 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
Advanced Match Usage
// 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.
Combining with Attributes

Constructor property promotion works beautifully with attributes for validation and serialization:

Promotion + Attributes
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.
Practical Examples
// 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:

Throw as Expression
// 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 8 Error Improvements
// 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.

Next Steps

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!

PHP PHP8 Features Backend Named Arguments Union Types JIT
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications.