Componenti interattivi

I componenti sono oggetti separati e riutilizzabili che vengono inseriti nelle pagine. Possono essere moduli, griglie di dati, sondaggi, in realtà qualsiasi cosa abbia senso usare ripetutamente. Mostreremo:

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

Nette ha un sistema di componenti integrato. I più anziani potrebbero ricordare qualcosa di simile da Delphi o ASP.NET Web Forms. React o Vue.js sono costruiti su qualcosa di vagamente simile. Tuttavia, nel mondo dei framework PHP, questa è una caratteristica del tutto unica.

Allo stesso tempo, i componenti cambiano radicalmente l'approccio allo sviluppo delle applicazioni. È possibile comporre pagine a partire da unità pre-preparate. Avete bisogno di una griglia di dati nell'amministrazione? Potete trovarla su Componette, un repository di componenti aggiuntivi open-source (non solo componenti) per Nette, e incollarla semplicemente nel presenter.

È possibile incorporare nel presenter un numero qualsiasi di componenti. E si possono inserire altri componenti in alcuni componenti. In questo modo si crea un albero di componenti con un presentatore come radice.

Metodi di fabbrica

Come vengono posizionati e successivamente utilizzati i componenti nel presentatore? Di solito utilizzando i metodi di fabbrica.

Il factory dei componenti è un modo elegante per creare i componenti solo quando sono realmente necessari (lazy / on-demand). L'intera magia sta nell'implementazione di un metodo chiamato createComponent<Name>()dove <Name> è il nome del componente, che verrà creato e restituito.

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->item;
		return $poll;
	}
}

Poiché tutti i componenti sono creati in metodi separati, il codice è più pulito e facile da leggere.

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

Le fabbriche non vengono mai chiamate direttamente, ma vengono richiamate automaticamente quando si utilizzano i componenti per la prima volta. Grazie a ciò, un componente viene creato al momento giusto e solo se è veramente necessario. Se non usassimo il componente (per esempio su qualche richiesta AJAX, in cui restituiamo solo una parte della pagina, o quando alcune parti vengono memorizzate nella cache), non verrebbe nemmeno creato e risparmieremmo le prestazioni del server.

// si accede al componente e se è la prima volta,
// si chiama createComponentPoll() per crearlo
$poll = $this->getComponent('poll');
// sintassi alternativa: $poll = $this['poll'];

Nel template, è possibile rendere un componente usando il tag {control}. Non è quindi necessario passare manualmente i componenti al template.

<h2>Please Vote</h2>

{control poll}

Stile Hollywood

I componenti utilizzano comunemente una tecnica interessante, che ci piace chiamare stile hollywoodiano. Sicuramente conoscete il cliché che gli attori sentono spesso ai casting call: “Non chiamateci, vi chiameremo noi”. Ed è proprio di questo che si tratta.

In Nette, invece di dover fare continuamente domande (“il modulo è stato inviato?”, “era valido?” o “qualcuno ha premuto questo pulsante?”), si dice al framework “quando succede questo, chiama questo metodo” e si lascia che il lavoro prosegua. Chi programma in JavaScript ha familiarità con questo stile di programmazione. Si scrivono funzioni che vengono chiamate quando si verifica un determinato evento. Il motore passa loro i parametri appropriati.

Questo cambia completamente il modo di scrivere le applicazioni. Più compiti si possono delegare al framework, meno lavoro si ha. E meno ci si può dimenticare.

Come scrivere un componente

Per componente si intendono solitamente i discendenti della classe Nette\Application\UI\Control. Anche il presentatore Nette\Application\UI\Presenter è un discendente della classe Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Rendering

Sappiamo già che il tag {control componentName} viene utilizzato per disegnare un componente. In realtà richiama il metodo render() del componente, in cui ci occupiamo del rendering. Abbiamo, proprio come nel presentatore, un modello Latte nella variabile $this->template, a cui passiamo i parametri. A differenza dell'uso nel presentatore, dobbiamo specificare un file di modello e lasciarlo renderizzare:

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

