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()
oisXyz()
, un setter con il nomesetXyz()
- Assicurarsi che getter e setter siano public o protected. Sono opzionali – possono quindi esistere come proprietà read-only o write-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);