Presenters
Veremos cómo se escriben en Nette los presenters y las plantillas. Después de leerlo entenderá:
- cómo funciona un presenter
- qué son los parámetros persistentes
- cómo se renderizan las plantillas
Ya sabemos que un presenter es una clase que representa una página concreta de la aplicación web, por ejemplo la portada, un producto de una tienda online, un formulario de acceso, un feed sitemap, etc. Una aplicación puede tener desde un presenter hasta miles. En otros frameworks se les llama también controladores.
Normalmente con el término presenter nos referimos a un descendiente de la clase Nette\Application\UI\Presenter, que es adecuado para generar interfaces web y al que se dedicará el resto de este capítulo. En sentido general, un presenter es cualquier objeto que implemente la interfaz Nette\Application\IPresenter.
Ciclo de vida del presenter
La tarea del presenter es procesar una petición y devolver una respuesta (que puede ser una página HTML, una imagen, una redirección, etc.).
Así pues, al principio se le pasa una petición. No es directamente la petición HTTP, sino un objeto Nette\Application\Request, en el que se transformó la petición HTTP con ayuda del router. Normalmente no trabajamos directamente con este objeto, porque el presenter delega ingeniosamente el procesamiento de la petición en otros métodos, que veremos ahora.
El diagrama muestra la lista de métodos que se llaman sucesivamente de arriba abajo, si es que existen. Ninguno es obligatorio; puede tener un presenter completamente vacío, sin un solo método, y construir sobre él un sitio web estático sencillo.
__construct()
El constructor no pertenece del todo al ciclo de vida del presenter, porque se llama en el momento de crear el objeto. Pero lo mencionamos por su importancia. El constructor (junto con el método inject) sirve para pasar las dependencias.
El presenter no debería ocuparse de la lógica de negocio de la aplicación, escribir o leer en la base de datos, hacer
cálculos, etc. De eso se encargan las clases de la capa que llamamos modelo. Por ejemplo, la clase ArticleRepository
puede encargarse de cargar y guardar los artículos. Para que el presenter pueda trabajar con ella, hay que pasársela mediante inyección de dependencias:
class ArticlePresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private ArticleRepository $articles,
) {
}
}
startup()
Inmediatamente después de recibir la petición se invoca el método startup(). Puede usarlo para inicializar
propiedades, comprobar los permisos del usuario, etc. Es obligatorio que este método llame siempre a su antecesor:
parent::startup().
action<Action>(args...)
Parecido al método render<View>(). Mientras que render<View>() está pensado para
preparar los datos de una plantilla concreta que después se renderizará, action<Action>() procesa la
petición sin que necesariamente se renderice después una plantilla. Por ejemplo, puede procesar datos, conectar o desconectar
al usuario, etc., y luego redirigir a otro sitio.
Es importante que action<Action>() se llame antes que render<View>(). Eso nos
permite cambiar el curso de la petición dentro del método de acción, por ejemplo cambiando la plantilla que se renderizará
o incluso el método render<View>() que se llamará, mediante setView('otherView').
Incluso puede cambiar a una acción completamente distinta con el método
switch('otherAction'). Aborta el método actual y ejecuta en su lugar los métodos
action<Action>() y render<View>() de la nueva acción (y desactiva la canonización automática). La propia petición continúa; solo se interrumpe el método que se
estaba ejecutando.
A este método se le pasan los parámetros de la petición. Es posible y recomendable indicar los tipos de estos parámetros,
p. ej. actionShow(int $id, ?string $slug = null). Si falta el parámetro id o no es un número entero,
el presenter devuelve un error 404 y termina.
handle<Signal>(args...)
Este método procesa las llamadas señales, que conoceremos en el capítulo dedicado a los componentes. Está pensado sobre todo para los componentes y el procesamiento de las peticiones AJAX.
A este método se le pasan los parámetros de la petición, igual que en action<Action>(), incluida la
comprobación de tipos.
beforeRender()
El método beforeRender, como su nombre indica, se llama antes de cada método render<View>().
Sirve para la configuración común de la plantilla, para pasar variables al layout y cosas parecidas.
render<View>(args...)
Aquí es donde preparamos la plantilla para su posterior renderizado, le pasamos los datos, etc.
A este método se le pasan los parámetros de la petición, igual que en action<Action>(), incluida la
comprobación de tipos.
public function renderShow(int $id): void
{
// obtiene los datos del modelo y se los pasa a la plantilla
$this->template->article = $this->articles->getById($id);
}
afterRender()
El método afterRender, de nuevo como su nombre indica, se llama después de cada método
render<View>(). Se usa más bien poco.
shutdown()
Se llama al final del ciclo de vida del presenter.
Eventos
Además de los métodos startup(), beforeRender() y shutdown(), que se llaman como parte
del ciclo de vida del presenter, se pueden definir otras funciones que se llamen automáticamente. El presenter define los
llamados eventos, y sus manejadores se añaden a los arrays
$onStartup, $onRender y $onShutdown.
class ArticlePresenter extends Nette\Application\UI\Presenter
{
public function __construct()
{
$this->onStartup[] = function () {
// ...
};
}
}
Los manejadores del array $onStartup se llaman justo antes del método startup(), los de
$onRender entre beforeRender() y render<View>(), y por último los de
$onShutdown justo antes de shutdown().
Un consejo antes de continuar: como ve, un presenter puede gestionar varias acciones/vistas, es decir, tener varios
métodos render<View>(). Pero recomendamos diseñar los presenters con una sola acción o con el menor número
posible de ellas.
Envío de una respuesta
La respuesta del presenter suele ser el renderizado de una plantilla en una página HTML, pero también puede ser el envío de un archivo, de JSON o incluso una redirección a otra página.
En cualquier momento del ciclo de vida podemos usar alguno de los siguientes métodos para enviar una respuesta y terminar al mismo tiempo el presenter:
redirect(),redirectPermanent(),redirectUrl()yforward()realizan una redirecciónerror()termina el presenter por un errorsendJson($data)termina el presenter y envía los datos en formato JSONsendTemplate()termina el presenter y renderiza inmediatamente la plantillasendResponse($response)termina el presenter y envía una respuesta propiaterminate()termina el presenter sin respuesta
Cada uno de estos métodos termina inmediatamente el presenter lanzando la excepción de terminación silenciosa
Nette\Application\AbortException.
Si no llama a ninguno de estos métodos, el presenter pasa automáticamente a renderizar la plantilla. ¿Por qué? Porque en el 99 % de los casos queremos renderizar una plantilla, así que el presenter adopta este comportamiento como predeterminado para facilitarnos el trabajo.
Creación de enlaces
El presenter tiene el método link(), con el que se crean enlaces URL a otros presenters. El primer parámetro es
el presenter y la acción de destino, seguido de los argumentos, que se pueden pasar como array:
$url = $this->link('Product:show', $id);
$url = $this->link('Product:show', [$id, 'lang' => 'en']);
En la plantilla, los enlaces a otros presenters y acciones se crean así:
<a n:href="Product:show $id">product detail</a>
Simplemente escriba en lugar de la URL real el conocido par Presenter:action e indique los parámetros que hagan
falta. El truco está en n:href, que le dice a Latte que procese este atributo y genere la URL real. En Nette no
tiene que pensar en absoluto en las URL, solo en los presenters y las acciones.
Encontrará más información en el capítulo Creación de enlaces URL.
Redirección
Para pasar a otro presenter sirven los métodos redirect() y forward(), que tienen una sintaxis muy
parecida a la del método link().
El método forward() pasa al nuevo presenter inmediatamente, sin redirección HTTP:
$this->forward('Product:show');
Ejemplo de redirección temporal con el código HTTP 302 (o 303, si el método de la petición actual es POST):
$this->redirect('Product:show', $id);
Para conseguir una redirección permanente con el código HTTP 301, use esto:
$this->redirectPermanent('Product:show', $id);
A otra URL fuera de la aplicación puede redirigir con el método redirectUrl(). El código HTTP se puede indicar
como segundo parámetro; el predeterminado es 302 (o 303, si el método de la petición actual es POST):
$this->redirectUrl('https://nette.org');
La redirección termina inmediatamente la actividad del presenter lanzando la llamada excepción de terminación silenciosa
Nette\Application\AbortException.
Antes de la redirección se pueden enviar Mensajes flash, es decir, mensajes que se mostrarán en la plantilla después de redirigir.
Mensajes flash
Son mensajes que suelen informar del resultado de alguna operación. Una propiedad importante de los mensajes flash es que siguen disponibles en la plantilla incluso después de una redirección. Una vez mostrados siguen activos otros 30 segundos: por ejemplo, si el usuario recarga la página debido a un error de transmisión, el mensaje no desaparecerá de inmediato.
Basta con llamar al método flashMessage() y el
presenter se encarga de pasarlo a la plantilla. El primer parámetro es el texto del mensaje y el segundo, opcional, es su tipo
(p. ej. error, warning, info). El método flashMessage() devuelve una instancia del mensaje flash, a la que se le
puede añadir más información.
$this->flashMessage('The item has been deleted.');
$this->redirect(/* ... */); // y redirige
En la plantilla, estos mensajes están disponibles en la variable $flashes como objetos stdClass que
contienen las propiedades message (el texto del mensaje), type (el tipo del mensaje) y, eventualmente,
la información añadida por el usuario ya mencionada. Los renderizamos así:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Error 404 y otros
Si no podemos atender la petición, por ejemplo porque el artículo que queremos mostrar no existe en la base de datos,
lanzamos un error 404 con el método error(string $message = '', int $httpCode = 404).
public function renderShow(int $id): void
{
$article = $this->articles->getById($id);
if (!$article) {
$this->error();
}
// ...
}
El código HTTP del error se puede pasar como segundo parámetro; el predeterminado es 404. El método funciona lanzando la
excepción Nette\Application\BadRequestException, tras la cual Application pasa el control al presenter
de error. Es un presenter cuya tarea es mostrar una página que informe del error ocurrido. El presenter de error se establece en
la configuración de la aplicación.
Envío de JSON
El método sendJson($data) codifica los datos indicados en JSON, los envía como respuesta HTTP y termina el
presenter. Ejemplo:
public function actionData(): void
{
$data = ['hello' => 'nette'];
$this->sendJson($data);
}
Parámetros de la petición
El presenter, y también cada componente, obtiene sus parámetros de la petición HTTP. Puede consultar sus valores con los
métodos getParameter($name) o getParameters(). Los valores son cadenas o arrays de cadenas, en esencia
datos en bruto obtenidos directamente de la URL.
Para mayor comodidad recomendamos acceder a los parámetros mediante propiedades. Basta con marcarlas con el atributo
#[Parameter]:
use Nette\Application\Attributes\Parameter; // esta línea es importante
class HomePresenter extends Nette\Application\UI\Presenter
{
#[Parameter]
public string $theme; // debe ser public
}
Para la propiedad recomendamos indicar el tipo de dato (p. ej. string) y Nette convertirá el valor
automáticamente. Los valores de los parámetros también se pueden validar.
Al crear un enlace puede establecer directamente el valor del parámetro:
<a n:href="Home:default theme: dark">click</a>
Parámetros persistentes
Los parámetros persistentes sirven para mantener el estado entre distintas peticiones. Su valor sigue siendo el mismo incluso
después de pulsar un enlace. A diferencia de los datos de la sesión, se transfieren en la URL. Y esto ocurre de forma
completamente automática, así que no hace falta indicarlos explícitamente en link() ni en n:href.
¿Un ejemplo de uso? Imagine que tiene una aplicación multilingüe. El idioma actual es un parámetro que debe formar parte
siempre de la URL. Pero sería increíblemente tedioso indicarlo en cada enlace. Así que lo convierte en el parámetro
persistente lang y se irá arrastrando solo. ¡Genial!
Crear un parámetro persistente en Nette es facilísimo. Basta con crear una propiedad pública y marcarla con el atributo:
(antes se usaba /** @persistent */)
use Nette\Application\Attributes\Persistent; // esta línea es importante
class ProductPresenter extends Nette\Application\UI\Presenter
{
#[Persistent]
public string $lang; // debe ser public
}
Si $this->lang tiene, por ejemplo, el valor 'en', los enlaces creados con link() o
n:href contendrán también el parámetro lang=en. Y después de pulsar el enlace,
$this->lang volverá a ser 'en'.
Para la propiedad recomendamos indicar el tipo de dato (p. ej. string) y también puede indicar un valor por
defecto. Los valores de los parámetros se pueden validar.
Los parámetros persistentes se transfieren normalmente entre todas las acciones de un presenter dado. Para transferirlos también entre varios presenters hay que definirlos:
- en un antecesor común del que hereden los presenters
- o en un trait que usen los presenters:
trait LanguageAware
{
#[Persistent]
public string $lang;
}
class ProductPresenter extends Nette\Application\UI\Presenter
{
use LanguageAware;
}
Al crear un enlace se puede cambiar el valor de un parámetro persistente:
<a n:href="Product:show $id, lang: cs">detail in Czech</a>
O se puede resetear, es decir, eliminar de la URL. Entonces adoptará su valor por defecto:
<a n:href="Product:show $id, lang: null">click</a>
Espacio común de parámetros
Los parámetros de la petición, los parámetros persistentes y los parámetros de
los métodos action, render y handle (señal) comparten un único espacio, en el que cada
uno se identifica por su nombre. Si el mismo nombre aparece en varios de ellos, se refieren a un único y mismo valor.
Esto se aprovecha a menudo. Por ejemplo, el parámetro persistente lang y el argumento $lang de un
método de acción o de señal son una y la misma cosa: puede leer el valor actual de un parámetro persistente con solo
indicarlo en la firma del método:
#[Persistent]
public string $lang;
public function handleSearch(string $query, string $lang): void
{
// $lang contiene el valor actual del parámetro persistente lang
}
Como este espacio es compartido, mantenga los nombres de los parámetros únicos, salvo que quiera deliberadamente que compartan un valor. Esto vale también para las señales, que además leen parámetros del cuerpo POST de la petición, véase Las señales en profundidad.
Componentes interactivos
Los presenters llevan incorporado un sistema de componentes. Los componentes son unidades independientes y reutilizables que insertamos en los presenters. Pueden ser formularios, datagrids, menús, en definitiva cualquier cosa que tenga sentido usar repetidamente.
¿Cómo se insertan los componentes en los presenters y cómo se usan después? Lo aprenderá en el capítulo Componentes. Descubrirá incluso qué tienen en común con Hollywood.
¿Y dónde puedo conseguir componentes? En Componette encontrará componentes de código abierto y muchos otros complementos para Nette, aportados por voluntarios de la comunidad del framework.
Profundizando
Lo que hemos visto hasta ahora en este capítulo bastará probablemente para la mayoría de los usos. Las secciones siguientes están pensadas para quien quiera profundizar en los presenters y saberlo absolutamente todo.
Validación de los parámetros
Los valores de los Parámetros de la petición y de los Parámetros persistentes recibidos de las URL los escribe en las propiedades el método
loadState(). Este comprueba también si coinciden con el tipo de dato indicado en la propiedad; en caso contrario
responderá con un error 404 y la página no se mostrará.
Nunca confíe ciegamente en los parámetros recibidos de la URL, porque el usuario puede sobrescribirlos fácilmente. Así, por
ejemplo, comprobaríamos si el idioma $this->lang está entre los admitidos. Una forma adecuada de hacerlo es
sobrescribir el mencionado método loadState():
class ProductPresenter extends Nette\Application\UI\Presenter
{
#[Persistent]
public string $lang;
public function loadState(array $params): void
{
parent::loadState($params); // aquí se establece $this->lang
// sigue la comprobación propia del valor:
if (!in_array($this->lang, ['en', 'cs'])) {
$this->error();
}
}
}
Guardar y restaurar la petición
La petición que procesa el presenter es un objeto Nette\Application\Request, que devuelve el
método getRequest() del presenter.
La petición actual se puede guardar en la sesión o, al revés, restaurarla desde ella y hacer que el presenter la ejecute
otra vez. Esto resulta útil, por ejemplo, cuando el usuario está rellenando un formulario y su sesión de acceso caduca. Para no
perder los datos, antes de redirigir a la página de acceso guardamos la petición actual en la sesión con
$reqId = $this->storeRequest(). Esto devuelve su identificador en forma de cadena corta, que luego pasamos como
parámetro al presenter de acceso.
Tras el acceso llamamos al método $this->restoreRequest($reqId), que recupera la petición de la sesión. Las
peticiones POST se le reenvían, mientras que las demás (GET) se redirigen a la URL de la petición. El método comprueba que la
petición la creó el mismo usuario que ahora está conectado. Si se conecta otro usuario o la clave no es válida, no hace nada
y el programa continúa con normalidad.
Véase la guía Cómo volver a una página anterior.
Canonización
Los presenters tienen una característica realmente excelente que contribuye a un mejor SEO (Search Engine Optimization).
Impiden automáticamente que exista contenido duplicado en URL distintas. Si a un destino concreto llevan varias URL, p. ej.
/index y /index?page=1, el framework designa una de ellas como principal (canónica) y redirige las
demás a ella con el código HTTP 301. Gracias a eso, los buscadores no indexan sus páginas dos veces ni diluyen su
page rank.
Este proceso se llama canonización. La URL canónica es la que genera el router, normalmente la primera ruta coincidente de la colección.
La canonización está activada de forma predeterminada y se puede desactivar con
$this->autoCanonicalize = false.
La redirección no se produce en las peticiones AJAX ni POST, porque podría provocar la pérdida de datos o no aportaría ningún valor SEO añadido.
También puede provocar la canonización manualmente con el método canonicalize(). Igual que al método
link(), se le pasan el presenter, la acción y los parámetros. Genera un enlace y lo compara con la dirección URL
actual. Si difieren, redirige al enlace generado.
public function actionShow(int $id, ?string $slug = null): void
{
$realSlug = $this->facade->getSlugForId($id);
// redirige si $slug es distinto de $realSlug
$this->canonicalize('Product:show', [$id, $realSlug]);
}
Para ver un patrón completo que combina los filtros de ruta con canonicalize() para obtener URL amigables para el
SEO, consulte URLs amigables con slugs.
Respuestas
La respuesta que devuelve el presenter es un objeto que implementa la interfaz Nette\Application\Response. Hay disponibles varias respuestas ya preparadas:
- Nette\Application\Responses\CallbackResponse – envía un callback
- Nette\Application\Responses\FileResponse – envía un archivo
- Nette\Application\Responses\ForwardResponse – forward()
- Nette\Application\Responses\JsonResponse – envía JSON
- Nette\Application\Responses\RedirectResponse – redirección
- Nette\Application\Responses\TextResponse – envía texto
- Nette\Application\Responses\VoidResponse – respuesta vacía
Las respuestas se envían con el método sendResponse():
use Nette\Application\Responses;
// Texto plano
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));
// Envía un archivo
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));
// Envía un callback
$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));
También puede escribir su propia respuesta. Basta con implementar la interfaz Nette\Application\Response, que
tiene un único método send() que recibe la petición y la respuesta HTTP. Esto es útil, por ejemplo, al transmitir
datos que no quiere mantener en memoria:
class CsvResponse implements Nette\Application\Response
{
public function __construct(
private string $fileName,
private iterable $rows,
) {
}
public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void
{
$response->setContentType('text/csv', 'utf-8');
$response->sendAsFile($this->fileName);
$handle = fopen('php://output', 'w');
foreach ($this->rows as $row) {
fputcsv($handle, $row);
}
fclose($handle);
}
}
Después la envía en el presenter como de
costumbre: $this->sendResponse(new CsvResponse('export.csv', $rows));
Caché HTTP
El método lastModified() permite aprovechar fácilmente la caché HTTP. Se le pasa la fecha y hora de la última
modificación del contenido (como timestamp, cadena u objeto DateTimeInterface) y, opcionalmente, un validador ETag
(una cadena corta que identifica la versión actual del contenido, por ejemplo su hash) y un tiempo de expiración. Si el
navegador ya tiene una versión coincidente, el presenter envía una respuesta 304 Not Modified y termina, con lo que
la página no se renderiza ni se transfiere innecesariamente:
public function renderArticle(int $id): void
{
$article = $this->articles->getById($id);
$this->lastModified($article->updatedAt);
// ...
}
Completar la plantilla
Cuando el presenter renderiza la plantilla, el método sendTemplate() llama a completeTemplate()
justo antes del renderizado. Este método rellena las variables marcadas con el atributo #[TemplateVariable] y
localiza el archivo de la plantilla (las variables predeterminadas ya las establece TemplateFactory al crear la
plantilla). Puede sobrescribir este método protegido para añadir variables compartidas por todas las vistas o para establecer
otro archivo:
protected function completeTemplate(Nette\Application\UI\Template $template): void
{
parent::completeTemplate($template);
$template->siteName = 'My App';
}
Restricción de acceso con #[Requires]
El atributo #[Requires] ofrece opciones avanzadas para restringir el acceso a los presenters y a sus métodos.
Sirve para indicar métodos HTTP, exigir una petición AJAX, limitarlo al mismo origen y permitir el acceso solo mediante forward.
El atributo se puede aplicar tanto a las clases de los presenters como a métodos concretos como
action<Action>(), render<View>(), handle<Signal>() y
createComponent<Name>().
Puede indicar estas restricciones:
- sobre los métodos HTTP:
#[Requires(methods: ['GET', 'POST'])] - exigiendo una petición AJAX:
#[Requires(ajax: true)] - acceso solo desde el mismo origen:
#[Requires(sameOrigin: true)] - acceso solo mediante forward:
#[Requires(forward: true)] - restricciones para acciones concretas:
#[Requires(actions: 'default')]
Desde la versión 3.3, la coincidencia de origen se comprueba mediante la cabecera Sec-Fetch-Site del
navegador (antes mediante una cookie SameSite), lo que es más fiable y comprueba la coincidencia exacta de esquema, dominio y
puerto.
Los detalles los encontrará en la guía Cómo usar el atributo Requires.
Comprobación del método HTTP
Los presenters en Nette comprueban automáticamente el método HTTP de cada petición entrante, sobre todo por motivos de
seguridad. De forma predeterminada se permiten los métodos GET, POST, HEAD,
PUT, DELETE, PATCH.
Si quiere permitir además, por ejemplo, el método OPTIONS, use el atributo #[Requires] (desde Nette
Application v3.2.3):
#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}
Desde la versión 3.1.13, la comprobación se realiza en checkHttpMethod(), que verifica si el método indicado en
la petición está incluido en el array $presenter->allowedMethods. Desde la versión 3.2.3, este enfoque está
obsoleto en favor de #[Requires]. El método se puede sobrescribir así:
class MyPresenter extends Nette\Application\UI\Presenter
{
protected function checkHttpMethod(): void
{
$this->allowedMethods[] = 'OPTIONS';
parent::checkHttpMethod();
}
}
Es importante subrayar que, si habilita el método OPTIONS, debe después gestionarlo adecuadamente dentro de su
presenter. Este método se usa a menudo como la llamada petición preflight, que el navegador envía automáticamente antes de la
petición propiamente dicha cuando hay que averiguar si la petición es admisible según la política CORS (Cross-Origin Resource
Sharing). Si habilita el método pero no implementa la respuesta correcta, puede provocar inconsistencias y posibles problemas de
seguridad.
Marcar acciones obsoletas
El atributo #[Deprecated] marca acciones, señales o presenters enteros como obsoletos y destinados a desaparecer
en el futuro. Al generar enlaces a partes obsoletas de la aplicación, Nette lanza una advertencia para avisar a los
desarrolladores.
El atributo se puede aplicar tanto a toda la clase del presenter como a métodos concretos action<Action>(),
render<View>() y handle<Signal>().