Presentatori

Impareremo a scrivere presentatori e modelli in Nette. Dopo la lettura saprete che:

  • come funziona il presentatore
  • cosa sono i parametri persistenti
  • come rendere un modello

Sappiamo già che un presenter è una classe che rappresenta una pagina specifica di un'applicazione web, come ad esempio una homepage, un prodotto in un e-shop, un modulo di iscrizione, un feed sitemap, ecc. L'applicazione può avere da uno a migliaia di presenter. In altri framework, sono noti anche come controllori.

Di solito, il termine presenter si riferisce a un discendente della classe Nette\Application\UI\Presenter, adatta alle interfacce web e di cui parleremo nel resto del capitolo. In senso generale, un presentatore è un qualsiasi oggetto che implementa l'interfaccia Nette\Application\IPresenter.

Ciclo di vita del presentatore

Il compito del presentatore è quello di elaborare la richiesta e restituire una risposta (che può essere una pagina HTML, un'immagine, un reindirizzamento, ecc.)

All'inizio c'è una richiesta. Non si tratta direttamente di una richiesta HTTP, ma di un oggetto Nette\Application\Request in cui la richiesta HTTP è stata trasformata tramite un router. Di solito non entriamo in contatto con questo oggetto, perché il presentatore delega abilmente l'elaborazione della richiesta a metodi speciali, che vedremo ora.

Ciclo di vita del presentatore

La figura mostra un elenco di metodi che vengono chiamati in sequenza dall'alto verso il basso, se esistono. Non è necessario che esistano, possiamo avere un presenter completamente vuoto, senza un solo metodo, e costruirci sopra un semplice web statico.

__construct()

Il costruttore non appartiene esattamente al ciclo di vita del presentatore, perché viene chiamato al momento della creazione dell'oggetto. Ma lo citiamo per la sua importanza. Il costruttore (insieme al metodo inject) è usato per passare le dipendenze.

Il costruttore non deve occuparsi della logica di business dell'applicazione, scrivere e leggere dal database, eseguire calcoli, ecc. Questo è il compito delle classi di un livello, che chiamiamo modello. Per esempio, la classe ArticleRepository può essere responsabile del caricamento e del salvataggio degli articoli. Affinché il presentatore possa utilizzarla, viene passata utilizzando la dependency injection:

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

startup()

Subito dopo la ricezione della richiesta, viene invocato il metodo startup (). Si può usare per inizializzare le proprietà, controllare i privilegi dell'utente, ecc. È necessario chiamare sempre l'antenato parent::startup().

action<Action>(args...)

Simile al metodo render<View>(). Mentre render<View>() è destinato a preparare i dati per un modello specifico, che viene successivamente reso, in action<Action>() una richiesta viene elaborata senza una successiva resa del template. Ad esempio, i dati vengono elaborati, l'utente viene connesso o disconnesso e così via e poi viene reindirizzato altrove.

È importante che action<Action>() sia chiamato prima di render<View>()quindi al suo interno si può eventualmente modificare il corso successivo del ciclo di vita, cioè cambiare il template che sarà reso e anche il metodo render<View>() che sarà chiamato, utilizzando setView('otherView').

I parametri della richiesta vengono passati al metodo. È possibile e consigliabile specificare i tipi di parametri, ad esempio actionShow(int $id, string $slug = null) – se il parametro id manca o non è un intero, il presentatore restituisce l'errore 404 e termina l'operazione.

handle<Signal>(args...)

Questo metodo elabora i cosiddetti segnali, di cui si parlerà nel capitolo sui componenti. È destinato principalmente ai componenti e all'elaborazione di richieste AJAX.

I parametri vengono passati al metodo, come nel caso di action<Action>(), compreso il controllo del tipo.

beforeRender()

Il metodo beforeRender, come suggerisce il nome, viene chiamato prima di ogni metodo render<View>(). È usato per la configurazione comune dei template, per passare le variabili per il layout e così via.

render<View>(args...)

Il luogo in cui si prepara il modello per il rendering successivo, si passano i dati ad esso, ecc.

I parametri vengono passati al metodo, come nel caso di action<Action>(), compreso il controllo del tipo.

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

afterRender()

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

shutdown()

Viene richiamato alla fine del ciclo di vita del presentatore.

Buon consiglio prima di andare avanti. Come si può vedere, il presentatore può gestire più azioni/visualizzazioni, cioè avere più metodi render<View>(). Ma si consiglia di progettare presentatori con una sola o il minor numero possibile di azioni.

Invio di una risposta

La risposta del presentatore è solitamente il rendering del template con la pagina HTML, ma può anche essere l'invio di un file, di JSON o persino il reindirizzamento a un'altra pagina.

In qualsiasi momento del ciclo di vita, è possibile utilizzare uno dei seguenti metodi per inviare una risposta e allo stesso tempo uscire dal presentatore:

  • redirect(), redirectPermanent(), redirectUrl() e forward() reindirizzamenti
  • error() chiude il presentatore per errore
  • sendJson($data) esce dal presentatore e invia i dati in formato JSON
  • sendTemplate() abbandona il presentatore e renderizzaimmediatamente il template
  • sendResponse($response) abbandona il presentatore e invia la propria risposta
  • terminate() abbandona il presentatore senza risposta

Se non si chiama nessuno di questi metodi, il presentatore procederà automaticamente al rendering del modello. Perché? Perché nel 99% dei casi si vuole disegnare un modello, quindi il presentatore assume questo comportamento come predefinito e vuole semplificarci il lavoro.

Il presentatore ha un metodo link(), utilizzato per creare collegamenti URL ad altri presentatori. Il primo parametro è il presentatore e l'azione di destinazione, seguito dagli argomenti, che possono essere passati come array:

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

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

Nel modello creiamo collegamenti ad altri presentatori e azioni come segue:

<a n:href="Product:show $id">product detail</a>

È sufficiente scrivere la nota coppia Presenter:action al posto dell'URL reale e includere eventuali parametri. Il trucco è n:href, che dice che questo attributo sarà elaborato da Latte e genererà un vero URL. In Nette non è necessario pensare agli URL, ma solo ai presentatori e alle azioni.

Per ulteriori informazioni, vedere Creazione di collegamenti.

Reindirizzamento

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

Il metodo forward() passa immediatamente al nuovo presentatore senza reindirizzamento HTTP:

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

Esempio di un cosiddetto reindirizzamento temporaneo con codice HTTP 302 (o 303, se il metodo di richiesta corrente è POST):

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

Per ottenere un reindirizzamento permanente con codice HTTP 301 utilizzare:

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

È possibile reindirizzare a un altro URL al di fuori dell'applicazione utilizzando il metodo redirectUrl(). Il codice HTTP può essere specificato come secondo parametro; il valore predefinito è 302 (o 303, se il metodo di richiesta corrente è POST):

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

Il reindirizzamento termina immediatamente il ciclo di vita del presentatore lanciando la cosiddetta eccezione di terminazione silenziosa Nette\Application\AbortException.

Prima del reindirizzamento, è possibile inviare un messaggio flash, che verrà visualizzato nel modello dopo il reindirizzamento.

Messaggi flash

Sono messaggi che di solito informano sul risultato di un'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.

Basta chiamare il metodo flashMessage() e il presentatore si occuperà di passare il messaggio al modello. Il primo parametro è il testo del messaggio e il secondo parametro opzionale è il suo tipo (errore, avviso, info ecc.). Il metodo flashMessage() restituisce un'istanza di messaggio flash, per consentire di aggiungere ulteriori informazioni.

$this->flashMessage('Item was removed.');
$this->redirect(/* ... */);

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}

