Componenti Interattivi

I componenti sono oggetti riutilizzabili indipendenti che inseriamo nelle pagine. Possono essere form, datagrid, sondaggi, praticamente qualsiasi cosa che abbia senso usare ripetutamente. Vedremo:

  • come usare i componenti?
  • come scriverli?
  • cosa sono i segnali?

Nette ha un sistema di componenti integrato. Qualcosa di simile potrebbe essere familiare ai veterani di Delphi o ASP.NET Web Forms, qualcosa di lontanamente simile è alla base di React o Vue.js. Tuttavia, nel mondo dei framework PHP, è una caratteristica unica.

Eppure, i componenti influenzano fondamentalmente l'approccio alla creazione di applicazioni. Puoi comporre le pagine da unità pre-preparate. Hai bisogno di un datagrid nell'amministrazione? Lo trovi su Componette, un repository di add-on open-source (quindi non solo componenti) per Nette, e lo inserisci semplicemente nel presenter.

Puoi incorporare un numero qualsiasi di componenti in un presenter. E in alcuni componenti puoi inserire altri componenti. Questo crea un albero di componenti, la cui radice è il presenter.

Metodi Factory

Come vengono inseriti i componenti nel presenter e successivamente utilizzati? Di solito tramite metodi factory.

Una factory di componenti è un modo elegante per creare componenti solo quando sono effettivamente necessari (lazy / on demand). L'intera magia sta nell'implementare un metodo chiamato createComponent<Name>(), dove <Name> è 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->item;
		return $poll;
	}
}

Grazie al fatto che tutti i componenti vengono creati in metodi separati, il codice guadagna in chiarezza.

I nomi dei componenti iniziano sempre con una lettera minuscola, anche se sono scritti con una lettera maiuscola nel nome del metodo.

Le factory non vengono mai chiamate direttamente; vengono chiamate automaticamente la prima volta che utilizziamo il componente. Grazie a ciò, il componente viene creato al momento giusto e solo quando è effettivamente necessario. Se non utilizziamo il componente (ad esempio, durante una richiesta AJAX in cui viene trasferita solo una parte della pagina, o durante la cache del template), non viene creato affatto e risparmiamo le 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 renderizzare un componente utilizzando il tag {control}. Pertanto, non è necessario passare manualmente i componenti al template.

<h2>Vota</h2>

{control poll}

Stile Hollywood

I componenti utilizzano comunemente una tecnica fresca che ci piace chiamare Stile Hollywood. Sicuramente conosci la frase famosa che i partecipanti ai casting cinematografici sentono così spesso: “Non chiamateci, vi chiameremo noi”. Ed è proprio di questo che si tratta.

In Nette, invece di dover chiedere costantemente qualcosa (“il form è stato inviato?”, “era valido?” o “l'utente ha premuto questo pulsante?”), dici al framework “quando succede, chiama questo metodo” e lasci il resto del lavoro a lui. Se programmi in JavaScript, conosci intimamente questo stile di programmazione. Scrivi funzioni che vengono chiamate quando si verifica un certo evento. E il linguaggio passa loro i parametri appropriati.

Questo cambia completamente la prospettiva sulla scrittura delle applicazioni. Più compiti puoi lasciare al framework, meno lavoro hai tu. E meno cose puoi dimenticare.

Scrivere un Componente

Con il termine componente, di solito intendiamo un discendente della classe Nette\Application\UI\Control. (Sarebbe quindi più preciso usare il termine “controlli”, ma “controlli” ha un significato completamente diverso in italiano e “componenti” si è affermato di più.) Il presenter stesso Nette\Application\UI\Presenter è, tra l'altro, anche un discendente della classe Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Rendering

Sappiamo già che per renderizzare un componente si usa il tag {control componentName}. Questo in realtà chiama il metodo render() del componente, in cui ci occupiamo del rendering. Abbiamo a disposizione, proprio come nel presenter, un template Latte nella variabile $this->template, a cui passiamo i parametri. A differenza del presenter, dobbiamo specificare il file del template e farlo renderizzare:

public function render(): void
{
	// inseriamo alcuni parametri nel template
	$this->template->param = $value;
	// e lo renderizziamo
	$this->template->render(__DIR__ . '/poll.latte');
}

Il tag {control} consente 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 renderizzare separatamente. Per ognuna di esse, creiamo il nostro metodo di rendering, qui nell'esempio renderPaginator():

public function renderPaginator(): void
{
	// ...
}

E nel template, lo chiamiamo poi usando:

{control poll:paginator}

Per una migliore comprensione, è utile sapere come questo tag viene tradotto in PHP.

{control poll}
{control poll:paginator 123, 'hello'}

viene tradotto come:

$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');

Il metodo getComponent() restituisce il componente poll e su questo componente chiama il metodo render(), rispettivamente renderPaginator() se nel tag dopo i due punti è specificato un modo di rendering diverso.

Attenzione, se da qualche parte nei parametri compare =>, tutti i parametri verranno impacchettati in un array e passati come primo argomento:

{control poll, id: 123, message: 'hello'}

viene tradotto come:

$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);

