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