Errore 404 ecc.

Quando non possiamo soddisfare la richiesta, perché ad esempio l'articolo che vogliamo visualizzare non esiste nel database, lanceremo l'errore 404 usando il metodo error(string $message = null, int $httpCode = 404), che rappresenta l'errore HTTP 404:

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

Il codice di errore HTTP può essere passato come secondo parametro; il valore predefinito è 404. Il metodo funziona lanciando l'eccezione Nette\Application\BadRequestException, dopo di che Application passa il controllo al presentatore dell'errore. Si tratta di un presentatore che ha il compito di visualizzare una pagina che informa dell'errore. Il presentatore di errori è impostato nella configurazione dell'applicazione.

Invio di JSON

Esempio di metodo-azione che invia dati in formato JSON ed esce dal presentatore:

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

Parametri di richiesta

Il presentatore, così come ogni componente, ottiene i suoi parametri dalla richiesta HTTP. I loro valori possono essere recuperati con il metodo getParameter($name) o getParameters(). I valori sono stringhe o array di stringhe, essenzialmente dati grezzi ottenuti direttamente dall'URL.

Per maggiore comodità, si consiglia di rendere i parametri accessibili tramite proprietà. È sufficiente annotarli con l'attributo #[Parameter] attributo:

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

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

Per le proprietà, si consiglia di specificare il tipo di dati (ad esempio, string). Nette calcolerà automaticamente il valore in base ad esso. Anche i valori dei parametri possono essere convalidati.

