Presenters

Nos familiarizaremos con cómo se escriben los presenters y las plantillas en Nette. Después de leerlo, sabrá:

  • cómo funciona un presenter
  • qué son los parámetros persistentes
  • cómo se dibujan las plantillas

Ya sabemos que un presenter es una clase que representa alguna página específica de una aplicación web, p. ej., la página de inicio; un producto en una tienda electrónica; un formulario de inicio de sesión; un feed sitemap, etc. Una aplicación puede tener desde uno hasta miles de presenters. En otros frameworks también se les llama controllers.

Generalmente, bajo el término presenter se entiende un descendiente de la clase Nette\Application\UI\Presenter, que es adecuado para generar interfaces web y al que nos dedicaremos en el resto de este capítulo. En sentido general, un presenter es cualquier objeto que implementa la interfaz Nette\Application\IPresenter.

Ciclo de vida del presenter

La tarea del presenter es gestionar una petición y devolver una respuesta (que puede ser una página HTML, una imagen, una redirección, etc.).

Por lo tanto, al principio se le pasa una petición. No es directamente una petición HTTP, sino un objeto Nette\Application\Request, al que se transformó la petición HTTP con la ayuda del router. Generalmente no interactuamos con este objeto, ya que el presenter delega inteligentemente el procesamiento de la petición a otros métodos, que ahora mostraremos.

Ciclo de vida del presenter

La imagen representa una lista de métodos que se llaman sucesivamente de arriba abajo, si existen. Ninguno de ellos tiene por qué existir, podemos tener un presenter completamente vacío sin un solo método y construir sobre él un sitio web estático simple.

__construct()

El constructor no pertenece exactamente al ciclo de vida del presenter, porque se llama en el momento de la creación del objeto. Pero lo mencionamos por su importancia. El constructor (junto con el método inject) sirve para pasar dependencias.

El presenter no debería encargarse de la lógica de negocio de la aplicación, escribir y leer de la base de datos, realizar cálculos, etc. Para eso están las clases de la capa que llamamos modelo. Por ejemplo, la clase ArticleRepository puede encargarse de cargar y guardar artículos. Para que el presenter pueda trabajar con ella, se la deja pasar 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 llama al método startup(). Puede usarlo para inicializar propiedades, verificar permisos de usuario, etc. Se requiere que el método siempre llame al ancestro parent::startup().

action<Action>(args...)

Análogo al método render<View>(). Mientras que render<View>() está destinado a preparar datos para una plantilla específica que luego se renderizará, en action<Action>() se procesa la petición sin conexión con la renderización de la plantilla. Por ejemplo, se procesan datos, se inicia o cierra sesión de usuario, y así sucesivamente, y luego se redirige a otro lugar.

Es importante que action<Action>() se llame antes que render<View>(), por lo que en él podemos cambiar el curso posterior de los acontecimientos, es decir, cambiar la plantilla que se dibujará, y también el método render<View>() que se llamará. Y esto usando setView('otraVista').

Al método se le pasan parámetros de la petición. Es posible y recomendable indicar tipos para los parámetros, p. ej., actionShow(int $id, ?string $slug = null) – si falta el parámetro id o si no es un entero, el presenter devolverá un error 404 y finalizará la actividad.

handle<Signal>(args...)

El método procesa las llamadas señales, con las que nos familiarizaremos en el capítulo dedicado a los componentes. De hecho, está destinado principalmente a componentes y al procesamiento de peticiones AJAX.

Al método se le pasan parámetros de la petición, como en el caso de action<Action>(), incluida la verificación de tipos.

beforeRender()

El método beforeRender, como su nombre indica, se llama antes de cada método render<View>(). Se utiliza para la configuración común de la plantilla, pasar variables para el layout y similares.

render<View>(args...)

El lugar donde preparamos la plantilla para su posterior renderización, le pasamos datos, etc.

Al método se le pasan parámetros de la petición, como en el caso de action<Action>(), incluida la verificación de tipos.

public function renderShow(int $id): void
{
	// obtenemos datos del modelo y los pasamos a la plantilla
	$this->template->article = $this->articles->getById($id);
}

