• Tutorials
  • Modern PHP Features - Part 1

    Views57

    PHP has come a long way from the procedural scripting language many developers remember from the early 2000s. Versions 8.0 through 8.4 introduced a wave of features that bring PHP much closer to modern statically-typed languages — without losing the pragmatism that made it popular in the first place. This article kicks off a series exploring the features that have most changed how idiomatic PHP code looks today.

    #1. Constructor Property Promotion

    Before PHP 8, defining a value object meant writing the same property name four times: once as a property declaration, once as a constructor parameter, and twice in the assignment. Constructor property promotion collapses all of that into a single declaration.

     1// The old way
     2class Point
     3{
     4    private float $x;
     5    private float $y;
     6
     7    public function __construct(float $x, float $y)
     8    {
     9        $this->x = $x;
    10        $this->y = $y;
    11    }
    12}
    13
    14// The modern way
    15class Point
    16{
    17    public function __construct(
    18        private float $x,
    19        private float $y,
    20    ) {}
    21}
    

    The promoted version is functionally identical. Visibility modifiers in the constructor signature declare the property, and PHP wires up the assignment automatically. This pairs especially well with readonly (covered below) for immutable value objects.

    #2. Readonly Properties and Classes

    PHP 8.1 added readonly properties, and 8.2 extended the modifier to entire classes. A readonly property can only be initialized once, from inside the declaring class.

     1final class Money
     2{
     3    public function __construct(
     4        public readonly int $amount,
     5        public readonly string $currency,
     6    ) {}
     7}
     8
     9$price = new Money(1999, 'USD');
    10echo $price->amount; // 1999
    11$price->amount = 2999; // Error: Cannot modify readonly property
    

    For PHP 8.2 and above, you can mark the whole class as readonly to avoid repeating the keyword:

     1readonly class Money
     2{
     3    public function __construct(
     4        public int $amount,
     5        public string $currency,
     6    ) {}
     7}
    

    This is the cleanest way to express a value object in modern PHP. Combined with constructor promotion, a complete immutable type fits on three lines.

    #3. Enums

    Enums finally arrived natively in PHP 8.1, replacing the old pattern of class constants paired with validation guards. They come in two flavors: pure enums and backed enums.

     1// Pure enum
     2enum Status
     3{
     4    case Draft;
     5    case Published;
     6    case Archived;
     7}
     8
     9// Backed enum — each case has a scalar value
    10enum HttpMethod: string
    11{
    12    case Get = 'GET';
    13    case Post = 'POST';
    14    case Put = 'PUT';
    15    case Delete = 'DELETE';
    16}
    17
    18$method = HttpMethod::Post;
    19echo $method->value; // "POST"
    20
    21// Convert from a scalar
    22$parsed = HttpMethod::from('GET');         // throws if invalid
    23$maybe  = HttpMethod::tryFrom('UNKNOWN');  // returns null if invalid
    

    Enums are first-class types, so you can use them in parameter and return type declarations. They can also hold methods, which is useful for behavior that belongs naturally to the case itself:

     1enum Status: string
     2{
     3    case Draft = 'draft';
     4    case Published = 'published';
     5    case Archived = 'archived';
     6
     7    public function isVisible(): bool
     8    {
     9        return match ($this) {
    10            Status::Published => true,
    11            Status::Draft, Status::Archived => false,
    12        };
    13    }
    14}
    

    #4. The match Expression

    match is a more disciplined sibling of switch. It returns a value, uses strict comparison, and requires exhaustive handling — if no arm matches and there is no default, it throws an UnhandledMatchError.

     1$statusCode = 418;
     2
     3$category = match (true) {
     4    $statusCode >= 200 && $statusCode < 300 => 'success',
     5    $statusCode >= 300 && $statusCode < 400 => 'redirect',
     6    $statusCode >= 400 && $statusCode < 500 => 'client error',
     7    $statusCode >= 500 => 'server error',
     8    default => 'informational',
     9};
    

    Multiple values can share an arm by separating them with commas, which makes mapping particularly clean:

     1$weekend = match ($day) {
     2    'Saturday', 'Sunday' => true,
     3    'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday' => false,
     4};
    

    Compared to switch, you get no fallthrough surprises, no loose == comparisons, and the result can be assigned directly to a variable.

    #5. Named Arguments

    Named arguments let you pass values to a function by parameter name instead of position. They are particularly useful for functions with many optional parameters, where positional calls become unreadable strings of null placeholders.

     1function createUser(
     2    string $email,
     3    string $name,
     4    bool $active = true,
     5    bool $verified = false,
     6    ?string $timezone = null,
     7): User {
     8    // ...
     9}
    10
    11// Without named arguments — what does `false, true, null` mean?
    12createUser('a@b.com', 'Ada', false, true, null);
    13
    14// With named arguments — intent is obvious, and order doesn't matter
    15createUser(
    16    email: 'a@b.com',
    17    name: 'Ada',
    18    verified: true,
    19    active: false,
    20);
    

    Named arguments also play well with array spreading in PHP 8.1+, where string keys are forwarded as named arguments:

     1$config = ['email' => 'a@b.com', 'name' => 'Ada', 'verified' => true];
     2createUser(...$config);
    

    #6. The Nullsafe Operator

    Chasing a value through a chain of possibly-null objects used to require a stack of guard clauses or intermediate variables. The ?-> operator short-circuits the whole chain to null the moment any link is missing.

     1// Before
     2$country = null;
     3if ($user !== null) {
     4    $address = $user->getAddress();
     5    if ($address !== null) {
     6        $country = $address->getCountry();
     7    }
     8}
     9
    10// After
    11$country = $user?->getAddress()?->getCountry();
    

    A small caveat: ?-> works for method calls and property access, but not for array index access or as the left side of an assignment. For arrays, you still reach for ?? or isset().

    #7. The Null Coalescing Operators

    These two operators are not the newest features in this list, but they remain among the most useful. ?? returns the left operand if it is not null, otherwise the right one. The assignment form ??= writes to the variable only if it was previously null or unset.

     1$pageSize = $_GET['size'] ?? 25;
     2
     3// Chain them: first non-null wins
     4$timezone = $userPref ?? $accountDefault ?? 'UTC';
     5
     6// Assign only if missing
     7$cache['user:42'] ??= fetchUser(42);
    

    Unlike ?:, the null coalescing operator checks specifically for null and does not complain about undefined array keys, which is what makes it the right tool for reading from request input or configuration arrays.

    #Wrapping Up

    These seven features — constructor promotion, readonly, enums, match, named arguments, nullsafe, and null coalescing — together represent the biggest shift in everyday PHP style over the last few years. A class that used to span 30 lines of boilerplate now often fits in five, and the result is more expressive and harder to misuse.

    Part 2 will continue with first-class callable syntax, the never and true/false return types, intersection and DNF types, new-in-initializers, and the property hooks that arrived with PHP 8.4. Stay tuned.

    profile image of Petar Vasilev

    Petar Vasilev

    Petar is a web developer at Mitkov Systems GmbH. He is fascinated with the web. Works with Laravel and its ecosystem. Loves learning new stuff.

    More posts from Petar Vasilev