Presenters

Vamos nos familiarizar com como escrever presenters e templates no Nette. Após a leitura, você saberá:

  • como funciona um presenter
  • o que são parâmetros persistentes
  • como os templates são renderizados

Já sabemos, que um presenter é uma classe que representa uma página específica de uma aplicação web, por exemplo, a página inicial; um produto em uma loja virtual; um formulário de login; um feed de sitemap, etc. Uma aplicação pode ter de um a milhares de presenters. Em outros frameworks, eles também são chamados de controllers.

Geralmente, sob o termo presenter, entende-se um descendente da classe Nette\Application\UI\Presenter, que é adequado para gerar interfaces web e ao qual nos dedicaremos no restante deste capítulo. Em um sentido geral, um presenter é qualquer objeto que implementa a interface Nette\Application\IPresenter.

Ciclo de vida do presenter

A tarefa do presenter é processar a requisição e retornar uma resposta (que pode ser uma página HTML, uma imagem, um redirecionamento, etc.).

Portanto, no início, a requisição é passada a ele. Não é diretamente uma requisição HTTP, mas um objeto Nette\Application\Request, no qual a requisição HTTP foi transformada com a ajuda do roteador. Geralmente não interagimos com este objeto, pois o presenter delega inteligentemente o processamento da requisição para outros métodos, que mostraremos agora.

Ciclo de vida do presenter

A imagem representa uma lista de métodos que são chamados sequencialmente de cima para baixo, se existirem. Nenhum deles precisa existir, podemos ter um presenter completamente vazio sem um único método e construir um site estático simples sobre ele.

__construct()

O construtor não pertence exatamente ao ciclo de vida do presenter, porque é chamado no momento da criação do objeto. Mas o mencionamos devido à sua importância. O construtor (juntamente com o método inject) serve para passar dependências.

O presenter não deve cuidar da lógica de negócios da aplicação, escrever e ler do banco de dados, realizar cálculos, etc. Para isso existem classes da camada que chamamos de model. Por exemplo, a classe ArticleRepository pode ser responsável por carregar e salvar artigos. Para que o presenter possa trabalhar com ela, ele a solicita via injeção de dependência:

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

startup()

Imediatamente após receber a requisição, o método startup() é chamado. Você pode usá-lo para inicializar propriedades, verificar permissões de usuário, etc. É necessário que o método sempre chame o ancestral parent::startup().

action<Action>(args...)

Análogo ao método render<View>(). Enquanto render<View>() se destina a preparar dados para um template específico que será subsequentemente renderizado, em action<Action>() a requisição é processada sem ligação à renderização do template. Por exemplo, os dados são processados, o usuário é logado ou deslogado, e assim por diante, e então redireciona para outro lugar.

O importante é que action<Action>() é chamado antes de render<View>(), então nele podemos eventualmente mudar o curso dos eventos, ou seja, mudar o template que será renderizado, e também o método render<View>() que será chamado. E isso usando setView('outroView').

Parâmetros da requisição são passados para o método. É possível e recomendado especificar tipos para os parâmetros, por exemplo, actionShow(int $id, ?string $slug = null) – se o parâmetro id estiver faltando ou não for um inteiro, o presenter retornará um erro 404 e encerrará a atividade.

handle<Signal>(args...)

O método processa os chamados sinais, com os quais nos familiarizaremos no capítulo dedicado aos componentes. Ele é destinado principalmente a componentes e ao processamento de requisições AJAX.

Parâmetros da requisição são passados para o método, como no caso de action<Action>(), incluindo verificação de tipo.

beforeRender()

O método beforeRender, como o nome sugere, é chamado antes de cada método render<View>(). É usado para configuração comum do template, passagem de variáveis para o layout e assim por diante.

render<View>(args...)

O local onde preparamos o template para a renderização subsequente, passamos dados para ele, etc.

Parâmetros da requisição são passados para o método, como no caso de action<Action>(), incluindo verificação de tipo.

public function renderShow(int $id): void
{
	// obtemos dados do model e passamos para o template
	$this->template->article = $this->articles->getById($id);
}

afterRender()

O método afterRender, como o nome novamente sugere, é chamado após cada método render<View>(). É usado de forma bastante excepcional.

shutdown()

É chamado no final do ciclo de vida do presenter.

Um bom conselho antes de prosseguirmos. Como pode ser visto, um presenter pode atender a várias ações/views, ou seja, ter vários métodos render<View>(). Mas recomendamos projetar presenters com uma ou o mínimo possível de ações.