Quando si crea un collegamento, è possibile impostare direttamente il valore dei parametri:

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

Parametri persistenti

I parametri persistenti sono usati per mantenere lo stato tra diverse richieste. Il loro valore rimane invariato anche dopo aver cliccato su un link. A differenza dei dati di sessione, vengono passati nell'URL. Questo è completamente automatico, quindi non è necessario dichiararli esplicitamente in link() o n:href.

Un esempio di utilizzo? Avete un'applicazione multilingue. La lingua attuale è un parametro che deve sempre far parte dell'URL. Ma sarebbe incredibilmente noioso includerlo in ogni link. Perciò lo si rende un parametro persistente, chiamato lang, che si autoalimenta. Forte!

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

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

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

Se $this->lang ha un valore come 'en', i link creati con link() o n:href conterranno anche il parametro lang=en. E quando il link viene cliccato, sarà di nuovo $this->lang = 'en'.

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

I parametri persistenti vengono passati per impostazione predefinita tra tutte le azioni di un determinato presentatore. Per passarli tra più presentatori, è necessario definirli:

  • in un antenato comune dal quale i presentatori ereditano
  • nel tratto che i presentatori utilizzano:
trait LanguageAware
{
	#[Persistent]
	public string $lang;
}

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

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

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

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

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

Componenti interattivi

I presentatori hanno un sistema di componenti incorporato. I componenti sono unità separate e riutilizzabili che vengono inserite nei presentatori. Possono essere moduli, griglie di dati, menu, qualsiasi cosa abbia senso usare ripetutamente.

Come si posizionano i componenti e come si utilizzano successivamente nel presentatore? Questo è spiegato nel capitolo Componenti. Scoprirete anche cosa hanno a che fare con Hollywood.

Dove posso trovare alcuni componenti? Alla pagina Componenti si possono trovare alcuni componenti open-source e altri addons per Nette, realizzati e condivisi dalla comunità di Nette Framework.

Approfondimento

Quanto mostrato finora in questo capitolo sarà probabilmente sufficiente. Le righe che seguono sono destinate a chi è interessato ad approfondire i presentatori e vuole sapere tutto.

Convalida dei parametri

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

Non fidarsi mai ciecamente dei parametri, perché possono essere facilmente sovrascritti dall'utente nell'URL. Ad esempio, è così che controlliamo se $this->lang è tra le lingue supportate. Un buon modo per farlo è sovrascrivere il metodo loadState() citato in precedenza:

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 dell'utente:
		if (!in_array($this->lang, ['en', 'cs'])) {
			$this->error();
		}
	}
}

Salvare e ripristinare la richiesta

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

È possibile salvare la richiesta corrente in una sessione o ripristinarla dalla sessione e lasciare che il presentatore la esegua di nuovo. Ciò è utile, ad esempio, quando un utente compila un modulo e il suo login scade. Per non perdere i dati, prima di reindirizzare alla pagina di accesso, salviamo la richiesta corrente nella sessione con il metodo $reqId = $this->storeRequest(), che restituisce un identificatore sotto forma di stringa breve e lo passa come parametro al presentatore di accesso.

