
Modelli e Traits
alberto • December 20, 2021
tutorial eloquentMi piace l'approccio Rich Domain Model, ovvero quella strategia che suggerisce di arricchire i Model, ovvero le classi che rappresentano le nostre entità business, di funzionalità e di metodi. Anche Eloquent sfrutta questa tecnica esponendo di fatto una serie di metodi, per esempio l'utilizzatissimo save
, direttamente dentro Illuminate\Database\Eloquent\Model
.
Mi piace arricchire i miei modelli con funzionalità relative ad essi, tramite per esempio accessor o mutators, per avere un'unica logica riutilizzabile per tutto l'applicativo e perchè mi piace ragionare ad oggetti, intesi proprio come entità virtuali che incapsulano non solo proprietà ma anche algoritmi e logica. Ovviamente, come per tutte le cose, bisogna sempre stare attenti a non abusare di questo approccio: se una cosa deve stare in un Model, è giusto che ci stia, ma non ci deve finire il mondo.
Oltre ai sopracitati accessor e mutators, Eloquent presenta una feature utile, nonostante sia un po' nascosta, per arricchire i nostri modelli, soprattutto quando la logica è condivisa tra di essi, sfruttando un costrutto nativo di PHP: i Traits.
Traits
I Traits sono delle speciali classi che possono essere installate (dovrei dire usate ma non mi piace proprio...) dentro una o piú classi e che ne aumentano le funzionalità. Rappresentano un po' una alternativa all'utopica Ereditarietà Multipla, una sorta di chimera nel mondo della programmazione ad oggetti.
Nonostante PHP presenti una ereditarietà singola, tramite i Traits è possibile implementare strutture funzionali interessanti (o nello stesso tempo schifezze imbarazzanti) evitando di dover replicare frammenti di codice.
Modelli (quasi) riutilizzabili
Come ogni articolo su Laravello.it che si rispetta, spostiamoci ora su un esempio.
Immaginiamo un applicativo dove alcuni modelli necessitano di avere persistito a database una history delle modifiche fatte in passato, con una referenza all'utente che ha modificato il loro contenuto.
Questo funzionalità potrebbe essere implementata tramite l'utilizzo degli eventi, ed in particolare di updated
e una soluzione potrebbe essere quella di creare una classe astratta, chiamata per esempio ModelWithHistory
, che si occupi di registrare evento e relativa closure e di estendere questa classe in ogni modello che necessità la funzionalità:
// app/Models/ModelWithHistory.php
abstract class ModelWithHistory extends Model
{
protected static function booted()
{
static::updated(function ($user) {
//inserisco a db una nuova "history" del modello
});
}
}
// app/Models/Post.php
class Post extends ModelWithHistory
{
}
// app/Models/Video.php
class Video extends ModelWithHistory
{
}
// app/Models/News.php
class News extends Model
{
}
Tutto funziona perfettamente, ad ogni modifica di un Post o di un Video viene invocata la closure iniettata tramite il metodo statico updated
mentre il modello News non fa scatenare questa funzionalità, semplicemente perchè non ci serve farlo (infatti estende Model
base).
Ma, come sempre, c'è un ma! Agendo sullo stack di gerarchia di una classe stiamo introducendo una forzatura, difficilmente sanabile in caso di modifiche successive.
Pensiamo infatti a questo scenario: ci viene richiesto di inviare una mail ai nostri utenti ogni volta che viene creato un Post o una News da parte dell'operatore del nostro backoffice. Si tratta quindi di un evento created
da aggiungere solamente su Post e News. Con la struttura attuale non esiste modo di farlo senza ripetere il codice, dato che non esiste la possibilità di modificare solamente le classi Post e News senza toccare Video.
Usare Traits nei modelli
Abbiamo capito che creare una gerarchia di classi, seppur spesso comodo e funzionale, significa introdurre rigidità nel codice. Non significa che non sia giusto a priori, ma deve essere una cosa da valutare molto attentamente prima di essere implementata.
Proviamo a rivedere le nostre funzionalità utilizzando due Traits: WithHistory
e NotifyWhenCreated
:
// app/Traits/WithHistory.php
trait WithHistory
{
protected static function booted()
{
static::updated(function ($user) {
//inserisco a db una nuova "history" del modello
});
}
}
// app/Traits/NotifyWhenCreated.php
trait NotifyWhenCreated
{
protected static function booted()
{
static::created(function ($user) {
//invio mail
});
}
}
// app/Models/Post.php
class Post extends ModelWithHistory
{
use WithHistory, NotifyWhenCreated;
}
// app/Models/Video.php
class Video extends ModelWithHistory
{
use WithHistory;
}
// app/Models/News.php
class News extends Model
{
use NotifyWhenCreated;
}
Avviamo tutto e... ci accorgiamo che non parte nessuna mail quando creiamo un Post :(
Tutto questo perchè il secondo trait, NotifyWhenCreated
, reimplementa, e di fatto sovrascrive, il metodo booted
, rendendo di fatto inutile quello contenuto in WithHistory
. Che palle questa programmazione ad oggetti!
Grazie Eloquent!
Ma la soluzione è dietro l'angolo.
Dato che Laravel ed Eloquent incentivano questo approccio, cosí da rendere anche piú facile la scrittura di librerie, ci hanno pensato loro a trovare una soluzione.
Esistono infatti due metodi: boot$trait
e init$trait
che, se presenti all'interno di un Trait installato in un Model, vengono invocati automaticamente dal framework permettendoci quindi di ragionare a blocchi e di non preoccuparci di ereditarietà e di metodi che vengono sovrascritti.
Rivediamo quindi i nostri due traits:
// app/Traits/WithHistory.php
trait WithHistory
{
protected static function bootWithHistory()
{
static::updated(function ($user) {
//inserisco a db una nuova "history" del modello
});
}
}
// app/Traits/NotifyWhenCreated.php
trait NotifyWhenCreated
{
protected static function bootNotifyWhenCreated()
{
static::created(function ($user) {
//invio mail
});
}
}
Abbiamo semplicemente modificato il nome del metodo, da booted
(metodo già presente nel framework) a boot$Trait
, sostituendo la variabile $trait con il nome esatto appunto del trait.
Grazie ad una magia del framework, i metodi bootWithHistory
e bootNotifyWhenCreated
vengono invocati al boot, nello stesso momento in cui veniva chiamato il vecchio booted
, ma senza possibilità di interferire tra di loro, dato che il loro nome è ora differente.
boot$traits
vs init$traits
Negli esempi sopra abbiamo sempre parlato di boot ovvero di quel momento in cui una classe viene caricata in memoria, ma esiste un altro momento in cui possiamo intervenire per rendere i nostri modelli ancora piú configurabili, ovvero l'init
.
Con la stessa logica, Eloquent invoca anche i metodi init$trait
.
Ma quali sono le differenze?
boot$trait
è un metodo statico, quindi puó agire solo a livello di classe,init$trait
puó agire sul singolo modello instanziato
Il primo spesso viene utilizzato, come nei nostri esempi, per impostare delle particolari callback a degli eventi, che vengono infatti impostati in modo statico sull'intera classe. Il secondo invece si utilizza per modificare puntualmente ciascun modello istanziato, per esempio per associare un token random e differente ad ogni oggetto.
Conclusioni
L'argomento dell'articolo di oggi, bisogna ammetterlo, è un po' di nicchia. Non viene riportato in nessuna documentazione ufficiale (almeno io non l'ho mai trovato) ma nonostate questo è parecchio utilizzato.
Ha il vantaggio di essere un enabler per un certo tipo di programmazione a blocchi grazie alla quale è possibile associare feature singole a diverse classi, un po' come fossero dei mattoncini funzionali che installiamo dove e quando ci servono. Grazie all'implementazione out-of-the-box di Laravel non dobbiamo preoccuparci di nulla se non di implementare la reale funzionalità.
Personalmente è un approccio che utilizzo parecchio e con il quale mi trovo bene. È molto utile anche per isolare funzionalità in pacchetti separati, cosí da riportarli anche in progetti differenti in tempi super rapidi. Talvolta mi piacerebbe avere una cosa simile anche all'interno dei Controller.
L'unico svantaggio che vedo è quello di essere parecchio laravel-centrico, qualora volessimo utilizzare gli stessi Traits fuori da un progetto Laravel, dovremmo andare a reimplementare noi la logica di triggering degli eventi.
...ma perchè mai dovremmo fare progetti non Laravel?
Per i piú curiosi
L'invocazione di questi metodi avviene qua, ovvero all'interno del metodo bootTraits
, invocato a sua volta da boot
.