Presenter

Impareremo come scrivere presenter e template in Nette. Dopo aver letto, saprai:

  • come funziona un presenter
  • cosa sono i parametri persistenti
  • come vengono renderizzati i template

Sappiamo già che un presenter è una classe che rappresenta una specifica pagina di un'applicazione web, ad es. la homepage; un prodotto in un e-shop; un modulo di login; un feed sitemap, ecc. Un'applicazione può avere da uno a migliaia di presenter. In altri framework vengono anche chiamati controller.

Di solito, con il termine presenter si intende un discendente della classe Nette\Application\UI\Presenter, che è adatto per generare interfacce web e al quale ci dedicheremo nel resto di questo capitolo. In senso generale, un presenter è qualsiasi oggetto che implementa l'interfaccia Nette\Application\IPresenter.

Ciclo di vita del presenter

Il compito del presenter è gestire la richiesta e restituire una risposta (che può essere una pagina HTML, un'immagine, un redirect, ecc.).

Quindi, all'inizio, gli viene passata la richiesta. Non è direttamente la richiesta HTTP, ma l'oggetto Nette\Application\Request, nel quale la richiesta HTTP è stata trasformata con l'aiuto del router. Di solito non entriamo in contatto con questo oggetto, poiché il presenter delega intelligentemente l'elaborazione della richiesta ad altri metodi, che ora vedremo.

Ciclo di vita del presenter

L'immagine rappresenta l'elenco dei metodi che vengono chiamati in sequenza dall'alto verso il basso, se esistono. Nessuno di essi deve esistere, possiamo avere un presenter completamente vuoto senza un singolo metodo e costruirci sopra un semplice sito web statico.

__construct()

Il costruttore non appartiene propriamente al ciclo di vita del presenter, perché viene chiamato al momento della creazione dell'oggetto. Ma lo menzioniamo per la sua importanza. Il costruttore (insieme al metodo inject) serve a passare le dipendenze.

Il presenter non dovrebbe occuparsi della logica di business dell'applicazione, scrivere e leggere dal database, eseguire calcoli, ecc. Per questo ci sono classi del layer che chiamiamo model. Ad esempio, la classe ArticleRepository può essere responsabile del caricamento e del salvataggio degli articoli. Affinché il presenter possa lavorarci, se la fa passare tramite dependency injection:

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articles,
	) {
	}
}

startup()

Subito dopo aver ricevuto la richiesta, viene chiamato il metodo startup(). Puoi usarlo per inizializzare le proprietà, verificare i permessi utente, ecc. È richiesto che il metodo chiami sempre il genitore parent::startup().

action<Action>(args...)

Analogo al metodo render<View>(). Mentre render<View>() è destinato a preparare i dati per un template specifico che verrà successivamente renderizzato, in action<Action>() la richiesta viene elaborata senza relazione con il rendering del template. Ad esempio, vengono elaborati i dati, l'utente viene loggato o sloggato, e così via, e poi reindirizza altrove.

È importante notare che action<Action>() viene chiamato prima di render<View>(), quindi al suo interno possiamo eventualmente cambiare il corso successivo degli eventi, cioè cambiare il template che verrà renderizzato, e anche il metodo render<View>() che verrà chiamato. E questo tramite setView('altraView').

Al metodo vengono passati i parametri dalla richiesta. È possibile e consigliato specificare i tipi per i parametri, ad es. actionShow(int $id, ?string $slug = null) – se il parametro id manca o se non è un intero, il presenter restituirà un errore 404 e terminerà l'attività.

handle<Signal>(args...)

Il metodo elabora i cosiddetti segnali, che conosceremo nel capitolo dedicato ai componenti. È infatti destinato principalmente ai componenti e all'elaborazione delle richieste AJAX.

Al metodo vengono passati i parametri dalla richiesta, come nel caso di action<Action>(), incluso il controllo del tipo.

beforeRender()

Il metodo beforeRender, come suggerisce il nome, viene chiamato prima di ogni metodo render<View>(). Viene utilizzato per la configurazione comune del template, il passaggio di variabili per il layout e simili.

render<View>(args...)

Il luogo dove prepariamo il template per il successivo rendering, gli passiamo i dati, ecc.

Al metodo vengono passati i parametri dalla richiesta, come nel caso di action<Action>(), incluso il controllo del tipo.

public function renderShow(int $id): void
{
	// otteniamo i dati dal model e li passiamo al template
	$this->template->article = $this->articles->getById($id);
}

afterRender()

Il metodo afterRender, come suggerisce nuovamente il nome, viene chiamato dopo ogni metodo render<View>(). Viene utilizzato piuttosto eccezionalmente.

shutdown()

Viene chiamato alla fine del ciclo di vita del presenter.

