Componentes interativos

Os componentes são objetos reutilizáveis separados que colocamos em páginas. Eles podem ser formas, datagrids, pesquisas, na verdade, qualquer coisa que faça sentido usar repetidamente. Vamos mostrar:

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

Nette tem um sistema de componentes incorporado. Os mais antigos de vocês podem se lembrar de algo similar dos formulários Delphi ou ASP.NET Web Forms. Reage ou Vue.js é construído sobre algo remotamente semelhante. Entretanto, no mundo das estruturas PHP, esta é uma característica completamente única.

Ao mesmo tempo, os componentes mudam fundamentalmente a abordagem para o desenvolvimento de aplicações. Você pode compor páginas a partir de unidades pré-preparadas. Você precisa de dadosagrid na administração? Você pode encontrá-lo na Componette, um repositório de add-ons de código aberto (não apenas componentes) para a Nette, e simplesmente colá-lo no apresentador.

Você pode incorporar qualquer número de componentes ao apresentador. E você pode inserir outros componentes em alguns componentes. Isto cria uma árvore de componentes com um apresentador como raiz.

Métodos de Fábrica

Como os componentes são colocados e posteriormente utilizados no apresentador? Geralmente utilizando métodos de fábrica.

A fábrica de componentes é uma maneira elegante de criar componentes somente quando eles são realmente necessários (preguiçosos / sob demanda). Toda a magia está na implementação de um método chamado createComponent<Name>()onde <Name> é o nome do componente, que criará e retornará.

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

Como todos os componentes são criados em métodos separados, o código é mais limpo e fácil de ler.

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

Nunca ligamos diretamente para as fábricas, elas são chamadas automaticamente, quando usamos componentes pela primeira vez. Graças a isso, um componente é criado no momento certo, e somente se for realmente necessário. Se não usássemos o componente (por exemplo, em algum pedido AJAX, onde retornamos apenas parte da página, ou quando as peças estão em cache), ele nem mesmo seria criado e poupamos o desempenho do servidor.

// temos acesso ao componente e se foi a primeira vez,
// chama createComponentPoll() para criá-lo
$poll = $this->getComponent('poll');
// sintaxe alternativa: $poll = $this['poll'];

No modelo, você pode renderizar um componente usando a etiqueta {controle}. Portanto, não há necessidade de passar manualmente os componentes para o modelo.

<h2>Please Vote</h2>

{control poll}

Estilo Hollywood

Os componentes geralmente usam uma técnica legal, que nós gostamos de chamar de estilo Hollywood. Certamente você conhece o clichê que os atores ouvem com freqüência nos casting calls: “Não nos chame, nós o chamaremos”. E é disso que se trata.

Em Nette, ao invés de ter que fazer perguntas constantemente (“o formulário foi apresentado?”, “ele era válido?” ou “alguém apertou este botão?”), você diz à estrutura “quando isto acontecer, chame este método” e deixa mais trabalho sobre ele. Se você programar em JavaScript, você está familiarizado com este estilo de programação. Você escreve funções que são chamadas quando um determinado evento ocorre. E o motor passa os parâmetros apropriados para elas.

Isto muda completamente a maneira como você escreve as aplicações. Quanto mais tarefas você puder delegar à estrutura, menos trabalho você terá. E quanto menos você puder esquecer.

Como Escrever um Componente

Por componente entendemos geralmente os descendentes da classe Nette\Application\UI\Control. O próprio apresentador Nette\Application\UI\Presenter é também um descendente da classe Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Renderização

Já sabemos que a tag {control componentName} é usada para desenhar um componente. Na verdade, ela chama o método render() do componente, no qual nós cuidamos da renderização. Temos, assim como no apresentador, um modelo Latte na variável $this->template, para o qual passamos os parâmetros. Ao contrário do uso em um apresentador, devemos especificar um arquivo de modelo e deixá-lo renderizar:

