PHP 8.4 and its New Features

PHP's predictable release cycle means a new version is released every November, and PHP 8.4 has a official release date of 21 November 2024. PHP 8.4 promises to bring several enhancements and new features that will further improve the language's performance and developer experience. With that in mind, let’s take a look at what's in store for this new release…

Property Hooks

Readers familiar with PHP frameworks like Symfony and Laravel will be aware of different methods to define access to object properties. Symfony utilizes getters and setters using methods, whilst Laravel employs magic get and set methods to intercept data assignment and reads. Each approach has its own merits and drawbacks. Property hooks, a new feature in PHP 8.4, provide a more targeted, purpose-built tool for all property interactions.

Property hooks offer a more granular and performant approach to property access control, allowing developers to define specific behaviors for individual properties without the overhead of magic methods or the verbosity of traditional property getters and setters.

Here are some examples to understand the impact of these changes.

PHP 7.4:

class User {
	private int $age; // We are enforcing data type here.

	public function __construct(int $age) {
		$this->age = $age;
	}

	public function getAge: int {
		return $this->age;
	}

	public function setAge(int $age): void {
		$this->age = $age;
	}
}

With PHP 8, if the requirement is only type enforcement, the code can be made simpler like:

PHP 8.x:

class User {
	public function __construct(public int $age) { }
}

If I want to add a validation that the age should be a non-negative number. There are two options for a developer today:

  1. Make the property private and add setters and getters.
  2. Use get and set to handle in an obscure way.

Using getters and setters

class Animal {
	public function __construct(private int $age) {}

	public function getAge(): int {
		return $this->age;
	}

	public function setAge(int $age): void {
		if($age < 0) {
			throw new \ValueError("Age should not be a negative");
		}

		$this->age = $age;
	}
}

As you can see, this is the same implementation as we started with, but adding an additional check. However, the second method is a bit more complex, as you can see below.

Using magic get and set

class Animal {
	public function __construct(private int $age) {}

	public function __get(string $property): mixed {
		return match($property) {
			'age' => $this->age,
			default => throw new \Error("Attempt to read undefined
property $property"),
		};
	}

	public function __set(string $property, $value): void {
		switch($property) {
			case 'age':
			if($age < 0) {
				throw new \ValueError("Age cannot be negative.");
			}
			$this->age = $value;
			break;
			default: throw new \Error("Attempt to write unknown property $property");
		}
	}
	public function __isset(string $property): bool {
		return $property === 'age';
	}
}

Although the above implementation works, it is error-prone and there’s little assistance from IDEs. Moreover, static analysis tools won’t be able to find any mistakes. With property hooks, our requirements will be satisfied with a simple approach like:

PHP 8.4

class Animal {
	public int $age {
		set {
			if($value < 0) {
				throw new \ValueError("Age cannot be negative.");
			}
			$this->age = $value;
		}
	}
}

This is just an introduction to property hooks and they work beyond simple use cases above. For those interested, you can read the property hooks RFC to understand the finer details.

Performance Impact: The final implementation of property hooks is now virtually negligible compared to a method. The method, in this context, refers to the getter method used extensively in the examples discussed above. When the RFC was originally proposed, the cost was "close to the cost of __get", which in layman's language is approximately twice the improvement in the final version. You may read more about the final changes to property hooks in this separate RFC.

Simplified Class Instantiation

PHP added support for accessing a newly instantiated class without assigning it to a member variable starting from PHP 5.4.0. This allowed developers to use (new App())->run() call to be made without first assigning the result of new App() to an intermediate variable.

However, that still requires wrapping the new instantiation within parentheses. That is going to change from PHP 8.4. Developers no longer need to wrap the new class within extra parentheses and PHP is now smart enough to make new App()->run() a valid function call on the new object. It is not just limited to function calls, but all kinds of behavior associated with a new class.

PHP 8.4

class MyClass {
	const CONSTANT = 'constant';
	public static $staticProperty = 'staticProperty';
	public static function staticMethod(): string { return 'staticMethod'; }
	public $property = 'property';
	public function method(): string { return 'method'; }
	public function __invoke(): string { return '__invoke'; }
}

var_dump(
	new MyClass()::CONSTANT,        // string(8)  "constant"
	new MyClass()::$staticProperty, // string(14) "staticProperty"
	new MyClass()::staticMethod(),  // string(12) "staticMethod"
	new MyClass()->property,        // string(8)  "property"
	new MyClass()->method(),        // string(6)  "method"
	new MyClass()(),                // string(8)  "__invoke"
);

This works with anonymous classes as well.

var_dump ( 	
	// string(6) "method" 	
	new class { 
		public function method() { 
			return 'method'; 
		}
	} 
	->method(),
);

Although new languages like Kotlin get away with the need for using the new keyword altogether, PHP still needs this. It is because PHP allows a class and function within the same scope with the same name.

Enhancements in Array and String Handling

PHP has a number of useful functions like array_filter which helps you filter elements based on the conditions that you specify. However, there are still no methods that help you find a single element in an array. PHP 8.4 introduces four new array find functions that help you achieve this task. Let's look at the syntax for them below.

Array Functions

array_find returns the first element for which the $callback returns true. If no element is found, then the function returns NULL.

// array_find signature. 
function array_find(array $array, callable $callback): mixed;

It seems to be a really killer feature since we must do it in foreach loop every time we want to find first element, so now we can exchange this:

/**
 * @param Element[] $elements
 */
public function findElement(array $elements, Uuid $id): ?Element
{
   foreach ($elements as $element) {
       if ($element->id->equals($id)) {
           return $element;
       }
   }

   return null;
}

Into this:

/**
 * @param Element[] $elements
 */
public function findElement(array $elements, Uuid $id): ?Element
{
   return array_find($elements, static fn (Element $element): bool => $element->id->equals($id));
}

array_find_key returns the key of the first element for which the $callback returns true. If no matching element is found the function returns NULL.

// array_find_key signature.
function array_find_key(array $array, callable $callback): mixed;

array_any returns true, if $callback returns true for any element. Otherwise, the function returns false.

// array_any signature
function array_any(array $array, callable $callback): bool;

array_all returns true, if $callback returns true for all elements. Otherwise, the function returns false.

// array_all signature
function array_all(array $array, callable $callback): bool;

These new array find functions always pass both $key and $value to the $callback, unlike array_filter, which must be explicitly configured to do so. array_map does not support the passing of $key at all.

But remember that:

array_map(?callable $callback, array $array, array ...$arrays): array;

So you can pass the $key with such a trick:

array_map(fn ($key, $value) => true, array_keys($array), $array);

Multi-byte string handling

PHP does not have an equivalent for ucfirst and lcfirst handling multibyte strings. It also lacks a way to trim, ltrim and rtrim on multibyte strings. While it is possible to get similar output using custom implementations, it is an error-prone approach. With PHP 8.4, that is going to change. These will improve the readability and clarity of the PHP code in addition to standardizing the implementation.

PHP 8.4 introduces five new functions that can handle multibyte strings:

  • mb_ucfirst
  • mb_lcfirst
  • mb_trim
  • mb_ltrim
  • mb_rtrim

While these functions make it easy to deal with Unicode characters, it’s still not enough to cover all the languages in the world. The difficulty arises due to:

  • The breadth of the unicode character set
  • It being difficult to search
  • It being difficult to store in memory
  • Mapping with other character codes creates compatibility concerns. (For example, to express Hiragana, UTF-8 uses [あ-ゞ], EUC-JP [あ-ゝゞ], and Shift_JIS [あ-ん].)

Developers can utilize the functions provided with appropriate parameters to achieve the results they want. Testing is the key. Readers curious about the changes can read more about PHP note on mb_trim and PHP note on mb_ucfirst.

New grapheme_str_split function

PHP already has a mb_str_split function, which splits strings into individual characters. However, developers who work with i18n know that mb_str_split is not enough in most cases. PHP 8.4 introduces a new function that splits string by “grapheme” (the smallest meaningful contrastive unit in a writing system). This is mainly a potential issue in non-Latin alphabets so it’ll be especially useful for developers building applications in Asian languages.

Here’s a snippet of the syntax in PHP 8.4:

print_r(mb_str_split("ഇവിടെ"));

Array (
	[0] =>[1] =>[2] => ി
	[3] =>[4] =>)

print_r(grapheme_str_split("ഇവിടെ"));

Array
(
	[0] =>[1] => വി
	[2] => ടെ
)

As you might notice, even if you don't know the language, the new function splits the string in a meaningful way.

These new functions provide a way for developers working with a wide variety of languages an opportunity to handle their work in a much easier manner. It could also mean that developers can refactor some of their existing code to utilize the standard PHP implementations.

Security Enhancements

With computers seemingly improving daily thanks to AI, it becomes imperative to review the default security choices made by the PHP team a while ago. PHP provided support for the bcrypt algorithm ever since it introduced password hashing API in PHP 5.5. It’s been a long 11 years since the default behaviors have changed.

In a new change landing in PHP 8.4, the default cost of bcrypt function will change from 10 to 12. Any increase of the cost by 1 doubles the time it takes to calculate a single bcrypt hash. Increasing the default cost of bcrypt from 10 to 12 makes the hashing process 4 times more computationally intensive, increasing the time and resources required for attackers to crack passwords. Hopefully, this will deter bad actors from trying to break into applications and steal user data.

DOM and Large XML Document Parsing Improvements

PHP has an ext-dom extension that allows parsing of HTML documents and should reduce parsing errors. The ext-dom extension uses libxml2 HTML parser to parse HTML documents, up to version 4. However, HTML5 has gained widespread support in the past decade.

The issues with the current parser include:

  • No support for semantic tags (<article>, <main>, <section>, etc.)
  • Nesting of elements which are allowed in HTML 5, but not in HTML 4
  • Closing tags within JavaScript string literals
  • Unable to parse large XML documents (> 10 MB) and other issues with large document parsing.

As a modern programming language, developers expect PHP to support HTML5. Support for this new parser option will arrive in PHP 8.4, with three new classes DOM\Document, DOM\HTMLDocument and DOM\XMLDocument.

For those unfamiliar, HTML parsing currently uses the DOMDocument class. The new DOM\XMLDocument class is a drop-in replacement for DOMDocument. To parse HTML5 documents, we need to use the DOM\HTMLDocument class. The new functionality is powered by the Lexbor library.

Performance

The new library's performance is much better than that of the current libxml2-based approach. Note that DOMHTML5Document was renamed to DOM\HTMLDocument during the development process.

PHP 8.4 runtime

It means that the new library will make it easy for developers to develop applications that make use of HTML5 document parsing, without the need for any additional dependency. The new extension also fixes the size limitation issues faced by the current extension and avoids the need for complex workarounds.

JIT and Performance Tweaks

Just-In-Time (JIT) compiling for PHP was introduced with PHP 8.0. It is disabled by default. The functionality is enabled by setting the opcache.jit_buffer_size INI value, which is not intuitive as opcache.jit also accepts disabled as a valid value.

Prior to PHP 8.4, the default configuration was as follows:

opcache.jit=tracing
opcache.jit_buffer_size=0

With PHP 8.4, the syntax becomes:

opcache.jit=disable
opcache.jit_buffer_size=64M

Performance Improvements

In the current JIT compiler, Zend Virtual Machine generates bytecode directly. This has limitations on improving performance and extending the JIT functionality to other architectures. With the new implementation, PHP VM is generating an Intermediate Representation (IR).

This IR is passed to the framework that performs: machine independent optimisations, register allocation, scheduling, and native code generation. The new back-end won’t have to worry about many old low-level details, e.g.: register assignment, different calling conventions, etc. When measured against benchmarks, the new JIT compiler produces slightly faster (5-10%) and a smaller amount of code.

Deprecated Features

GET/POST Sessions

PHP supports two ways to propagate session information - cookies and GET/POST parameters. Cookies are the default preferred way to use sessions. However, using GET/POST to pass session information is not considered good security practice. The suggested alternative is to use cookies.

Implicitly nullable parameter declarations

Adding type declarations to function parameters has been allowed in PHP since PHP 5.1. PHP 7.x and 8.x added additional features that made enforcing types easy. With this deprecation, a function must be made to accept NULL values explicitly.

// Valid in PHP 7.x, 8.x and invalid in PHP 8.4
function greet(string $name = null) {}

// Valid alternative in PHP 8.4
function greet(?string $name = null) {}

function geeet(string|null $name = null)

Remove E_STRICT error level and E_STRICT constant

E_STRICT was introduced in PHP 5 to point out bad coding practices. Most of those errors were reclassified in PHP 7. Remaining usages were removed from mysqli extension and htmlentities function, and references to E_STRICT within engine tests were removed in 2019. Now this constant and error level are no longer used and are marked as deprecated.

Deprecate session_set_save_handler function overload

This is part of a larger RFC to remove overloaded functions. This function is not used by many and the suggested replacement is to use SessionHandlerInterface.

Looking Ahead: What's Not Here Yet

A list of features that were not included but are expected to land in future releases is as follows:

Impact on Existing Projects

If you are a developer using one of the popular frameworks and you are running one of their supported versions, then you have nothing to worry about. The framework will take care of most things. Most frameworks will announce their compatibility with the releases.

Property hooks is one killer feature that everyone can use. It may take some time for your framework to implement the necessary functionality. Keep checking your favorite framework's documentation to find the exact details.

However, if you are rolling out your own methods of handling things, then you should clearly look at the deprecated functions. One important thing to watch out for is the implicitly nullable parameter declarations.

Rector is a great project I recommend checking out. It helps you automate PHP version upgrades. But, like any other upgrade, be sure to test it works the way it's intended before you start using it in production.

Conclusion

PHP 8.4 is continuing to push the language in a forward-thinking direction. It’s borrowing ideas from other communities (like property hooks) and deprecating functionalities which are no longer relevant, which should reduce the amount of boilerplate code used in apps. These changes streamline development and bolster performance and security, making PHP a more robust choice for modernizing applications.

PHP is evolving and maintaining its relevance in an ever-changing programming landscape. Hopefully, this update will reinforce its position as a vital tool for web development and developers aiming to create efficient and secure applications.

icon

Hire PHP modernization experts

We work fast and effective, in parallel with releasing new features. Free code review included.

Improve Code

Related posts