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()
orisXyz()
, a setter namedsetXyz()
- 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);