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() o isXyz(), un setter con el nombre setXyz()
  • Asegurarte de que el getter y el setter sean public o protected. 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);
versión: 4.0