Dopo l'accesso, chiamiamo il metodo $this->restoreRequest($reqId), che preleva la richiesta dalla sessione e la inoltra ad essa. Il metodo verifica che la richiesta sia stata creata dallo stesso utente che ha effettuato l'accesso. Se un altro utente accede o la chiave non è valida, non fa nulla e il programma continua.

Vedere il ricettario Come tornare a una pagina precedente.

Canonizzazione

I presentatori hanno una caratteristica davvero eccezionale che migliora la SEO (ottimizzazione della ricercabilità su Internet). Impediscono automaticamente l'esistenza di contenuti duplicati su URL diversi. Se più URL conducono a una determinata destinazione, ad esempio /index e /index?page=1, il framework ne designa uno come principale (canonico) e reindirizza gli altri verso di esso utilizzando il codice HTTP 301. In questo modo, i motori di ricerca non indicizzano le pagine due volte e non ne indeboliscono il page rank.

Questo processo è chiamato canonizzazione. L'URL canonico è l'URL generato dal router, di solito il primo percorso appropriato della collezione.

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

Il reindirizzamento non avviene con una richiesta AJAX o POST, perché comporterebbe una perdita di dati o non avrebbe alcun valore aggiunto dal punto di vista SEO.

Si può anche invocare la canonizzazione manualmente con il metodo canonicalize(), che, come il metodo link(), riceve come argomenti il presentatore, le azioni e i parametri. Crea un link e lo confronta con l'URL corrente. Se è diverso, reindirizza al link generato.

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

Eventi

Oltre ai metodi startup(), beforeRender() e shutdown(), che vengono richiamati durante il ciclo di vita del presentatore, è possibile definire altre funzioni da richiamare automaticamente. Il presentatore definisce i cosiddetti eventi, i cui gestori vengono aggiunti agli array $onStartup, $onRender e $onShutdown.

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

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

Le risposte

La risposta restituita dal presentatore è un oggetto che implementa l'interfaccia Nette\Application\Response. Esiste una serie di risposte già pronte:

Le risposte sono inviate con il metodo sendResponse():

use Nette\Application\Responses;

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

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

// Invia 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));

Limitazione dell'accesso tramite #[Requires]

L'attributo #[Requires] fornisce opzioni avanzate per limitare l'accesso ai presentatori e ai loro metodi. Può essere usato per specificare metodi HTTP, richiedere richieste AJAX, limitare l'accesso alla stessa origine e limitare l'accesso al solo inoltro. L'attributo può essere applicato alle classi di presentatori e ai singoli metodi, come ad esempio action<Action>(), render<View>(), handle<Signal>(), e createComponent<Name>().

È possibile specificare queste restrizioni:

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

Per i dettagli, vedere Come usare l'attributo Requires.

Controllo del metodo HTTP

In Nette, i presentatori verificano automaticamente il metodo HTTP di ogni richiesta in arrivo, principalmente per motivi di sicurezza. Per impostazione predefinita, sono ammessi i metodi GET, POST, HEAD, PUT, DELETE, PATCH.

Se si desidera abilitare altri metodi, come ad esempio OPTIONS, è possibile utilizzare l'attributo #[Requires] (dall'applicazione Nette 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 controlla se il metodo specificato nella richiesta è incluso nell'array $presenter->allowedMethods. Aggiungere un metodo come questo:

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

È fondamentale sottolineare che se si abilita il metodo OPTIONS, è necessario gestirlo correttamente anche nel presenter. Questo metodo è spesso usato come una cosiddetta richiesta di preflight, che i browser inviano automaticamente prima della richiesta vera e propria quando è necessario determinare se la richiesta è consentita dal punto di vista della politica CORS (Cross-Origin Resource Sharing). Se si consente questo metodo ma non si implementa una risposta appropriata, si possono creare incongruenze e potenziali problemi di sicurezza.

Ulteriori letture

versione: 4.0