SmartObject

SmartObject correggeva il comportamento degli oggetti in molti modi, ma il PHP di oggi include già la maggior parte di questi miglioramenti in modo nativo. Tuttavia, aggiunge ancora il supporto per le proprietà.

Installazione:

composer require nette/utils

Proprietà, Getter e Setter

Nei moderni linguaggi orientati agli oggetti (ad esempio, C#, Python, Ruby, JavaScript), il termine property si riferisce a membri speciali delle classi che sembrano variabili, ma in realtà sono rappresentati da metodi. Quando il valore di questa “variabile” viene assegnato o letto, viene richiamato il metodo corrispondente (chiamato getter o setter). Si tratta di una cosa molto comoda, che ci dà il pieno controllo sull'accesso alle variabili. Possiamo convalidare l'input o generare risultati solo quando la proprietà viene letta.

Le proprietà PHP non sono supportate, ma il trait Nette\SmartObject può imitarle. Come si usa?

  • Aggiungere un'annotazione alla classe nella forma @property <type> $xyz
  • Creare un getter chiamato getXyz() o isXyz(), un setter chiamato setXyz()
  • Il getter e il setter devono essere pubbliciprotetti e sono opzionali, quindi può esserci una proprietà di sola letturadi sola scrittura.

Utilizzeremo la proprietà per la classe Circle per garantire che nella variabile $radius vengano inseriti solo numeri non negativi. Sostituire public $radius con la proprietà:

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

	private float $radius = 0.0; // non pubblico

	// getter per la proprietà $radius
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter per la proprietà $radius
	protected function setRadius(float $radius): void
	{
		// sanitizza il valore prima di salvarlo
		$this->radius = max(0.0, $radius);
	}

	// getter per la proprietà $visible
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10; // in realtà chiama setRadius(10)
echo $circle->radius; // chiama getRadius()
echo $circle->visible; // chiama isVisible()

Le proprietà sono principalmente “zucchero sintattico” ((syntactic sugar)), che ha lo scopo di rendere più dolce la vita del programmatore semplificando il codice. Se non le si vuole, non è necessario usarle.

Uno sguardo alla storia

SmartObject era solito perfezionare il comportamento degli oggetti in molti modi, ma il PHP di oggi incorpora già la maggior parte di questi miglioramenti in modo nativo. Il testo che segue è uno sguardo nostalgico alla storia, che ci ricorda come si sono evolute le cose.

Fin dall'inizio, il modello a oggetti di PHP soffriva di una miriade di gravi mancanze e carenze. Questo ha portato alla creazione della classe Nette\Object (nel 2007), che mirava a correggere questi problemi e a migliorare il comfort dell'utilizzo di PHP. Bastava che altre classi ereditassero da essa per ottenere i vantaggi che offriva. Quando PHP 5.4 ha introdotto il supporto ai tratti, la classe Nette\Object è stata sostituita dal tratto Nette\SmartObject. Questo ha eliminato la necessità di ereditare da un antenato comune. Inoltre, il tratto poteva essere usato in classi che già ereditavano da un'altra classe. La fine definitiva di Nette\Object avvenne con il rilascio di PHP 7.2, che proibiva alle classi di chiamarsi Object.

Con il proseguire dello sviluppo di PHP, il suo modello di oggetti e le capacità del linguaggio sono migliorati. Diverse funzioni della classe SmartObject sono diventate superflue. Dal rilascio di PHP 8.2, rimane solo una caratteristica non supportata direttamente da PHP: la possibilità di usare le cosiddette proprietà.

Quali caratteristiche offrivano Nette\Object e, per estensione, Nette\SmartObject? Ecco una panoramica. (Negli esempi viene utilizzata la classe Nette\Object, ma la maggior parte delle caratteristiche si applicano anche al tratto Nette\SmartObject ).

Errori inconsistenti

PHP aveva un comportamento incoerente quando si accedeva a membri non dichiarati. Lo stato al momento di Nette\Object era il seguente:

echo $obj->undeclared; // E_NOTICE, successivamente E_WARNING
$obj->undeclared = 1; // passa in modo silenzioso, senza segnalare nulla
$obj->unknownMethod(); // Errore fatale (non catturabile con try/catch)

L'errore fatale terminava l'applicazione senza alcuna possibilità di reagire. Scrivere silenziosamente su membri inesistenti senza preavviso poteva portare a errori gravi, difficili da individuare. Nette\Object Tutti questi casi sono stati catturati ed è stata lanciata l'eccezione MemberAccessException.

echo $obj->undeclared; // lancia l'eccezione Nette\MemberAccessException
$obj->undeclared = 1; // lanciare l'eccezione Nette\MemberAccessException
$obj->unknownMethod(); // lanciare l'eccezione Nette\MemberAccessException

A partire da PHP 7.0, PHP non causa più errori fatali non catturabili e l'accesso a membri non dichiarati è un bug da PHP 8.2.

Intendevi dire?

Se veniva lanciato un errore Nette\MemberAccessException, magari a causa di un errore di battitura nell'accesso a una variabile oggetto o nella chiamata di un metodo, Nette\Object cercava di dare un suggerimento nel messaggio di errore su come correggere l'errore, sotto forma dell'iconico addendum "intendevi?

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()?"

Sebbene il PHP di oggi non disponga di una funzione di “hai inteso?”, questa frase può essere aggiunta agli errori da Tracy. Può anche correggere automaticamente tali errori.

Metodi di estensione

Ispirato ai metodi di estensione di C#. Offrono la possibilità di aggiungere nuovi metodi a classi esistenti. Ad esempio, si può aggiungere il metodo addDateTime() a un modulo per aggiungere il proprio DateTimePicker.

Form::extensionMethod(
	'addDateTime',
	fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);

$form = new Form;
$form->addDateTime('date');

I metodi di estensione si sono rivelati poco pratici, perché i loro nomi non venivano autocompilati dagli editor, che invece segnalavano che il metodo non esisteva. Pertanto, il loro supporto è stato interrotto.

Ottenere il nome della classe

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

Accesso alla riflessione e alle annotazioni

Nette\Object ha offerto l'accesso alla riflessione e alle annotazioni utilizzando i metodi getReflection() e getAnnotation():

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

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

A partire da PHP 8.0, è possibile accedere alle meta-informazioni sotto forma di attributi:

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

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

Metodi Getter

Nette\Object offre un modo elegante per trattare i metodi come se fossero variabili:

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 partire da PHP 8.1, è possibile utilizzare la cosiddetta sintassi callable di prima classe:

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

Eventi

Nette\Object ha offerto uno zucchero sintattico per innescare l'evento:

class Circle extends Nette\Object
{
	public array $onChange = [];

	public function setRadius(float $radius): void
	{
		$this->onChange($this, $radius);
		$this->radius = $radius;
	}
}

Il codice $this->onChange($this, $radius) è equivalente al seguente:

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

Per motivi di chiarezza si consiglia di evitare il metodo magico $this->onChange(). Un sostituto pratico è la funzione Nette\Utils\Arrays::invoke:

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