SmartObject

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

Installazione:

composer require nette/utils

SmartObject è nato nel 2007 come soluzione rivoluzionaria alle mancanze del modello a oggetti di PHP dell'epoca. In un momento in cui PHP soffriva di numerosi problemi di design degli oggetti, ha portato un significativo miglioramento e semplificazione del lavoro per gli sviluppatori. È diventato una parte leggendaria del framework Nette. Offriva funzionalità che PHP ha acquisito solo molti anni dopo – dal controllo dell'accesso alle proprietà degli oggetti a sofisticati zuccheri sintattici. Con l'arrivo di PHP 8.4 ha completato la sua missione storica, poiché tutte le sue funzioni sono diventate parte nativa del linguaggio. Ha anticipato lo sviluppo di PHP di ben 17 anni.

Tecnicamente, SmartObject ha subito un'interessante evoluzione. Originariamente era implementato come classe Nette\Object, da cui altre classi ereditavano la funzionalità necessaria. Un cambiamento fondamentale è avvenuto con PHP 5.4, che ha introdotto il supporto per i trait. Ciò ha permesso la trasformazione nella forma del trait Nette\SmartObject, che ha portato maggiore flessibilità – gli sviluppatori potevano utilizzare la funzionalità anche in classi che già ereditavano da un'altra classe. Mentre la classe originale Nette\Object è scomparsa con l'arrivo di PHP 7.2 (che ha vietato la denominazione delle classi con la parola Object), il trait Nette\SmartObject vive ancora.

Esaminiamo le proprietà che un tempo Nette\Object e successivamente Nette\SmartObject offrivano. Ognuna di queste funzioni, a suo tempo, rappresentava un significativo passo avanti nel campo della programmazione orientata agli oggetti in PHP.

Stati di errore coerenti

Uno dei problemi più spinosi del primo PHP era il comportamento incoerente nel lavorare con gli oggetti. Nette\Object ha portato ordine e prevedibilità in questo caos. Vediamo come appariva il comportamento originale di PHP:

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

Un fatal error terminava l'applicazione senza alcuna possibilità di reagire. La scrittura silenziosa su membri inesistenti senza preavviso poteva portare a gravi errori difficili da individuare. Nette\Object catturava tutti questi casi e lanciava un'eccezione MemberAccessException, consentendo ai programmatori di reagire agli errori e risolverli.

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ù fatal error non catturabili e da PHP 8.2 l'accesso a membri non dichiarati è considerato un errore.

Aiuto “Did you mean?”

Nette\Object ha introdotto una funzione molto piacevole: un aiuto intelligente 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 una mano sotto forma di suggerimento del nome corretto. Questo messaggio iconico, noto come “did you mean?”, ha risparmiato ai programmatori ore di ricerca di 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()?"

Il PHP di oggi non ha alcuna forma di “did you mean?”, ma questa aggiunta può essere inserita negli errori da Tracy. E può persino correggerli automaticamente.

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 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). Ciò consente di convalidare gli input o generare valori solo al momento della lettura.

Per utilizzare le proprietà è necessario:

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

Vediamo un esempio pratico sulla classe Circle, dove utilizziamo le proprietà per garantire che il raggio sia sempre un numero non negativo. Sostituiamo l'originale 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 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()

Da PHP 8.4 è possibile ottenere la stessa funzionalità utilizzando gli hook delle proprietà, 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 ha introdotto in PHP un altro concetto interessante ispirato ai moderni linguaggi di programmazione – i metodi di estensione. Questa funzione, presa in prestito da C#, ha permesso agli sviluppatori di estendere elegantemente le classi esistenti con nuovi metodi senza la necessità di modificarle o ereditarle. Ad esempio, si poteva aggiungere al form un metodo addDateTime() che aggiungeva 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, poiché i loro nomi non venivano suggeriti dagli editor, anzi 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

Per ottenere il nome della classe, SmartObject offriva un metodo semplice:

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

Accesso a reflection e annotazioni

Nette\Object offriva l'accesso a reflection e annotazioni tramite i metodi getReflection() e getAnnotation(). Questo approccio ha semplificato notevolmente il lavoro con le metainformazioni 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 metainformazioni sotto forma di attributi, che offrono ancora maggiori 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 di metodi

Nette\Object offriva un modo elegante per passare 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 cosiddetta sintassi callable di prima classe, che porta questo concetto ancora oltre:

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

Eventi

SmartObject offre una sintassi semplificata per lavorare con gli eventi. Gli eventi consentono 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 motivi di chiarezza, si consiglia di evitare il metodo magico $this->onChange(). Un sostituto pratico è, ad esempio, la funzione Nette\Utils\Arrays::invoke:

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