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 exemplohttp://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étodoflashMessage()
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?
- é renderizável em um modelo
- ele sabe qual parte de si mesmo deve ser renderizada durante uma solicitação AJAX (snippets)
- tem a capacidade de armazenar seu estado em uma URL (parâmetros persistentes)
- tem a capacidade de responder às ações (sinais) do usuário
- 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.