Envio da resposta

A resposta do presenter geralmente é a renderização de um template com uma página HTML, mas também pode ser o envio de um arquivo, JSON ou talvez um redirecionamento para outra página.

A qualquer momento durante o ciclo de vida, podemos enviar uma resposta usando um dos seguintes métodos e, ao mesmo tempo, encerrar o presenter:

Se você não chamar nenhum desses métodos, o presenter automaticamente procederá à renderização do template. Por quê? Porque em 99% dos casos queremos renderizar um template, então o presenter considera esse comportamento como padrão e quer facilitar nosso trabalho.

O presenter possui o método link(), com o qual é possível criar links URL para outros presenters. O primeiro parâmetro é o presenter & ação de destino, seguido pelos argumentos passados, que podem ser especificados como um array:

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

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

No template, links para outros presenters & ações são criados desta forma:

<a n:href="Product:show $id">detalhe do produto</a>

Simplesmente, em vez da URL real, você escreve o par conhecido Presenter:action e especifica quaisquer parâmetros. O truque está no n:href, que diz que este atributo será processado pelo Latte e gerará a URL real. No Nette, você não precisa pensar em URLs, apenas em presenters e ações.

Mais informações podem ser encontradas no capítulo Criando Links URL.

Redirecionamento

Para ir para outro presenter, usam-se os métodos redirect() e forward(), que têm uma sintaxe muito semelhante ao método link().

O método forward() vai para o novo presenter imediatamente sem um redirecionamento HTTP:

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

Exemplo do chamado redirecionamento temporário com código HTTP 302 (ou 303, se o método da requisição atual for POST):

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

O redirecionamento permanente com código HTTP 301 é alcançado assim:

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

Para redirecionar para outra URL fora da aplicação, pode-se usar o método redirectUrl(). O código HTTP pode ser passado como segundo parâmetro, o padrão é 302 (ou 303, se o método da requisição atual for POST):

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

O redirecionamento encerra imediatamente a atividade do presenter lançando a chamada exceção de terminação silenciosa Nette\Application\AbortException.

Antes do redirecionamento, é possível enviar uma flash message, ou seja, mensagens que serão exibidas no template após o redirecionamento.

Mensagens Flash

São mensagens que geralmente informam sobre o resultado de alguma operação. Uma característica importante das mensagens flash é que elas estão disponíveis no template mesmo após um redirecionamento. Mesmo após serem exibidas, elas permanecem ativas por mais 30 segundos – por exemplo, caso o usuário atualize a página devido a um erro de transmissão – a mensagem não desaparecerá imediatamente.

Basta chamar o método flashMessage() e o presenter se encarrega de passá-la para o template. O primeiro parâmetro é o texto da mensagem e o segundo parâmetro opcional é o seu tipo (error, warning, info, etc.). O método flashMessage() retorna uma instância da mensagem flash, à qual informações adicionais podem ser adicionadas.

$this->flashMessage('O item foi excluído.');
$this->redirect(/* ... */); // e redirecionamos

No template, essas mensagens estão disponíveis na variável $flashes como objetos stdClass, que contêm as propriedades message (texto da mensagem), type (tipo da mensagem) e podem conter as informações do usuário já mencionadas. Nós as renderizamos assim, por exemplo:

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

Erro 404 e cia.

Se a requisição não puder ser atendida, por exemplo, porque o artigo que queremos exibir não existe no banco de dados, lançamos um erro 404 com o método error(?string $message = null, int $httpCode = 404).

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

O código HTTP do erro pode ser passado como segundo parâmetro, o padrão é 404. O método funciona lançando a exceção Nette\Application\BadRequestException, após o qual Application passa o controle para o error-presenter. Que é um presenter cuja tarefa é exibir uma página informando sobre o erro ocorrido. A configuração do error-presenter é feita na configuração da aplicação.

Envio de JSON

Exemplo de um método de ação que envia dados no formato JSON e encerra o presenter:

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

Parâmetros da requisição

O presenter e também cada componente obtêm seus parâmetros da requisição HTTP. Você pode descobrir seu valor usando o método getParameter($name) ou getParameters(). Os valores são strings ou arrays de strings, são basicamente dados brutos obtidos diretamente da URL.

Para maior conveniência, recomendamos tornar os parâmetros acessíveis através de propriedades. Basta marcá-los com o atributo #[Parameter]:

use Nette\Application\Attributes\Parameter;  // esta linha é importante

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