Il tag {control} permette di passare i parametri al metodo render():

{control poll $id, $message}
public function render(int $id, string $message): void
{
	// ...
}

A volte un componente può essere costituito da diverse parti che vogliamo rendere separatamente. Per ognuna di esse creeremo un proprio metodo di rendering, ad esempio renderPaginator():

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

E nel template lo chiamiamo usando:

{control poll:paginator}

Per una migliore comprensione, è bene sapere come il tag viene compilato in codice PHP.

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

Questo si compila in:

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

getComponent() restituisce il componente poll e su di esso viene richiamato il metodo render() o renderPaginator(), rispettivamente.

Se in un punto qualsiasi della parte dei parametri viene usato =>, tutti i parametri saranno avvolti da un array e passati come primo argomento:

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

si compila in:

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

Rendering del sottocomponente:

{control cartControl-someForm}

si compila in:

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

I componenti, come i presentatori, passano automaticamente diverse variabili utili ai modelli:

  • $basePath è un percorso URL assoluto alla cartella principale (per esempio /CD-collection)
  • $baseUrl è un URL assoluto alla cartella principale (per esempio http://localhost/CD-collection)
  • $user è un oggetto che rappresenta l'utente
  • $presenter è il presentatore corrente
  • $control è il componente corrente
  • $flashes è l'elenco dei messaggi inviati dal metodo flashMessage()

Segnale

Sappiamo già che la navigazione nell'applicazione Nette consiste nel collegamento o nel reindirizzamento a coppie Presenter:action. Ma cosa succede se vogliamo semplicemente eseguire un'azione sulla pagina corrente? Per esempio, cambiare l'ordine delle colonne nella tabella; cancellare un elemento; cambiare la modalità luce/buio; inviare il modulo; votare nel sondaggio; ecc.

Questo tipo di richiesta si chiama segnale. E come le azioni invocano metodi action<Action>() o render<Action>()i segnali chiamano i metodi handle<Signal>(). Mentre il concetto di azione (o vista) si riferisce solo ai presentatori, i segnali si applicano a tutti i componenti. E quindi anche ai presentatori, perché UI\Presenter è un discendente di UI\Control.

public function handleClick(int $x, int $y): void
{
	// ... elaborazione del segnale ...
}

Il collegamento che richiama il segnale viene creato nel modo consueto, cioè nel template tramite l'attributo n:href o il tag {link}, nel codice tramite il metodo link(). Maggiori informazioni nel capitolo Creazione di collegamenti URL.

<a n:href="click! $x, $y">click here</a>

Il segnale viene sempre richiamato nel presentatore e nella vista correnti, quindi non è possibile collegarsi al segnale in presentatori/azioni diversi.

Pertanto, il segnale provoca il ricaricamento della pagina esattamente come nella richiesta originale, solo che in aggiunta richiama il metodo di gestione del segnale con i parametri appropriati. Se il metodo non esiste, viene lanciata l'eccezione Nette\Application\UI\BadSignalException, che viene visualizzata dall'utente come pagina di errore 403 Forbidden.

Snippet e AJAX

I segnali possono ricordare un po' AJAX: gestori che vengono chiamati sulla pagina corrente. E avete ragione, i segnali vengono spesso chiamati utilizzando AJAX, e poi trasmettiamo al browser solo le parti modificate della pagina. Sono chiamati snippet. Maggiori informazioni si trovano nella pagina su AJAX.

Messaggi Flash

Un componente ha una propria memoria di messaggi flash indipendente dal presentatore. Si tratta di messaggi che, ad esempio, informano sul risultato dell'operazione. Una caratteristica importante dei messaggi flash è che sono disponibili nel modello anche dopo il reindirizzamento. Anche dopo essere stati visualizzati, rimarranno in vita per altri 30 secondi – ad esempio, nel caso in cui l'utente dovesse involontariamente aggiornare la pagina – il messaggio non andrà perso.

L'invio avviene tramite il metodo flashMessage. Il primo parametro è il testo del messaggio o l'oggetto stdClass che rappresenta il messaggio. Il secondo parametro opzionale è il tipo di messaggio (errore, avviso, informazione, ecc.). Il metodo flashMessage() restituisce un'istanza di messaggio flash come oggetto stdClass a cui è possibile passare informazioni.

$this->flashMessage('L'articolo è stato cancellato.');
$this->redirect(/* ... */); // e reindirizzamento

Nel modello, questi messaggi sono disponibili nella variabile $flashes come oggetti stdClass, che contengono le proprietà message (testo del messaggio), type (tipo di messaggio) e possono contenere le già citate informazioni sull'utente. Li disegniamo come segue:

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

Parametri persistenti

I parametri persistenti sono utilizzati per mantenere lo stato dei componenti tra diverse richieste. Il loro valore rimane invariato anche dopo che si è fatto clic su un collegamento. A differenza dei dati di sessione, vengono trasferiti nell'URL. Vengono trasferiti automaticamente, compresi i collegamenti creati in altri componenti della stessa pagina.

Ad esempio, si dispone di un componente di paginazione dei contenuti. In una pagina possono esserci diversi componenti di questo tipo. Si vuole che tutti i componenti rimangano nella pagina corrente quando si fa clic sul collegamento. Pertanto, il numero di pagina (page) è un parametro persistente.

Creare un parametro persistente è estremamente facile in Nette. È sufficiente creare una proprietà pubblica e contrassegnarla con l'attributo: (in precedenza si usava /** @persistent */ )

use Nette\Application\Attributes\Persistent; // questa linea è importante

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // deve essere pubblico
}

Si consiglia di includere nella proprietà il tipo di dati (ad esempio, int) e si può anche includere un valore predefinito. I valori dei parametri possono essere convalidati.

È possibile modificare il valore di un parametro persistente durante la creazione di un collegamento:

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

Oppure può essere ripristinato, cioè rimosso dall'URL. In questo caso, assumerà il valore predefinito:

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

Componenti persistenti

Non solo i parametri, ma anche i componenti possono essere persistenti. I loro parametri persistenti vengono trasferiti anche tra azioni diverse o tra presentatori diversi. I componenti persistenti vengono contrassegnati con queste annotazioni per la classe presentatore. Per esempio, qui contrassegniamo i componenti calendar e poll come segue:

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

Non è necessario contrassegnare i sottocomponenti come persistenti, lo sono automaticamente.

In PHP 8, si possono usare anche gli 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 “incasinare” i presentatori che li utilizzeranno? Grazie alle caratteristiche intelligenti del contenitore DI di Nette, come per l'uso dei servizi tradizionali, possiamo 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 di un sondaggio, per il quale viene creato il componente
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($id, $voteId);
		// ...
	}
}

