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() o isXyz(), un setter llamado setXyz()
  • Asegurar que el getter y setter sean publicprotected. Son opcionales – por lo tanto pueden existir como propiedades de solo lecturasolo 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);
versión: 4.0