Recomendamos especificar o tipo de dados para a propriedade (por exemplo, string) e o Nette converterá automaticamente o valor de acordo com ele. Os valores dos parâmetros também podem ser validados.

Ao criar um link, o valor dos parâmetros pode ser definido diretamente:

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

Parâmetros persistentes

Parâmetros persistentes são usados para manter o estado entre diferentes requisições. Seu valor permanece o mesmo mesmo após clicar em um link. Ao contrário dos dados na sessão, eles são transmitidos na URL. E isso de forma totalmente automática, não sendo necessário especificá-los explicitamente em link() ou n:href.

Exemplo de uso? Você tem uma aplicação multilíngue. O idioma atual é um parâmetro que deve estar constantemente presente na URL. Mas seria incrivelmente tedioso especificá-lo em cada link. Então você o transforma em um parâmetro persistente lang e ele será transmitido por si só. Ótimo!

Criar um parâmetro persistente no Nette é extremamente simples. Basta criar uma propriedade pública e marcá-la com um atributo: (anteriormente usava-se /** @persistent */)

use Nette\Application\Attributes\Persistent;  // esta linha é importante

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

Se $this->lang tiver o valor, por exemplo, 'en', então os links criados usando link() ou n:href também conterão o parâmetro lang=en. E após clicar no link, novamente $this->lang = 'en'.

Recomendamos especificar o tipo de dados para a propriedade (por exemplo, string) e você também pode especificar um valor padrão. Os valores dos parâmetros podem ser validados.

Parâmetros persistentes são normalmente transmitidos entre todas as ações de um determinado presenter. Para que sejam transmitidos também entre vários presenters, é necessário defini-los:

  • em um ancestral comum do qual os presenters herdam
  • em uma trait que os presenters usam:
trait LanguageAware
{
	#[Persistent]
	public string $lang;
}

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

Ao criar um link, o valor do parâmetro persistente pode ser alterado:

<a n:href="Product:show $id, lang: pt">detalhe em português</a>

Ou pode ser resetado, ou seja, removido da URL. Então ele assumirá seu valor padrão:

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

Componentes interativos

Presenters têm um sistema de componentes embutido. Componentes são unidades reutilizáveis independentes que inserimos nos presenters. Podem ser formulários, datagrids, menus, na verdade, qualquer coisa que faça sentido usar repetidamente.

Como os componentes são inseridos no presenter e subsequentemente usados? Isso você aprenderá no capítulo Componentes. Você descobrirá até o que eles têm em comum com Hollywood.

E onde posso obter componentes? Na página Componette você encontrará componentes open-source e também uma série de outros add-ons para Nette, que foram colocados lá por voluntários da comunidade em torno do framework.

Vamos aprofundar

Com o que mostramos até agora neste capítulo, você provavelmente se sairá bem. As linhas a seguir são destinadas àqueles que estão interessados em presenters em profundidade e querem saber absolutamente tudo.

Validação de parâmetros

Os valores dos parâmetros da requisição e parâmetros persistentes recebidos da URL são escritos nas propriedades pelo método loadState(). Ele também verifica se o tipo de dados especificado na propriedade corresponde, caso contrário, responde com um erro 404 e a página não é exibida.

Nunca confie cegamente nos parâmetros, pois eles podem ser facilmente sobrescritos pelo usuário na URL. Assim, por exemplo, verificamos se o idioma $this->lang está entre os suportados. Uma maneira adequada é sobrescrever o método mencionado loadState():

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

	public function loadState(array $params): void
	{
		parent::loadState($params); // aqui $this->lang é definido
		// segue a verificação personalizada do valor:
		if (!in_array($this->lang, ['en', 'pt'])) {
			$this->error();
		}
	}
}

Salvar e restaurar requisição

A requisição que o presenter processa é um objeto Nette\Application\Request e é retornado pelo método do presenter getRequest().

A requisição atual pode ser salva na sessão ou, inversamente, restaurada dela e deixar o presenter executá-la novamente. Isso é útil, por exemplo, em uma situação em que o usuário está preenchendo um formulário e sua sessão expira. Para não perder os dados, antes de redirecionar para a página de login, salvamos a requisição atual na sessão usando $reqId = $this->storeRequest(), que retorna seu identificador na forma de uma string curta e o passamos como parâmetro para o presenter de login.

