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.
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:
redirect()
,redirectPermanent()
,redirectUrl()
eforward()
reindirizzanoerror()
termina il presenter a causa di un erroresendJson($data)
termina il presenter e invia dati in formato JSONsendTemplate()
termina il presenter e immediatamente renderizza il templatesendResponse($response)
termina il presenter e invia una risposta personalizzataterminate()
termina il presenter senza risposta
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.
Creazione di link
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:
- Nette\Application\Responses\CallbackResponse – invia un callback
- Nette\Application\Responses\FileResponse – invia un file
- Nette\Application\Responses\ForwardResponse – forward()
- Nette\Application\Responses\JsonResponse – invia JSON
- Nette\Application\Responses\RedirectResponse – redirect
- Nette\Application\Responses\TextResponse – invia testo
- Nette\Application\Responses\VoidResponse – risposta vuota
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.