Rendering di un sub-componente:

{control cartControl-someForm}

viene tradotto come:

$control->getComponent("cartControl-someForm")->render();

I componenti, come i presenter, passano automaticamente diverse variabili utili ai template:

  • $basePath è il percorso URL assoluto alla directory principale (es. /eshop)
  • $baseUrl è l'URL assoluto alla directory principale (es. http://localhost/eshop)
  • $user è l'oggetto che rappresenta l'utente
  • $presenter è il presenter corrente
  • $control è il componente corrente
  • $flashes è l'array di messaggi inviati dalla funzione flashMessage()

Segnale

Sappiamo già che la navigazione in un'applicazione Nette consiste nel collegare o reindirizzare a coppie Presenter:action. Ma cosa succede se vogliamo solo eseguire un'azione sulla pagina corrente? Ad esempio, cambiare l'ordinamento delle colonne in una tabella; eliminare un elemento; passare alla modalità chiaro/scuro; inviare un form; votare in un sondaggio; ecc.

Questo tipo di richiesta è chiamato segnale. E proprio come le azioni invocano i metodi action<Action>() o render<Action>(), i segnali chiamano i metodi handle<Signal>(). Mentre il concetto di azione (o view) è legato puramente ai 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 viene creato nel modo consueto, cioè nel template con l'attributo n:href o 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 invocarlo su un altro presenter o un'altra azione.

Quindi, un segnale provoca il ricaricamento della pagina proprio come nella richiesta originale, ma in più chiama il metodo di gestione del segnale con i parametri appropriati. Se il metodo non esiste, viene lanciata un'eccezione Nette\Application\UI\BadSignalException, che viene mostrata all'utente come una pagina di errore 403 Forbidden.

Snippet e AJAX

I segnali potrebbero ricordarvi un po' AJAX: gestori che vengono invocati sulla pagina corrente. E avete ragione, i segnali vengono infatti spesso chiamati tramite AJAX e successivamente vengono trasferite al browser solo le parti modificate della pagina. Ovvero i cosiddetti snippet. Maggiori informazioni si trovano sulla pagina dedicata ad AJAX.

Messaggi flash

Un componente ha il proprio storage di messaggi flash indipendente dal presenter. Si tratta di messaggi che, ad esempio, informano sul risultato di un'operazione. Una caratteristica importante dei messaggi flash è che sono disponibili nel template anche dopo un redirect. Anche dopo essere stati visualizzati, rimangono attivi per altri 30 secondi – ad esempio, nel caso in cui l'utente aggiorni la pagina a causa di un errore di trasmissione – il messaggio non scomparirà immediatamente.

L'invio è gestito dal metodo flashMessage. Il primo parametro è il testo del messaggio o un oggetto stdClass che rappresenta il messaggio. Il secondo parametro opzionale è il suo tipo (error, warning, info, ecc.). Il metodo flashMessage() restituisce un'istanza del messaggio flash come oggetto stdClass, a cui è possibile aggiungere ulteriori informazioni.

$this->flashMessage('L\'elemento è stato eliminato.');
$this->redirect(/* ... */); // e reindirizziamo