Após o login, chamamos o método $this->restoreRequest($reqId), que recupera a requisição da sessão e encaminha para ela. O método verifica se a requisição foi criada pelo mesmo usuário que está logado agora. Se outro usuário fizer login ou a chave for inválida, ele não faz nada e o programa continua.

Veja o tutorial Como retornar à página anterior.

Canonização

Presenters têm uma característica realmente ótima que contribui para um melhor SEO (otimização para motores de busca). Eles impedem automaticamente a existência de conteúdo duplicado em URLs diferentes. Se houver várias URLs que levam ao mesmo destino, por exemplo, /index e /index?page=1, o framework determina uma delas como primária (canônica) e redireciona as outras para ela usando o código HTTP 301. Graças a isso, os motores de busca não indexam suas páginas duas vezes e não diluem seu page rank.

Este processo é chamado de canonização. A URL canônica é aquela gerada pelo roteador, geralmente a primeira rota correspondente na coleção.

A canonização está ativada por padrão e pode ser desativada através de $this->autoCanonicalize = false.

O redirecionamento não ocorre durante uma requisição AJAX ou POST, pois isso causaria perda de dados ou não teria valor agregado do ponto de vista de SEO.

Você também pode invocar a canonização manualmente usando o método canonicalize(), ao qual, de forma semelhante ao método link(), são passados o presenter, a ação e os parâmetros. Ele cria um link e o compara com a URL atual. Se diferirem, ele redireciona para o link gerado.

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

Eventos

Além dos métodos startup(), beforeRender() e shutdown(), que são chamados como parte do ciclo de vida do presenter, é possível definir outras funções que devem ser chamadas automaticamente. O presenter define os chamados eventos, cujos manipuladores você adiciona aos arrays $onStartup, $onRender e $onShutdown.

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

Os manipuladores no array $onStartup são chamados logo antes do método startup(), $onRender entre beforeRender() e render<View>() e, finalmente, $onShutdown logo antes de shutdown().

Respostas

A resposta que o presenter retorna é um objeto que implementa a interface Nette\Application\Response. Há uma série de respostas prontas disponíveis:

As respostas são enviadas pelo método sendResponse():

use Nette\Application\Responses;

// Texto simples
$this->sendResponse(new Responses\TextResponse('Olá Nette!'));

// Envia um arquivo
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));

// A resposta será um callback
$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
	if ($httpResponse->getHeader('Content-Type') === 'text/html') {
		echo '<h1>Olá</h1>';
	}
};
$this->sendResponse(new Responses\CallbackResponse($callback));

Restrição de acesso usando #[Requires]

O atributo #[Requires] oferece opções avançadas para restringir o acesso a presenters e seus métodos. Pode ser usado para especificar métodos HTTP, exigir requisição AJAX, restringir à mesma origem (same origin) e acesso apenas via encaminhamento (forwarding). O atributo pode ser aplicado tanto a classes de presenters quanto a métodos individuais action<Action>(), render<View>(), handle<Signal>() e createComponent<Name>().

Você pode especificar estas restrições:

  • em métodos HTTP: #[Requires(methods: ['GET', 'POST'])]
  • exigir requisição AJAX: #[Requires(ajax: true)]
  • acesso apenas da mesma origem: #[Requires(sameOrigin: true)]
  • acesso apenas via forward: #[Requires(forward: true)]
  • restrição a ações específicas: #[Requires(actions: 'default')]

Detalhes podem ser encontrados no tutorial Como usar o atributo Requires.

Verificação do método HTTP

Presenters no Nette verificam automaticamente o método HTTP de cada requisição recebida. A razão para esta verificação é principalmente a segurança. Por padrão, os métodos GET, POST, HEAD, PUT, DELETE, PATCH são permitidos.

Se você quiser permitir adicionalmente, por exemplo, o método OPTIONS, use o atributo #[Requires] (a partir do Nette Application v3.2):

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

Na versão 3.1, a verificação é feita em checkHttpMethod(), que verifica se o método especificado na requisição está contido no array $presenter->allowedMethods. Adicione o método assim:

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

É importante enfatizar que, se você permitir o método OPTIONS, deverá subsequentemente tratá-lo adequadamente dentro do seu presenter. O método é frequentemente usado como a chamada requisição preflight, que o navegador envia automaticamente antes da requisição real, quando é necessário verificar se a requisição é permitida do ponto de vista da política CORS (Cross-Origin Resource Sharing). Se você permitir o método, mas não implementar a resposta correta, isso pode levar a inconsistências e potenciais problemas de segurança.

Leitura adicional

versão: 4.0