SmartObject
SmartObject mejoró durante años el comportamiento de los objetos en PHP. Desde la versión PHP 8.4, todas sus funciones forman parte nativa del propio PHP, completando así su misión histórica como pionero del enfoque orientado a objetos moderno en PHP.
Instalación:
composer require nette/utils
SmartObject surgió en 2007 como una solución revolucionaria a las deficiencias del modelo de objetos de PHP de la época. En un momento en que PHP sufría numerosos problemas con el diseño orientado a objetos, aportó mejoras significativas y simplificó el trabajo de los desarrolladores. Se convirtió en una parte legendaria del framework Nette. Ofrecía funcionalidades que PHP no obtendría hasta muchos años después: desde la validación del acceso a propiedades hasta el manejo sofisticado de errores. Con la llegada de PHP 8.4, completó su misión histórica, ya que todas sus funciones se convirtieron en partes nativas del lenguaje. Se adelantó al desarrollo de PHP en notables 17 años.
SmartObject pasó por una interesante evolución técnica. Inicialmente, se implementó como la clase
Nette\Object
, de la cual otras clases heredaban la funcionalidad necesaria. Un cambio significativo llegó con PHP
5.4, que introdujo el soporte para traits. Esto permitió la transformación al trait Nette\SmartObject
, aportando
mayor flexibilidad – los desarrolladores podían utilizar la funcionalidad incluso en clases que ya heredaban de otra clase.
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 vigente.
Veamos las características que Nette\Object
y posteriormente Nette\SmartObject
ofrecían. Cada una
de estas funciones representó un paso significativo en la programación orientada a objetos en PHP de su época.
Estados de Error Consistentes
Uno de los problemas más apremiantes del PHP temprano era el comportamiento inconsistente al trabajar con objetos.
Nette\Object
trajo orden y previsibilidad a este caos. Veamos cómo se comportaba PHP originalmente:
echo $obj->undeclared; // E_NOTICE, después E_WARNING
$obj->undeclared = 1; // pasa silenciosamente sin advertencia
$obj->unknownMethod(); // Error fatal (no capturable con try/catch)
El error fatal terminaba la aplicación sin posibilidad de reaccionar. La escritura silenciosa en miembros inexistentes sin
advertencia podía llevar a errores graves difíciles de detectar. Nette\Object
capturaba todos estos casos y lanzaba
una MemberAccessException
, permitiendo a los programadores reaccionar y manejar estos errores:
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 causa errores fatales no capturables, y desde PHP 8.2, el acceso a miembros no declarados se considera un error.
Ayudante “Did you mean?”
Nette\Object
vino con una característica muy conveniente: sugerencias inteligentes para errores de escritura.
Cuando un desarrollador cometía un error en el nombre de un método o variable, no solo informaba del error, sino que también
ofrecía ayuda sugiriendo el nombre correcto. Este mensaje icónico, conocido como “did you mean?”, ahorró a los
programadores horas de búsqueda de errores tipográficos:
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 PHP en sí no tiene ninguna forma de “did you mean?”, esta característica ahora la proporciona Tracy. Incluso puede auto-corregir estos errores.
Propiedades con Acceso Controlado
Una innovación significativa que SmartObject trajo a PHP fueron las propiedades con acceso controlado. Este concepto, común en lenguajes como C# o Python, permitió a los desarrolladores controlar elegantemente el acceso a los datos del objeto y asegurar su consistencia. Las propiedades son una herramienta poderosa de la programación orientada a objetos. Funcionan como variables pero en realidad están representadas por métodos (getters y setters). Esto permite validar las entradas o generar valores en el momento de la lectura.
Para usar propiedades, debes:
- Agregar la anotación
@property <type> $xyz
a la clase - Crear un getter llamado
getXyz()
oisXyz()
, un setter llamadosetXyz()
- Asegurar que el getter y setter sean public o protected. Son opcionales – por lo tanto pueden existir como propiedades de solo lectura o solo escritura
Veamos un ejemplo práctico usando la clase Circle, donde usaremos propiedades para asegurar que el radio sea siempre no
negativo. Reemplazaremos public $radius
con una propiedad:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // ¡no es public!
// getter para la propiedad $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter para la propiedad $radius
protected function setRadius(float $radius): void
{
// sanitizamos el valor antes de guardarlo
$this->radius = max(0.0, $radius);
}
// getter para 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, se puede lograr la misma funcionalidad usando 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 otro concepto interesante a PHP inspirado en lenguajes de programación modernos – los
métodos de extensión. Esta característica, tomada de C#, permitía a los desarrolladores extender elegantemente las clases
existentes con nuevos métodos sin modificarlas ni heredar de ellas. Por ejemplo, podías agregar un método
addDateTime()
a un formulario que añade un DateTimePicker personalizado:
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 no sugerían sus nombres y, en cambio, informaban que el método no existía. Por lo tanto, se descontinuó su soporte. Hoy en día, es más común usar composición o herencia para extender la funcionalidad de las clases.
Obtener el Nombre de la Clase
SmartObject ofrecía un método simple para obtener el nombre de la clase:
$class = $obj->getClass(); // usando Nette\Object
$class = $obj::class; // desde PHP 8.0
Acceso a Reflexión y Anotaciones
Nette\Object
proporcionaba acceso a la reflexión y anotaciones a través de los métodos
getReflection()
y getAnnotation()
. Este enfoque simplificó significativamente 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, es posible acceder a la metainformación a través de atributos, que ofrecen aún más posibilidades y mejor verificació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, puedes usar la first-class callable syntax, 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 sobre cambios en su estado:
class Circle extends Nette\Object
{
public array $onChange = [];
public function setRadius(float $radius): void
{
$this->onChange($this, $radius);
$this->radius = $radius;
}
}
El código $this->onChange($this, $radius)
es equivalente al siguiente bucle:
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
Por claridad, recomendamos evitar el método mágico $this->onChange()
. Un reemplazo práctico es la función
Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);