Nel template, questi messaggi sono disponibili nella variabile $flashes come oggetti stdClass, che contengono le proprietà message (testo del messaggio), type (tipo del messaggio) e possono contenere le informazioni utente già menzionate. Li renderizziamo ad esempio così:

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Redirect dopo un segnale

Dopo l'elaborazione di un segnale di componente, spesso segue un redirect. È una situazione simile a quella dei form – dopo il loro invio reindirizziamo anche, in modo che l'aggiornamento della pagina nel browser non provochi un nuovo invio dei dati.

$this->redirect('this') // reindirizza al presenter e all'azione correnti

Poiché un componente è un elemento riutilizzabile e di solito 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 è necessario reindirizzare a un altro presenter o azione, è possibile 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 rimane lo stesso anche dopo aver cliccato su un link. A differenza dei dati nella sessione, vengono trasferiti nell'URL. E questo avviene in modo completamente automatico, inclusi i link creati in altri componenti sulla stessa pagina.

Ad esempio, hai un componente per la paginazione del contenuto. Possono esserci più componenti di questo tipo su una pagina. E desideriamo che, dopo aver cliccato su un link, tutti i componenti rimangano sulla loro pagina corrente. Pertanto, rendiamo il numero di pagina (page) un parametro persistente.

La creazione di un parametro persistente in Nette è estremamente semplice. Basta creare una proprietà pubblica e contrassegnarla con un 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
}

Per la proprietà, si consiglia di specificare anche il tipo di dati (es. int) e si può anche specificare un valore predefinito. I valori dei parametri possono essere validati.

Durante la creazione di un link, è possibile modificare il valore del parametro persistente:

<a n:href="this page: $page + 1">successivo</a>

Oppure può essere resettato, cioè rimosso dall'URL. Assumerà quindi il suo valore predefinito:

<a n:href="this page: null">reset</a>

Componenti persistenti

Non solo i parametri, ma anche i componenti possono essere persistenti. Per un tale componente, i suoi parametri persistenti vengono trasferiti anche tra diverse azioni del presenter o tra più presenter. I componenti persistenti sono contrassegnati da un'annotazione nella classe del presenter. Ad esempio, in questo modo contrassegniamo i componenti calendar e poll:

/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

I sottocomponenti all'interno di questi componenti non devono essere contrassegnati, diventeranno persistenti anch'essi.

In PHP 8, è possibile utilizzare anche attributi per contrassegnare i componenti persistenti:

use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Componenti con dipendenze

Come creare componenti con dipendenze senza “inquinare” i presenter che li utilizzeranno? Grazie alle proprietà intelligenti del container DI in Nette, proprio come nell'uso dei servizi classici, è possibile lasciare la maggior parte del lavoro al framework.

Prendiamo come esempio 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 risolvere. Il container DI si occuperebbe invisibilmente di passare tutte le dipendenze. Ma con i componenti, di solito li gestiamo creando una nuova istanza direttamente nel presenter nei metodi factory createComponent…(). Ma passare tutte le dipendenze di tutti i componenti al presenter per poi passarle ai componenti è macchinoso. E quanto codice scritto…

La domanda logica è: perché non registriamo semplicemente il componente come un servizio classico, lo passiamo al presenter e poi lo restituiamo nel metodo createComponent…()? Tale approccio è però inappropriato, perché vogliamo poter creare il componente anche più volte.

La soluzione corretta è scrivere una factory per il componente, cioè una classe che ci creerà il componente:

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 utilizziamo 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 fantastica è che Nette DI può generare tali semplici factory, quindi invece del suo intero codice, basta scrivere solo la sua interfaccia:

interface PollControlFactory
{
	public function create(int $id): PollControl;
}

E questo è tutto. Nette implementa internamente questa interfaccia e la passa al presenter, dove possiamo già utilizzarla. Aggiunge magicamente anche il parametro $id e l'istanza della classe PollFacade al nostro componente.

Componenti in profondità

