Apresentadores

Aprenderemos como escrever apresentadores e modelos em Nette. Após a leitura, você saberá:

  • como funciona o apresentador
  • o que são parâmetros persistentes
  • como fazer um modelo

Já sabemos que um apresentador é uma classe que representa uma página específica de uma aplicação web, como uma página inicial; produto em e-shop; formulário de login; feed de mapa do site, etc. A aplicação pode ter de um a milhares de apresentadores. Em outras estruturas, eles também são conhecidos como controladores.

Normalmente, o termo apresentador se refere a um descendente da classe Nette\Application\UI\Presenter, que é adequado para interfaces web e que discutiremos no resto deste capítulo. Em um sentido geral, um apresentador é qualquer objeto que implemente a interface Nette\Application\IPresenter.

Ciclo de vida do apresentador

O trabalho do apresentador é processar a solicitação e devolver uma resposta (que pode ser uma página HTML, imagem, redirecionamento, etc.).

Portanto, no início é um pedido. Não é diretamente uma solicitação HTTP, mas um objeto Nette\Application\Request no qual a solicitação HTTP foi transformada usando um roteador. Normalmente não entramos em contato com este objeto, porque o apresentador delega inteligentemente o processamento da solicitação a métodos especiais, o que veremos agora.

***Ciclo de vida do apresentador*

A figura mostra uma lista de métodos que são chamados sequencialmente de cima para baixo, se eles existirem. Nenhum deles precisa existir, podemos ter um apresentador completamente vazio sem um único método e construir uma simples teia estática sobre ele.

__construct()

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

O apresentador não deve cuidar da lógica comercial da aplicação, escrever e ler a partir do banco de dados, realizar cálculos, etc. Esta é a tarefa para as classes de uma camada, que chamamos de modelo. Por exemplo, a classe ArticleRepository pode ser responsável por carregar e salvar artigos. Para que o apresentador possa utilizá-la, ela é passada usando a injeção de dependência:

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

startup()

Imediatamente após o recebimento do pedido, é invocado o método startup (). Você pode utilizá-lo para inicializar as propriedades, verificar os privilégios do usuário, etc. É necessário chamar sempre o ancestral parent::startup().

action<Action>(args...)

Semelhante ao método render<View>(). Enquanto render<View>() é destinado a preparar dados para um modelo específico, que é posteriormente apresentado, em action<Action>() uma solicitação é processada sem acompanhamento da apresentação do modelo. Por exemplo, os dados são processados, um usuário entra ou sai, e assim por diante, e depois é redirecionado para outro lugar.

É importante que action<Action>() é chamado antes render<View>()Assim, dentro dele podemos possivelmente mudar o próximo ciclo de vida, ou seja, mudar o modelo que será apresentado e também o método render<View>() que será chamado, usando setView('otherView').

Os parâmetros do pedido 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 se não for um número inteiro, o apresentador retorna o erro 404 e encerra a operação.

handle<Signal>(args...)

Este método processa os chamados sinais, que discutiremos no capítulo sobre Componentes. Ele se destina principalmente a componentes e processamento de pedidos AJAX.

Os parâmetros são passados para o método, como no caso de action<Action>()incluindo a verificação do tipo.

beforeRender()

O método beforeRender, como o nome sugere, é chamado antes de cada método render<View>(). É utilizado para configuração de modelos comuns, passando variáveis para layout e assim por diante.

render<View>(args...)

O local onde preparamos o modelo para posterior renderização, passamos dados para ele, etc.

Os parâmetros são passados para o método, como no caso de action<Action>()incluindo a verificação do tipo.

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

afterRender()

O método afterRender, como o nome sugere novamente, é chamado depois de cada render<View>() método. Ele é usado muito raramente.

shutdown()

É chamado no final do ciclo de vida do apresentador.

**Bom conselho antes de seguirmos em frente***. Como você pode ver, o apresentador pode lidar com mais ações/visões, ou seja, ter mais métodos render<View>(). Mas recomendamos projetar apresentadores com uma ou o menor número possível de ações.

Enviando uma resposta

A resposta do apresentador geralmente é renderizar o modelo com a página HTML, mas também pode estar enviando um arquivo, JSON ou mesmo redirecionando para outra página.

A qualquer momento durante o ciclo de vida, você pode usar qualquer um dos seguintes métodos para enviar uma resposta e sair do apresentador ao mesmo tempo:

Se você não chamar nenhum destes métodos, o apresentador procederá automaticamente para renderizar o modelo. Por quê? Bem, porque em 99% dos casos queremos desenhar um modelo, então o apresentador toma este comportamento como padrão e quer tornar nosso trabalho mais fácil.

O apresentador tem um método link(), que é usado para criar links de URL para outros apresentadores. O primeiro parâmetro é o apresentador alvo & ação, seguido dos argumentos, que podem ser passados como array:

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

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