afterRender()

El método afterRender, como su nombre indica de nuevo, se llama después de cada método render<View>(). Se usa más bien excepcionalmente.

shutdown()

Se llama al final del ciclo de vida del presenter.

Un buen consejo antes de continuar. Como puede ver, un presenter puede manejar múltiples acciones/vistas, es decir, tener múltiples métodos render<View>(). Pero recomendamos diseñar presenters con una o la menor cantidad posible de acciones.

Envío de la respuesta

La respuesta del presenter suele ser la renderización de una plantilla con una página HTML, pero también puede ser el envío de un archivo, JSON o incluso una redirección a otra página.

En cualquier momento durante el ciclo de vida, podemos enviar una respuesta con uno de los siguientes métodos y, al mismo tiempo, finalizar el presenter:

  • redirect(), redirectPermanent(), redirectUrl() y forward() redirigen
  • error() finaliza el presenter debido a un error
  • sendJson($data) finaliza el presenter y envía datos en formato JSON
  • sendTemplate() finaliza el presenter e inmediatamente renderiza la plantilla
  • sendResponse($response) finaliza el presenter y envía una respuesta propia
  • terminate() finaliza el presenter sin respuesta

Si no llama a ninguno de estos métodos, el presenter procederá automáticamente a renderizar la plantilla. ¿Por qué? Porque en el 99% de los casos queremos renderizar una plantilla, por lo que el presenter toma este comportamiento como predeterminado y quiere facilitarnos el trabajo.

Creación de enlaces

El presenter dispone del método link(), mediante el cual se pueden crear enlaces URL a otros presenters. El primer parámetro es el presenter y la acción de destino, seguido de los argumentos pasados, que pueden indicarse como un array:

$url = $this->link('Product:show', $id);

$url = $this->link('Product:show', [$id, 'lang' => 'cs']);

En la plantilla, se crean enlaces a otros presenters y acciones de esta manera:

<a n:href="Product:show $id">detalle del producto</a>

Simplemente, en lugar de la URL real, escribe el par conocido Presenter:action e indica los parámetros necesarios. El truco está en n:href, que indica que este atributo será procesado por Latte y generará la URL real. En Nette, por lo tanto, no necesita 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 se utilizan los métodos redirect() y forward(), que tienen una sintaxis muy similar al método link().

El método forward() pasa al nuevo presenter inmediatamente sin redirección HTTP:

$this->forward('Product:show');

Ejemplo de la llamada redirección temporal con código HTTP 302 (o 303, si el método de la petición actual es POST):

$this->redirect('Product:show', $id);

La redirección permanente con código HTTP 301 se logra así:

$this->redirectPermanent('Product:show', $id);

A otra URL fuera de la aplicación se puede redirigir con el método redirectUrl(). Como segundo parámetro se puede indicar el código HTTP, 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 finaliza inmediatamente la actividad del presenter lanzando la llamada excepción de finalización silenciosa Nette\Application\AbortException.

Antes de la redirección se puede enviar un mensaje flash, es decir, mensajes que se mostrarán en la plantilla después de la redirección.

Mensajes flash

Son mensajes que generalmente informan sobre el resultado de alguna 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.

Basta con llamar al método flashMessage() y el presenter se encargará de pasarlo a la plantilla. El primer parámetro es el texto del mensaje y el segundo parámetro opcional es su tipo (error, warning, info, etc.). El método flashMessage() devuelve una instancia del mensaje flash, a la 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}

Error 404 y cía.

Si no se puede cumplir 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 = null, 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 lo cual Application pasa el control al error-presenter. Que es un presenter cuya tarea es mostrar una página informando sobre el error ocurrido. La configuración del error-presenter se realiza en la configuración de application.

Envío de JSON

Ejemplo de un método de acción que envía datos en formato JSON y finaliza el presenter:

public function actionData(): void
{
	$data = ['hello' => 'nette'];
	$this->sendJson($data);
}

Parámetros de la petición

