Componentes interativos

Componentes são objetos reutilizáveis independentes que inserimos nas páginas. Podem ser formulários, datagrids, enquetes, na verdade, qualquer coisa que faça sentido usar repetidamente. Vamos mostrar:

  • como usar componentes?
  • como escrevê-los?
  • o que são sinais?

O Nette possui um sistema de componentes embutido. Algo semelhante pode ser familiar para veteranos do Delphi ou ASP.NET Web Forms, e algo remotamente parecido é a base do React ou Vue.js. No entanto, no mundo dos frameworks PHP, é uma característica única.

Ao mesmo tempo, os componentes influenciam fundamentalmente a abordagem para a criação de aplicações. Você pode montar páginas a partir de unidades pré-preparadas. Precisa de um datagrid na administração? Encontre-o na Componette, um repositório de add-ons open-source (ou seja, não apenas componentes) para o Nette e simplesmente insira-o no presenter.

Você pode incorporar qualquer número de componentes em um presenter. E em alguns componentes, você pode inserir outros componentes. Isso cria uma árvore de componentes, cuja raiz é o presenter.

Métodos de fábrica

Como os componentes são inseridos no presenter e subsequentemente usados? Geralmente através de métodos de fábrica.

A fábrica de componentes representa uma maneira elegante de criar componentes apenas quando eles são realmente necessários (lazy / on demand). Toda a mágica reside na implementação de um método chamado createComponent<Name>(), onde <Name> é o nome do componente a ser criado, e que cria e retorna o componente.

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->item;
		return $poll;
	}
}

Graças ao fato de que todos os componentes são criados em métodos separados, o código ganha clareza.

Os nomes dos componentes sempre começam com letra minúscula, embora no nome do método sejam escritos com letra maiúscula.

As fábricas nunca são chamadas diretamente; elas são chamadas automaticamente na primeira vez que usamos o componente. Graças a isso, o componente é criado no momento certo e apenas se for realmente necessário. Se não usarmos o componente (por exemplo, durante uma requisição AJAX em que apenas parte da página é transferida, ou ao armazenar o template em cache), ele não será criado de forma alguma e economizaremos o desempenho do servidor.

// acessamos o componente e, se for a primeira vez,
// createComponentPoll() é chamado para criá-lo
$poll = $this->getComponent('poll');
// sintaxe alternativa: $poll = $this['poll'];

No template, é possível renderizar o componente usando a tag {control}. Portanto, não é necessário passar manualmente os componentes para o template.

<h2>Vote</h2>

{control poll}

Estilo Hollywood

Os componentes geralmente usam uma técnica inovadora que gostamos de chamar de Estilo Hollywood. Você certamente conhece a frase famosa que os participantes de audições de cinema ouvem com tanta frequência: “Não nos ligue, nós ligaremos para você”. E é exatamente disso que se trata.

No Nette, em vez de ter que perguntar constantemente (“o formulário foi enviado?”, “era válido?” ou “o usuário pressionou este botão?”), você diz ao framework “quando isso acontecer, chame este método” e deixa o resto do trabalho para ele. Se você programa em JavaScript, está intimamente familiarizado com este estilo de programação. Você escreve funções que são chamadas quando um determinado evento ocorre. E a linguagem passa os parâmetros apropriados para elas.

Isso muda completamente a perspectiva sobre a escrita de aplicações. Quanto mais tarefas você puder deixar para o framework, menos trabalho você terá. E menos coisas você pode esquecer.

Escrevendo um componente

Sob o termo componente, geralmente entendemos um descendente da classe Nette\Application\UI\Control. (Seria mais preciso usar o termo “controls”, mas “controles” tem um significado diferente em português e “componentes” se tornou mais comum.) O próprio presenter Nette\Application\UI\Presenter também é, aliás, um descendente da classe Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Renderização

Já sabemos que para renderizar um componente, usamos a tag {control componentName}. Ela basicamente chama o método render() do componente, no qual cuidamos da renderização. Temos à nossa disposição, exatamente como no presenter, um template Latte na variável $this->template, para a qual passamos parâmetros. Ao contrário do presenter, precisamos especificar o arquivo de template e deixá-lo renderizar:

public function render(): void
{
	// inserimos alguns parâmetros no template
	$this->template->param = $value;
	// e o renderizamos
	$this->template->render(__DIR__ . '/poll.latte');
}

A tag {control} permite passar parâmetros para o método render():

{control poll $id, $message}
public function render(int $id, string $message): void
{
	// ...
}

Às vezes, um componente pode consistir em várias partes que queremos renderizar separadamente. Para cada uma delas, criamos nosso próprio método de renderização, aqui no exemplo, renderPaginator():

public function renderPaginator(): void
{
	// ...
}

E no template, então a chamamos usando:

{control poll:paginator}

Para uma melhor compreensão, é bom saber como esta tag é traduzida para PHP.

{control poll}
{control poll:paginator 123, 'hello'}

é traduzido como:

$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');

O método getComponent() retorna o componente poll e chama o método render() neste componente, ou renderPaginator() se um método de renderização diferente for especificado na tag após os dois pontos.

Atenção, se => aparecer em qualquer lugar nos parâmetros, todos os parâmetros serão agrupados em um array e passados como o primeiro argumento:

{control poll, id: 123, message: 'hello'}

é traduzido como:

$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);

Renderização de subcomponente:

{control cartControl-someForm}

é traduzido como:

$control->getComponent("cartControl-someForm")->render();

Componentes, assim como presenters, passam automaticamente várias variáveis úteis para os templates:

  • $basePath é o caminho URL absoluto para o diretório raiz (por exemplo, /loja)
  • $baseUrl é a URL absoluta para o diretório raiz (por exemplo, http://localhost/loja)
  • $user é o objeto representando o usuário
  • $presenter é o presenter atual
  • $control é o componente atual
  • $flashes array de mensagens enviadas pela função flashMessage()

Sinal

Já sabemos que a navegação em uma aplicação Nette consiste em vincular ou redirecionar para pares Presenter:action. Mas e se quisermos apenas executar uma ação na página atual? Por exemplo, alterar a ordenação das colunas em uma tabela; excluir um item; alternar entre modo claro/escuro; enviar um formulário; votar em uma enquete; etc.

Esse tipo de requisição é chamado de sinal. E, assim como as ações invocam métodos action<Action>() ou render<Action>(), os sinais chamam métodos handle<Signal>(). Enquanto o conceito de ação (ou view) está puramente relacionado aos presenters, os sinais se aplicam a todos os componentes. E, portanto, também aos presenters, porque UI\Presenter é um descendente de UI\Control.

public function handleClick(int $x, int $y): void
{
	// ... processamento do sinal ...
}

Criamos o link que chama o sinal da maneira usual, ou seja, no template com o atributo n:href ou a tag {link}, no código com o método link(). Mais no capítulo Criando Links URL.

<a n:href="click! $x, $y">clique aqui</a>

O sinal é sempre chamado no presenter e action atuais, não é possível chamá-lo em outro presenter ou outra action.

Portanto, o sinal causa o recarregamento da página exatamente como na requisição original, mas adicionalmente chama o método de manipulação do sinal com os parâmetros apropriados. Se o método não existir, uma exceção Nette\Application\UI\BadSignalException é lançada, que é exibida ao usuário como uma página de erro 403 Forbidden.

Snippets e AJAX

Sinais podem lembrá-lo um pouco de AJAX: manipuladores que são invocados na página atual. E você está certo, sinais são frequentemente chamados via AJAX e, subsequentemente, apenas as partes alteradas da página são transmitidas para o navegador. Ou seja, os chamados snippets. Mais informações podem ser encontradas na página dedicada ao AJAX.

Mensagens Flash

O componente tem seu próprio armazenamento de mensagens flash independente do presenter. São mensagens que, por exemplo, informam sobre o resultado de uma 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.

O envio é feito pelo método flashMessage. O primeiro parâmetro é o texto da mensagem ou um objeto stdClass representando a mensagem. O segundo parâmetro opcional é o seu tipo (erro, aviso, informação, etc.). O método flashMessage() retorna uma instância da mensagem flash como um objeto stdClass, ao 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}

Redirecionamento após sinal

Após o processamento de um sinal de componente, frequentemente segue-se um redirecionamento. É uma situação semelhante à dos formulários – após o envio deles, também redirecionamos para que, ao atualizar a página no navegador, os dados não sejam enviados novamente.

$this->redirect('this'); // redireciona para o presenter e action atuais

Como o componente é um elemento reutilizável e geralmente não deve ter um vínculo direto com presenters específicos, os métodos redirect() e link() interpretam automaticamente o parâmetro como um sinal do componente:

$this->redirect('click'); // redireciona para o sinal 'click' do mesmo componente

Se precisar redirecionar para outro presenter ou ação, você pode fazer isso através do presenter:

$this->getPresenter()->redirect('Product:show'); // redireciona para outro presenter/action

Parâmetros persistentes

Parâmetros persistentes são usados para manter o estado nos componentes 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, inclusive em links criados em outros componentes na mesma página.

Por exemplo, você tem um componente para paginação de conteúdo. Pode haver vários desses componentes em uma página. E desejamos que, após clicar em um link, todos os componentes permaneçam em sua página atual. Portanto, transformamos o número da página (page) em um parâmetro persistente.

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 PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // deve ser público
}

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

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

<a n:href="this page: $page + 1">próximo</a>

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

<a n:href="this page: null">resetar</a>

Componentes persistentes

Não apenas parâmetros, mas também componentes podem ser persistentes. Em tal componente, seus parâmetros persistentes são transmitidos mesmo entre diferentes ações do presenter ou entre vários presenters. Marcamos componentes persistentes com uma anotação na classe do presenter. Por exemplo, marcamos os componentes calendar e poll assim:

/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Subcomponentes dentro desses componentes não precisam ser marcados, eles também se tornarão persistentes.

No PHP 8, você também pode usar atributos para marcar componentes persistentes:

use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Componentes com dependências

Como criar componentes com dependências sem “poluir” os presenters que os usarão? Graças às propriedades inteligentes do contêiner de DI no Nette, assim como no uso de serviços clássicos, podemos deixar a maior parte do trabalho para o framework.

Vamos pegar como exemplo um componente que tem dependência do serviço PollFacade:

class PollControl extends Control
{
	public function __construct(
		private int $id, // ID da enquete para a qual estamos criando o componente
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($this->id, $voteId);
		// ...
	}
}

Se estivéssemos escrevendo um serviço clássico, não haveria problema. O contêiner de DI cuidaria invisivelmente da passagem de todas as dependências. Mas com componentes, geralmente lidamos de forma que criamos sua nova instância diretamente no presenter nos métodos de fábrica createComponent…(). Mas passar todas as dependências de todos os componentes para o presenter, para então passá-las aos componentes, é complicado. E a quantidade de código escrito…

A questão lógica é: por que simplesmente não registramos o componente como um serviço clássico, o passamos para o presenter e depois o retornamos no método createComponent…()? Essa abordagem, no entanto, é inadequada, porque queremos ter a possibilidade de criar o componente várias vezes, se necessário.

A solução correta é escrever uma fábrica para o componente, ou seja, uma classe que criará o componente para nós:

class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}

Registramos essa fábrica em nosso contêiner na configuração:

services:
	- PollControlFactory

e finalmente a usamos em nosso presenter:

class PollPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

	protected function createComponentPollControl(): PollControl
	{
		$pollId = 1; // podemos passar nosso parâmetro
		return $this->pollControlFactory->create($pollId);
	}
}

O ótimo é que o Nette DI pode gerar essas fábricas simples, então, em vez de todo o seu código, basta escrever apenas sua interface:

interface PollControlFactory
{
	public function create(int $id): PollControl;
}

E isso é tudo. O Nette implementará internamente esta interface e a passará para o presenter, onde já podemos usá-la. Ele magicamente adiciona o parâmetro $id e a instância da classe PollFacade ao nosso componente.

Componentes em profundidade

Componentes na Nette Application representam partes reutilizáveis de uma aplicação web que inserimos nas páginas e às quais, aliás, todo este capítulo é dedicado. Quais são exatamente as capacidades de tal componente?

  1. é renderizável no template
  2. sabe qual parte sua deve ser renderizada durante uma requisição AJAX (snippets)
  3. tem a capacidade de armazenar seu estado na URL (parâmetros persistentes)
  4. tem a capacidade de reagir a ações do usuário (sinais)
  5. cria uma estrutura hierárquica (onde a raiz é o presenter)

Cada uma dessas funções é cuidada por alguma das classes da linha de herança. A renderização (1 + 2) é responsabilidade de Nette\Application\UI\Control, a integração no ciclo de vida (3, 4) da classe Nette\Application\UI\Component e a criação da estrutura hierárquica (5) das classes Container e Component.

Nette\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
	|
	+- Nette\Application\UI\Component  { SignalReceiver, StatePersistent }
		|
		+- Nette\Application\UI\Control  { Renderable }
			|
			+- Nette\Application\UI\Presenter  { IPresenter }

Ciclo de vida do componente

Ciclo de vida do componente

Validação de parâmetros persistentes

Os valores dos 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 persistentes, pois eles podem ser facilmente sobrescritos pelo usuário na URL. Assim, por exemplo, verificamos se o número da página $this->page é maior que 0. Uma maneira adequada é sobrescrever o método mencionado loadState():

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // aqui $this->page é definido
		// segue a verificação personalizada do valor:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

O processo oposto, ou seja, coletar valores das propriedades persistentes, é responsabilidade do método saveState().

Sinais em profundidade

Um sinal causa o recarregamento da página exatamente como na requisição original (exceto quando chamado via AJAX) e invoca o método signalReceived($signal), cuja implementação padrão na classe Nette\Application\UI\Component tenta chamar um método composto pelas palavras handle{signal}. O processamento adicional depende do objeto em questão. Objetos que herdam de Component (ou seja, Control e Presenter) reagem tentando chamar o método handle{signal} com os parâmetros apropriados.

Em outras palavras: pega-se a definição da função handle{signal} e todos os parâmetros que vieram com a requisição, e os parâmetros da URL são atribuídos aos argumentos pelo nome e tenta-se chamar o método dado. Por exemplo, o valor do parâmetro id na URL é passado como parâmetro $id, something da URL é passado como $something, etc. E se o método não existir, o método signalReceived lança uma exceção.

O sinal pode ser recebido por qualquer componente, presenter ou objeto que implemente a interface SignalReceiver e esteja conectado à árvore de componentes.

Os principais receptores de sinais serão Presenters e componentes visuais que herdam de Control. O sinal deve servir como um sinal para o objeto de que ele deve fazer algo – a enquete deve contar o voto do usuário, o bloco de notícias deve se expandir e exibir o dobro de notícias, o formulário foi enviado e deve processar os dados, e assim por diante.

A URL para o sinal é criada usando o método Component::link(). Como parâmetro $destination, passamos a string {signal}! e como $args, um array de argumentos que queremos passar para o sinal. O sinal é sempre chamado no presenter e action atuais com os parâmetros atuais, os parâmetros do sinal são apenas adicionados. Além disso, o parâmetro ?do, que especifica o sinal, é adicionado logo no início.

Seu formato é {signal} ou {signalReceiver}-{signal}. {signalReceiver} é o nome do componente no presenter. É por isso que um hífen não pode estar no nome do componente – ele é usado para separar o nome do componente e o sinal, mas é possível aninhar vários componentes dessa maneira.

O método isSignalReceiver() verifica se o componente (primeiro argumento) é o receptor do sinal (segundo argumento). Podemos omitir o segundo argumento – então ele verifica se o componente é o receptor de qualquer sinal. true pode ser passado como segundo parâmetro para verificar se não apenas o componente especificado, mas também qualquer um de seus descendentes é o receptor.

Em qualquer fase anterior a handle{signal}, podemos executar o sinal manualmente chamando o método processSignal(), que se encarrega de tratar o sinal – pega o componente que foi determinado como o receptor do sinal (se nenhum receptor de sinal for especificado, é o próprio presenter) e envia o sinal para ele.

Exemplo:

if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
	$this->processSignal();
}

Assim, o sinal é executado prematuramente e não será chamado novamente.

versão: 4.0