SmartObject: PHP Object Enhancements

SmartObject extends PHP's object capabilities with a few syntactic candies. Do you like candy? Read and learn

  • why it is good to use Nette\SmartObject
  • what are properties
  • how to trigger events

In this chapter, we are focusing on Nette\SmartObject, a trait which extends PHP's object capabilities. This trait is used by almost all classes in the Nette Framework. At the same time, it is transparent enough to be used in your classes. Let's try to say why you should do it.

Trait Nette\SmartObject is a simplified successor to the obsolete class Nette\Object from Nette 2.x.

Installation:

composer require nette/utils

Strict Classes

PHP gives huge freedom to developers, which makes it a perfect language for making mistakes. But you can stop this bad behavior and start writing applications without hard-to-discover bugs. Do you wonder how? It's really simple – you just need to have stricter rules.

Can you find an error in this example?

class Circle
{
	public $radius = 0.0;

	public function getArea(): float
	{
		return $this->radius * $this->radius * M_PI;
	}
}

$circle = new Circle;
$circle->raduis = 10;
echo $circle->getArea(); // 10² * π ≈ 314

On the first look, it seems that code will print out 314; but it returns 0. How is this even possible? Accidentally, $circle->radius was mistyped to raduis. Just a small typo, which will give you a hard time correcting it because PHP does not say a thing when something is wrong. Not even a Warning or Notice error message. Because PHP does not think it is an error.

The mentioned mistake could be corrected immediately, if class Circle would use Nette\SmartObject:

class Circle
{
	use Nette\SmartObject;
}

Whereas the former code executed successfully (although it contained an error), the latter did not:

Trait Nette\SmartObject made Circle more strict and threw an exception when you tried to access an undeclared property. And Tracy displayed an error message about it. Line of code with a fatal typo is now highlighted and the error message has a meaningful description: Cannot write to an undeclared property Circle::$raduis, did you mean $radius?. The programmer can now fix the mistake he might have otherwise missed and which could be a real pain to find later.

One of many remarkable abilities of Nette\SmartObject is throwing exceptions when accessing undeclared members.

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

But it has much more to offer!

Use SmartObject only for base classes that do not inherit from anyone, the functionality will be forwarded in all its descendants.

Did You Mean?

If you make a typo when accessing an object variable or when calling a method, an exception is thrown and tries to tell you where the error is. It contains the iconic “did you mean?” addendum.

class Foo
{
	use Nette\SmartObject;

	public static function from($var)
	{
		// ...
	}
}

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

Some typos can be literally invisible. The brain is used to seeing both form and from, and it can easily happen that you look at the method name and just don't see the error, even if you don't have dyslexia. But if you read Foo::form(), did you mean from()? in the exception text, you will immediately realize the typo.

SmartObject includes in the suggestions not only all methods and properties of the class, but also magic/virtual members defined by the @method and @property annotations. And best of all, Tracy can fix these errors automatically fix.

Properties, Getters and Setters

(For more experienced programmers)

In modern object-oriented languages (eg C#, Python, Ruby, JavaScript), the term property refers to special members of a classes, which look like variables but are represented by methods. When reading or assigning values to those “variables”, methods are called instead (so-called getters and setters). It is a really useful feature, which allows us to control access to these variables. Using this we can validate inputs or postpone the computation of values of these variables to the time when it is actually accessed.

Any class that uses Nette\SmartObject acquires the ability to imitate properties. How to do it?

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

We will make use of properties in the class Circle to make sure the variable $radius contains only non-negative numbers. We will replace public $ radius with the property:

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

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

	// 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;  // calls setRadius(10)
echo $circle->radius;  // calls getRadius()
echo $circle->visible; // calls isVisible()

Properties are mostly a syntactic sugar to beautify the code and make programmer's life easier. You do not have to use them, if you don't want to.

Events

(For more experienced programmers)

Now we are going to create functions, which will be called when radius changes. Let's call it change event and those functions event handlers:

class Circle
{
	use Nette\SmartObject;

	/** @var array */
	public $onChange = [];

	public function setRadius(float $radius): void
	{
		// it calls callbacks in $onChange with parameters $this, $radius
		$this->onChange($this, $radius);
		// better: Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);

		$this->radius = max(0.0, $radius);
	}
}

$circle = new Circle;

// adding an event handler
$circle->onChange[] = function (Circle $circle, float $newValue): void {
	echo 'there was a change!';
};

$circle->setRadius(10);

In the code of the setRadius method, you see syntactic sugar – instead of iteration on $onChange array and calling each callbacks, you just have to write simple onChange(...) and specify the parameters that are passed to each callback. So SmartObject creates a fictitious onChange() method named after the $onChange array. A convention of on + word is required.

For the sake of clarity of the code, we recommend that you avoid this syntactic sugar and use the function Nette\Utils\Arrays::invoke to call callbacks.

version: 4.0 3.x 2.x