Componenti interattivi
I componenti sono oggetti riutilizzabili autonomi che incorporiamo nelle pagine. Possono essere form, datagrid, sondaggi, in pratica tutto ciò che ha senso usare più volte. Vi mostreremo:
- come usare i componenti?
- come scriverli?
- cosa sono i segnali?
Nette ha un sistema di componenti integrato. Qualcosa di simile sarà familiare ai veterani di Delphi o di ASP.NET Web Forms; React o Vue.js si basano su qualcosa di vagamente affine. Nel mondo dei framework PHP, però, è una funzionalità unica.
Allo stesso tempo i componenti influenzano profondamente il modo di sviluppare le applicazioni. Potete comporre le pagine a partire da unità già pronte. Vi serve una datagrid nella vostra amministrazione? Cercatela su Componette, un repository di estensioni open source (non solo componenti) per Nette, e inseritela semplicemente nel presenter.
Nel presenter potete incorporare quanti componenti volete. E dentro alcuni componenti potete incorporarne altri. Nasce così un albero di componenti, con il presenter come radice.
Metodi factory
Come si inseriscono i componenti nel presenter e come li si usa poi? Di solito tramite metodi factory.
Una factory di componenti è un modo elegante di creare i componenti solo quando servono davvero (lazy / on demand). Tutta la
magia sta nell'implementare un metodo chiamato createComponent<Nome>(), dove <Nome> è il
nome del componente da creare, che crea e restituisce il componente.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->items;
return $poll;
}
}
Poiché tutti i componenti vengono creati in metodi separati, il codice risulta più chiaro.
I nomi dei componenti iniziano sempre con una lettera minuscola, anche se nel nome del metodo sono scritti con la maiuscola.
Non chiamiamo mai le factory direttamente: vengono chiamate automaticamente la prima volta che usiamo il componente. Grazie a questo il componente viene creato al momento giusto e solo se serve davvero. Se non usiamo il componente (per esempio durante una richiesta AJAX in cui viene trasferita solo una parte della pagina, oppure quando il template è in cache), non verrà creato affatto, risparmiando prestazioni del server.
// accediamo al componente e, se è la prima volta,
// viene chiamato createComponentPoll(), che lo crea
$poll = $this->getComponent('poll');
// sintassi alternativa: $poll = $this['poll'];
Nel template è possibile disegnare un componente con il tag {control}. Non c'è quindi bisogno di passare manualmente i componenti al template.
<h2>Vota</h2>
{control poll}
Per creare dinamicamente un numero variabile di componenti usate Multiplier.
I metodi factory createComponent<Nome>() non funzionano solo nei presenter. Allo stesso modo potete
annidare un componente dentro un altro componente, componendoli in un albero: comodo per esempio per un form disegnato
separatamente dentro un componente.
Stile hollywoodiano
I componenti usano abitualmente una tecnica fresca che ci piace chiamare stile hollywoodiano. Conoscete di sicuro il cliché che sentono spesso i partecipanti ai provini cinematografici: “Non chiamateci, vi chiameremo noi.” Ed è esattamente di questo che si tratta.
In Nette, invece di dover porre continuamente domande (“il form è stato inviato?”, “era valido?”, “l'utente ha premuto questo pulsante?”), dite al framework “quando succede questo, chiama questo metodo” e gli lasciate il resto del lavoro. Se programmate in JavaScript conoscete benissimo questo stile di programmazione: scrivete funzioni che vengono chiamate quando si verifica un certo evento. E il linguaggio passa loro i parametri appropriati.
Questo cambia completamente la prospettiva sullo scrivere applicazioni. Più compiti riuscite a lasciare al framework, meno lavoro avete. E meno cose potete trascurare.
Scrivere un componente
Con il termine componente intendiamo di norma un discendente della classe Nette\Application\UI\Control. (Sarebbe più
preciso usare il termine “control”, ma in alcune lingue ha un significato diverso e “componente” si è affermato di più.)
Anche il presenter Nette\Application\UI\Presenter è un
discendente della classe Control.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Rendering
Sappiamo già che per disegnare un componente si usa il tag {control nomeComponente}. In realtà esso chiama il
metodo render() del componente, nel quale ci occupiamo del rendering. Abbiamo a disposizione, come nel presenter, un
template Latte nella variabile $this->template, alla
quale passiamo i parametri. A differenza del presenter, dobbiamo indicare il file del template e farlo disegnare:
public function render(): void
{
// inserisce alcuni parametri nel template
$this->template->param = $value;
// e lo disegna
$this->template->render(__DIR__ . '/poll.latte');
}
Il tag {control} permette di passare parametri al metodo render():
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
A volte un componente può essere composto da più parti che vogliamo disegnare separatamente. Per ognuna di esse creiamo un
proprio metodo di rendering, qui nell'esempio renderPaginator():
public function renderPaginator(): void
{
// ...
}
E nel template lo richiamiamo poi così:
{control poll:paginator}
Per capire meglio è utile sapere come questo tag si traduce in codice PHP.
{control poll}
{control poll:paginator 123, 'hello'}
si traduce in:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
Il metodo getComponent() restituisce il componente poll e su questo componente viene chiamato il
metodo render(), oppure renderPaginator() se nel tag, dopo i due punti, è indicato un metodo di
rendering diverso.
Attenzione: se nei parametri compare => fuori dalle parentesi quadre, tutti i parametri
verranno racchiusi in un array e passati come primo argomento:
{control poll, id: 123, message: 'hello'}
si traduce in:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Rendering di un sottocomponente:
{control cartControl-someForm}
si traduce in:
$control->getComponent("cartControl-someForm")->render();
I componenti, come i presenter, passano automaticamente ai template alcune variabili utili:
$basePathè il percorso URL assoluto della directory radice (per esempio/eshop)$baseUrlè l'URL assoluto della directory radice (per esempiohttp://localhost/eshop)$userè un oggetto che rappresenta l'utente$presenterè il presenter corrente$controlè il componente corrente$flashesè un array dei messaggi inviati dalla funzioneflashMessage()
Segnale
Sappiamo già che la navigazione in un'applicazione Nette consiste nel collegare o nel reindirizzare verso coppie
Presenter:azione. Ma cosa succede se vogliamo solo eseguire un'azione sulla pagina corrente? Per esempio
cambiare l'ordinamento delle colonne di una tabella, eliminare un elemento, passare dalla modalità chiara a quella scura, inviare
un form, votare in un sondaggio e così via.
Questo tipo di richiesta si chiama segnale. E come le azioni richiamano i metodi action<Azione>() o
render<Azione>(), i segnali chiamano i metodi handle<Segnale>(). Mentre il concetto di
azione (o vista) riguarda esclusivamente i presenter, i segnali riguardano tutti i componenti. E quindi anche i presenter,
perché UI\Presenter è un discendente di UI\Control.
public function handleClick(int $x, int $y): void
{
// ... elaborazione del segnale ...
}
Un link che chiama un segnale si crea nel modo consueto, cioè nel template con l'attributo n:href o con il tag
{link}, nel codice con il metodo link(). Maggiori informazioni nel capitolo Creazione di link URL.
<a n:href="click! $x, $y">clicca qui</a>
Un segnale viene sempre chiamato sul presenter e sull'azione correnti; non è possibile richiamarlo su un altro presenter o su un'altra azione.
Un segnale provoca quindi il ricaricamento della pagina esattamente come la richiesta originale, ma in più chiama il metodo di gestione del segnale con i parametri appropriati. Se il metodo non esiste, viene sollevata l'eccezione Nette\Application\UI\BadSignalException, che viene mostrata all'utente come pagina di errore 403 Forbidden.
Snippet e AJAX
I segnali vi ricorderanno un po' AJAX: handler richiamati sulla pagina corrente. E avete ragione, i segnali vengono davvero chiamati spesso tramite AJAX e, di conseguenza, al browser vengono trasferite solo le parti della pagina che sono cambiate. Sono i cosiddetti snippet. Maggiori informazioni si trovano nella pagina dedicata ad AJAX.
Messaggi flash
Un componente ha un proprio archivio di messaggi flash, indipendente da quello del presenter. Sono messaggi che, per esempio, informano sull'esito di un'operazione. Una caratteristica importante dei messaggi flash è che sono disponibili nel template anche dopo un redirect. Anche dopo essere stati mostrati restano attivi per altri 30 secondi, per esempio nel caso in cui l'utente ricarichi la pagina per un errore di trasmissione: il messaggio non sparisce subito.
Dell'invio si occupa il metodo flashMessage. Il primo
parametro è il testo del messaggio (string, Stringable) oppure un oggetto stdClass che
rappresenta il messaggio. Il secondo parametro facoltativo è il suo tipo (error, warning, info ecc.). Il metodo
flashMessage() restituisce un'istanza del messaggio flash come oggetto stdClass, al quale si possono
aggiungere altre informazioni.
$this->flashMessage('Item was deleted.');
$this->redirect(/* ... */); // e reindirizza
Questi messaggi sono disponibili nel template nella variabile $flashes come oggetti stdClass, che
contengono le proprietà message (testo del messaggio), type (tipo del messaggio) e possono contenere le
informazioni aggiunte dall'utente. Li disegniamo per esempio così:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Redirect dopo l'elaborazione di un segnale
L'elaborazione del segnale di un componente è spesso seguita da un redirect. È come per i form: dopo il loro invio reindirizziamo, per impedire il reinvio dei dati se la pagina viene ricaricata nel browser.
$this->redirect('this'); // reindirizza al presenter e all'azione correnti
Poiché un componente è un elemento riutilizzabile e di norma non dovrebbe avere un legame diretto con presenter specifici,
i metodi redirect() e link() interpretano automaticamente il parametro come un segnale del
componente:
$this->redirect('click'); // reindirizza al segnale 'click' dello stesso componente
Se avete bisogno di reindirizzare a un altro presenter o a un'altra azione, potete farlo tramite il presenter:
$this->getPresenter()->redirect('Product:show'); // reindirizza a un altro presenter/azione
Parametri persistenti
I parametri persistenti servono a mantenere lo stato nei componenti tra richieste diverse. Il loro valore resta lo stesso anche dopo aver cliccato un link. A differenza dei dati di sessione, vengono trasferiti nell'URL. E questo avviene in modo completamente automatico, compresi i link creati in altri componenti della stessa pagina.
Avete per esempio un componente per la paginazione dei contenuti. Su una pagina possono essercene diversi. E vogliamo che tutti
i componenti restino sulla propria pagina corrente dopo aver cliccato un link. Rendiamo quindi il numero di pagina
(page) un parametro persistente.
Creare un parametro persistente in Nette è estremamente semplice. Basta creare una proprietà pubblica e contrassegnarla con
l'attributo: (in precedenza si usava /** @persistent */)
use Nette\Application\Attributes\Persistent; // questa riga è importante
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // deve essere public
}
Consigliamo di indicare il tipo di dato della proprietà (per esempio int) e potete anche fornire un valore
predefinito. I valori dei parametri si possono validare.
Quando si crea un link, il valore di un parametro persistente si può cambiare:
<a n:href="this page: $page + 1">successiva</a>
Oppure lo si può azzerare, cioè rimuovere dall'URL. Assumerà allora il proprio valore predefinito:
<a n:href="this page: null">azzera</a>
Componenti persistenti
Non solo i parametri, ma anche i componenti possono essere persistenti. I loro parametri persistenti vengono allora
trasferiti anche tra azioni diverse del presenter o tra più presenter. Contrassegniamo i componenti persistenti con un
attributo sulla classe del presenter. Contrassegniamo per esempio così i componenti calendar e
poll:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
I sottocomponenti di questi componenti non devono essere contrassegnati: diventano persistenti anch'essi.
La vecchia annotazione @persistent funziona ancora, ma è deprecata ed emette un avviso:
/**
* @persistent(calendar, poll)
*/
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Componenti con dipendenze
Come creare componenti con dipendenze senza “ingombrare” i presenter che li useranno? Grazie alle funzionalità intelligenti del container DI in Nette, come per i servizi classici, gran parte del lavoro si può lasciare al framework.
Prendiamo l'esempio di un componente che ha una dipendenza dal servizio PollFacade:
class PollControl extends Control
{
public function __construct(
private int $id, // ID del sondaggio per cui creiamo il componente
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($this->id, $voteId);
// ...
}
}
Se stessimo scrivendo un servizio classico non ci sarebbe nulla da discutere: il container DI si occuperebbe invisibilmente di
passare tutte le dipendenze. Con i componenti, però, di norma li gestiamo creando una nuova istanza direttamente nel presenter,
nei metodi factory createComponent…(). Ma passare al presenter tutte le
dipendenze di tutti i componenti solo per girarle ai componenti è scomodo. E quanto codice da scrivere…
La domanda logica è: perché non registriamo semplicemente il componente come servizio classico, non lo passiamo al presenter
e non lo restituiamo nel metodo createComponent…()? Questo approccio è però inadatto, perché vogliamo poter
creare il componente più volte, se serve.
La soluzione corretta è scrivere una factory per il componente, cioè una classe che crei il componente al posto nostro:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Registriamo questa factory nel nostro container nella configurazione:
services:
- PollControlFactory
e infine la usiamo nel nostro presenter:
class PollPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private PollControlFactory $pollControlFactory,
) {
}
protected function createComponentPollControl(): PollControl
{
$pollId = 1; // possiamo passare il nostro parametro
return $this->pollControlFactory->create($pollId);
}
}
La cosa bella è che Nette DI sa generare factory semplici come questa, quindi invece di scriverne tutto il codice vi basta scriverne l'interfaccia:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
E questo è tutto. Nette implementa internamente questa interfaccia e la inietta nel presenter, dove possiamo usarla. Aggiunge
come per magia al nostro componente il parametro $id e un'istanza della classe PollFacade.
I componenti in profondità
I componenti in Nette Application rappresentano parti riutilizzabili di un'applicazione web che incorporiamo nelle pagine, ed è a essi che è dedicato tutto questo capitolo. Quali sono esattamente le capacità di un componente del genere?
- È disegnabile in un template
- Sa quale parte di sé disegnare durante una richiesta AJAX (snippet)
- Ha la capacità di salvare il proprio stato nell'URL (parametri persistenti)
- Ha la capacità di reagire alle azioni dell'utente (segnali)
- Crea una struttura gerarchica (la cui radice è il presenter)
Di ognuna di queste funzioni si occupa una delle classi della catena di ereditarietà. Del rendering (1 + 2) si occupa Nette\Application\UI\Control, dell'integrazione nel ciclo di vita (3, 4) la classe Nette\Application\UI\Component e della creazione della struttura gerarchica (5) le classi Container e Component.
Nette\ComponentModel\Component { IComponent }
|
+- Nette\ComponentModel\Container { IContainer }
|
+- Nette\Application\UI\Component { SignalReceiver, StatePersistent }
|
+- Nette\Application\UI\Control { Renderable }
|
+- Nette\Application\UI\Presenter { IPresenter }
Ciclo di vita del componente
Validazione dei parametri persistenti
I valori dei parametri persistenti ricevuti dagli URL vengono scritti nelle
proprietà dal metodo loadState(). Esso controlla anche che il tipo di dato indicato per la proprietà corrisponda;
in caso contrario risponde con un errore 404 e la pagina non viene mostrata.
Non fidatevi mai ciecamente dei parametri persistenti, perché l'utente può facilmente sovrascriverli nell'URL. Ecco come
controlliamo, per esempio, che il numero di pagina $this->page sia maggiore di 0. Un modo adatto è sovrascrivere
il metodo loadState() già menzionato:
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // qui viene impostato $this->page
// segue il controllo personalizzato del valore:
if ($this->page < 1) {
$this->error();
}
}
}
Del processo inverso, cioè della raccolta dei valori dalle proprietà persistenti, si occupa il metodo
saveState().
Collegamento al presenter
Nel momento in cui un componente entra a far parte della gerarchia del presenter, vengono richiamate le sue callback salvate
nell'array $onAnchor. Da quel momento in poi il componente ha a disposizione il presenter, può creare link in
sicurezza, leggere i parametri persistenti e così via.
$control->onAnchor[] = function ($control): void {
// il componente ha ora a disposizione il presenter
};
I segnali in profondità
Un segnale provoca il ricaricamento della pagina esattamente come la richiesta originale (tranne quando viene chiamato via
AJAX) e richiama il metodo signalReceived($signal), la cui implementazione predefinita nella classe
Nette\Application\UI\Component prova a chiamare un metodo composto dalle parole handle<Segnale>.
L'ulteriore elaborazione spetta all'oggetto in questione. Gli oggetti che ereditano da Component (cioè
Control e Presenter) reagiscono provando a chiamare il metodo handle<Segnale> con
i parametri appropriati.
In altre parole: si prende la definizione della funzione handle<Segnale>, insieme a tutti i parametri
arrivati con la richiesta, si assegnano per nome agli argomenti i parametri provenienti dall'URL e si prova a chiamare il metodo.
Per esempio il valore del parametro id nell'URL viene passato come argomento $id, something
dall'URL viene passato come $something e così via. E se il metodo non esiste, il metodo signalReceived
solleva un'eccezione.
Oltre ai parametri provenienti dall'URL, un segnale legge anche i parametri inviati nel corpo POST della richiesta. Questo torna comodo, perché i segnali vengono spesso richiamati via JavaScript, dove è naturale inviare i dati con il metodo POST. Se però un parametro con lo stesso nome arriva sia dall'URL sia dal corpo POST, ha la precedenza il valore proveniente dall'URL. Evitate quindi di dare a un campo POST lo stesso nome di un parametro dell'URL o della route, altrimenti il valore dell'URL lo sovrascriverebbe in silenzio. I parametri dei segnali condividono lo spazio con i parametri delle azioni e con quelli persistenti, vedi Spazio condiviso dei parametri.
Un segnale può essere ricevuto da qualsiasi componente, presenter o oggetto che implementi l'interfaccia
SignalReceiver e sia collegato all'albero dei componenti.
I principali destinatari dei segnali saranno i Presenter e i componenti visivi che ereditano da
Control. Un segnale è pensato per fare da segnale a un oggetto affinché faccia qualcosa: un sondaggio deve
conteggiare un voto dell'utente, un blocco di notizie deve espandersi e mostrare il doppio delle notizie, un form è stato inviato
e deve elaborare i dati e così via.
L'URL di un segnale si crea con il metodo Component::link(). Come
parametro $destination passiamo la stringa {segnale}! e come $args un array di argomenti
che vogliamo passare al segnale. Il segnale viene sempre chiamato sul presenter e sull'azione correnti, con i parametri correnti;
i parametri del segnale si limitano ad aggiungersi. In più viene aggiunto il parametro ?do, che indica il
segnale.
Il suo formato è {segnale} oppure {destinatarioDelSegnale}-{segnale}.
{destinatarioDelSegnale} è il nome del componente nel presenter. Nel nome del componente non si può quindi usare il
trattino: serve a separare il nome del componente dal segnale, anche se è possibile annidare così più componenti.
Il metodo isSignalReceiver()
controlla se il componente (primo argomento) è il destinatario del segnale (secondo argomento). Il secondo argomento si può
omettere: in tal caso controlla se il componente è destinatario di un qualsiasi segnale. Se il secondo parametro è impostato a
true, verifica se il componente indicato o uno qualsiasi dei suoi discendenti è il destinatario.
In qualsiasi fase precedente a handle<Segnale> possiamo eseguire manualmente il segnale chiamando il metodo
processSignal(),
che si occupa della gestione del segnale: prende il componente individuato come destinatario del segnale (se non è indicato alcun
destinatario, è il presenter stesso) e gli invia il segnale.
Esempio:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Così il segnale viene eseguito in anticipo e non verrà chiamato di nuovo.