I componenti in Nette Application rappresentano parti riutilizzabili dell'applicazione web che inseriamo nelle pagine e a cui, del resto, è dedicato l'intero capitolo. Quali capacità ha esattamente un tale componente?

  1. è renderizzabile nel template
  2. sa quale sua parte deve renderizzare durante una richiesta AJAX (snippet)
  3. ha la capacità di memorizzare il proprio stato nell'URL (parametri persistenti)
  4. ha la capacità di reagire alle azioni dell'utente (segnali)
  5. crea una struttura gerarchica (dove la radice è il presenter)

Ognuna di queste funzioni è gestita da una delle classi della linea ereditaria. Il rendering (1 + 2) è gestito da Nette\Application\UI\Control, l'integrazione nel ciclo di vita (3, 4) dalla classe Nette\Application\UI\Component e la creazione della struttura gerarchica (5) dalle 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

Ciclo di vita del componente

Validazione dei parametri persistenti

I valori dei parametri persistenti ricevuti dall'URL vengono scritti nelle proprietà dal metodo loadState(). Questo controlla anche se il tipo di dati specificato nella proprietà corrisponde, altrimenti risponde con un errore 404 e la pagina non viene visualizzata.

Non fidarti mai ciecamente dei parametri persistenti, perché possono essere facilmente sovrascritti dall'utente nell'URL. In questo modo, ad esempio, verifichiamo se il numero di pagina $this->page è maggiore di 0. Un modo appropriato è sovrascrivere il metodo loadState() 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 del valore personalizzato:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

Il processo opposto, cioè la raccolta dei valori dalle proprietà persistenti, è gestito dal metodo saveState().

Segnali in profondità

Un segnale provoca il ricaricamento della pagina proprio come nella richiesta originale (tranne quando viene chiamato tramite AJAX) e invoca il metodo signalReceived($signal), la cui implementazione predefinita nella classe Nette\Application\UI\Component tenta di chiamare un metodo composto dalle parole handle{signal}. L'ulteriore elaborazione dipende dall'oggetto specifico. Gli oggetti che ereditano da Component (cioè Control e Presenter) reagiscono cercando di chiamare il metodo handle{signal} con i parametri appropriati.

In altre parole: prende la definizione della funzione handle{signal} e tutti i parametri che sono arrivati con la richiesta, e agli argomenti vengono assegnati i parametri dall'URL in base al nome e tenta di chiamare il metodo dato. Ad esempio, come parametro $id viene passato il valore dal parametro id nell'URL, come $something viene passato something dall'URL, ecc. E se il metodo non esiste, il metodo signalReceived lancia un'eccezione.

Un segnale può essere ricevuto da qualsiasi componente, presenter o oggetto che implementa l'interfaccia SignalReceiver ed è connesso all'albero dei componenti.

I principali destinatari dei segnali saranno i Presenter e i componenti visivi che ereditano da Control. Un segnale serve come indicazione per un oggetto che deve fare qualcosa – un sondaggio deve contare il voto di un 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 per un segnale viene creato utilizzando il metodo Component::link(). Come parametro $destination passiamo la stringa {signal}! 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, vengono aggiunti solo i parametri del segnale. Inoltre, viene aggiunto all'inizio il parametro ?do, che specifica il segnale.

Il suo formato è {signal} o {signalReceiver}-{signal}. {signalReceiver} è il nome del componente nel presenter. Pertanto, non può esserci un trattino nel nome del componente – viene utilizzato per separare il nome del componente e il segnale, tuttavia è possibile nidificare più componenti in questo modo.

Il metodo isSignalReceiver() verifica se il componente (primo argomento) è il destinatario del segnale (secondo argomento). Possiamo omettere il secondo argomento – quindi verifica se il componente è il destinatario di qualsiasi segnale. Come secondo parametro è possibile specificare true e verificare così se il destinatario non è solo il componente specificato, ma anche uno qualsiasi dei suoi discendenti.

In qualsiasi fase precedente a handle{signal} possiamo eseguire il segnale manualmente chiamando il metodo processSignal(), che si occupa di gestire il segnale – prende il componente che è stato determinato come destinatario del segnale (se non è specificato alcun destinatario del segnale, è il presenter stesso) e gli invia il segnale.

Esempio:

if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
	$this->processSignal();
}

In questo modo il segnale viene eseguito prematuramente e non verrà più chiamato.

versione: 4.0