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()
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.
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);