Se stessimo scrivendo un servizio classico, non ci sarebbe nulla di cui preoccuparsi. Il contenitore DI si occuperebbe in modo invisibile di passare tutte le dipendenze. Ma di solito gestiamo i componenti creando una nuova istanza di essi direttamente nel presentatore, con metodi di fabbrica createComponent...(). Ma passare tutte le dipendenze di tutti i componenti al presentatore per poi passarle ai componenti è macchinoso. E la quantità di codice scritto…

La domanda logica è: perché non registrare il componente come un servizio classico, passarlo al presentatore e poi restituirlo nel metodo createComponent...()? Ma questo approccio è inadeguato, perché vogliamo poter creare il componente più volte.

La soluzione corretta è scrivere un factory per il componente, cioè una classe che crei il componente per noi:

class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}

Ora registriamo il nostro servizio al contenitore DI per la configurazione:

services:
	- PollControlFactory

Infine, utilizzeremo questo factory 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 può generare factory così semplici, quindi invece di scrivere l'intero codice, è sufficiente scrivere la sua interfaccia:

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

Tutto qui. Nette implementa internamente questa interfaccia e la inietta nel nostro presenter, dove possiamo usarla. Inoltre, passa magicamente il nostro parametro $id e l'istanza della classe PollFacade nel nostro componente.

