Skip to content

PHP 8 Features — Complete Guide to New and Improved PHP 8 Capabilities

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about PHP 8 Features. We cover key concepts, practical examples, and best practices to help you master this topic.

PHP 8 features represent the most significant language evolution in PHP history, introducing named arguments, attributes, constructor property promotion, the match expression, enums, readonly classes, union types, intersection types, and the JIT compiler that fundamentally change how PHP code is written. This tutorial covers each major PHP 8.x feature with practical examples, explains how they improve code readability and safety, and shows Migration patterns from older PHP versions.

What You'll Learn

  • Using named arguments to skip default parameters and improve readability
  • Declaring PHP attributes for metadata on classes, methods, and properties
  • Leveraging constructor property promotion to eliminate boilerplate
  • Using match expression as a type-safe alternative to switch
  • Defining enums with backed values and methods
  • Creating readonly classes and properties for immutable data
  • Using union and intersection types for precise type declarations
  • Enabling and tuning the JIT compiler for CPU-intensive code
  • Using the nullsafe operator and str_contains family of functions

Why It Matters

PHP 8 is not just a minor upgrade. Features like named arguments reduce cognitive load, constructor promotion eliminates hundreds of lines of boilerplate, match expression prevents type-coercion bugs, and enums make domain modeling precise. The JIT compiler brings PHP's performance closer to compiled languages for CPU-bound tasks. Upgrading to PHP 8 and adopting these features makes your code safer, shorter, and faster.

Real-World Use

Durga Antivirus Pro's signature parser was rewritten using PHP 8 enums for virus types, match expressions for pattern matching, and named arguments for complex configuration objects. The rewrite reduced the codebase by 40% while improving type safety, and the JIT compiler accelerated signature matching by 3x.

Learning Path

flowchart LR
  A[Performance] --> B[PHP 8 Features\nYou are here]
  B --> C[Projects]
  style B fill:#f90,color:#fff

Named Arguments

Named arguments allow passing arguments by parameter name instead of position. This eliminates the need to remember parameter order and avoids passing null for optional parameters.

<?php
function createUser(
  string $name,
  string $email,
  bool $admin = false,
  bool $active = true,
  ?string $avatar = null
): string {
  return json_encode(compact('name', 'email', 'admin', 'active', 'avatar'));
}

// Old way: positional with null placeholders
echo createUser('Alice', 'alice@example.com', false, true, null);

// PHP 8 way: named arguments, skip defaults
echo createUser(
  name: 'Bob',
  email: 'bob@example.com',
  admin: true
);

Output:

{"name":"Alice","email":"alice@example.com","admin":false,"active":true,"avatar":null}
{"name":"Bob","email":"bob@example.com","admin":true,"active":true,"avatar":null}

Attributes

Attributes (annotations) add structured metadata to classes, methods, properties, and parameters. They replace PHPDoc annotations with native syntax:

<?php
#[Attribute]
class Route
{
  public function __construct(
    public string $path,
    public array $methods = ['GET']
  ) {}
}

#[Attribute]
class Validate
{
  public function __construct(
    public string $rule
  ) {}
}

#[Route('/api/users', ['GET', 'POST'])]
class UserController
{
  #[Validate('required|email')]
  private string $email;

  #[Route('/{id}', ['GET'])]
  public function show(int $id): string
  {
    return "User: $id";
  }
}

// Reading attributes at runtime
$reflection = new ReflectionClass(UserController::class);
$routeAttr = $reflection->getAttributes(Route::class)[0];
$route = $routeAttr->newInstance();
echo "Class route: {$route->path}\n";

$methodReflection = $reflection->getMethod('show');
$methodRouteAttr = $methodReflection->getAttributes(Route::class)[0];
$methodRoute = $methodRouteAttr->newInstance();
echo "Method route: {$methodRoute->path}\n";

Output:

Class route: /api/users
Method route: /{id}

Constructor Property Promotion

Constructor promotion combines property declaration and constructor assignment into a single syntax:

<?php
// Before PHP 8: verbose boilerplate
class UserOld
{
  private string $name;
  private string $email;

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

