SmartObject
SmartObject mejoró el comportamiento de los objetos en PHP durante años. Desde la versión PHP 8.4, todas sus funciones ya forman parte del propio PHP, completando así su misión histórica como pionero en el enfoque moderno de objetos en PHP.
Instalación:
composer require nette/utils
SmartObject nació en 2007 como una solución revolucionaria a las deficiencias del modelo de objetos de PHP de entonces. En un momento en que PHP sufría numerosos problemas de diseño de objetos, aportó mejoras significativas y simplificó el trabajo para los desarrolladores. Se convirtió en una parte legendaria del framework Nette. Ofrecía funcionalidades que PHP no adquirió hasta muchos años después, desde el control de acceso a las propiedades de los objetos hasta sofisticado syntax sugar. Con la llegada de PHP 8.4, completó su misión histórica al convertirse todas sus funciones en parte nativa del lenguaje. Se adelantó al desarrollo de PHP la notable cifra de 17 años.
Técnicamente, SmartObject ha experimentado una interesante evolución. Originalmente se implementó como la clase
Nette\Object
, de la que otras clases heredaban la funcionalidad necesaria. Un cambio fundamental llegó con PHP 5.4,
que introdujo el soporte para traits. Esto permitió la transformación en el trait Nette\SmartObject
, lo que aportó
una 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
desapareció con la llegada de PHP 7.2 (que prohibió nombrar clases con
la palabra Object
), el trait Nette\SmartObject
perdura.
Repasemos las características que ofrecieron en su día Nette\Object
y más tarde Nette\SmartObject
.
Cada una de estas funciones representó en su momento un importante avance en el campo de la programación orientada a objetos
en PHP.
Estados de error consistentes
Uno de los problemas más apremiantes de los primeros PHP era el comportamiento inconsistente al trabajar con objetos.
Nette\Object
trajo orden y previsibilidad a este caos. Veamos cómo era el comportamiento original de PHP:
echo $obj->undeclared; // E_NOTICE, más tarde E_WARNING
$obj->undeclared = 1; // pasa silenciosamente sin notificación
$obj->unknownMethod(); // Error fatal (no capturable mediante try/catch)
Un error fatal terminaba la aplicación sin posibilidad alguna de reaccionar. La escritura silenciosa en miembros inexistentes
sin notificación podía provocar errores graves difíciles de detectar. Nette\Object
capturaba todos estos casos y
lanzaba la excepción MemberAccessException
, lo que permitía a los programadores reaccionar a los errores y
solucionarlos.
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.
Ayuda “¿Quisiste decir?”
Nette\Object
introdujo una característica muy útil: ayuda inteligente para errores tipográficos. 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
una mano amiga sugiriendo el nombre correcto. Este icónico mensaje, conocido como “¿quisiste decir?”, 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 el PHP actual no tiene una función similar a “¿quisiste decir?”, Tracy puede añadir este texto a los errores. E incluso corregir automáticamente tales 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 de forma elegante el acceso a los datos del objeto y asegurar su consistencia. Las propiedades son una herramienta potente en la programación orientada a objetos. Funcionan como variables, pero en realidad están representadas por métodos (getters y setters). Esto permite validar entradas o generar valores justo en el momento de la lectura.
Para usar propiedades, debes:
- Añadir a la clase una anotación con el formato
@property <type> $xyz
- Crear un getter con el nombre
getXyz()
oisXyz()
, un setter con el nombresetXyz()
- Asegurarte de que el getter y el setter sean
public
oprotected
. Son opcionales, por lo que pueden existir como propiedades de solo lectura (read-only) o solo escritura (write-only)
Veamos un ejemplo práctico con la clase Circle
, donde usaremos propiedades para asegurar que el radio sea siempre
un número no negativo. Reemplazaremos la propiedad pública $radius
original por una propiedad:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // ¡no es pública!
// getter para la propiedad $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter para la propiedad $radius
protected function setRadius(float $radius): void
{
// saneamos 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
introdujo otro concepto interesante en PHP inspirado en lenguajes de programación modernos: los
métodos de extensión (extension methods). Esta característica, tomada de C#, permitió a los desarrolladores extender
de forma elegante clases existentes con nuevos métodos sin necesidad de modificarlas ni heredar de ellas. Por ejemplo, podías
añadir un método addDateTime()
a un formulario que añadiera 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 ser poco prácticos porque sus nombres no eran sugeridos por los editores de código; al contrario, informaban que el método no existía. Por lo tanto, se dejó de darles soporte. Hoy en día, es más común usar composición o herencia para ampliar la funcionalidad de las clases.
Obtención del nombre de la clase
Para obtener el nombre de la clase, SmartObject ofrecía un método sencillo:
$class = $obj->getClass(); // usando Nette\Object
$class = $obj::class; // desde PHP 8.0
Acceso a la reflexión y anotaciones
Nette\Object
ofrecía acceso a la reflexión y a las anotaciones usando 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 en forma de atributos, que ofrecen aún más posibilidades y un mejor control de tipos:
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
Getters de método
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, es posible usar la llamada 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 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 razones de claridad, recomendamos evitar el método mágico $this->onChange()
. Una alternativa práctica
es, por ejemplo, la función Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);