No modelo, criamos links para outros apresentadores e ações da seguinte forma:

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

Basta escrever o familiar par Presenter:action ao invés da URL real e incluir quaisquer parâmetros. O truque é n:href, que diz que este atributo será processado por Latte e gera uma URL real. Em Nette, você não precisa pensar em URLs de forma alguma, apenas em apresentadores e ações.

Para mais informações, consulte Criação de links.

Redirecionamento

Os métodos redirect() e forward() são usados para saltar para outro apresentador, que tem uma sintaxe muito semelhante à do link do método.

O forward() muda imediatamente para o novo apresentador sem redirecionamento HTTP:

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

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

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

Para obter um redirecionamento permanente com o uso do código HTTP 301:

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

Você pode redirecionar para outra URL fora da aplicação usando o método redirectUrl(). O código HTTP pode ser especificado como o segundo parâmetro, sendo o padrão 302 (ou 303, se o método de solicitação atual for POST):

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

A redireção encerra imediatamente o ciclo de vida do apresentador, lançando a chamada exceção de terminação silenciosa Nette\Application\AbortException.

Antes do redirecionamento, é possível enviar uma mensagem flash, mensagens que serão exibidas no modelo após o redirecionamento.

Mensagens Flash

Estas são mensagens que normalmente informam sobre o resultado de uma operação. Uma característica importante das mensagens flash é que elas estão disponíveis no modelo, mesmo após o redirecionamento. Mesmo após serem exibidas, elas permanecerão vivas por mais 30 segundos – por exemplo, caso o usuário atualize involuntariamente a página – a mensagem não será perdida.

Basta ligar para o método flashMessage() e o apresentador se encarregará de passar a mensagem para o modelo. O primeiro argumento é o texto da mensagem e o segundo argumento opcional é seu tipo (erro, aviso, informação, etc.). O método flashMessage() retorna uma instância de mensagem flash, para nos permitir acrescentar mais informações.

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

No modelo, estas mensagens estão disponíveis na variável $flashes como objetos stdClass, que contém as propriedades message (texto da mensagem), type (tipo de mensagem) e podem conter as informações de usuário já mencionadas. Nós as desenhamos da seguinte forma:

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

Erro 404 etc.

Quando não pudermos atender ao pedido porque, por exemplo, o artigo que queremos exibir não existe no banco de dados, vamos jogar fora o erro 404 usando o método error(string $message = null, int $httpCode = 404), que representa o erro HTTP 404:

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

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

Enviando o JSON

Exemplo de método de ação que envia dados em formato JSON e sai do apresentador:

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

Parâmetros de solicitação

O apresentador, assim como todos os componentes, obtém seus parâmetros da solicitação HTTP. Seus valores podem ser recuperados usando o método getParameter($name) ou getParameters(). Os valores são cadeias de caracteres ou matrizes de cadeias de caracteres, essencialmente dados brutos obtidos diretamente do URL.

Para maior comodidade, recomendamos tornar os parâmetros acessíveis por meio de propriedades. Basta anotá-los com o atributo #[Parameter] atributo:

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

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

Para propriedades, sugerimos especificar o tipo de dados (por exemplo, string). O Nette converterá automaticamente o valor com base nele. Os valores dos parâmetros também podem ser validados.

Ao criar um link, você pode definir diretamente o valor dos parâmetros:

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

Parâmetros Persistentes

Parâmetros persistentes são usados para manter o estado entre diferentes solicitações. Seu valor permanece o mesmo mesmo, mesmo depois que um link é clicado. Ao contrário dos dados da sessão, eles são passados na URL. Isto é completamente automático, portanto não há necessidade de declará-los explicitamente em link() ou n:href.

Exemplo de uso? Você tem uma aplicação multilíngüe. O idioma real é um parâmetro que precisa fazer parte da URL o tempo todo. Mas seria incrivelmente entediante incluí-lo em cada link. Portanto, você o torna um parâmetro persistente chamado lang e ele se carregará sozinho. Legal!