public function render(): void
{
	// vamos colocar alguns parâmetros no modelo
	$this->template->param = $value;
	// e desenhá-la
	$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 de várias partes que queremos renderizar separadamente. Para cada uma delas, criaremos um método próprio de renderização, aqui está, por exemplo, renderPaginator():

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

E no modelo que então chamamos de usar:

{control poll:paginator}

Para melhor compreensão, é bom saber como a tag é compilada para o código PHP.

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

Isto se compila a:

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

getComponent() devolve o componente poll e depois o método render() ou renderPaginator(), respectivamente, é chamado sobre ele.

Se em qualquer parte da parte do parâmetro => for usado, todos os parâmetros serão envolvidos com uma matriz e passados como o primeiro argumento:

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

compila para:

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

Renderização de sub-componente:

{control cartControl-someForm}

compila para:

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

Os componentes, como os apresentadores, passam automaticamente várias variáveis úteis para os modelos:

  • $basePath é um caminho absoluto de URL para o dir raiz (por exemplo /CD-collection)
  • $baseUrl é um URL absoluto para o dir raiz (por exemplo http://localhost/CD-collection)
  • $user é um objeto que representa o usuário
  • $presenter é o atual apresentador
  • $control é o componente atual
  • $flashes lista de mensagens enviadas por método flashMessage()

Sinal

Já sabemos que a navegação na aplicação Nette consiste em ligar ou redirecionar para pares Presenter:action. Mas e se quisermos apenas realizar uma ação na **página atual***? Por exemplo, alterar a ordem de classificação da coluna na tabela; apagar item; mudar o modo luz/escuro; enviar o formulário; votar na pesquisa; etc.

Este tipo de pedido é chamado de sinal. E como as ações invocam métodos action<Action>() ou render<Action>(), sinaliza métodos de chamada handle<Signal>(). Enquanto o conceito de ação (ou visão) se refere apenas aos apresentadores, os sinais se aplicam a todos os componentes. E, portanto, também aos apresentadores, pois UI\Presenter é descendente de UI\Control.

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

O link que chama o sinal é criado da maneira usual, ou seja, no modelo pelo atributo n:href ou na tag {link}, no código pelo método link(). Mais no capítulo Criação de links URL.

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

O sinal é sempre chamado no apresentador e na visão atual, de modo que não é possível fazer a ligação com o sinal em diferentes apresentadores/ações.

Assim, o sinal faz com que a página seja recarregada exatamente da mesma forma que no pedido original, só que, além disso, ele chama o método de manuseio do sinal com os parâmetros apropriados. Se o método não existir, é lançada a exceção Nette\Application\UI\BadSignalException, que é exibida ao usuário como erro página 403 Proibida.

Snippets e AJAX

Os sinais podem lembrar um pouco o AJAX: manipuladores que são chamados na página atual. E você está certo, os sinais são realmente chamados com freqüência usando AJAX, e então nós só transmitimos partes alteradas da página para o navegador. Eles são chamados de snippets. Mais informações podem ser encontradas na página sobre o AJAX.

Mensagens Flash

Um componente tem seu próprio armazenamento de mensagens flash, independente do apresentador. Estas são mensagens que, por exemplo, informam sobre o resultado da 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.

O envio é feito através do método flashMessage. O primeiro parâmetro é o texto da mensagem ou o objeto stdClass que representa a mensagem. O segundo parâmetro opcional é seu tipo (erro, aviso, informação, etc.). O método flashMessage() retorna uma instância de mensagem flash como objeto stdClass, para a qual você pode passar informações.

$this->flashMessage('Item foi excluído');
$this->redirect(/* ... */); // e redirecionar

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}

Parâmetros Persistentes

Parâmetros persistentes são usados para manter o estado em componentes entre diferentes solicitações. Seu valor permanece o mesmo, mesmo depois que um link é clicado. Ao contrário dos dados da sessão, eles são transferidos na URL. E eles são transferidos automaticamente, incluindo links criados em outros componentes na mesma página.

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

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

Recomendamos que você inclua o tipo de dados (por exemplo int) com o imóvel, e você também pode incluir um valor padrão. Os valores dos parâmetros podem ser validados.

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

<a n:href="this page: $page + 1">next</a>

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

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

Componentes Persistentes

Não apenas os parâmetros, mas também os componentes podem ser persistentes. Seus parâmetros persistentes também são transferidos entre diferentes ações ou entre diferentes apresentadores. Marcamos os componentes persistentes com estas anotações para a classe do apresentador. Por exemplo, aqui marcamos os componentes calendar e poll como a seguir:

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

Não é necessário marcar os subcomponentes como persistentes, eles são persistentes automaticamente.

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 “bagunçar” os apresentadores que irão utilizá-los? Graças às características inteligentes do contêiner DI em Nette, como no uso de serviços tradicionais, podemos deixar a maior parte do trabalho para a estrutura.

Tomemos como exemplo um componente que depende do serviço PollFacade:

class PollControl extends Control
{
	public function __construct(
		private int $id, // Id de uma pesquisa, para a qual o componente é criado
		private PollFacade $facade,
	) {
	}

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

Se estivéssemos escrevendo um serviço clássico, não haveria nada com que se preocupar. O recipiente DI se encarregaria invisivelmente de passar todas as dependências. Mas normalmente lidamos com componentes criando uma nova instância deles diretamente no apresentador nos métodos de fábrica createComponent...(). Mas passar todas as dependências de todos os componentes para o apresentador para depois passá-los aos componentes é incômodo. E a quantidade de código escrito…

A questão lógica é: por que não registramos o componente como um serviço clássico, passamos para o apresentador e depois o devolvemos no método createComponent...()? Mas esta abordagem é inapropriada porque queremos ser capazes de criar o componente várias vezes.

A solução correta é escrever uma fábrica para o componente, ou seja, uma classe que cria 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);
	}
}

Agora registramos nosso serviço de DI container à configuração:

services:
	- PollControlFactory

Finalmente, utilizaremos esta fábrica em nosso apresentador:

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 a Nette DI pode gerar fábricas tão simples, portanto, em vez de escrever o código inteiro, basta escrever sua interface:

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

Isso é tudo. A Nette implementa internamente esta interface e a injeta em nosso apresentador, onde podemos utilizá-la. Ela também passa magicamente nosso parâmetro $id e instância da classe PollFacade para nosso componente.

Componentes em profundidade

Os componentes de uma aplicação Nette são as partes reutilizáveis de uma aplicação web que incorporamos nas páginas, que é o tema deste capítulo. Quais são exatamente as capacidades de um componente desse tipo?

  1. é renderizável em um modelo
  2. ele sabe qual parte de si mesmo deve ser renderizada durante uma solicitação AJAX (snippets)
  3. tem a capacidade de armazenar seu estado em uma URL (parâmetros persistentes)
  4. tem a capacidade de responder às ações (sinais) do usuário
  5. cria uma estrutura hierárquica (onde a raiz é o apresentador)

Cada uma destas funções é tratada por uma das classes de linhagem de herança. A renderização (1 + 2) é tratada por Nette\Application\UI\Control, a incorporação ao ciclo de vida (3, 4) pela classe Nette\Application\UI\Component e a criação da estrutura hierárquica (5) pelas 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 de parâmetros persistentes recebidos de URLs são escritos nas propriedades pelo método loadState(). Ele também verifica se o tipo de dados especificado para a propriedade corresponde, caso contrário ele responderá com um erro 404 e a página não será exibida.

Nunca confie cegamente em parâmetros persistentes porque eles podem ser facilmente sobrescritos pelo usuário no URL. Por exemplo, é assim que verificamos se o número da página $this->page é maior que 0. Uma boa maneira de fazer isso é sobrescrever o método loadState() mencionado acima:

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

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

O processo oposto, ou seja, a coleta de valores de properites persistentes, é tratado pelo método saveState().

Sinais em profundidade

Um sinal causa uma recarga de página como a solicitação original (com exceção do 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 posterior depende do objeto em questão. Os objetos que são descendentes de Component (ou seja, Control e Presenter) tentam chamar handle{Signal} com parâmetros relevantes.

Em outras palavras: a definição do método handle{Signal} é tomada e todos os parâmetros que foram recebidos no pedido são combinados com os parâmetros do método. Isso significa que o parâmetro id da URL é comparado com o parâmetro do método $id, something a $something e assim por diante. 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, apresentador do objeto que implementa a interface SignalReceiver se estiver conectado à árvore de componentes.

Os principais receptores de sinais são Presenters e componentes visuais que se estendem a Control. Um sinal é um sinal para um objeto que tem que fazer algo – a pesquisa conta em um voto do usuário, a caixa com notícias tem que se desdobrar, o formulário foi enviado e tem que processar dados e assim por diante.

A URL para o sinal é criada usando o método Componente::link(). Como parâmetro $destination passamos a string {signal}! e como $args uma série de argumentos que queremos passar para o manipulador do sinal. Os parâmetros de sinal são anexados à URL do apresentador/visualizador atual. O parâmetro ?do no URL determina o sinal chamado.

Seu formato é {signal} ou {signalReceiver}-{signal}. {signalReceiver} é o nome do componente no apresentador. É por isso que o hífen (traço inexato) não pode estar presente no nome dos componentes – é usado para dividir o nome do componente e do sinal, mas é possível compor vários componentes.

O método éSignalReceiver() verifica se um componente (primeiro argumento) é um receptor de um sinal (segundo argumento). O segundo argumento pode ser omitido – então ele descobre se o componente é um receptor de qualquer sinal. Se o segundo parâmetro for true, verifica se o componente ou seus descendentes são receptores de um sinal.

Em qualquer fase anterior a handle{Signal}, o sinal pode ser executado manualmente chamando o método ProcessSignal(), que assume a responsabilidade pela execução do sinal. Pega o componente receptor (se não for definido, é o próprio apresentador) e lhe envia o sinal.

Exemplo:

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

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

versão: 4.0