SmartObject

SmartObject ha migliorato per molti anni il comportamento degli oggetti in PHP. Da PHP 8.4 tutte le sue funzionalità sono diventate parte nativa di PHP stesso, e così si è compiuta la sua missione storica di pioniere dell'approccio orientato agli oggetti moderno in PHP.

Installazione:

composer require nette/utils

SmartObject nacque nel 2007 come soluzione rivoluzionaria alle carenze del modello a oggetti di PHP dell'epoca. In un periodo in cui PHP affrontava numerosi problemi di progettazione orientata agli oggetti, portò migliorie significative e semplificò il lavoro degli sviluppatori. Divenne un componente leggendario di Nette Framework. SmartObject offriva funzionalità che PHP acquisì solo molti anni dopo, dal controllo degli accessi alle proprietà degli oggetti a un raffinato zucchero sintattico. Con l'uscita di PHP 8.4 ha compiuto la sua missione storica, perché la maggior parte delle sue funzionalità è diventata parte nativa del linguaggio. Era in anticipo sullo sviluppo di PHP di ben 17 anni.

Dal punto di vista tecnico SmartObject ha attraversato un'evoluzione interessante. All'inizio era implementato come classe Nette\Object, dalla quale le altre classi ereditavano le funzionalità necessarie. Un cambiamento importante arrivò con PHP 5.4, che introdusse il supporto ai trait. Questo permise la trasformazione nel trait Nette\SmartObject, con una flessibilità maggiore: gli sviluppatori potevano usare le funzionalità anche in classi che già ereditavano da un'altra classe. La classe originale Nette\Object ha cessato di esistere con PHP 7.2 (che ha vietato di chiamare le classi con la parola Object), ma il trait Nette\SmartObject vive ancora.

Vediamo le funzionalità che Nette\Object e poi Nette\SmartObject offrivano. Ognuna di esse rappresentava allora un passo avanti significativo nella programmazione orientata agli oggetti in PHP.

Stati di errore coerenti

Uno dei problemi più pressanti dei primi PHP era il comportamento incoerente nel lavoro con gli oggetti. Nette\Object portò ordine e prevedibilità in questo caos. Vediamo come si comportava originariamente PHP:

echo $obj->undeclared;    // E_NOTICE, poi E_WARNING
$obj->undeclared = 1;     // passa in silenzio, senza avvisi
$obj->unknownMethod();    // Fatal error (non intercettabile con try/catch)

Un errore fatale interrompeva l'applicazione senza alcuna possibilità di reagire. La scrittura silenziosa su membri inesistenti, senza avvisi, poteva portare a errori gravi e difficili da individuare. Nette\Object intercettava tutti questi casi e sollevava una MemberAccessException, permettendo ai programmatori di reagire a questi errori e di gestirli:

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

Da PHP 7.0 il linguaggio non causa più errori fatali non intercettabili e da PHP 8.2 l'accesso a membri non dichiarati è considerato un errore.

L'aiuto “intendevi forse?”

Nette\Object portò con sé una funzionalità molto comoda: i suggerimenti intelligenti per gli errori di battitura. Quando uno sviluppatore sbagliava il nome di un metodo o di una variabile, non si limitava a segnalare l'errore, ma offriva aiuto suggerendo il nome corretto. Questo iconico messaggio, noto come “did you mean?”, ha risparmiato ai programmatori ore di caccia agli errori di battitura:

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

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

PHP attuale non ha alcuna forma di “did you mean?”, ma questo suffisso può essere aggiunto agli errori da Tracy. E può perfino correggere automaticamente errori del genere.

Proprietà con accesso controllato

Un'innovazione importante che SmartObject portò in PHP furono le proprietà con accesso controllato. Questo concetto, comune in linguaggi come C# o Python, permetteva agli sviluppatori di controllare elegantemente l'accesso ai dati degli oggetti e di garantirne la coerenza. Le proprietà sono uno strumento potente della programmazione orientata agli oggetti. Funzionano come variabili, ma in realtà sono rappresentate da metodi (getter e setter). Questo permette di validare l'input o di generare il valore al momento della lettura.

Per usare le proprietà bisognava:

  • aggiungere alla classe l'annotazione @property <type> $xyz
  • creare un getter chiamato getXyz() o isXyz() e un setter chiamato setXyz()
  • assicurarsi che il getter e il setter fossero publicprotected. Erano facoltativi, quindi potevano esistere proprietà di sola lettura o di sola scrittura

Vediamo un esempio pratico con la classe Circle, dove useremo una proprietà per garantire che il raggio non sia mai negativo. Sostituiamo public $radius con una proprietà:

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

	private float $radius = 0.0; // non public!

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

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

	// getter della 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()

Da PHP 8.4 lo stesso risultato si ottiene con i property hook, che offrono una sintassi molto più elegante e concisa:

class Circle
{
	public float $radius = 0.0 {
		set => max(0.0, $value);
	}

	public bool $visible {
		get => $this->radius > 0;
	}
}

Metodi di estensione

Nette\Object portò in PHP un altro concetto interessante, ispirato ai linguaggi di programmazione moderni: i metodi di estensione. Questa funzionalità, presa in prestito dal C#, permetteva agli sviluppatori di estendere elegantemente le classi esistenti con nuovi metodi, senza modificarle né ereditare da esse. Potevate per esempio aggiungere a un form un metodo addDateTime() che aggiunge un DateTimePicker personalizzato:

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 suggeriti dagli editor di codice, che segnalavano invece che il metodo non esisteva. Il loro supporto è quindi stato interrotto. Oggi è più comune usare la composizione o l'ereditarietà per estendere le funzionalità di una classe.

Ottenere il nome della classe

SmartObject offriva un metodo semplice per ottenere il nome della classe:

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

Accesso alla reflection e alle annotazioni

Nette\Object offriva l'accesso alla reflection e alle annotazioni tramite i metodi getReflection() e getAnnotation(). Questo approccio semplificava notevolmente il lavoro con le meta-informazioni delle classi:

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

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

Da PHP 8.0 è possibile accedere alle meta-informazioni tramite gli attributi, che offrono ancora più possibilità e un migliore controllo dei tipi:

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

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

Getter dei metodi

Nette\Object offriva un modo elegante di passare 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

Da PHP 8.1 potete usare la first-class callable syntax, che porta questo concetto ancora più avanti:

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

Eventi

SmartObject offre una sintassi semplificata per lavorare con gli eventi. Gli eventi permettono agli oggetti di informare altre parti dell'applicazione dei cambiamenti del proprio stato:

class Circle
{
	use Nette\SmartObject;

	public array $onChange = [];

	private float $radius = 0.0;

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

Il codice $this->onChange($this, $radius) equivale al ciclo seguente:

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

Per chiarezza consigliamo di evitare il metodo magico $this->onChange(). Una sostituzione pratica è la funzione Nette\Utils\Arrays::invoke:

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