  public function getName(): string { return $this->name; }
  public function getEmail(): string { return $this->email; }
}

// PHP 8: constructor promotion
class User
{
  public function __construct(
    private string $name,
    private string $email,
    private bool $isAdmin = false
  ) {}

  public function getName(): string { return $this->name; }
  public function getEmail(): string { return $this->email; }
  public function isAdmin(): bool { return $this->isAdmin; }
}

$user = new User('Alice', 'alice@example.com', true);
echo "{$user->getName()} ({$user->getEmail()}) - Admin: " . ($user->isAdmin() ? 'yes' : 'no');

Output:

Alice (alice@example.com) - Admin: yes

Match Expression

The match expression is a type-safe, expression-based alternative to switch that returns a value and does not require break statements:

<?php
function httpStatusMessage(int $code): string
{
  return match (true) {
    $code >= 100 && $code < 200 => 'Informational',
    $code >= 200 && $code < 300 => 'Success',
    $code >= 300 && $code < 400 => 'Redirection',
    $code >= 400 && $code < 500 => 'Client Error',
    $code >= 500 && $code < 600 => 'Server Error',
    default => 'Unknown Status',
  };
}

echo httpStatusMessage(200) . "\n";
echo httpStatusMessage(404) . "\n";
echo httpStatusMessage(500) . "\n";

// Match with multiple conditions
$role = 'editor';
$permissions = match ($role) {
  'admin' => ['read', 'write', 'delete', 'manage_users'],
  'editor', 'author' => ['read', 'write'],
  'subscriber' => ['read'],
  default => [],
};

echo "Permissions: " . implode(', ', $permissions) . "\n";

Output:

Success
Client Error
Server Error
Permissions: read, write

Enums

PHP 8.1 introduced native enums. They can be pure (no value) or backed (string or int value), and can have methods:

<?php
enum OrderStatus: string
{
  case Pending = 'pending';
  case Confirmed = 'confirmed';
  case Shipped = 'shipped';
  case Delivered = 'delivered';
  case Cancelled = 'cancelled';

  public function label(): string
  {
    return match ($this) {
      self::Pending => 'Awaiting Payment',
      self::Confirmed => 'Order Confirmed',
      self::Shipped => 'On the Way',
      self::Delivered => 'Completed',
      self::Cancelled => 'Cancelled',
    };
  }

  public function isActive(): bool
  {
    return $this !== self::Cancelled && $this !== self::Delivered;
  }
}

$status = OrderStatus::Shipped;
echo "Status: {$status->value}\n";
echo "Label: {$status->label()}\n";
echo "Active: " . ($status->isActive() ? 'yes' : 'no') . "\n";

// Enum in a match expression
$nextAction = match ($status) {
  OrderStatus::Pending => 'process_payment',
  OrderStatus::Confirmed => 'prepare_shipment',
  OrderStatus::Shipped => 'track_delivery',
  OrderStatus::Delivered => 'request_review',
  OrderStatus::Cancelled => 'process_refund',
};
echo "Next action: $nextAction\n";

Output:

Status: shipped
Label: On the Way
Active: yes
Next action: track_delivery

Readonly Classes

PHP 8.2 introduced readonly classes. All properties are implicitly readonly and can only be initialized once:

<?php
readonly class Configuration
{
  public function __construct(
    public string $databaseUrl,
    public string $appEnv,
    public int $cacheTtl,
    public bool $debug
  ) {}
}

$config = new Configuration(
  databaseUrl: 'mysql://localhost:3306/db',
  appEnv: 'production',
  cacheTtl: 3600,
  debug: false
);

echo "Environment: {$config->appEnv}\n";

// This causes a compile error: Cannot modify readonly property
// $config->appEnv = 'development';

Output:

Environment: production

Nullsafe Operator and String Functions

The nullsafe operator (?->) shortens null checks when accessing properties or methods:

<?php
class Address
{
  public function __construct(
    public ?string $city = null
  ) {}
}

