Componentes interactivos
Los componentes son objetos reutilizables independientes que insertamos en las páginas. Pueden ser formularios, datagrids, encuestas, en realidad cualquier cosa que tenga sentido usar repetidamente. Mostraremos:
- ¿cómo usar componentes?
- ¿cómo escribirlos?
- ¿qué son las señales?
Nette tiene incorporado un sistema de componentes. Algo similar pueden recordar los veteranos de Delphi o ASP.NET Web Forms, algo remotamente similar es la base de React o Vue.js. Sin embargo, en el mundo de los frameworks PHP, es una característica única.
Mientras tanto, los componentes influyen fundamentalmente en el enfoque para la creación de aplicaciones. Puede componer páginas a partir de unidades prefabricadas. ¿Necesita un datagrid en la administración? Lo encontrará en Componette, un repositorio de complementos de código abierto (es decir, no solo componentes) para Nette y simplemente insértelo en el presenter.
Puede incorporar cualquier número de componentes en el presenter. Y en algunos componentes puede insertar otros componentes. Esto crea un árbol de componentes, cuya raíz es el presenter.
Métodos de fábrica
¿Cómo se insertan los componentes en el presenter y se usan posteriormente? Generalmente mediante métodos de fábrica.
Una fábrica de componentes es una forma elegante de crear componentes solo cuando realmente se necesitan (lazy / on demand).
Toda la magia reside en la implementación de un método llamado createComponent<Name>()
, donde
<Name>
es el nombre del componente a crear, y que crea y devuelve el componente.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->item;
return $poll;
}
}
Gracias a que todos los componentes se crean en métodos separados, el código gana en claridad.
Los nombres de los componentes siempre comienzan con una letra minúscula, aunque en el nombre del método se escriban con mayúscula.
Nunca llamamos a las fábricas directamente, se llaman solas en el momento en que usamos el componente por primera vez. Gracias a esto, el componente se crea en el momento adecuado y solo si realmente es necesario. Si no usamos el componente (por ejemplo, en una petición AJAX donde solo se transfiere una parte de la página, o al almacenar en caché la plantilla), no se crea en absoluto y ahorramos rendimiento del servidor.
// accedemos al componente y si fue la primera vez,
// se llama a createComponentPoll() que lo crea
$poll = $this->getComponent('poll');
// sintaxis alternativa: $poll = $this['poll'];
En la plantilla, es posible renderizar el componente usando la etiqueta {control}. Por lo tanto, no es necesario pasar manualmente los componentes a la plantilla.
<h2>Votar</h2>
{control poll}
Estilo Hollywood
Los componentes suelen utilizar una técnica fresca, que nos gusta llamar estilo Hollywood. Seguramente conoce la frase célebre que tan a menudo escuchan los participantes en las audiciones de cine: “No nos llame, nosotros le llamaremos”. Y de eso se trata precisamente.
En Nette, en lugar de tener que preguntar constantemente (“¿se envió el formulario?”, “¿fue válido?” o “¿presionó el usuario este botón?”), le dice al framework “cuando suceda, llama a este método” y deja el resto del trabajo en él. Si programa en JavaScript, conoce íntimamente este estilo de programación. Escribe funciones que se llaman cuando ocurre un evento determinado. Y el lenguaje les pasa los parámetros apropiados.
Esto cambia por completo la perspectiva sobre la escritura de aplicaciones. Cuantas más tareas pueda dejar en manos del framework, menos trabajo tendrá usted. Y menos cosas podrá olvidar.
Escribiendo un componente
Bajo el término componente, generalmente nos referimos a un descendiente de la clase Nette\Application\UI\Control. (Por lo tanto,
sería más preciso usar el término “controls”, pero “controles” tiene un significado completamente diferente en español
y más bien se ha impuesto “componentes”.) El propio presenter Nette\Application\UI\Presenter es, por
cierto, también un descendiente de la clase Control
.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Renderizado
Ya sabemos que para renderizar un componente se usa la etiqueta {control componentName}
. Esta en realidad llama al
método render()
del componente, en el que nos encargamos del renderizado. Tenemos disponible, al igual que en el
presenter, una plantilla Latte en la variable
$this->template
, a la que pasamos parámetros. A diferencia del presenter, debemos indicar el archivo con la
plantilla y hacer que se renderice:
public function render(): void
{
// insertamos algunos parámetros en la plantilla
$this->template->param = $value;
// y la renderizamos
$this->template->render(__DIR__ . '/poll.latte');
}
La etiqueta {control}
permite pasar parámetros al método render()
:
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
A veces, un componente puede constar de varias partes que queremos renderizar por separado. Para cada una de ellas, creamos
nuestro propio método de renderizado, aquí en el ejemplo, por ejemplo, renderPaginator()
:
public function renderPaginator(): void
{
// ...
}
Y en la plantilla, luego la llamamos usando:
{control poll:paginator}
Para una mejor comprensión, es bueno saber cómo se traduce esta etiqueta a PHP.
{control poll}
{control poll:paginator 123, 'hello'}
se traduce como:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
El método getComponent()
devuelve el componente poll
y sobre este componente llama al método
render()
, respectivamente renderPaginator()
si se indica otro método de renderizado en la etiqueta
después de los dos puntos.
Atención, si en cualquier lugar de los parámetros aparece =>
, todos los parámetros se
empaquetarán en un array y se pasarán como primer argumento:
{control poll, id: 123, message: 'hello'}
se traduce como:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Renderizado de un subcomponente:
{control cartControl-someForm}
se traduce como:
$control->getComponent("cartControl-someForm")->render();
Los componentes, al igual que los presenters, pasan automáticamente varias variables útiles a las plantillas:
$basePath
es la ruta URL absoluta al directorio raíz (p. ej.,/eshop
)$baseUrl
es la URL absoluta al directorio raíz (p. ej.,http://localhost/eshop
)$user
es el objeto que representa al usuario$presenter
es el presenter actual$control
es el componente actual$flashes
array de mensajes enviados por la funciónflashMessage()
Señal
Ya sabemos que la navegación en una aplicación Nette consiste en enlazar o redirigir a pares Presenter:action
.
Pero, ¿qué pasa si solo queremos realizar una acción en la página actual? Por ejemplo, cambiar el orden de las columnas
en una tabla; eliminar un elemento; cambiar el modo claro/oscuro; enviar un formulario; votar en una encuesta; etc.
Este tipo de peticiones se llaman señales. Y de manera similar a como las acciones invocan los métodos
action<Action>()
o render<Action>()
, las señales llaman a los métodos
handle<Signal>()
. Mientras que el concepto de acción (o vista) está relacionado puramente con los
presenters, las señales se aplican a todos los componentes. Y, por lo tanto, también a los presenters, porque
UI\Presenter
es un descendiente de UI\Control
.
public function handleClick(int $x, int $y): void
{
// ... procesamiento de la señal ...
}
El enlace que llama a la señal se crea de la manera habitual, es decir, en la plantilla con el atributo n:href
o la etiqueta {link}
, en el código con el método link()
. Más en el capítulo Creación de enlaces URL.
<a n:href="click! $x, $y">haz clic aquí</a>
La señal siempre se llama en el presenter y la acción actuales, no es posible invocarla en otro presenter u otra acción.
Por lo tanto, la señal provoca la recarga de la página exactamente igual que en la petición original, solo que además llama al método de manejo de la señal con los parámetros correspondientes. Si el método no existe, se lanza una excepción Nette\Application\UI\BadSignalException, que se muestra al usuario como una página de error 403 Forbidden.
Fragmentos (Snippets) y AJAX
Las señales pueden recordarle un poco a AJAX: manejadores que se invocan en la página actual. Y tiene razón, las señales realmente se llaman a menudo usando AJAX y posteriormente transferimos al navegador solo las partes modificadas de la página. Es decir, los llamados fragmentos (snippets). Encontrará más información en la página dedicada a AJAX.
Mensajes flash
El componente tiene su propio almacenamiento de mensajes flash independiente del presenter. Son mensajes que, por ejemplo, informan sobre el resultado de una operación. Una característica importante de los mensajes flash es que están disponibles en la plantilla incluso después de una redirección. Incluso después de mostrarse, permanecen activos durante otros 30 segundos, por ejemplo, en caso de que el usuario actualice la página debido a una transmisión errónea, el mensaje no desaparecerá de inmediato.
El envío lo realiza el método flashMessage. El primer
parámetro es el texto del mensaje o un objeto stdClass
que representa el mensaje. El segundo parámetro opcional es
su tipo (error, warning, info, etc.). El método flashMessage()
devuelve una instancia del mensaje flash como un
objeto stdClass
, al que se le puede agregar información adicional.
$this->flashMessage('El elemento ha sido eliminado.');
$this->redirect(/* ... */); // y redirigimos
En la plantilla, estos mensajes están disponibles en la variable $flashes
como objetos stdClass
, que
contienen las propiedades message
(texto del mensaje), type
(tipo de mensaje) y pueden contener la
información de usuario ya mencionada. Los renderizamos, por ejemplo, así:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Redirección después de una señal
Después de procesar una señal de componente, a menudo sigue una redirección. Es una situación similar a la de los formularios: después de enviarlos, también redirigimos para que al actualizar la página en el navegador no se vuelvan a enviar los datos.
$this->redirect('this') // redirige al presenter y acción actuales
Dado que el componente es un elemento reutilizable y generalmente no debería tener una vinculación directa con presenters
específicos, los métodos redirect()
y link()
interpretan automáticamente el parámetro como una
señal del componente:
$this->redirect('click') // redirige a la señal 'click' del mismo componente
Si necesita redirigir a otro presenter o acción, puede hacerlo a través del presenter:
$this->getPresenter()->redirect('Product:show'); // redirige a otro presenter/acción
Parámetros persistentes
Los parámetros persistentes sirven para mantener el estado en los componentes entre diferentes peticiones. Su valor permanece igual incluso después de hacer clic en un enlace. A diferencia de los datos en la sesión, se transfieren en la URL. Y esto de forma totalmente automática, incluidos los enlaces creados en otros componentes en la misma página.
Tiene, por ejemplo, un componente para paginar contenido. Puede haber varios de estos componentes en una página. Y deseamos
que después de hacer clic en un enlace, todos los componentes permanezcan en su página actual. Por lo tanto, hacemos que el
número de página (page
) sea un parámetro persistente.
Crear un parámetro persistente es extremadamente simple en Nette. Basta con crear una propiedad pública y marcarla con un
atributo: (anteriormente se usaba /** @persistent */
)
use Nette\Application\Attributes\Persistent; // esta línea es importante
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // debe ser public
}
Recomendamos indicar también el tipo de dato para la propiedad (p. ej., int
) y puede indicar también un valor
predeterminado. Los valores de los parámetros se pueden validar.
Al crear un enlace, se puede cambiar el valor del parámetro persistente:
<a n:href="this page: $page + 1">siguiente</a>
O se puede resetear, es decir, eliminar de la URL. Entonces tomará su valor predeterminado:
<a n:href="this page: null">resetear</a>
Componentes persistentes
No solo los parámetros, sino también los componentes pueden ser persistentes. En tal componente, sus parámetros persistentes
se transfieren también entre diferentes acciones del presenter o entre varios presenters. Marcamos los componentes persistentes
con una anotación en la clase del presenter. Por ejemplo, así marcamos los componentes calendar
y
poll
:
/**
* @persistent(calendar, poll)
*/
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Los subcomponentes dentro de estos componentes no necesitan ser marcados, también se volverán persistentes.
En PHP 8, también puede usar atributos para marcar componentes persistentes:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Componentes con dependencias
¿Cómo crear componentes con dependencias sin “ensuciar” los presenters que los usarán? Gracias a las propiedades inteligentes del contenedor DI en Nette, al igual que al usar servicios clásicos, se puede dejar la mayor parte del trabajo al framework.
Tomemos como ejemplo un componente que tiene una dependencia del servicio PollFacade
:
class PollControl extends Control
{
public function __construct(
private int $id, // Id de la encuesta para la que creamos el componente
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($this->id, $voteId);
// ...
}
}
Si estuviéramos escribiendo un servicio clásico, no habría nada que resolver. El contenedor DI se encargaría invisiblemente
de pasar todas las dependencias. Pero con los componentes, generalmente los tratamos de tal manera que creamos su nueva instancia
directamente en el presenter en los métodos de fábrica createComponent…()
. Pero
pasar todas las dependencias de todos los componentes al presenter para luego pasarlas a los componentes es engorroso. Y la
cantidad de código escrito…
La pregunta lógica es, ¿por qué simplemente no registramos el componente como un servicio clásico, lo pasamos al presenter
y luego lo devolvemos en el método createComponent…()
? Sin embargo, tal enfoque es inapropiado, porque queremos
poder crear el componente incluso varias veces.
La solución correcta es escribir una fábrica para el componente, es decir, una clase que nos cree el componente:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Así registramos la fábrica en nuestro contenedor en la configuración:
services:
- PollControlFactory
y finalmente la usamos en nuestro presenter:
class PollPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private PollControlFactory $pollControlFactory,
) {
}
protected function createComponentPollControl(): PollControl
{
$pollId = 1; // podemos pasar nuestro parámetro
return $this->pollControlFactory->create($pollId);
}
}
Lo genial es que Nette DI puede generar tales fábricas simples, por lo que en lugar de todo su código, basta con escribir solo su interfaz:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
Y eso es todo. Nette implementa internamente esta interfaz y la pasa al presenter, donde ya podemos usarla. Mágicamente,
también agrega a nuestro componente el parámetro $id
y la instancia de la clase PollFacade
.
Componentes en profundidad
Los componentes en Nette Application representan partes reutilizables de una aplicación web que insertamos en las páginas y a las que, por cierto, se dedica todo este capítulo. ¿Qué capacidades exactas tiene tal componente?
- es renderizable en la plantilla
- sabe qué parte suya debe renderizar en una petición AJAX (fragmentos)
- tiene la capacidad de guardar su estado en la URL (parámetros persistentes)
- tiene la capacidad de reaccionar a las acciones del usuario (señales)
- crea una estructura jerárquica (donde la raíz es el presenter)
Cada una de estas funciones la realiza alguna de las clases de la línea de herencia. El renderizado (1 + 2) está a cargo de Nette\Application\UI\Control, la inclusión en el ciclo de vida (3, 4) de la clase Nette\Application\UI\Component y la creación de la estructura jerárquica (5) de las clases Container y 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 del componente
Validación de parámetros persistentes
Los valores de los parámetros persistentes recibidos de la URL se escriben en las
propiedades mediante el método loadState()
. Este también comprueba si el tipo de dato indicado en la propiedad
coincide, de lo contrario responde con un error 404 y la página no se muestra.
Nunca confíe ciegamente en los parámetros persistentes, ya que pueden ser fácilmente sobrescritos por el usuario en la URL.
Así, por ejemplo, verificamos si el número de página $this->page
es mayor que 0. Una forma adecuada es
sobrescribir el método mencionado loadState()
:
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // aquí se establece $this->page
// sigue la verificación propia del valor:
if ($this->page < 1) {
$this->error();
}
}
}
El proceso opuesto, es decir, la recopilación de valores de las propiedades persistentes, está a cargo del método
saveState()
.
Señales en profundidad
Una señal provoca la recarga de la página exactamente igual que en la petición original (excepto en el caso de que se llame
por AJAX) e invoca el método signalReceived($signal)
, cuya implementación predeterminada en la clase
Nette\Application\UI\Component
intenta llamar a un método compuesto por las palabras handle{signal}
. El
procesamiento posterior depende del objeto en cuestión. Los objetos que heredan de Component
(es decir,
Control
y Presenter
) reaccionan intentando llamar al método handle{signal}
con los
parámetros correspondientes.
En otras palabras: se toma la definición de la función handle{signal}
y todos los parámetros que llegaron con
la petición, y a los argumentos se les asignan los parámetros de la URL según el nombre e intenta llamar al método dado. Por
ejemplo, como parámetro $id
se pasa el valor del parámetro id
en la URL, como $something
se pasa something
de la URL, etc. Y si el método no existe, el método signalReceived
lanza una excepción.
La señal puede ser recibida por cualquier componente, presenter u objeto que implemente la interfaz
SignalReceiver
y esté conectado al árbol de componentes.
Los principales receptores de señales serán los Presenters
y los componentes visuales que heredan de
Control
. La señal debe servir como una indicación para el objeto de que debe hacer algo: la encuesta debe contar el
voto del usuario, el bloque de noticias debe expandirse y mostrar el doble de noticias, el formulario se envió y debe procesar
los datos, y así sucesivamente.
La URL para la señal la creamos usando el método Component::link(). Como
parámetro $destination
pasamos la cadena {signal}!
y como $args
un array de argumentos que
queremos pasar a la señal. La señal siempre se llama en el presenter y acción actuales con los parámetros actuales, los
parámetros de la señal simplemente se agregan. Además, se agrega al principio el parámetro ?do
, que determina
la señal.
Su formato es {signal}
o {signalReceiver}-{signal}
. {signalReceiver}
es el nombre del
componente en el presenter. Por eso no puede haber un guion en el nombre del componente; se usa para separar el nombre del
componente y la señal, sin embargo, es posible anidar varios componentes de esta manera.
El método isSignalReceiver()
verifica si el componente (primer argumento) es el receptor de la señal (segundo argumento). Podemos omitir el segundo argumento;
entonces verifica si el componente es receptor de cualquier señal. Como segundo parámetro se puede indicar true
y
así verificar si el receptor no es solo el componente indicado, sino también cualquiera de sus descendientes.
En cualquier fase anterior a handle{signal}
podemos ejecutar la señal manualmente llamando al método processSignal(), que
se encarga de gestionar la señal: toma el componente que se determinó como receptor de la señal (si no se especifica un
receptor de señal, es el propio presenter) y le envía la señal.
Ejemplo:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
De esta manera, la señal se ejecuta prematuramente y ya no se volverá a llamar.