Un buon consiglio prima di andare avanti. Come si vede, un presenter può gestire più azioni/view, cioè avere più metodi render<View>(). Ma consigliamo di progettare presenter con una o il minor numero possibile di azioni.

Invio della risposta

La risposta del presenter è di solito il rendering di un template con una pagina HTML, ma può anche essere l'invio di un file, JSON o magari un redirect a un'altra pagina.

In qualsiasi momento durante il ciclo di vita, possiamo inviare una risposta con uno dei seguenti metodi e allo stesso tempo terminare il presenter:

Se non chiami nessuno di questi metodi, il presenter procederà automaticamente al rendering del template. Perché? Perché nel 99% dei casi vogliamo renderizzare un template, quindi il presenter considera questo comportamento come predefinito e vuole semplificarci il lavoro.

Il presenter dispone del metodo link(), tramite il quale è possibile creare link URL ad altri presenter. Il primo parametro è il presenter & azione di destinazione, seguono gli argomenti passati, che possono essere specificati come array:

$url = $this->link('Product:show', $id);

$url = $this->link('Product:show', [$id, 'lang' => 'cs']);

Nel template, i link ad altri presenter & azioni vengono creati in questo modo:

<a n:href="Product:show $id">dettaglio prodotto</a>

Semplicemente, invece dell'URL reale, scrivi la coppia nota Presenter:action e specifichi eventuali parametri. Il trucco sta in n:href, che dice che questo attributo sarà elaborato da Latte e genererà l'URL reale. In Nette, quindi, non devi affatto pensare agli URL, ma solo ai presenter e alle azioni.

Maggiori informazioni si trovano nel capitolo Creazione di link URL.

Redirect

Per passare a un altro presenter si usano i metodi redirect() e forward(), che hanno una sintassi molto simile al metodo link().

Il metodo forward() passa immediatamente al nuovo presenter senza redirect HTTP:

$this->forward('Product:show');

Esempio del cosiddetto redirect temporaneo con codice HTTP 302 (o 303, se il metodo della richiesta corrente è POST):

$this->redirect('Product:show', $id);

Il redirect permanente con codice HTTP 301 si ottiene così:

$this->redirectPermanent('Product:show', $id);

È possibile reindirizzare a un altro URL al di fuori dell'applicazione con il metodo redirectUrl(). Come secondo parametro è possibile specificare il codice HTTP, il predefinito è 302 (o 303, se il metodo della richiesta corrente è POST):

$this->redirectUrl('https://nette.org');

Il redirect termina immediatamente l'attività del presenter lanciando la cosiddetta eccezione di terminazione silenziosa Nette\Application\AbortException.

Prima del redirect è possibile inviare un messaggio flash, cioè messaggi che verranno visualizzati nel template dopo il redirect.

Messaggi flash

Si tratta di messaggi che solitamente informano sul risultato di qualche 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.

Basta chiamare il metodo flashMessage() e il presenter si occuperà di passarlo al template. Il primo parametro è il testo del messaggio e il secondo parametro opzionale è il suo tipo (error, warning, info, ecc.). Il metodo flashMessage() restituisce un'istanza del messaggio flash, alla quale è 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}

Errore 404 e simili

Se non è possibile soddisfare la richiesta, ad esempio perché l'articolo che vogliamo visualizzare non esiste nel database, lanciamo un errore 404 con il metodo error(?string $message = null, int $httpCode = 404).

public function renderShow(int $id): void
{
	$article = $this->articles->getById($id);
	if (!$article) {
		$this->error();
	}
	// ...
}

Il codice HTTP dell'errore può essere passato come secondo parametro, il predefinito è 404. Il metodo funziona lanciando l'eccezione Nette\Application\BadRequestException, dopodiché Application passa il controllo all'error-presenter. Che è un presenter il cui compito è visualizzare una pagina che informa sull'errore verificatosi. L'impostazione dell'error-presenter viene eseguita nella configurazione application.

Invio di JSON

Esempio di un metodo action che invia dati in formato JSON e termina il presenter:

public function actionData(): void
{
	$data = ['hello' => 'nette'];
	$this->sendJson($data);
}

Parametri della richiesta

Il presenter e anche ogni componente ottengono i propri parametri dalla richiesta HTTP. Il loro valore si ottiene con il metodo getParameter($name) o getParameters(). I valori sono stringhe o array di stringhe, si tratta fondamentalmente di dati grezzi ottenuti direttamente dall'URL.

Per maggiore comodità, consigliamo di rendere accessibili i parametri tramite property. Basta contrassegnarli con l'attributo #[Parameter]:

use Nette\Application\Attributes\Parameter;  // questa riga è importante

class HomePresenter extends Nette\Application\UI\Presenter
{
	#[Parameter]
	public string $theme; // deve essere public
}

