SmartObject: Mejoras en los objetos PHP

SmartObject extiende las capacidades de objetos de PHP con algunos dulces sintácticos. ¿Le gustan los dulces? Lea y aprenda

  • por qué es bueno usar Nette\SmartObject
  • qué son las *propiedades
  • cómo activar eventos

En este capítulo, nos centraremos en Nette\SmartObject, un rasgo que extiende las capacidades de objetos de PHP. Este trait es usado por casi todas las clases en el Nette Framework. Al mismo tiempo, es lo suficientemente transparente como para ser usado en tus clases. Intentemos decir por qué deberías hacerlo.

Trait Nette\SmartObject es un sucesor simplificado de la clase obsoleta Nette\Object de Nette 2.x.

Instalación:

composer require nette/utils

Clases estrictas

PHP da una enorme libertad a los desarrolladores, lo que lo convierte en un lenguaje perfecto para cometer errores. Pero usted puede detener este mal comportamiento y comenzar a escribir aplicaciones sin errores difícilmente descubribles. ¿Se pregunta cómo? Es realmente simple – sólo necesitas tener reglas más estrictas.

¿Puedes encontrar un error en este ejemplo?

class Circle
{
	public $radius = 0.0;

	public function getArea(): float
	{
		return $this->radius * $this->radius * M_PI;
	}
}

$circle = new Circle;
$circle->raduis = 10;
echo $circle->getArea(); // 10² * π ≈ 314

A primera vista, parece que el código imprimirá 314; pero devuelve 0. ¿Cómo es posible? Accidentalmente, $circle->radius fue mal escrito a raduis. Sólo un pequeño error tipográfico, que le costará corregir porque PHP no dice nada cuando algo está mal. Ni siquiera un mensaje de error de Advertencia o Aviso. Porque PHP no cree que sea un error.

El error mencionado podría ser corregido inmediatamente, si la clase Circle usara Nette\SmartObject:

class Circle
{
	use Nette\SmartObject;
}

Mientras que el primer código se ejecutó correctamente (aunque contenía un error), el segundo no:

Trait Nette\SmartObject hacía Circle más estricto y lanzaba una excepción cuando se intentaba acceder a una propiedad no declarada. Y Tracy mostraba un mensaje de error al respecto. La línea de código con una errata fatal está ahora resaltada y el mensaje de error tiene una descripción significativa: Cannot write to an undeclared property Circle::$raduis, ¿querías decir $radius?. El programador puede ahora corregir el error que de otro modo podría haber pasado por alto y que podría ser un verdadero dolor de cabeza encontrar más tarde.

Una de las muchas capacidades notables de Nette\SmartObject es lanzar excepciones cuando se accede a miembros no declarados.

$circle = new Circle;
echo $circle->undeclared; // throws Nette\MemberAccessException
$circle->undeclared = 1; // throws Nette\MemberAccessException
$circle->unknownMethod(); // throws Nette\MemberAccessException

Pero tiene mucho más que ofrecer.

Utilice SmartObject sólo para las clases base que no heredan de nadie, la funcionalidad se transmitirá en todos sus descendientes.

¿Ha querido decir?

Si cometes un error tipográfico al acceder a una variable de objeto o al llamar a un método, se lanza una excepción que intenta indicarte dónde está el error. Contiene el icónico apéndice “¿querías decir?”.

class Foo
{
	use Nette\SmartObject;

	public static function from($var)
	{
		// ...
	}
}

$foo = Foo::form($var);
// it throws Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"

Algunas erratas pueden ser literalmente invisibles. El cerebro está acostumbrado a ver tanto form como from, y puede ocurrir fácilmente que usted mire el nombre del método y simplemente no vea el error, incluso si no tiene dislexia. Pero si lee Foo::form(), did you mean from()? en el texto de la excepción, se dará cuenta inmediatamente de la errata.

SmartObject incluye en las sugerencias no sólo todos los métodos y propiedades de la clase, sino también los miembros mágicos/virtuales definidos por las anotaciones @method y @property. Y lo mejor de todo es que Tracy puede corregir estos errores automáticamente.

Propiedades, Getters y Setters

(Para programadores más experimentados)

En los lenguajes modernos orientados a objetos (p.e. C#, Python, Ruby, JavaScript), el término propiedad se refiere a miembros especiales de una clase, que parecen variables pero están representados por métodos. Cuando se leen o asignan valores a esas “variables”, en su lugar se llama a métodos (los llamados getters y setters). Es una característica realmente útil, que nos permite controlar el acceso a estas variables. Usando esto podemos validar entradas o posponer el cálculo de valores de estas variables al momento en que realmente se accede a ellas.

Cualquier clase que utilice Nette\SmartObject adquiere la capacidad de imitar propiedades. ¿Cómo hacerlo?

  • Añade una anotación a la clase de la forma @property <type> $xyz
  • Crea un getter llamado getXyz() o isXyz(), un setter llamado setXyz()
  • Getter y setter deben ser publicprotected y ambos son opcionales, por lo que puede haber propiedades read-only o *write-only

Haremos uso de las propiedades de la clase Circle para asegurarnos de que la variable $radius sólo contiene números no negativos. Sustituiremos public $ radius por la propiedad

/**
 * @property float $radius
 * @property-read bool $visible
 */
class Circle
{
	use Nette\SmartObject;

	private float $radius = 0.0; // ¡ya no es público!

	// getter for property $radius
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter for property $radius
	protected function setRadius(float $radius): void
	{
		// sanitizing value before saving it
		$this->radius = max(0.0, $radius);
	}

	// getter for property $visible
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10;  // calls setRadius(10)
echo $circle->radius;  // calls getRadius()
echo $circle->visible; // calls isVisible()

Las propiedades son principalmente un azúcar sintáctico para embellecer el código y hacer la vida del programador más fácil. Usted no tiene que usarlos, si no quieres.

Eventos

(Para programadores más experimentados)

Ahora vamos a crear funciones, que serán llamadas cuando radius cambie. Vamos a llamarlo change evento y esas funciones manejadores de eventos:

class Circle
{
	use Nette\SmartObject;

	public array $onChange = [];

	public function setRadius(float $radius): void
	{
		// it calls callbacks in $onChange with parameters $this, $radius
		$this->onChange($this, $radius);
		// better: Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);

		$this->radius = max(0.0, $radius);
	}
}

$circle = new Circle;

// adding an event handler
$circle->onChange[] = function (Circle $circle, float $newValue): void {
	echo 'there was a change!';
};

$circle->setRadius(10);

En el código del método setRadius, ves azúcar sintáctico – en lugar de iterar sobre el array $onChange y llamar a cada callback, sólo tienes que escribir un simple onChange(...) y especificar los parámetros que se pasan a cada callback. Así, SmartObject crea un método ficticio onChange() con el nombre del array $onChange. Se requiere una convención de on + palabra.

En aras de la claridad del código, le recomendamos que evite este azúcar sintáctico y utilice la función Nette\Utils\Arrays::invoke para llamar a los callbacks.

Clases estáticas

Puede marcar las clases estáticas, es decir, las clases que no están destinadas a ser instanciadas, con Nette\StaticClass trait:

class Strings
{
	use Nette\StaticClass;
}

Al intentar instanciarla, se lanza una excepción Error con la información de que la clase especificada es static.