El presenter y también cada componente obtienen sus parámetros de la petición HTTP. Puede obtener su valor con el método getParameter($name) o getParameters(). Los valores son cadenas o arrays de cadenas, son básicamente datos brutos obtenidos directamente de la URL.

Para mayor comodidad, recomendamos acceder a los parámetros a través de 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
}

Recomendamos indicar también el tipo de dato para la propiedad (p. ej., string) y Nette lo convertirá automáticamente según él. Los valores de los parámetros también se pueden validar.

Al crear un enlace, se puede establecer directamente el valor de los parámetros:

<a n:href="Home:default theme: dark">click</a>

Parámetros persistentes

Los parámetros persistentes sirven para mantener el estado 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, por lo que no es necesario indicarlos explícitamente en link() o n:href.

¿Un ejemplo de uso? Tiene una aplicación multilingüe. El idioma actual es un parámetro que debe estar constantemente presente en la URL. Pero sería increíblemente tedioso indicarlo en cada enlace. Así que lo convierte en un parámetro persistente lang y se transferirá solo. ¡Genial!

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 ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang; // debe ser public
}

Si $this->lang tiene el valor, por ejemplo, 'en', entonces también los enlaces creados mediante link() o n:href contendrán el parámetro lang=en. Y después de hacer clic en el enlace, nuevamente $this->lang = 'en'.

Recomendamos indicar también el tipo de dato para la propiedad (p. ej., string) y puede indicar también un valor predeterminado. Los valores de los parámetros se pueden validar.

Los parámetros persistentes se transfieren estándarmente entre todas las acciones del presenter dado. Para que se transfieran también entre varios presenters, es necesario definirlos ya sea:

  • en un ancestro común del que heredan los presenters
  • 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 del parámetro persistente:

<a n:href="Product:show $id, lang: cs">detalle en checo</a>

O se puede resetear, es decir, eliminar de la URL. Entonces tomará su valor predeterminado:

<a n:href="Product:show $id, lang: null">haz clic</a>

Componentes interactivos

Los presenters tienen incorporado un sistema de componentes. Los componentes son unidades reutilizables independientes que insertamos en los presenters. Pueden ser formularios, datagrids, menús, en realidad cualquier cosa que tenga sentido usar repetidamente.

¿Cómo se insertan los componentes en el presenter y se usan posteriormente? Eso lo aprenderá en el capítulo Componentes. Incluso descubrirá qué tienen en común con Hollywood.

¿Y dónde puedo obtener componentes? En la página Componette encontrará componentes de código abierto y también muchos otros complementos para Nette, que voluntarios de la comunidad alrededor del framework han colocado aquí.

Vamos a profundizar

Con lo que hemos mostrado hasta ahora en este capítulo, probablemente le sea suficiente. Las siguientes líneas están destinadas a aquellos que se interesan por los presenters en profundidad y quieren saber absolutamente todo.

Validación de parámetros

Los valores de los parámetros de la petición y 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, ya que pueden ser fácilmente sobrescritos por el usuario en la URL. Así, por ejemplo, verificamos si el idioma $this->lang está entre los soportados. Una forma adecuada es sobrescribir el método mencionado 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 verificación propia del valor:
		if (!in_array($this->lang, ['en', 'cs'])) {
			$this->error();
		}
	}
}

Guardado y restauración de la petición

La petición que gestiona el presenter es un objeto Nette\Application\Request y lo devuelve el método del presenter getRequest().

La petición actual se puede guardar en la sesión o, por el contrario, restaurarla desde ella y hacer que el presenter la ejecute de nuevo. Esto es útil, por ejemplo, en una situación en la que el usuario está rellenando un formulario y su sesión expira. Para no perder los datos, antes de redirigir a la página de inicio de sesión, guardamos la petición actual en la sesión usando $reqId = $this->storeRequest(), que devuelve su identificador en forma de cadena corta y lo pasamos como parámetro al presenter de inicio de sesión.

Después de iniciar sesión, llamamos al método $this->restoreRequest($reqId), que recupera la petición de la sesión y hace forward a ella. El método, al mismo tiempo, verifica que la petición la creó el mismo usuario que ahora ha iniciado sesión. Si iniciara sesión otro usuario o la clave fuera inválida, no hace nada y el programa continúa.