Per la property, consigliamo di specificare anche il tipo di dati (es. string) e Nette convertirà automaticamente il valore in base ad esso. I valori dei parametri possono anche essere validati.

Durante la creazione di un link, è possibile impostare direttamente il valore dei parametri:

<a n:href="Home:default theme: dark">clicca</a>

Parametri persistenti

I parametri persistenti servono a mantenere lo stato 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, non è quindi necessario specificarli esplicitamente in link() o n:href.

Esempio di utilizzo? Hai un'applicazione multilingue. La lingua corrente è un parametro che deve essere costantemente parte dell'URL. Ma sarebbe incredibilmente noioso specificarlo in ogni link. Quindi lo rendi un parametro persistente lang e verrà trasferito da solo. Fantastico!

La creazione di un parametro persistente in Nette è estremamente semplice. Basta creare una property pubblica e contrassegnarla con un attributo: (in precedenza si usava /** @persistent */)

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

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang; // deve essere public
}

Se $this->lang avrà il valore, ad esempio, 'en', anche i link creati tramite link() o n:href conterranno il parametro lang=en. E dopo aver cliccato sul link, sarà di nuovo $this->lang = 'en'.

Per la property, consigliamo di specificare anche il tipo di dati (es. string) e puoi anche specificare un valore predefinito. I valori dei parametri possono essere validati.

I parametri persistenti vengono standardmente trasferiti tra tutte le azioni del presenter dato. Affinché vengano trasferiti anche tra più presenter, è necessario definirli o:

  • in un antenato comune da cui ereditano i presenter
  • in un trait che i presenter utilizzeranno:
trait LanguageAware
{
	#[Persistent]
	public string $lang;
}

class ProductPresenter extends Nette\Application\UI\Presenter
{
	use LanguageAware;
}

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

<a n:href="Product:show $id, lang: cs">dettaglio in ceco</a>

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

<a n:href="Product:show $id, lang: null">clicca</a>

Componenti Interattivi

I presenter hanno un sistema di componenti integrato. I componenti sono unità riutilizzabili separate che inseriamo nei presenter. Possono essere form, datagrid, menu, praticamente qualsiasi cosa che abbia senso usare ripetutamente.

Come vengono inseriti i componenti nel presenter e successivamente utilizzati? Lo imparerai nel capitolo Componenti. Scoprirai persino cosa hanno in comune con Hollywood.

E dove posso ottenere i componenti? Sulla pagina Componette troverai componenti open-source e anche numerosi altri add-on per Nette, che sono stati inseriti qui da volontari della comunità attorno al framework.

Andiamo in profondità

Con quello che abbiamo mostrato finora in questo capitolo, probabilmente ti basterà. Le righe seguenti sono destinate a coloro che sono interessati ai presenter in profondità e vogliono sapere assolutamente tutto.

Validazione dei parametri

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

Non fidarti mai ciecamente dei parametri, perché possono essere facilmente sovrascritti dall'utente nell'URL. In questo modo, ad esempio, verifichiamo se la lingua $this->lang è tra quelle supportate. Un modo appropriato è sovrascrivere il metodo loadState() menzionato:

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang;

	public function loadState(array $params): void
	{
		parent::loadState($params); // qui viene impostato $this->lang
		// segue il controllo del valore personalizzato:
		if (!in_array($this->lang, ['en', 'cs'])) {
			$this->error();
		}
	}
}

Salvataggio e ripristino della richiesta

La richiesta che il presenter gestisce è un oggetto Nette\Application\Request e viene restituita dal metodo del presenter getRequest().

La richiesta corrente può essere salvata nella sessione o, al contrario, ripristinata da essa e fatta eseguire nuovamente dal presenter. Questo è utile, ad esempio, nella situazione in cui l'utente sta compilando un modulo e la sua sessione scade. Per non perdere i dati, prima del redirect alla pagina di login, salviamo la richiesta corrente nella sessione tramite $reqId = $this->storeRequest(), che restituisce il suo identificatore sotto forma di una breve stringa e lo passiamo come parametro al presenter di login.

Dopo il login, chiamiamo il metodo $this->restoreRequest($reqId), che recupera la richiesta dalla sessione e vi inoltra. Il metodo verifica nel frattempo che la richiesta sia stata creata dallo stesso utente che si è appena loggato. Se si loggasse un altro utente o la chiave non fosse valida, non fa nulla e il programma continua.

Dai un'occhiata alla guida Come tornare alla pagina precedente.

Canonizzazione

I presenter hanno una caratteristica davvero fantastica che contribuisce a un migliore SEO (ottimizzazione della reperibilità su Internet). Impediscono automaticamente l'esistenza di contenuti duplicati su URL diversi. Se a una determinata destinazione portano più indirizzi URL, ad es. /index e /index?page=1, il framework ne determina uno come primario (canonico) e reindirizza gli altri ad esso tramite il codice HTTP 301. Grazie a ciò, i motori di ricerca non indicizzano le pagine due volte e non diluiscono il loro page rank.

