SmartObject

SmartObject enhanced PHP object behavior for many years. Since PHP 8.4, all of its features have become native parts of PHP itself, thus completing its historic mission as a pioneer of modern object-oriented approach in PHP.

Installation:

composer require nette/utils

SmartObject was introduced in 2007 as a revolutionary solution to the shortcomings of PHP's object model at the time. In an era when PHP faced numerous issues with object-oriented design, it brought significant improvements and simplified developers' workflows. It became a legendary component of the Nette framework. SmartObject offered functionality that PHP only acquired many years later – from access control for object properties to sophisticated syntactic sugar. With the release of PHP 8.4, it fulfilled its historical mission, as all its features became a native part of the language. It was ahead of PHP's development by an impressive 17 years.

SmartObject went through an interesting technical evolution. Initially, it was implemented as the Nette\Object class, from which other classes inherited the needed functionality. A significant change came with PHP 5.4, which introduced trait support. This enabled transformation into the Nette\SmartObject trait, bringing greater flexibility – developers could use the functionality even in classes that already inherited from another class. While the original Nette\Object class ceased to exist with PHP 7.2 (which prohibited naming classes with the word ‘Object’), the Nette\SmartObject trait lives on.

Let's explore the features that Nette\Object and later Nette\SmartObject offered. Each of these functions represented a significant step forward in PHP object-oriented programming at the time.

Consistent Error States

One of the most pressing issues of early PHP was inconsistent behavior when working with objects. Nette\Object brought order and predictability to this chaos. Let's look at how PHP originally behaved:

echo $obj->undeclared;    // E_NOTICE, later E_WARNING
$obj->undeclared = 1;     // passes silently without warning
$obj->unknownMethod();    // Fatal error (uncatchable by try/catch)

Fatal error would terminate the application without any possibility to react. Silent writing to non-existent members without warning could lead to serious errors that were difficult to detect. Nette\Object caught all these cases and threw a MemberAccessException, allowing programmers to react to and handle these errors:

echo $obj->undeclared;   // throws Nette\MemberAccessException
$obj->undeclared = 1;    // throws Nette\MemberAccessException
$obj->unknownMethod();   // throws Nette\MemberAccessException

Since PHP 7.0, the language no longer causes uncatchable fatal errors, and since PHP 8.2, access to undeclared members is considered an error.

“Did you mean?” Helper

Nette\Object came with a very convenient feature: intelligent suggestions for typos. When a developer made a mistake in a method or variable name, it not only reported the error but also offered help by suggesting the correct name. This iconic message, known as “did you mean?”, saved programmers hours of hunting for typos:

class Foo extends Nette\Object
{
	public static function from($var)
	{
	}
}

$foo = Foo::form($var);
// throws Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"

While PHP itself doesn't have any form of “did you mean?”, this feature is now provided by Tracy. It can even auto-fix such errors.

Properties with Controlled Access

A significant innovation that SmartObject brought to PHP was properties with controlled access. This concept, common in languages like C# or Python, allowed developers to elegantly control access to object data and ensure their consistency. Properties are a powerful tool of object-oriented programming. They function like variables but are actually represented by methods (getters and setters). This allows input validation or value generation at the time of reading.

To use properties, you must:

  • Add the annotation @property <type> $xyz to the class
  • Create a getter named getXyz() or isXyz(), a setter named setXyz()
  • Ensure the getter and setter are public or protected. They are optional – thus can exist as read-only or write-only properties

Let's look at a practical example using the Circle class, where we'll use properties to ensure that the radius is always non-negative. We'll replace public $radius with a property:

/**
 * @property float $radius
 * @property-read bool $visible
 */
class Circle
{
	use Nette\SmartObject;

	private float $radius = 0.0; // not public!

	// getter for $radius property
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter for $radius property
	protected function setRadius(float $radius): void
	{
		// sanitize the value before saving
		$this->radius = max(0.0, $radius);
	}

	// getter for $visible property
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10;  // actually calls setRadius(10)
echo $circle->radius;  // calls getRadius()
echo $circle->visible; // calls isVisible()

Since PHP 8.4, the same functionality can be achieved using property hooks, which offer a much more elegant and concise syntax:

class Circle
{
	public float $radius = 0.0 {
		set => max(0.0, $value);
	}

	public bool $visible {
		get => $this->radius > 0;
	}
}

Extension Methods

Nette\Object brought another interesting concept to PHP inspired by modern programming languages – extension methods. This feature, borrowed from C#, allowed developers to elegantly extend existing classes with new methods without modifying them or inheriting from them. For instance, you could add an addDateTime() method to a form that adds a custom DateTimePicker:

Form::extensionMethod(
	'addDateTime',
	fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);

$form = new Form;
$form->addDateTime('date');

Extension methods proved impractical because editors wouldn't suggest their names and would instead report that the method doesn't exist. Therefore, their support was discontinued. Today, it's more common to use composition or inheritance to extend class functionality.

Getting Class Name

SmartObject offered a simple method for getting the class name:

$class = $obj->getClass(); // using Nette\Object
$class = $obj::class;      // since PHP 8.0

Reflection and Annotation Access

Nette\Object provided access to reflection and annotations through methods getReflection() and getAnnotation(). This approach significantly simplified working with class meta-information:

/**
 * @author John Doe
 */
class Foo extends Nette\Object
{
}

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // returns 'John Doe'

Since PHP 8.0, it's possible to access meta-information through attributes, which offer even more possibilities and better type checking:

#[Author('John Doe')]
class Foo
{
}

$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];

Method Getters

Nette\Object offered an elegant way to pass methods as if they were variables:

class Foo extends Nette\Object
{
	public function adder($a, $b)
	{
		return $a + $b;
	}
}

$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5

Since PHP 8.1, you can use the first-class callable syntax, which takes this concept even further:

$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5

Events

SmartObject offers simplified syntax for working with events. Events allow objects to inform other parts of the application about changes in their state:

class Circle extends Nette\Object
{
	public array $onChange = [];

	public function setRadius(float $radius): void
	{
		$this->onChange($this, $radius);
		$this->radius = $radius;
	}
}

The code $this->onChange($this, $radius) is equivalent to the following loop:

foreach ($this->onChange as $callback) {
	$callback($this, $radius);
}

For clarity, we recommend avoiding the magic $this->onChange() method. A practical replacement is the Nette\Utils\Arrays::invoke function:

Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);
version: 4.0 3.x 2.x