SmartObject and StaticClass

SmartObject adds support for property to PHP classes. StaticClass is used to denote static classes.

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.

Static Classes

Static classes, i.e. classes that are not intended to be instantiated, can be marked with the trait Nette\StaticClass:

class Strings
{
	use Nette\StaticClass;
}

When you try to create an instance, the Error exception is thrown, indicating that the class is static.

A Look into the History

SmartObject used to improve and fix class behavior in many ways, but the evolution of PHP has made most of the original features redundant. So the following is a look into the history of how things have evolved.

From the beginning, the PHP object model suffered from a number of serious flaws and inefficiencies. This was the reason for the creation of the Nette\Object class (in 2007), which attempted to remedy them and improve the experience of using PHP. It was enough for other classes to inherit from it, and gain the benefits it brought. When PHP 5.4 came with trait support, the Nette\Object class was replaced by Nette\SmartObject. Thus, it was no longer necessary to inherit from a common ancestor. In addition, trait could be used in classes that already inherited from another class. The final end of Nette\Object came with the release of PHP 7.2, which forbade classes to be named Object.

As PHP development went on, the object model and language capabilities were improved. The individual functions of the SmartObject class became redundant. Since the release of PHP 8.2, the only feature that remains that is not yet directly supported in PHP is the ability to use so-called properties.

What features did Nette\Object and Nette\Object once offer? Here is an overview. (The examples use the Nette\Object class, but most of the properties 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)

Fatal error terminated the application without any possibility to react. Silently writing to non-existent members without warning could lead to serious errors that were difficult to detect. Nette\Object All of these cases were caught and an exception MemberAccessException was thrown.

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

Since PHP 7.0, PHP no longer causes not catchable fatal errors, and accessing undeclared members has been a bug since 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);
// throw Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"

Today's PHP may not have any form of “did you mean?”, but Tracy adds this addendum to errors. And it can even fix such errors itself.

Extension methods

Inspired by extension methods from C#. They gave the possibility to add new methods to existing classes. For example, you could add the addDateTime() method to a form to add your own DateTimePicker.

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

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

Extension methods proved to be impractical because their names was not autocompleted by editors, instead they reported that the method did not exist. Therefore, their support was discontinued.

Getting the Class Name

$class = $obj->getClass(); // using Nette\Object
$class = $obj::class;      // since 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 the sake of clarity we recommend to avoid the magic method $this->onChange(). A practical substitute is the Nette\Utils\Arrays::invoke function:

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