Questo processo si chiama canonizzazione. L'URL canonico è quello generato dal router, di solito quindi la prima route corrispondente nella collezione.

La canonizzazione è attivata per impostazione predefinita e può essere disattivata tramite $this->autoCanonicalize = false.

Il redirect non avviene durante una richiesta AJAX o POST, perché si verificherebbe una perdita di dati o non avrebbe valore aggiunto dal punto di vista SEO.

La canonizzazione può essere invocata anche manualmente tramite il metodo canonicalize(), al quale, in modo simile al metodo link(), vengono passati il presenter, l'azione e i parametri. Crea un link e lo confronta con l'URL corrente. Se differiscono, reindirizza al link generato.

public function actionShow(int $id, ?string $slug = null): void
{
	$realSlug = $this->facade->getSlugForId($id);
	// reindirizza, se $slug differisce da $realSlug
	$this->canonicalize('Product:show', [$id, $realSlug]);
}

Eventi

Oltre ai metodi startup(), beforeRender() e shutdown(), che vengono chiamati come parte del ciclo di vita del presenter, è possibile definire altre funzioni che devono essere chiamate automaticamente. Il presenter definisce i cosiddetti eventi, i cui handler aggiungi agli array $onStartup, $onRender e $onShutdown.

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct()
	{
		$this->onStartup[] = function () {
			// ...
		};
	}
}

Gli handler nell'array $onStartup vengono chiamati poco prima del metodo startup(), poi $onRender tra beforeRender() e render<View>() e infine $onShutdown poco prima di shutdown().

Risposte

La risposta restituita dal presenter è un oggetto che implementa l'interfaccia Nette\Application\Response. Sono disponibili numerose risposte pronte:

Le risposte vengono inviate con il metodo sendResponse():

use Nette\Application\Responses;

// Testo semplice
$this->sendResponse(new Responses\TextResponse('Ciao Nette!'));

// Invia un file
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));

// La risposta sarà un callback
$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
	if ($httpResponse->getHeader('Content-Type') === 'text/html') {
		echo '<h1>Ciao</h1>';
	}
};
$this->sendResponse(new Responses\CallbackResponse($callback));

Restrizione dell'accesso tramite #[Requires]

L'attributo #[Requires] offre opzioni avanzate per limitare l'accesso ai presenter e ai loro metodi. Può essere utilizzato per specificare metodi HTTP, richiedere una richiesta AJAX, limitare alla stessa origine (same origin) e l'accesso solo tramite forward. L'attributo può essere applicato sia alle classi dei presenter che ai singoli metodi action<Action>(), render<View>(), handle<Signal>() e createComponent<Name>().

È possibile specificare queste restrizioni:

  • sui metodi HTTP: #[Requires(methods: ['GET', 'POST'])]
  • richiesta di una richiesta AJAX: #[Requires(ajax: true)]
  • accesso solo dalla stessa origine: #[Requires(sameOrigin: true)]
  • accesso solo tramite forward: #[Requires(forward: true)]
  • restrizione ad azioni specifiche: #[Requires(actions: 'default')]

I dettagli si trovano nella guida Come usare l'attributo Requires.

Controllo del metodo HTTP

I presenter in Nette verificano automaticamente il metodo HTTP di ogni richiesta in arrivo. Il motivo di questo controllo è principalmente la sicurezza. Standardmente sono consentiti i metodi GET, POST, HEAD, PUT, DELETE, PATCH.

Se si desidera consentire inoltre, ad esempio, il metodo OPTIONS, utilizzare l'attributo #[Requires] (da Nette Application v3.2):

#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

Nella versione 3.1, la verifica viene eseguita in checkHttpMethod(), che verifica se il metodo specificato nella richiesta è contenuto nell'array $presenter->allowedMethods. L'aggiunta del metodo si fa così:

class MyPresenter extends Nette\Application\UI\Presenter
{
    protected function checkHttpMethod(): void
    {
        $this->allowedMethods[] = 'OPTIONS';
        parent::checkHttpMethod();
    }
}

È importante sottolineare che se si consente il metodo OPTIONS, è necessario successivamente gestirlo adeguatamente all'interno del proprio presenter. Il metodo è spesso utilizzato come cosiddetta richiesta preflight, che il browser invia automaticamente prima della richiesta effettiva, quando è necessario verificare se la richiesta è consentita dal punto di vista della politica CORS (Cross-Origin Resource Sharing). Se si consente il metodo, ma non si implementa la risposta corretta, ciò può portare a incoerenze e potenziali problemi di sicurezza.

Ulteriori letture

versione: 4.0