Skip to main content

PHP 8.4 was released a little over two months ago, on November 21, 2024. Now seems like the perfect time to dive into some of the key features of this release, so let’s take a closer look at what’s new. This overview will touch on several important updates without getting into every detail, as there’s a lot to explore.

Here’s what I’ll be covering in this post:

  • Property Hooks (a major change in 8.4)
  • Virtual Properties
  • Default Property Values
  • New Class Invocation Syntax (without parentheses)
  • Asymmetric Visibility
  • Lazy Objects
  • The #[\Deprecated] Attribute
  • Parsing Non-POST HTTP Requests
  • New Array Functions

Let’s take a closer look at these exciting changes!

Property HooksIf you've been writing object-oriented code in PHP for any length of time, the following will be very familiar 

<?php
                declare(strict_types=1);
                final class PhoneNumber
                {
                    private string $areaCode;
                    private string|null $countryCode;
                    private string $localPhoneNumber;
                    public function __construct(
                        string $areaCode, 
                        string $localPhoneNumber, 
                        string|null $countryCode = null
                    ) {
                        $this->areaCode = $areaCode;
                        $this->countryCode = $countryCode;
                        $this->localPhoneNumber = $localPhoneNumber;
                    }
                }
 

Sure! Here's a rewritten version of the provided explanation, with a clearer structure and explanation of the business logic:


This class is a simple implementation for modeling a phone number, consisting of three properties: the country code, area code, and local phone number. These properties are stored privately within the class, meaning they can't be accessed or modified directly once the object is instantiated.

However, this setup makes the class somewhat limited, as the private visibility of the properties restricts access to them unless you use PHP's Reflection API. Without Reflection, you’d typically rely on getter and setter methods, or the magic __get() and __set() methods, to access or modify the properties.

Before PHP 8.4, there were two main approaches to make these private properties accessible:

  1. Getters and setters: Explicit methods that retrieve or modify the property values.
  2. Magic __get() and __set() methods: These allow you to dynamically access or set private properties.

Now, let's dive into the business logic that this class should follow based on Australian phone number conventions:

Business Logic for Australian Phone Numbers:

  1. Area Code:
    • The area code must be two digits in length.
    • The first digit must always be 0.
    • The second digit must be one of 2, 3, 4, 7, or 8 (e.g., 02, 03, 04, 07, 08).
  2. Country Code (Optional):
    • The country code is optional.
    • If provided, it can be 1 to 4 digits in length.
  3. Local Phone Number:
    • The local phone number must consist of exactly 8 digits (values between 0 and 9).
  4. Printing Logic:
    • If the country code is provided, it must be displayed with a preceding plus (+) symbol.
    • The area code should not have a leading zero when the country code is present (i.e., remove the first 0).
    • If the country code is not provided, the area code should include a leading zero.
    • The phone number should be printed in groups of three digits, separated by a single space.

Example Output:

  • With country code: +61 2 987 654 32
  • Without country code: 02 987 654 32

Let me know if you'd like to see the actual PHP implementation based on this business logic!

Using getters and setters

Here's an example of how you could refactor the class using getter and setter functions:

<?php
                            declare(strict_types=1);
                            namespace Settermjd\Scratch;
                            use function chunk_split;
                            use function preg_match;
                            use function sprintf;
                            use function substr;
                            use function trim;
                            final class PhoneNumber
                            {
                                public const int MATCHES_REGEX = 1;
                                public const int NUMBER_SPACING = 3;
                                public const string COUNTRY_CODE_PREFIX = "+";
                                public const string REGEX_AREA_CODE = "/0[23478]/";
                                public const string REGEX_COUNTRY_CODE = "/[1-9][0-9]{0,3}/";
                                public const string REGEX_LOCAL_NUMBER = "/[0-9]{8}/";
                               
                                private string $areaCode;
                                private string|null $countryCode;
                                private string $localPhoneNumber;
                                public function __construct(string $areaCode, string $localPhoneNumber, string|null $countryCode)
                                {
                                    $this->setAreaCode($areaCode);
                                    $this->setCountryCode($countryCode);
                                    $this->setLocalPhoneNumber($localPhoneNumber);
                                }
                                public function getAreaCode(): string
                                {
                                    return $this->countryCode === null
                                        ? $this->areaCode
                                        : substr($this->areaCode, 1);
                                }
                                public function setAreaCode(string $areaCode): void
                                {
                                    $this->areaCode = $this->matchesRegex(self::REGEX_AREA_CODE, $areaCode)
                                        ? $areaCode
                                        : null;
                                }
                                public function getCountryCode(): string|null
                                {
                                    return $this->countryCode !== null
                                        ? self::COUNTRY_CODE_PREFIX . $this->countryCode
                                        : null;
                                }
                                public function setCountryCode(string|null $countryCode): void
                                {
                                    $this->countryCode = $this->matchesRegex(self::REGEX_COUNTRY_CODE, $countryCode)
                                        ? $countryCode
                                        : null;
                                }
                                public function getLocalPhoneNumber(): string
                                {
                                    return $this->localPhoneNumber ?? "";
                                }
                                public function setLocalPhoneNumber(string $localPhoneNumber): void
                                {
                                    $this->localPhoneNumber = $this->matchesRegex(self::REGEX_LOCAL_NUMBER, $localPhoneNumber)
                                        ? $localPhoneNumber
                                        : null;
                                }
                                public function getPhoneNumber(): string
                                {
                                    return $this->countryCode === null
                                        ? sprintf("%s %s",
                                            substr($this->getAreaCode() . $this->getLocalPhoneNumber(), 0, 4),
                                            trim(chunk_split(substr($this->getAreaCode() . $this->getLocalPhoneNumber(), 4), self::NUMBER_SPACING, " "))
                                        )
                                        : sprintf("%s %s",
                                            $this->getCountryCode(),
                                            trim(chunk_split($this->getAreaCode() . $this->getLocalPhoneNumber(), self::NUMBER_SPACING, " "))
                                        );
                                }
                                private function matchesRegex(string $regex, string|null $value): bool
                                {
                                    return $value !== null && preg_match($regex, $value) === self::MATCHES_REGEX;
                                }
                            

In this class, a series of constants are defined to enhance readability and avoid using hardcoded values, also known as "magic variables." Among these constants, three are particularly important for ensuring that the area code, country code, and local phone number are validated correctly. These constants are:

  • REGEX_AREA_CODE: Ensures the area code is valid.
  • REGEX_COUNTRY_CODE: Ensures the country code is valid.
  • REGEX_LOCAL_NUMBER: Ensures the local phone number is valid.

Regular expressions (regexes) are used to validate the phone number components. If you're unfamiliar with regex, you can check out this comprehensive guide at Regular-Expressions.info.

Next, the class defines three private properties: $areaCode, $countryCode, and $localPhoneNumber. These properties are initialized through the class's constructor, which invokes setter methods to set their values while enforcing validation using the regular expression constants.

For each of these properties, getter and setter methods are defined to allow access to the values while ensuring that they meet the validation rules specified in the regular expressions.

Key Points:

  1. Constructor: The constructor accepts the values for the area code, local phone number, and an optional country code. The setter methods are called to validate and assign these values.
  2. Getter and Setter Methods: These methods allow for safe access to the private properties. Each setter validates the input using the corresponding regular expression constant.
  3. Magic Methods: The __get() and __set() magic methods are implemented to dynamically handle access to the private properties. This allows for controlled access to the object's properties and provides error handling if invalid properties are requested or set.
  4. Regex-Based Validation: The regular expression constants ensure that each phone number component (area code, country code, and local number) is valid according to the established rules.
  5. Phone Number Formatting: Functions like getPhoneNumber() and matchesRegex() implement the logic for correctly formatting and displaying the phone number based on the presence of the country code.

    <?php
    
    declare(strict_types=1);
    
    namespace Settermjd\Scratch;
    
    use function chunk_split;
    use function preg_match;
    use function sprintf;
    use function substr;
    use function trim;
    
    final class PhoneNumber
    {
        public const int MATCHES_REGEX = 1;
        public const int NUMBER_SPACING = 3;
        public const string COUNTRY_CODE_PREFIX = "+";
        public const string REGEX_AREA_CODE = "/0[23478]/";
        public const string REGEX_COUNTRY_CODE = "/[1-9][0-9]{0,3}/";
        public const string REGEX_LOCAL_NUMBER = "/[0-9]{8}/";
    
        private string $areaCode;
        private string|null $countryCode;
        private string $localPhoneNumber;
    
        public function __construct(
            string $areaCode, 
            string $localPhoneNumber, 
            string|null $countryCode
        ) {
            $this->__set("areaCode", $areaCode);
            $this->__set("countryCode", $countryCode);
            $this->__set("localPhoneNumber", $localPhoneNumber);
        }
    
        public function __get(string $name): string|null
        {
            $value = "";
            switch ($name) {
                case "areaCode":
                    $value = $this->countryCode === null
                        ? $this->areaCode
                        : substr($this->areaCode, 1);
                    break;
                case "countryCode":
                    $value = $this->countryCode !== null
                        ? self::COUNTRY_CODE_PREFIX . $this->countryCode
                        : null;
                    break;
                case "localPhoneNumber":
                    $value = $this->localPhoneNumber ?? "";
                    break;
                case "phoneNumber":
                    $value = empty($this->countryCode)
                        ? sprintf("%s %s",
                            substr($this->__get("areaCode") . $this->__get("localPhoneNumber"), 0, 4),
                            trim(
                                chunk_split(
                                    substr(
                                        $this->__get("areaCode") . $this->__get("localPhoneNumber"),
                                        4
                                    ),
                                    self::NUMBER_SPACING,
                                    " "
                                )
                            )
                        )
                        : sprintf("%s %s",
                            $this->__get("countryCode"),
                            trim(
                                chunk_split(
                                    $this->__get("areaCode") . $this->__get("localPhoneNumber"),
                                    self::NUMBER_SPACING,
                                    " "
                                )
                            )
                        );
            }
            return $value;
        }
    
        public function __set(string $name, string|null $value): void
        {
            if (property_exists($this, $name)) {
                switch ($name) {
                    case "areaCode":
                        $this->areaCode = $this->matchesRegex(
                            self::REGEX_AREA_CODE, 
                            $value
                        )
                            ? $value
                            : null;
                        break;
                    case "countryCode":
                        $this->countryCode = $this->matchesRegex(
                            self::REGEX_COUNTRY_CODE, 
                            $value
                        )
                            ? $value
                            : null;
                        break;
                    case "localPhoneNumber":
                        $this->localPhoneNumber = $this->matchesRegex(
                            self::REGEX_LOCAL_NUMBER, 
                            $value
                        )
                            ? $value
                            : null;
                        break;
                }
                $this->{$name} = $value;
            }
        }
    
        private function matchesRegex(string $regex, string|null $value): bool
        {
            return $value !== null 
                    && preg_match($regex, $value) === self::MATCHES_REGEX;
        }
    }

This example is shorter, which can make it more appealing for simplicity. However, it takes a blunter approach because the magic methods (__get and __set) will be invoked whenever you try to access or modify class properties that are either not explicitly defined or not visible from the calling scope.

This behavior applies not only to the three initially defined properties (area code, country code, and local phone number) but also to any future properties added to the class that fit the same criteria. Consequently, this could lead to unexpected behavior or unnecessary boilerplate code to prevent issues when accessing or modifying properties.

What are Property Hooks?

Property Hooks provide a solution to this problem. If you’ve worked with languages like Kotlin, C#, Swift, JavaScript, Python, or Ruby, you’re probably familiar with the concept of property accessors. In these languages, property accessors allow you to define additional logic when getting or setting object properties. PHP now supports this concept as well.

In PHP, property hooks let you introduce additional logic when interacting with object properties. They allow you to define custom behavior for getting and setting properties. You can define these hooks on a case-by-case basis, or fallback to PHP's default behavior when you don’t need custom logic for certain properties.

There are two types of hooks for each property:

  1. Get Hook: Defines custom behavior for retrieving a property.
  2. Set Hook: Defines custom behavior for setting a property.

Property hooks provide flexibility and control, allowing you to customize the logic of property access without relying on the more blunt magic methods or adding excessive boilerplate.

// Full block form
set ($value) { $this->propertyName = $value }

// Block form with implicit $value
set { $this->propertyName = $value }

// Expression form with explicit $value
set ($value) => { expression }

// Expression form with implicit $value
set => { expression }

The variety of forms provided by property hooks offers a lot of flexibility, allowing you to choose the approach that best suits your needs or goals. So far, I prefer a combination of the full block form (as seen at the top of the list) along with the expression form that explicitly includes $value in the set hook. This combination provides the most control over property access. However, other forms, such as PHP’s Arrow Functions, are particularly useful when working with small expressions or simpler logic.

One of the biggest advantages of property hooks is that they eliminate the need (or the perceived need) to define traditional getters and setters just to control access to one or more object properties. While there may be cases where defining getters and setters is necessary, relying on them unnecessarily can result in bloating the class API and creating more work than needed.

How Do You Use Property Hooks?

Let's walk through a refactored example of the PhoneNumber class below to see property hooks in action and how they simplify property access and control.

<?php

declare(strict_types=1);

namespace Settermjd\Scratch;

final class PhoneNumber
{
    public const int MATCHES_REGEX = 1;
    public const int NUMBER_SPACING = 3;
    public const string COUNTRY_CODE_PREFIX = "+";
    public const string REGEX_AREA_CODE = "/0[23478]/";
    public const string REGEX_COUNTRY_CODE = "/[1-9][0-9]{0,3}/";
    public const string REGEX_LOCAL_NUMBER = "/[0-9]{8}/";

    private string|null $countryCode {
        set (string|null $countryCode) {
            $this->countryCode = $this->matchesRegex(
                self::REGEX_COUNTRY_CODE, 
                $countryCode
            )
                ? $countryCode
                : null;
        }
        get {
            return $this->countryCode !== null
                ? self::COUNTRY_CODE_PREFIX . $this->countryCode
                : null;
        }
    }

    private string $areaCode {
        set (string $areaCode) {
            $this->areaCode = $this->matchesRegex(
                self::REGEX_AREA_CODE, 
                $areaCode
            )
                ? $areaCode
                : null;
        }
        get {
            return $this->countryCode === null
                ? $this->areaCode
                : substr($this->areaCode, 1);
        }
    }

    private string $localPhoneNumber {
        set (string $localPhoneNumber) {
            $this->localPhoneNumber = $this->matchesRegex(
                self::REGEX_LOCAL_NUMBER, 
                $localPhoneNumber
            )
                ? $localPhoneNumber
                : null;
        }
        get => $this->localPhoneNumber ?? "";
    }

    public string $phoneNumber {
        get {
            return $this->countryCode === null
                ? sprintf("%s %s",
                    substr($this->areaCode . $this->localPhoneNumber, 0, 4),
                    trim(
                        chunk_split(
                            substr(
                                $this->areaCode . $this->localPhoneNumber, 
                                4
                            ), 
                            self::NUMBER_SPACING, 
                            " "
                        ))
                )
                : sprintf("%s %s",
                    $this->countryCode,
                    trim(
                        chunk_split(
                            $this->areaCode . $this->localPhoneNumber, 
                            self::NUMBER_SPACING, 
                            " "
                        ))
                );
        }
    }

    private function matchesRegex(string $regex, string|null $value): bool
    {
        return $value !== null && preg_match($regex, $value) === self::MATCHES_REGEX;
    }

    public function __construct(
        string $areaCode, 
        string $localPhoneNumber, 
        string|null $countryCode = null
    ) {
        $this->areaCode = $areaCode;
        $this->countryCode = $countryCode;
        $this->localPhoneNumber = $localPhoneNumber;
    }
}

All the logic that was previously handled by the getters, setters, or the __get and __set magic methods has now been refactored into the get and set hooks of the respective object properties. I won’t go through every single one, as that would be somewhat redundant. Instead, I’ll walk you through just one example to illustrate how everything works.

Let's focus on the $countryCode property.

Tags

Recent Blogs

A glimpse at the journey from Drupal 10 to Drupal 11

Read more

Indeed, CKEditor 5 and Drupal 10 form a powerful duo for content creation, enhancing the…Read more

The significance of Drupal 10 lies in its seamless and easy upgrade process. Unlike major…Read more