Modern PHPExtra· 45 min read

Modern PHP 8 Features

The typed, expressive syntax that real PHP 8 codebases use — type declarations, the match expression, enums, named arguments, arrow functions and the nullsafe operator.

What you will learn

  • Add type declarations to parameters, returns and properties
  • Use match, enums and named/variadic arguments
  • Write shorter code with arrow functions and the nullsafe operator

Why modern syntax matters

The PHP you have written so far works, but it looks like PHP from ten years ago. Every paid course today teaches PHP 8 — and the code you will see in real jobs and in Laravel leans heavily on the features below. They make code safer (the engine checks your types) and shorter (less boilerplate). None of this changes how PHP runs; it just lets you say more, more clearly.

Type declarations

A type declaration is a label that says what kind of value a parameter takes, what a function returns, or what a property holds. PHP then enforces it — pass the wrong type and you get a clear error instead of a silent bug. This is the single biggest upgrade to your everyday code.

Below, int before each parameter says "this must be a whole number", and the : int after the brackets says "this function returns a whole number":

Typed parameters and return type
<?php
  declare(strict_types=1);   // make PHP strict about types

  function add(int $a, int $b): int {
    return $a + $b;
  }

  echo add(5, 3);     // 8
  // echo add("5", "3");  // TypeError — a string is not an int
?>

Reading it: declare(strict_types=1) at the very top tells PHP not to quietly convert types for you, so mistakes surface immediately. add(int $a, int $b): int promises both inputs and the output are integers. Call it with two numbers and it returns their sum; call it with text and PHP throws a TypeError before any wrong answer can spread.

Note: Output (in the browser): 8 The typed version catches a whole class of bugs the moment you call the function. Put declare(strict_types=1) at the top of every PHP file you write from now on — it is standard in modern projects.

You can type properties too. A *typed property* guarantees an object always holds the right kind of data:

Typed properties (with a nullable type)
<?php
  class Product {
    public string $name;
    public float $price;
    public ?string $note = null;   // ? means "string OR null"
  }

  $p = new Product();
  $p->name = "Keyboard";
  $p->price = 1299.00;
?>

Each property declares its type: $name is always a string, $price always a float (a decimal number). The ?string on $note is a nullable type — the ? means the value may be a string or null (nothing yet). Try to set $p->price to text and PHP rejects it.

Note: There is no visible output here — the class just defines a safe shape. The payoff is that anywhere you use $p->price later, you can trust it is a number, not accidentally a string.

The match expression

You met switch earlier. match is its modern replacement: it compares with strict === (no surprises), needs no break, and — crucially — it returns a value you can store. It is tidier and safer for turning one value into another.

match returns a value
<?php
  $status = "shipped";

  $label = match ($status) {
    "pending"  => "Waiting to be processed",
    "shipped"  => "On its way",
    "delivered" => "Arrived",
    default    => "Unknown status",
  };

  echo $label;
?>

Read match ($status) as "look at $status and pick the matching arm". Here $status is "shipped", so the "shipped" => arm wins and its text becomes the value of the whole expression, which we store in $label. default catches anything not listed. Unlike switch, there is no fall-through to worry about and the result drops straight into a variable.

Note: Output (in the browser): On its way With switch you would need a break on every case and a separate variable to hold the result. match does both jobs in one clean expression — that is why modern code prefers it.

Enums — a fixed set of choices

An enum (short for *enumeration*) defines a small, fixed list of allowed values, each with a name. Instead of passing around loose strings like "shipped" (easy to mistype), you use OrderStatus::Shipped — and PHP guarantees it is one of the real options.

An enum with a match
<?php
  enum OrderStatus: string {
    case Pending  = "pending";
    case Shipped  = "shipped";
    case Delivered = "delivered";
  }

  function describe(OrderStatus $status): string {
    return match ($status) {
      OrderStatus::Pending  => "Waiting",
      OrderStatus::Shipped  => "On its way",
      OrderStatus::Delivered => "Arrived",
    };
  }

  echo describe(OrderStatus::Shipped);
?>

The enum OrderStatus: string lists exactly three valid cases, each tied to a string value. The describe function is typed to accept only an OrderStatus — you literally cannot pass it a misspelled status, because nothing but OrderStatus::Pending, ::Shipped or ::Delivered exists. We then match on which case it is to build the label.

Note: Output (in the browser): On its way Enums remove a whole category of typo bugs. If a status can only ever be one of a handful of values, an enum makes that rule part of the code, checked by PHP.

Named arguments & variadic functions

Named arguments let you pass values by their parameter name instead of by position — clearer, and you can skip optional ones. A variadic function (the ... before a parameter) accepts *any number* of arguments, bundled into an array.

Named arguments and a variadic function
<?php
  function makeUser(string $name, string $role = "member", bool $active = true): string {
    return "$name ($role) — " . ($active ? "active" : "inactive");
  }

  // named arguments — skip $role, set $active by name
  echo makeUser(name: "Asha", active: false);

  function sum(int ...$numbers): int {
    return array_sum($numbers);
  }
  echo "<br>" . sum(1, 2, 3, 4);   // any count of numbers
?>

In makeUser(name: "Asha", active: false) we label each value, so we can leave $role at its default "member" while still setting $active — no need to remember the position or pass placeholder values. In sum(int ...$numbers), the ... collects every argument into the $numbers array, so sum(1, 2, 3, 4) adds all four; you could pass two or twenty.

Note: Output (in the browser): Asha (member) — inactive 10 Named arguments make a call self-documenting (you can see what false *means*). Variadics let one function handle a list of any length.

Arrow functions & the nullsafe operator

An arrow function (fn) is a one-line shorthand for a small function — perfect inside array_map or array_filter. The nullsafe operator ?-> safely reaches into an object that *might* be null: instead of crashing, the whole expression just becomes null.

Arrow function and nullsafe access
<?php
  $prices = [100, 250, 90];
  $withTax = array_map(fn($p) => $p * 1.18, $prices);
  print_r($withTax);

  $user = null;
  echo $user?->name ?? "Guest";   // no crash — gives "Guest"
?>

The arrow function fn($p) => $p * 1.18 is a compact recipe "take a price, add 18% tax". array_map runs it on every item in $prices and returns a new array of taxed prices. Lower down, $user is null; writing $user->name would normally crash, but $user?->name stops safely at the null, and the ?? "Guest" then supplies a fallback.

Note: Output (in the browser): Array ( [0] => 118 [1] => 295 [2] => 106.2 ) Guest print_r shows the new array with tax added to each price. The nullsafe line never crashed even though $user was nothing — exactly what you want when data might be missing.

Q. What does the match expression do that switch does not?

Answer: match returns a value you can store, uses strict === comparison, and needs no break — making it the modern, safer replacement for switch.

✍️ Practice

  1. Rewrite an earlier function with typed parameters and a return type, and add declare(strict_types=1).
  2. Replace a small switch with a match that stores its result in a variable.

🏠 Homework

  1. Create an enum Priority (Low/Medium/High) and a typed function that returns a colour name for each using match.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →