SmartObject

SmartObject used to fix objects behavior in many ways, but today's PHP already includes most of these improvements natively. However, it still adds support for properties.

Installation:

composer require nette/utils

Properties, Getters and Setters

In modern object-oriented languages (e.g. C#, Python, Ruby, JavaScript), the term property refers to special members of classes that look like variables but are actually represented by methods. When the value of this “variable” is assigned or read, the corresponding method (called getter or setter) is called. This is a very handy thing to do, it gives us full control over access to variables. We can validate the input or generate results only when the property is read.

PHP properties are not supported, but trait Nette\SmartObject can imitate them. How to use it?

  • Add an annotation to the class in the form @property <type> $xyz
  • Create a getter named getXyz() or isXyz(), a setter named setXyz()
  • The getter and setter must be public or protected and are optional, so there can be a read-only or write-only property

We will use the property for the Circle class to ensure that only non-negative numbers are put into the $radius variable. Replace public $radius with property:

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

	private float $radius = 0.0; // not public

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

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

	// getter for property $visible
	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()

Properties are primarily syntactic sugar, which is intended to make the programmer's life sweeter by simplifying the code. If you don't want them, you don't have to use them.

A Glimpse into History

SmartObject used to refine the behavior of objects in numerous ways, but today's PHP already incorporates most of these enhancements natively. The following text is a nostalgic look back at history, reminding us of how things evolved.

From its inception, PHP's object model suffered from a myriad of serious shortcomings and deficiencies. This led to the creation of the Nette\Object class (in 2007), which aimed to rectify these issues and enhance the comfort of using PHP. All that was needed was for other classes to inherit from it, and they would gain the benefits it offered. When PHP 5.4 introduced support for traits, the Nette\Object class was replaced by the Nette\SmartObject trait. This eliminated the need to inherit from a common ancestor. Moreover, the trait could be used in classes that already inherited from another class. The definitive end of Nette\Object came with the release of PHP 7.2, which prohibited classes from being named Object.

As PHP development continued, its object model and language capabilities improved. Various functions of the SmartObject class became redundant. Since the release of PHP 8.2, there remains only one feature not directly supported in PHP: the ability to use so-called properties.

What features did Nette\Object and, by extension, Nette\SmartObject offer? Here's an overview. (In the examples, the Nette\Object class is used, but most features also apply to the Nette\SmartObject trait).

Inconsistent Errors

PHP had inconsistent behavior when accessing undeclared members. The state at the time of Nette\Object was as follows:

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

A fatal error would terminate the application without any chance of response. Silently writing to non-existent members without warning could lead to serious errors that were hard to detect. Nette\Object caught all these cases and threw a MemberAccessException exception.

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

From PHP version 7.0 onwards, uncatchable fatal errors no longer occur, and accessing undeclared members becomes an error from PHP 8.2.

Did you mean?

If an Nette\MemberAccessException error was thrown, perhaps due to a typo when accessing an object variable or calling a method, Nette\Object attempted to give a hint in the error message on how to fix the error, in the form of the iconic “did you mean?” addendum.

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 today's PHP doesn't have a “did you mean?” feature, this phrase can be added to errors by Tracy. It can even auto-correct such errors.

Extension Methods

Inspired by the extension methods from the C# language, they provided the ability to add new methods to existing classes. For instance, you could add a addDateTime() method to a form, which would introduce a custom DateTimePicker.

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

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

Extension methods turned out to be impractical because their names were not suggested by editors; on the contrary, they reported that the method did not exist. Therefore, their support was discontinued.

Determining the Class Name

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

Access to Reflection and Annotations

Nette\Object offered access to reflection and annotation using the methods getReflection() and getAnnotation():

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

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

As of PHP 8.0, it is possible to access meta-information in the form of attributes:

#[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 deal with 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

As of PHP 8.1, you can use the so-called first-class callable syntax:

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

Events

Nette\Object offered syntactic sugar to trigger the event:

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:

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

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

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