Consulte el tutorial Cómo volver a una página anterior.

Canonización

Los presenters tienen una característica realmente genial que contribuye a un mejor SEO (optimización para motores de búsqueda). Evitan automáticamente la existencia de contenido duplicado en diferentes URL. Si a un destino determinado conducen varias direcciones URL, p. ej., /index y /index?page=1, el framework determina una de ellas como primaria (canónica) y redirige las demás a ella usando el código HTTP 301. Gracias a esto, los motores de búsqueda no indexan sus páginas dos veces y no diluyen su page rank.

Este proceso se llama canonización. La URL canónica es la que genera el router, generalmente, por lo tanto, la primera ruta correspondiente en la colección.

La canonización está activada por defecto y se puede desactivar mediante $this->autoCanonicalize = false.

La redirección no ocurre en una petición AJAX o POST, porque se perderían datos o no tendría valor añadido desde el punto de vista del SEO.

También puede invocar la canonización manualmente usando el método canonicalize(), al que, de manera similar al método link(), se le pasa el presenter, la acción y los parámetros. Crea 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 difiere de $realSlug
	$this->canonicalize('Product:show', [$id, $realSlug]);
}

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 deben llamarse automáticamente. El presenter define los llamados eventos, cuyos manejadores agrega a los arrays $onStartup, $onRender y $onShutdown.

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct()
	{
		$this->onStartup[] = function () {
			// ...
		};
	}
}

Los manejadores en el array $onStartup se llaman justo antes del método startup(), luego $onRender entre beforeRender() y render<View>() y finalmente $onShutdown justo antes de shutdown().

Respuestas

La respuesta que devuelve el presenter es un objeto que implementa la interfaz Nette\Application\Response. Hay disponibles varias respuestas preparadas:

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'));

// La respuesta será 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));

Restricción de acceso mediante #[Requires]

El atributo #[Requires] proporciona opciones avanzadas para restringir el acceso a presenters y sus métodos. Se puede usar para especificar métodos HTTP, requerir una petición AJAX, restringir al mismo origen (same origin) y acceso solo a través de forward. El atributo se puede aplicar tanto a clases de presenters como a métodos individuales action<Action>(), render<View>(), handle<Signal>() y createComponent<Name>().

Puede especificar estas restricciones:

  • a métodos HTTP: #[Requires(methods: ['GET', 'POST'])]
  • requerir una petición AJAX: #[Requires(ajax: true)]
  • acceso solo desde el mismo origen: #[Requires(sameOrigin: true)]
  • acceso solo a través de forward: #[Requires(forward: true)]
  • restricción a acciones específicas: #[Requires(actions: 'default')]

Encontrará detalles en el tutorial Cómo usar el atributo Requires.

Verificación del método HTTP

Los presenters en Nette verifican automáticamente el método HTTP de cada petición entrante. La razón de esta verificación es principalmente la seguridad. Por defecto, se permiten los métodos GET, POST, HEAD, PUT, DELETE, PATCH.

Si desea permitir adicionalmente, por ejemplo, el método OPTIONS, use para ello el atributo #[Requires] (desde Nette Application v3.2):

#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

En la versión 3.1, la verificación se realiza en checkHttpMethod(), que comprueba si el método especificado en la petición está contenido en el array $presenter->allowedMethods. La adición del método se hace así:

class MyPresenter extends Nette\Application\UI\Presenter
{
    protected function checkHttpMethod(): void
    {
        $this->allowedMethods[] = 'OPTIONS';
        parent::checkHttpMethod();
    }
}

Es importante destacar que si permite el método OPTIONS, debe luego también gestionarlo adecuadamente dentro de su presenter. El método se usa a menudo como la llamada petición preflight, que el navegador envía automáticamente antes de la petición real, cuando es necesario averiguar si la petición está permitida desde el punto de vista de la política CORS (Cross-Origin Resource Sharing). Si permite el método, pero no implementa la respuesta correcta, puede llevar a inconsistencias y posibles problemas de seguridad.

Lectura adicional

versión: 4.0