SmartObject

SmartObject ha migliorato per anni il comportamento degli oggetti in PHP. Dalla versione PHP 8.4, tutte le sue funzionalità sono diventate parte nativa di PHP stesso, completando così la sua missione storica di essere pioniere dell'approccio orientato agli oggetti moderno in PHP.

Installazione:

composer require nette/utils

SmartObject è nato nel 2007 come soluzione rivoluzionaria alle carenze del modello a oggetti di PHP dell'epoca. In un periodo in cui PHP soffriva di numerosi problemi di design orientato agli oggetti, ha portato significativi miglioramenti e semplificazioni nel lavoro degli sviluppatori. È diventato una parte leggendaria del framework Nette. Ha offerto funzionalità che PHP avrebbe acquisito solo molti anni dopo – dalla validazione dell'accesso alle proprietà alla gestione sofisticata degli errori. Con l'arrivo di PHP 8.4, ha completato la sua missione storica, poiché tutte le sue funzionalità sono diventate parti native del linguaggio. Ha anticipato lo sviluppo di PHP di ben 17 anni.

SmartObject ha attraversato un'interessante evoluzione tecnica. Inizialmente, è stato implementato come classe Nette\Object, dalla quale altre classi ereditavano la funzionalità necessaria. Un cambiamento significativo è arrivato con PHP 5.4, che ha introdotto il supporto per i trait. Questo ha permesso la trasformazione nel trait Nette\SmartObject, portando maggiore flessibilità – gli sviluppatori potevano utilizzare la funzionalità anche nelle classi che già ereditavano da un'altra classe. Mentre la classe originale Nette\Object ha cessato di esistere con PHP 7.2 (che ha proibito di nominare le classi con la parola ‘Object’), il trait Nette\SmartObject continua a esistere.

Esaminiamo le funzionalità che Nette\Object e successivamente Nette\SmartObject offrivano. Ognuna di queste funzioni rappresentava un significativo passo avanti nella programmazione orientata agli oggetti in PHP.

Stati di Errore Consistenti

Uno dei problemi più urgenti del primo PHP era il comportamento inconsistente nel lavoro con gli oggetti. Nette\Object ha portato ordine e prevedibilità in questo caos. Vediamo come si comportava PHP originariamente:

echo $obj->undeclared;    // E_NOTICE, successivamente E_WARNING
$obj->undeclared = 1;     // passa silenziosamente senza avviso
$obj->unknownMethod();    // Fatal error (non catturabile con try/catch)

Fatal error terminava l'applicazione senza possibilità di reazione. La scrittura silenziosa su membri non esistenti senza avviso poteva portare a errori gravi difficili da rilevare. Nette\Object catturava tutti questi casi e lanciava una MemberAccessException, permettendo ai programmatori di reagire e gestire questi errori:

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

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

Suggerimento “Did you mean?”

Nette\Object è arrivato con una funzionalità molto conveniente: i suggerimenti intelligenti per gli errori di battitura. Quando uno sviluppatore commetteva un errore nel nome di un metodo o di una variabile, non solo segnalava l'errore ma offriva anche aiuto suggerendo il nome corretto. Questo messaggio iconico, noto come “did you mean?”, ha risparmiato agli sviluppatori ore di ricerca degli errori di battitura:

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

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

Mentre PHP stesso non ha alcuna forma di “did you mean?”, questa funzionalità è ora fornita da Tracy. Può persino correggere automaticamente questi errori.

Proprietà con Accesso Controllato

Un'innovazione significativa che SmartObject ha portato in PHP sono state le proprietà con accesso controllato. Questo concetto, comune in linguaggi come C# o Python, ha permesso agli sviluppatori di controllare elegantemente l'accesso ai dati dell'oggetto e garantire la loro consistenza. Le proprietà sono uno strumento potente della programmazione orientata agli oggetti. Funzionano come variabili ma sono in realtà rappresentate da metodi (getter e setter). Questo permette la validazione degli input o la generazione dei valori al momento della lettura.

Per utilizzare le proprietà, è necessario:

  • Aggiungere l'annotazione @property <type> $xyz alla classe
  • Creare un getter chiamato getXyz() o isXyz(), un setter chiamato setXyz()
  • Assicurarsi che getter e setter siano publicprotected. Sono opzionali – quindi possono esistere come proprietà read-onlywrite-only

Vediamo un esempio pratico usando la classe Circle, dove useremo le proprietà per garantire che il raggio sia sempre non negativo. Sostituiremo 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 per la proprietà $radius
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter per la proprietà $radius
	protected function setRadius(float $radius): void
	{
		// sanitizziamo il valore prima del salvataggio
		$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()

Da PHP 8.4, la stessa funzionalità può essere ottenuta usando i property hooks, 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;
	}
}

Extension Methods

Nette\Object ha portato in PHP un altro concetto interessante ispirato ai linguaggi di programmazione moderni – i metodi di estensione. Questa funzionalità, presa in prestito da C#, permetteva agli sviluppatori di estendere elegantemente le classi esistenti con nuovi metodi senza modificarle o ereditare da esse. Per esempio, si poteva aggiungere un metodo addDateTime() a un form che aggiungesse 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é gli editor non suggerivano i loro nomi e invece segnalavano che il metodo non esisteva. Pertanto, il loro supporto è stato interrotto. Oggi, è più comune utilizzare la composizione o l'ereditarietà per estendere la funzionalità delle classi.

Ottenere il Nome della Classe

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

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

Accesso a Reflection e Annotazioni

Nette\Object forniva accesso a reflection e annotazioni attraverso i metodi getReflection() e getAnnotation(). Questo approccio semplificava significativamente 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 attraverso 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];

Method Getters

Nette\Object offriva un modo elegante per 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, è possibile utilizzare 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 sui cambiamenti del loro stato:

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 ciclo:

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

Per chiarezza, raccomandiamo di evitare il metodo magico $this->onChange(). Un'alternativa pratica è la funzione Nette\Utils\Arrays::invoke:

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