SmartObject y StaticClass

SmartObject añade soporte para propiedades a las clases PHP. StaticClass se utiliza para denotar clases estáticas.

Instalación:

composer require nette/utils

Propiedades, getters y setters

En los lenguajes modernos orientados a objetos (por ejemplo, C#, Python, Ruby, JavaScript), el término propiedad se refiere a miembros especiales de las clases que parecen variables pero que en realidad están representados por métodos. Cuando se asigna o se lee el valor de esta “variable”, se llama al método correspondiente (llamado getter o setter). Esto es muy práctico, nos da un control total sobre el acceso a las variables. Podemos validar la entrada o generar resultados sólo cuando la propiedad es leída.

Las propiedades PHP no están soportadas, pero trait Nette\SmartObject puede imitarlas. ¿Cómo usarlo?

  • 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()
  • El getter y el setter deben ser publicprotected y son opcionales, por lo que puede haber una propiedad read-only o *write-only

Utilizaremos la propiedad de la clase Circle para asegurarnos de que sólo se introducen números no negativos en la variable $radius. Sustituye public $radius por propiedad:

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

	private float $radius = 0.0; // not public

	// 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;  // actually calls setRadius(10)
echo $circle->radius;  // calls getRadius()
echo $circle->visible; // calls isVisible()

Las propiedades son principalmente azúcar sintáctico, que pretende hacer la vida del programador más dulce simplificando el código. Si no las quieres, no tienes por qué usarlas.

Clases estáticas

Las clases estáticas, es decir, las clases que no están destinadas a ser instanciadas, pueden marcarse con el rasgo Nette\StaticClass:

class Strings
{
	use Nette\StaticClass;
}

Cuando se intenta crear una instancia, se lanza la excepción Error, indicando que la clase es estática.

Un vistazo a la historia

SmartObject solía mejorar y arreglar el comportamiento de las clases de muchas maneras, pero la evolución de PHP ha hecho que la mayoría de las características originales sean redundantes. Así que lo siguiente es una mirada a la historia de cómo han evolucionado las cosas.

Desde el principio, el modelo de objetos de PHP sufrió de un número de serios defectos e ineficiencias. Este fue el motivo de la creación de la clase Nette\Object (en 2007), que intentaba remediarlos y mejorar la experiencia de uso de PHP. Fue suficiente para que otras clases heredaran de ella y obtuvieran los beneficios que aportaba. Cuando PHP 5.4 llegó con soporte para traits, la clase Nette\Object fue reemplazada por Nette\SmartObject. Así, ya no era necesario heredar de un ancestro común. Además, trait podía usarse en clases que ya heredaban de otra clase. El fin definitivo de Nette\Object llegó con el lanzamiento de PHP 7.2, que prohibió que las clases se llamaran Object.

A medida que avanzaba el desarrollo de PHP, el modelo de objetos y las capacidades del lenguaje fueron mejorando. Las funciones individuales de la clase SmartObject se volvieron redundantes. Desde el lanzamiento de PHP 8.2, la única característica que aún no está soportada directamente en PHP es la capacidad de usar las llamadas propiedades.

¿Qué características ofrecían Nette\Object y Nette\Object? He aquí un resumen. (Los ejemplos usan la clase Nette\Object, pero la mayoría de las propiedades también se aplican al rasgo Nette\SmartObject ).

Errores incoherentes

PHP tenía un comportamiento inconsistente al acceder a miembros no declarados. El estado en el momento de Nette\Object era el siguiente:

echo $obj->undeclared; // E_NOTICE, later E_WARNING
$obj->undeclared = 1;  // passes silently without reporting
$obj->unknownMethod(); // Fatal error (not catchable by try/catch)

Un error fatal terminaba la aplicación sin posibilidad de reaccionar. La escritura silenciosa en miembros inexistentes sin previo aviso podía dar lugar a errores graves difíciles de detectar. Nette\Object Todos estos casos eran detectados y se lanzaba una excepción MemberAccessException.

echo $obj->undeclared;   // throw Nette\MemberAccessException
$obj->undeclared = 1;    // throw Nette\MemberAccessException
$obj->unknownMethod();   // throw Nette\MemberAccessException

Desde PHP 7.0, PHP ya no causa errores fatales no capturables, y acceder a miembros no declarados ha sido un error desde PHP 8.2.

¿Te refieres a?

Si se lanzaba un error Nette\MemberAccessException, quizás debido a un error tipográfico al acceder a una variable de objeto o al llamar a un método, Nette\Object intentaba dar una pista en el mensaje de error sobre cómo solucionar el error, en la forma del icónico apéndice “¿querías decir?”.

class Foo extends Nette\Object
{
	public static function from($var)
	{
	}
}

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

Puede que el PHP actual no tenga ninguna forma de “¿querías decir?”, pero Tracy añade este apéndice a los errores. E incluso puede corregir esos errores por sí mismo.

Métodos de extensión

Inspirado en los métodos de extensión de C#. Ofrecen la posibilidad de añadir nuevos métodos a clases existentes. Por ejemplo, podrías añadir el método addDateTime() a un formulario para añadir tu propio DateTimePicker.

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 autocompletados por los editores, sino que informaban de que el método no existía. Por lo tanto, su soporte fue descontinuado.

Obtención del nombre de la clase

$class = $obj->getClass(); // using Nette\Object
$class = $obj::class;      // since PHP 8.0

Acceso a la reflexión y a las anotaciones

Nette\Object ofrece acceso a la reflexión y a las anotaciones mediante los métodos getReflection() y getAnnotation():

/**
 * @author John Doe
 */
class Foo extends Nette\Object
{
}

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // returns 'John Doe

A partir de PHP 8.0, es posible acceder a meta-información en forma de atributos:

#[Author('John Doe')]
class Foo
{
}

$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];

Método getters

Nette\Object ofrecían una forma elegante de tratar los 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

A partir de PHP 8.1, se puede utilizar la llamada sintaxis callable de primera clase:

$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5

Eventos

Nette\Object ofrece azúcar sintáctico para activar el evento:

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:

foreach ($this->onChange as $callback) {
	$callback($this, $radius);
}

Por claridad, recomendamos evitar el método mágico $this->onChange(). Un buen sustituto es Nette\Utils\Arrays::invoke:

Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);
versión: 4.0