Componenti in profondità

I componenti di un'applicazione Nette sono le parti riutilizzabili di un'applicazione Web che vengono incorporate nelle pagine, argomento di questo capitolo. Quali sono esattamente le funzionalità di un componente?

  1. è renderizzabile in un modello
  2. sa quale parte di se stesso rendere durante una richiesta AJAX (snippet)
  3. ha la capacità di memorizzare il proprio stato in un URL (parametri persistenti)
  4. ha la capacità di rispondere alle azioni dell'utente (segnali)
  5. crea una struttura gerarchica (dove la radice è il presenter)

Ciascuna di queste funzioni è gestita da una delle classi di ereditarietà. Il rendering (1 + 2) è gestito da Nette\Application\UI\Control, l'incorporazione 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 di un componente

Ciclo di vita del componente

Convalida dei parametri persistenti

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

Non fidarsi mai ciecamente dei parametri persistenti, perché possono essere facilmente sovrascritti dall'utente nell'URL. Per esempio, ecco come verificare se il numero di pagina $this->page è maggiore di 0. Un buon modo per farlo è sovrascrivere il metodo loadState() citato in precedenza:

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // qui viene impostato $this->pagina
		// segue il controllo del valore dell'utente:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

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

Segnali in profondità

Un segnale causa un ricaricamento della pagina come la richiesta originale (con l'eccezione di 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 si basa sull'oggetto dato. Gli oggetti che sono discendenti di Component (cioè Control e Presenter) cercano di chiamare handle{Signal} con i relativi parametri.

In altre parole, viene presa la definizione del metodo handle{Signal} e tutti i parametri ricevuti nella richiesta vengono abbinati ai parametri del metodo. Ciò significa che il parametro id dell'URL viene abbinato al parametro del metodo $id, something a $something e così via. Se il metodo non esiste, il metodo signalReceived lancia un'eccezione.

Il segnale può essere ricevuto da qualsiasi componente, presentatore di oggetti che implementano l'interfaccia SignalReceiver se è collegato all'albero dei componenti.

I principali destinatari dei segnali sono Presenters e i componenti visuali che estendono Control. Un segnale è un segnale che indica a un oggetto che deve fare qualcosa: un sondaggio conta i voti degli utenti, un riquadro con le notizie deve essere aperto, un modulo è stato inviato e deve elaborare i dati e così via.

L'URL per il segnale viene creato con il metodo Component::link(). Come parametro $destination si passa la stringa {signal}! e come $args un array di argomenti da passare al gestore del segnale. I parametri del segnale sono collegati all'URL del presentatore/vista corrente. **Il parametro ?do nell'URL determina il segnale chiamato.

Il suo formato è {signal} o {signalReceiver}-{signal}. {signalReceiver} è il nome del componente nel presentatore. Per questo motivo il trattino (imprecisamente dash) non può essere presente nel nome dei componenti: serve a dividere il nome del componente e del segnale, ma è possibile comporre più componenti.

Il metodo isSignalReceiver() verifica se un componente (primo argomento) è un ricevitore di un segnale (secondo argomento). Il secondo argomento può essere omesso: in questo caso si scopre se il componente è un ricevitore di qualsiasi segnale. Se il secondo parametro è true, verifica se il componente o i suoi discendenti sono ricevitori di un segnale.

In qualsiasi fase precedente a handle{Signal} il segnale può essere eseguito manualmente chiamando il metodo processSignal() che si assume la responsabilità dell'esecuzione del segnale. Prende il componente ricevente (se non è impostato è il presentatore stesso) e gli invia il segnale.

Esempio:

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

Il segnale viene eseguito prematuramente e non verrà più richiamato.

versione: 4.0