SmartObject
SmartObject mejoró durante muchos años el comportamiento de los objetos en PHP. Desde PHP 8.4, todas sus funciones forman parte nativa del propio PHP, con lo que ha culminado su misión histórica como pionero del enfoque orientado a objetos moderno en PHP.
Instalación:
composer require nette/utils
SmartObject nació en 2007 como una solución revolucionaria a las carencias del modelo de objetos de PHP de entonces. En una época en que PHP arrastraba numerosos problemas de diseño orientado a objetos, aportó mejoras notables y simplificó el trabajo de los desarrolladores. Se convirtió en una pieza legendaria de Nette Framework. SmartObject ofrecía funcionalidad que PHP no adquirió hasta muchos años después: desde el control de acceso a las propiedades de los objetos hasta un sofisticado azúcar sintáctico. Con la llegada de PHP 8.4 cumplió su misión histórica, ya que la mayoría de sus funciones pasaron a formar parte nativa del lenguaje. Se adelantó al desarrollo de PHP en unos impresionantes 17 años.
Técnicamente, SmartObject pasó por una evolución interesante. Al principio se implementó como la clase
Nette\Object, de la que las demás clases heredaban la funcionalidad necesaria. Un cambio importante llegó con PHP
5.4, que introdujo el soporte de traits. Eso permitió transformarlo en el trait Nette\SmartObject y ganar
flexibilidad: los desarrolladores podían usar la funcionalidad incluso en clases que ya heredaban de otra. Mientras que la clase
original Nette\Object dejó de existir con PHP 7.2 (que prohibió nombrar clases con la palabra Object),
el trait Nette\SmartObject sigue vivo.
Repasemos las funciones que ofrecían Nette\Object y, más tarde, Nette\SmartObject. Cada una de
ellas supuso en su momento un paso adelante importante en la programación orientada a objetos en PHP.
Estados de error coherentes
Uno de los problemas más acuciantes del PHP inicial era el comportamiento incoherente al trabajar con objetos.
Nette\Object puso orden y previsibilidad en ese caos. Veamos cómo se comportaba PHP originalmente:
echo $obj->undeclared; // E_NOTICE, más tarde E_WARNING
$obj->undeclared = 1; // pasa en silencio sin advertencia
$obj->unknownMethod(); // Error fatal (no se puede capturar con try/catch)
Un error fatal terminaba la aplicación sin posibilidad alguna de reaccionar. Escribir en silencio en miembros inexistentes,
sin aviso, podía provocar errores graves difíciles de detectar. Nette\Object capturaba todos esos casos y lanzaba
una MemberAccessException, lo que permitía a los programadores reaccionar ante los errores y tratarlos:
echo $obj->undeclared; // lanza Nette\MemberAccessException
$obj->undeclared = 1; // lanza Nette\MemberAccessException
$obj->unknownMethod(); // lanza Nette\MemberAccessException
Desde PHP 7.0, el lenguaje ya no provoca errores fatales incapturables y, desde PHP 8.2, el acceso a miembros no declarados se considera un error.
Ayuda „did you mean?“
Nette\Object traía una función muy cómoda: sugerencias inteligentes ante las erratas. Cuando un desarrollador
se equivocaba en el nombre de un método o de una variable, no solo informaba del error, sino que le echaba una mano
sugiriéndole el nombre correcto. Este mensaje icónico, conocido como „did you mean?“, ahorró a los programadores horas de
caza de erratas:
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// lanza Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"
Aunque el PHP actual no tiene ninguna forma de „did you mean?“, Tracy sabe añadir ese complemento a los errores. E incluso sabe corregirlos automáticamente.
Propiedades con acceso controlado
Una innovación importante que SmartObject aportó a PHP fueron las propiedades con acceso controlado. Este concepto, habitual en lenguajes como C# o Python, permitía a los desarrolladores controlar con elegancia el acceso a los datos del objeto y garantizar su coherencia. Las propiedades son una herramienta potente de la programación orientada a objetos. Funcionan como variables, pero en realidad están representadas por métodos (getters y setters). Eso permite validar la entrada o generar el valor en el momento de la lectura.
Para usar las propiedades había que:
- Añadir a la clase la anotación
@property <type> $xyz - Crear un getter llamado
getXyz()oisXyz(), y un setter llamadosetXyz() - Asegurar que el getter y el setter fueran public o protected. Eran opcionales, así que podían existir propiedades de solo lectura o de solo escritura
Veamos un ejemplo práctico con la clase Circle, donde usaremos propiedades para garantizar que el radio nunca sea
negativo. Sustituimos public $radius por una propiedad:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // ¡no es public!
// getter de la propiedad $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter de la propiedad $radius
protected function setRadius(float $radius): void
{
// sanea el valor antes de guardarlo
$this->radius = max(0.0, $radius);
}
// getter de la propiedad $visible
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // en realidad llama a setRadius(10)
echo $circle->radius; // llama a getRadius()
echo $circle->visible; // llama a isVisible()
Desde PHP 8.4, la misma funcionalidad se puede lograr con los property hooks, que ofrecen una sintaxis mucho más elegante y concisa:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Métodos de extensión
Nette\Object trajo a PHP otro concepto interesante, inspirado en los lenguajes de programación modernos: los
métodos de extensión. Esta función, tomada de C#, permitía a los desarrolladores ampliar con elegancia las clases existentes
con métodos nuevos, sin modificarlas ni heredar de ellas. Podía, por ejemplo, añadir a un formulario un método
addDateTime() que insertara un DateTimePicker propio:
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
Los métodos de extensión resultaron poco prácticos, porque los editores de código no sugerían sus nombres y, al contrario, avisaban de que el método no existía. Por eso se dejó de darles soporte. Hoy es más habitual usar composición o herencia para ampliar la funcionalidad de las clases.
Obtener el nombre de la clase
SmartObject ofrecía un método sencillo para obtener el nombre de la clase:
$class = $obj->getClass(); // con Nette\Object
$class = $obj::class; // desde PHP 8.0
Acceso a la reflexión y a las anotaciones
Nette\Object daba acceso a la reflexión y a las anotaciones mediante los métodos getReflection() y
getAnnotation(). Este enfoque simplificaba notablemente el trabajo con la metainformación de las clases:
/**
* @author John Doe
*/
class Foo extends Nette\Object
{
}
$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // devuelve 'John Doe'
Desde PHP 8.0 se puede acceder a la metainformación mediante atributos, que ofrecen aún más posibilidades y mejor comprobación de tipos:
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
Getters de métodos
Nette\Object ofrecía una forma elegante de pasar métodos como si fueran 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
Desde PHP 8.1 puede usar la sintaxis callable de primera clase, que lleva este concepto aún más lejos:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
Eventos
SmartObject ofrece una sintaxis simplificada para trabajar con eventos. Los eventos permiten a los objetos informar a otras partes de la aplicación de los cambios en su estado:
class Circle
{
use Nette\SmartObject;
public array $onChange = [];
private float $radius = 0.0;
public function setRadius(float $radius): void
{
$this->onChange($this, $radius);
$this->radius = $radius;
}
}
El código $this->onChange($this, $radius) equivale al siguiente bucle:
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
Por claridad, recomendamos evitar el método mágico $this->onChange(). Un sustituto práctico es la función
Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);