SmartObject

SmartObject solía arreglar el comportamiento de los objetos de muchas maneras, pero el PHP actual ya incluye la mayoría de estas mejoras de forma nativa. Sin embargo, aún añade soporte para property.

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.

Un vistazo a la Historia

SmartObject solía refinar el comportamiento de los objetos de numerosas maneras, pero el PHP actual ya incorpora la mayoría de estas mejoras de forma nativa. El siguiente texto es una mirada nostálgica a la historia, recordándonos como evolucionaron las cosas.

Desde sus inicios, el modelo de objetos de PHP sufrió de una miríada de serios defectos y deficiencias. Esto llevó a la creación de la clase Nette\Object (en 2007), que pretendía rectificar estos problemas y mejorar la comodidad de uso de PHP. Todo lo que se necesitaba era que otras clases heredaran de ella, y obtendrían los beneficios que ofrecía. Cuando PHP 5.4 introdujo el soporte para traits, la clase Nette\Object fue reemplazada por el trait Nette\SmartObject. Esto eliminó la necesidad de heredar de un ancestro común. Además, el trait podía ser usado 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 el desarrollo de PHP continuaba, su modelo de objetos y las capacidades del lenguaje mejoraron. Varias funciones de la clase SmartObject se volvieron redundantes. Desde el lanzamiento de PHP 8.2, sólo queda una característica no soportada directamente en PHP: la capacidad de usar las llamadas propiedades.

¿Qué características ofrecían Nette\Object y, por extensión, Nette\SmartObject? He aquí un resumen. (En los ejemplos, se usa la clase Nette\Object, pero la mayoría de las características 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()?"

Aunque el PHP actual no tiene la función “¿Querías decir?”, esta frase puede ser añadida a los errores por Tracy. Incluso puede autocorregir dichos errores.

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