PHP Traits and dynamic object properties in PHP 8.2+

Here’s a technical post if ever there was one.

I do most of my work in frontend tech these days — HTML, JavaScript and CSS — but there are also certain tasks which require me to delve into PHP. I learned object-oriented (class-based) PHP coding when I worked on TYPO3 projects and all of the code I now write for WordPress is class-based. (The advantages are too complex to list here. Another time, maybe, if anyone is interested. Let me know.)

Anyway. PHP 8.3 landed last week and so I need to through a few dozen WordPress projects on which I provide maintenance through the day job. All but one of them are running PHP 8.x already, so the code changes are comparatively minimal.

Deprecated dynamic properties

However, PHP 8.2 deprecated dynamic properties and they’ll result in a fatal error from PHP 9 onwards. I’ve used dynamic class properties on every project since 2019, so some work was needed. The basic premise of my code is that I have a base Theme or Plugin class which is set up using a Singleton concept. The code in my WordPress installation extends that base class with Model and Controller objects (according to MVC principles).

The way I’ve set that up in the singleton is by looping through an array of classes and appending a new object for each one to the main Theme or Plugin class.

/**
 * Loads and initializes the provided classes.
 *
 * @param array $classes
 * @return void
 */
private function loadClasses(array $classes): void
{
	foreach ($classes as $class) {
		$class_parts = explode('\\', $class);
		$class_short = end($class_parts);
		$class_set   = $class_parts[count($class_parts) - 2];

		if (!isset(sht_theme()->{$class_set}) || !is_object(sht_theme()->{$class_set})) {
			sht_theme()->{$class_set} = new stdClass();
		}

		if (property_exists(sht_theme()->{$class_set}, $class_short)) {
			wp_die(sprintf(_x('There's a problem in the Theme. Only one class with the name “%1$s” may be assigned to the Theme class “%2$s”.', 'Duplicate PHP class assignment in Theme', 'sht'), $class_short, $class_set), 500);
		}

		sht_theme()->{$class_set}->{$class_short} = new $class();

		if (method_exists(sht_theme()->{$class_set}->{$class_short}, 'run')) {
			sht_theme()->{$class_set}->{$class_short}->run();
		}
	}
}

This allows me to call public methods using the following syntax.

$video_thumbnail_url = sht_theme()->Controller->Media->getVideoThumbnailUrl();

Because of the deprecation of dynamic object properties, I’ve had to implement a new approach to the main logic. The code above still works but in order to avoid the deprecation, I need to ensure that the class can continue to accept dynamic properties.

Using Traits and magic methods

The approach I need is to add a single private $properties property — an array — to the class and then use the magic __set and __get methods to hang the sub-objects onto this array. Although the Controllers are all hung on the main Theme or Plugin class in my construct, I don’t want to have to remember to add the magic methods to classes in future, so I looked up the PHP Trait syntax and found that I can define them in their own file, then load them as required.

<?php

namespace SayHello\Theme\Trait;

trait MagicProperties
{
	private $properties = [];

	public function __get($name)
	{
		// Logic for getting property
		return $this->properties[$name] ?? null;
	}

	public function __set($name, $value)
	{
		// Logic for setting property
		$this->properties[$name] = $value;
	}
}

My autoloader (using spl_autoload_register for anything in the root namespace SayHello) allows me to add the single line in the main Theme class which adds these properties and methods in automatically.

<?php

namespace SayHello\Theme;

/**
 * Theme class which gets loaded in functions.php.
 * Defines the Starting point of the Theme and registers Controllers and Models.
 *
 * @author Mark Howells-Mead <mark@sayhello.ch>
 */
class Theme
{

	use Trait\MagicProperties;

	…
}

See, I told you that this would be a techy post! Any questions or suggestions? Leave a comment here or hit me up on Twitter/X or Mastodon.