Criar um parâmetro persistente é extremamente fácil em Nette. Basta criar uma propriedade pública e etiquetá-la com o atributo: (anteriormente foi utilizado /** @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 tem um valor como 'en', então os links criados usando link() ou n:href também conterão o parâmetro lang=en. E quando o link for clicado, ele será novamente $this->lang = 'en'.

Para propriedades, recomendamos que você inclua o tipo de dados (por exemplo, string) e também pode incluir um valor padrão. Os valores dos parâmetros podem ser validados.

Parâmetros persistentes são passados por padrão entre todas as ações de um determinado apresentador. Para passá-los entre vários apresentadores, você precisa defini-los também:

  • em um ancestral comum do qual os apresentadores herdam
  • no traço que os apresentadores usam:
trait LangAware
{
	#[Persistent]
	public string $lang;
}

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

Você pode alterar o valor de um parâmetro persistente ao criar um link:

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

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

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

Componentes interativos

Os apresentadores têm um sistema de componentes incorporado. Os componentes são unidades reutilizáveis separadas que colocamos nos apresentadores. Eles podem ser formas, datagrids, menus, na verdade qualquer coisa que faça sentido usar repetidamente.

Como os componentes são colocados e posteriormente utilizados no apresentador? Isto é explicado no capítulo Componentes. Você descobrirá até mesmo o que eles têm a ver com Hollywood.

Onde posso obter alguns componentes? Na página Componente você pode encontrar alguns componentes de código aberto e outros addons para Nette que são feitos e compartilhados pela comunidade de Nette Framework.

Indo mais fundo

O que mostramos até agora neste capítulo provavelmente será suficiente. As seguintes linhas destinam-se àqueles que estão interessados em apresentadores em profundidade e querem saber tudo.

Validação de parâmetros

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

Nunca confie cegamente nos parâmetros, pois eles podem ser facilmente substituídos pelo usuário no URL. Por exemplo, é assim que verificamos se $this->lang está entre os idiomas suportados. Uma boa maneira de fazer isso é substituir o método loadState() mencionado acima:

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

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

Salvar e Restaurar o Pedido

A solicitação que o apresentador trata é um objeto Nette\Application\Request e é retornada pelo método do apresentador getRequest().

Você pode salvar a solicitação atual em uma sessão ou restaurá-la da sessão e permitir que o apresentador a execute novamente. Isso é útil, por exemplo, quando um usuário preenche um formulário e seu login expira. Para não perder dados, antes de redirecionar para a página de login, salvamos a solicitação atual na sessão usando $reqId = $this->storeRequest(), que retorna um identificador na forma de uma cadeia de caracteres curta e o passa como parâmetro para o apresentador de login.

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

Veja o livro de receitas Como voltar a uma página anterior.

Canonização

Os apresentadores têm uma característica realmente grande que melhora a SEO (otimização da capacidade de busca na Internet). Eles evitam automaticamente a existência de conteúdo duplicado em diferentes URLs. Se múltiplas URLs levam a um determinado destino, por exemplo, /index e /index?page=1, a estrutura designa uma delas como a principal (canônica) e redireciona as outras para ela usando o código HTTP 301. Graças a isso, os mecanismos de busca não indexam páginas duas vezes e não enfraquecem sua classificação de páginas.

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

A canonização está ligada por padrão e pode ser desligada via $this->autoCanonicalize = false.

O redirecionamento não ocorre com um pedido AJAX ou POST porque resultaria em perda de dados ou sem valor agregado SEO.

Você também pode invocar a canonização manualmente usando o método canonicalize(), que, como o método link(), recebe o apresentador, ações e parâmetros como argumentos. Ele cria um link e o compara com a URL atual. Se for diferente, ele se 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 apresentador, outras funções podem ser definidas para serem chamadas automaticamente. O apresentador define os chamados eventos, e você adiciona seus manipuladores às arrays $onStartup, $onRender e $onShutdown.

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

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

Respostas

A resposta devolvida pelo apresentador é um objeto que implementa a interface Nette\Application\Response. Há uma série de respostas prontas:

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

use Nette\Application\Responses;

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

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

// Envia uma ligação de retorno
$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
	if ($httpResponse->getHeader('Content-Type') === 'text/html') {
		echo '<h1>Hello</h1>';
	}
};
$this->sendResponse(new Responses\CallbackResponse($callback));

Verificação do método HTTP

Os apresentadores do Nette verificam automaticamente o método HTTP de cada solicitação recebida. O principal motivo para essa verificação é a segurança. A verificação do método é realizada em checkHttpMethod(), que determina se o método especificado na solicitação está incluído na matriz $presenter->allowedMethods. Por padrão, essa matriz contém os itens GET, POST, HEAD, PUT, DELETE, PATCH, o que significa que esses métodos são permitidos.

Se você quiser permitir adicionalmente o método OPTIONS, isso pode ser feito da seguinte maneira:

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

É fundamental enfatizar que, se você habilitar o método OPTIONS, também deverá tratá-lo adequadamente em seu apresentador. Esse método é frequentemente usado como a chamada solicitação de preflight, que os navegadores enviam automaticamente antes da solicitação real quando é necessário determinar se a solicitação é permitida do ponto de vista da política de CORS (Cross-Origin Resource Sharing). Se você permitir esse método, mas não implementar uma resposta adequada, isso poderá gerar inconsistências e possíveis problemas de segurança.

Leitura adicional

versão: 4.0