class UserProfile
{
  public function __construct(
    public ?Address $address = null
  ) {}
}

function getUserCity(?UserProfile $profile): string
{
  // Without nullsafe
  // if ($profile !== null && $profile->address !== null) {
  //   return $profile->address->city ?? 'Unknown';
  // }
  // return 'Unknown';

  // With nullsafe
  return $profile?->address?->city ?? 'Unknown';
}

$profile = new UserProfile(new Address('New York'));
echo getUserCity($profile) . "\n";
echo getUserCity(null) . "\n";

Output:

New York
Unknown

PHP 8 string functions: str_contains, str_starts_with, str_ends_with:

<?php
$email = 'alice@example.com';

echo str_contains($email, '@') ? 'Has @' : 'No @';
echo str_starts_with($email, 'alice') ? ', Starts with alice' : ', Does not start with alice';
echo str_ends_with($email, '.com') ? ', Ends with .com' : ', Does not end with .com';

Output:

Has @, Starts with alice, Ends with .com

Common Mistakes

  1. Using match when switch is simpler: match requires an exhaustive check or default. For simple value checks where you need side effects, switch with break is sometimes clearer.
  2. Forgetting that match is strict (===): match uses strict comparison (===), while switch uses loose (==). match ('1') will not match 1.
  3. Making too many classes readonly: Readonly classes are great for value objects and DTOs but inappropriate for entities, services, or any class with mutable state.
  4. Overusing named arguments in long chains: Named arguments improve readability for functions with many optional parameters, but using them for every call to short functions adds noise.
  5. Not upgrading due to perceived migration pain: PHP 8 is highly backward-compatible. The upgrade from 7.4 is smoother than any previous major version upgrade.

Practice Questions

  1. What is the difference between match and switch?

    • match is an expression that returns a value, uses strict comparison (===), and does not require break statements. switch is a statement that uses loose comparison (==) and requires break.
  2. How do you declare a backed enum in PHP 8.1?

    • enum Status: string { case Active = 'active'; }. The backing type must be string or int.
  3. What is constructor property promotion?

    • It combines declaring a property and assigning it in the constructor into a single parameter declaration with visibility: public function __construct(private string $name) {}.
  4. How do you use named arguments to skip optional parameters?

    • Specify only the parameters you need by name: createUser(name: 'Alice', email: 'alice@example.com', admin: true).
  5. Challenge: Refactor a legacy PHP 7 class that uses switch statements, manual property declarations, PHPDoc annotations for routing, and null checks with ternary operators into PHP 8 style using match, constructor promotion, attributes, and the nullsafe operator.

Mini Project

Build a PHP 8 command-line tool that demonstrates all major PHP 8 features:

  • Define a Command attribute to annotate CLI commands.
  • Create readonly Configuration class for app settings.
  • Use an enum for log levels (Debug, Info, Warning, Error).
  • Use match expression to route commands to handlers.
  • Use named arguments in method calls for clarity.
  • Use str_contains and nullsafe for input validation.
  • Write a benchmark that compares PHP 8 JIT performance against PHP 7.4 on a CPU-intensive task.

FAQ

What PHP version introduced named arguments?

Named arguments were introduced in PHP 8.0. They allow passing arguments by parameter name rather than position.

Are PHP 8 features backward compatible?

PHP 8.0 is largely backward-compatible with PHP 7.4. Some deprecated features were removed. Tools like Rector can automate the upgrade.

What are PHP attributes?

Attributes are native annotation syntax (#[AttributeName]) that add structured metadata to classes, methods, properties, and constants. They replace PHPDoc annotations.

Do enums in PHP support methods?

Yes. PHP enums can have methods, implement interfaces, and use traits. They are full-fledged classes with restricted instantiation.

Does JIT make PHP as fast as Go or Rust?

JIT narrows the gap for CPU-bound code but PHP's memory model and runtime overhead mean it is unlikely to match compiled languages. For typical web workloads (I/O-bound), JIT offers modest gains.

What's Next

Now that you know all PHP 8 features, apply your skills to build real-world projects in the upcoming project-based lessons.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro