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 de ser 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 de 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 sofisticados azúcares sintácticos. Con la llegada de PHP 8.4, completó su misión histórica, ya que todas sus funciones se convirtieron en parte nativa del lenguaje. Se adelantó al desarrollo de PHP unos notables 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 sigue vivo.

Repasemos las características que ofrecían en su día Nette\Object y más tarde Nette\SmartObject. Cada una de estas funciones representó en su momento un importante paso adelante en el campo de la programación orientada a objetos en PHP.

Estados de error consistentes

Uno de los problemas más acuciantes 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 informe
$obj->unknownMethod();    // Error fatal (no capturable con try/catch)

Un error fatal terminaba la aplicación sin posibilidad de reaccionar de ninguna manera. La escritura silenciosa en miembros inexistentes sin previo aviso 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 agradable: 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 buscando 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 ninguna forma de „¿quisiste decir?“, Tracy puede añadir este sufijo 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 elegantemente el acceso a los datos del objeto y asegurar su consistencia. Las propiedades son una herramienta poderosa 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 solo en el momento de la lectura.

Para usar propiedades, debe:

  • Añadir una anotación a la clase con el formato @property <type> $xyz
  • Crear un getter llamado getXyz() o isXyz(), un setter llamado setXyz()
  • Asegurarse de que el getter y el setter sean publicprotected. Son opcionales, por lo que pueden existir como propiedades de solo lecturasolo escritura

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 el public $radius original 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 introdujo otro concepto interesante en PHP inspirado en lenguajes de programación modernos: los métodos de extensión. Esta característica, tomada de C#, permitió a los desarrolladores extender elegantemente clases existentes con nuevos métodos sin necesidad de modificarlas o heredar de ellas. Por ejemplo, podrías añadir un método addDateTime() a un formulario que añada 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 sus nombres no eran sugeridos por los editores; por el contrario, informaban que el método no existía. Por lo tanto, su soporte fue descontinuado. Hoy en día, es más común usar composición o herencia para extender 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 simple:

$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 anotaciones usando los métodos getReflection() y getAnnotation(). Este enfoque simplificó significativamente el trabajo con metainformación de 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