Presentadores

Aprenderemos a escribir presentadores y plantillas en Nette. Después de leer sabrás:

  • cómo funciona el presentador
  • qué son los parámetros persistentes
  • cómo renderizar una plantilla

Ya sabemos que un presentador es una clase que representa una página específica de una aplicación web, como una página de inicio; un producto en una tienda electrónica; un formulario de registro; un feed de mapa del sitio, etc. La aplicación puede tener de uno a miles de presentadores. En otros frameworks, también se conocen como controladores.

Normalmente, el término presentador se refiere a un descendiente de la clase Nette\Application\UI\Presenter, que es adecuada para interfaces web y de la que hablaremos en el resto de este capítulo. En un sentido general, un presentador es cualquier objeto que implemente la interfaz Nette\Application\IPresenter.

Ciclo de vida del presentador

El trabajo del presentador es procesar la solicitud y devolver una respuesta (que puede ser una página HTML, una imagen, una redirección, etc.).

Así que al principio hay una petición. No es directamente una petición HTTP, sino un objeto Nette\Application\Request en el que se ha transformado la petición HTTP mediante un enrutador. Normalmente no entramos en contacto con este objeto, porque el presentador delega inteligentemente el procesamiento de la petición a métodos especiales, que ahora veremos.

Ciclo de vida del presentador

La figura muestra una lista de métodos que son llamados secuencialmente de arriba a abajo, si es que existen. No es necesario que exista ninguno de ellos, podemos tener un presentador completamente vacío sin un solo método y construir una simple web estática sobre él.

__construct()

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

El presentador no debe encargarse de la lógica de negocio de la aplicación, escribir y leer de la base de datos, realizar cálculos, etc. Esta es la tarea para las clases de una capa, que llamamos modelo. Por ejemplo, la clase ArticleRepository puede encargarse de cargar y guardar artículos. Para que el presentador pueda utilizarla, se le pasa mediante inyección de dependencia:

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 (). Se puede utilizar para inicializar propiedades, comprobar privilegios de usuario, etc. Es necesario llamar siempre al ancestro parent::startup().

action<Action>(args...)

Similar al método render<View>(). Mientras que render<View>() está destinado a preparar datos para una plantilla específica, que posteriormente se renderiza, en action<Action>() se procesa una solicitud sin renderizar posteriormente la plantilla. Por ejemplo, los datos se procesan, un usuario se conecta o desconecta, y así sucesivamente, y luego se redirige a otro lugar.

Es importante que action<Action>() se llame antes que render<View>()para que dentro de él podamos posiblemente cambiar el siguiente curso del ciclo de vida, es decir, cambiar la plantilla que será renderizada y también el método render<View>() que será llamado, usando setView('otherView').

Los parámetros de la petición se pasan al método. Es posible y recomendable especificar tipos para los parámetros, por ejemplo actionShow(int $id, string $slug = null) – si el parámetro id falta o si no es un entero, el presentador devuelve el error 404 y termina la operación.

handle<Signal>(args...)

Este método procesa las llamadas señales, de las que hablaremos en el capítulo sobre Componentes. Está pensado principalmente para componentes y procesamiento de peticiones AJAX.

Los parámetros se pasan al método, como en el caso de action<Action>()incluyendo la comprobació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 de plantillas comunes, pasando variables para el diseño y así sucesivamente.

render<View>(args...)

El lugar donde preparamos la plantilla para su posterior renderizado, le pasamos datos, etc.

Los parámetros se pasan al método, como en el caso de action<Action>()incluyendo la comprobació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()

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

shutdown()

Se llama al final del ciclo de vida del presentador.

Un buen consejo antes de continuar. Como puedes ver, el presentador puede manejar más acciones/vistas, es decir, tener más métodos render<View>(). Pero recomendamos diseñar presentadores con una o tan pocas acciones como sea posible.

Envío de una respuesta

La respuesta del presentador suele ser renderizar la plantilla con la página HTML, pero también puede ser enviar un archivo, JSON o incluso redirigir a otra página.

En cualquier momento durante el ciclo de vida, puede utilizar cualquiera de los siguientes métodos para enviar una respuesta y salir del presentador al mismo tiempo:

Si no llama a ninguno de estos métodos, el presentador procederá automáticamente a renderizar la plantilla. ¿Por qué? Pues porque en el 99% de los casos queremos dibujar una plantilla, así que el presentador toma este comportamiento por defecto y quiere facilitarnos el trabajo.

Presenter tiene un método link(), que se utiliza para crear enlaces URL a otros presentadores. El primer parámetro es el presentador y la acción de destino, seguido de los argumentos, que pueden pasarse como matriz:

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

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

En la plantilla creamos enlaces a otros presentadores y acciones de la siguiente manera:

<a n:href="Product:show $id">product detail</a>

Basta con escribir el conocido par Presenter:action en lugar de la URL real e incluir cualquier parámetro. El truco es n:href, que dice que este atributo será procesado por Latte y genera una URL real. En Nette, no tienes que pensar en URLs en absoluto, sólo en presentadores y acciones.

Para más información, vea Creando Enlaces.

Redirección

Para saltar a otro presentador se utilizan los métodos redirect() y forward(), que tienen una sintaxis muy similar a la del método link().

El forward() cambia al nuevo presentador inmediatamente sin redirección HTTP:

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

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

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

Para conseguir una redirección permanente con código HTTP 301 utilice:

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

Puede redirigir a otra URL fuera de la aplicación utilizando el método redirectUrl(). El código HTTP puede especificarse como segundo parámetro, siendo el predeterminado 302 (o 303, si el método de solicitud actual es POST):

$this->redirectUrl('https://nette.org');

La redirección termina inmediatamente el ciclo de vida del presentador lanzando la llamada excepción de terminación silenciosa Nette\Application\AbortException.

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

Mensajes flash

Son mensajes que suelen informar 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 la redirección. Incluso después de ser mostrados, permanecerán vivos durante otros 30 segundos – por ejemplo, en caso de que el usuario refrescara involuntariamente la página – el mensaje no se perderá.

Basta con llamar al método flashMessage() y el presentador se encargará de pasar el mensaje a la plantilla. El primer argumento es el texto del mensaje y el segundo argumento opcional es su tipo (error, advertencia, información, etc.). El método flashMessage() devuelve una instancia de flash message, para permitirnos añadir más información.

$this->flashMessage('Item was removed.');
$this->redirect(/* ... */);

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 ya mencionada información del usuario. Los dibujamos como sigue:

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Error 404 etc.

Cuando no podamos satisfacer la petición porque por ejemplo el artículo que queremos mostrar no existe en la base de datos, lanzaremos el error 404 utilizando el método error(string $message = null, int $httpCode = 404), que representa el error HTTP 404:

public function renderShow(int $id): void
{
	$article = $this->articles->getById($id);
	if (!$article) {
		$this->error();
	}
	// ...
}

El código de error HTTP se puede pasar como segundo parámetro, por defecto es 404. El método funciona lanzando la excepción Nette\Application\BadRequestException, tras lo cual Application pasa el control al presentador del error. Que es un presentador cuyo trabajo es mostrar una página informando sobre el error. El presentador de errores se establece en la configuración de la aplicación.

Envío de JSON

Ejemplo de action-method que envía datos en formato JSON y sale del presentador:

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

Parámetros de solicitud

El presentador, al igual que todos los componentes, obtiene sus parámetros de la petición HTTP. Sus valores pueden recuperarse utilizando el método getParameter($name) o getParameters(). Los valores son cadenas o matrices de cadenas, esencialmente datos sin procesar obtenidos directamente de la URL.

Para mayor comodidad, recomendamos que los parámetros sean accesibles a través de propiedades. Basta con anotarlas con el atributo #[Parameter] atributo:

use Nette\Application\Attributes\Parameter;  // esta línea es importante

class HomePresenter extends Nette\Application\UI\Presenter
{
	#[Parameter]
	public string $theme; // debe ser pública
}

Para las propiedades, le sugerimos que especifique el tipo de datos (por ejemplo, string). A continuación, Nette asignará automáticamente el valor basándose en él. Los valores de los parámetros también pueden validarse.

Al crear un enlace, 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 se utilizan para mantener el estado entre diferentes peticiones. Su valor sigue siendo el mismo incluso después de hacer clic en un enlace. A diferencia de los datos de sesión, se pasan en la URL. Esto es completamente automático, por lo que no es necesario indicarlos explícitamente en link() o n:href.

¿Ejemplo de uso? Tiene una aplicación multilingüe. El idioma real es un parámetro que debe formar parte de la URL en todo momento. Pero sería increíblemente tedioso incluirlo en cada enlace. Así que lo conviertes en un parámetro persistente llamado lang y se guardará solo. Genial.

Crear un parámetro persistente es extremadamente fácil en Nette. Basta con crear una propiedad pública y etiquetarla con el atributo: (antes se utilizaba /** @persistent */ )

use Nette\Application\Attributes\Persistent; // esta línea es importante

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang; // debe ser público
}

Si $this->lang tiene un valor como 'en', entonces los enlaces creados usando link() o n:href también contendrán el parámetro lang=en. Y cuando se haga clic en el enlace, volverá a ser $this->lang = 'en'.

Para las propiedades, le recomendamos que incluya el tipo de datos (por ejemplo, string) y también puede incluir un valor por defecto. Los valores de los parámetros se pueden validar.

Los parámetros persistentes se pasan entre todas las acciones de un presentador determinado por defecto. Para pasarlos entre varios presentadores, es necesario definirlos:

  • en un ancestro común del que hereden los presentadores
  • en el rasgo que utilizan los presentadores:
trait LangAware
{
	#[Persistent]
	public string $lang;
}

class ProductPresenter extends Nette\Application\UI\Presenter
{
	use LangAware;
}

Puede cambiar el valor de un parámetro persistente al crear un enlace:

<a n:href="Product:show $id, lang: cs">detail in Czech</a>

O puede ser reset, es decir, eliminado de la URL. Entonces tomará su valor por defecto:

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

Componentes interactivos

Los presentadores incorporan un sistema de componentes. Los componentes son unidades separadas reutilizables que colocamos en los presentadores. Pueden ser formularios, cuadrículas de datos, menús, de hecho cualquier cosa que tenga sentido utilizar repetidamente.

¿Cómo se colocan y utilizan posteriormente los componentes en el presentador? Esto se explica en el capítulo Componentes. Incluso descubrirá qué tienen que ver con Hollywood.

¿Dónde puedo conseguir algunos componentes? En la página Componette puedes encontrar algunos componentes de código abierto y otros addons para Nette que están hechos y compartidos por la comunidad de Nette Framework.

Profundizando

Lo que hemos mostrado hasta ahora en este capítulo probablemente será suficiente. Las líneas siguientes están pensadas para quienes estén interesados en los presentadores en profundidad y quieran saberlo todo.

Validación de parámetros

Los valores de los parámetros de petición y de los parámetros persistentes recibidos de las URLs son escritos en propiedades por el método loadState(). También comprueba si el tipo de datos especificado en la propiedad coincide, de lo contrario responderá con un error 404 y la página no se mostrará.

Nunca confíes ciegamente en los parámetros, ya que pueden ser fácilmente sobrescritos por el usuario en la URL. Por ejemplo, así es como comprobamos si $this->lang está entre los idiomas soportados. Una buena forma de hacerlo es sobrescribir el método loadState() mencionado anteriormente:

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang;

	public function loadState(array $params): void
	{
		parent::loadState($params); // aquí se establece el $this->lang
		// sigue la comprobación del valor del usuario:
		if (!in_array($this->lang, ['en', 'cs'])) {
			$this->error();
		}
	}
}

Guardar y restaurar la petición

La solicitud que gestiona el presentador es un objeto Nette\Application\Request y la devuelve el método del presentador getRequest().

Puede guardar la solicitud actual en una sesión o restaurarla desde la sesión y dejar que el presentador la ejecute de nuevo. Esto es útil, por ejemplo, cuando un usuario rellena un formulario y su login caduca. Para no perder datos, antes de redirigir a la página de inicio de sesión, guardamos la solicitud actual en la sesión mediante $reqId = $this->storeRequest(), que devuelve un identificador en forma de cadena corta y lo pasa como parámetro al presentador de inicio de sesión.

Después de iniciar sesión, llamamos al método $this->restoreRequest($reqId), que recoge la solicitud de la sesión y se la reenvía. El método verifica que la petición ha sido creada por el mismo usuario que ahora ha iniciado la sesión. Si otro usuario inicia sesión o la clave no es válida, no hace nada y el programa continúa.

Consulte el libro de recetas Cómo volver a una página anterior.

Canonización

Los presentadores tienen una función realmente fantástica que mejora el SEO (optimización de la capacidad de búsqueda en Internet). Evitan automáticamente la existencia de contenido duplicado en distintas URL. Si varias URL llevan a un determinado destino, por ejemplo /index y /index?page=1, el framework designa una de ellas como la principal (canónica) y redirige a las demás hacia ella utilizando el código HTTP 301. Gracias a ello, los motores de búsqueda no indexan las páginas dos veces y no debilitan su page rank.

Este proceso se denomina canonización. La URL canónica es la URL generada por el enrutador, normalmente la primera ruta apropiada de la colección.

La canonización está activada por defecto y puede desactivarse a través de $this->autoCanonicalize = false.

La redirección no se produce con una solicitud AJAX o POST porque provocaría una pérdida de datos o no aportaría ningún valor añadido SEO.

También puede invocar la canonización manualmente mediante el método canonicalize(), que, al igual que el método link(), recibe el presentador, las acciones y los parámetros como argumentos. Crea un enlace y lo compara con la URL actual. Si es diferente, redirige al enlace generado.

public function actionShow(int $id, string $slug = null): void
{
	$realSlug = $this->facade->getSlugForId($id);
	// redirects if $slug is different from $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 presentador, se pueden definir otras funciones para que se llamen automáticamente. El presentador define los llamados eventos, y usted añade sus manejadores a las matrices $onStartup, $onRender y $onShutdown.

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

Los manejadores de la matriz $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 devuelta por el presentador es un objeto que implementa la interfaz Nette\Application\Response. Existen varias respuestas ya preparadas:

Las respuestas se envían por el método sendResponse():

use Nette\Application\Responses;

// Texto sin formato
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));

// Envío de un archivo
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));

// Envío de una devolución de llamada
$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));

Comprobación del método HTTP

Los presentadores en Nette verifican automáticamente el método HTTP de cada solicitud entrante. La razón principal de esta verificación es la seguridad. La comprobación del método se lleva a cabo en checkHttpMethod(), que determina si el método especificado en la petición está incluido en la matriz $presenter->allowedMethods. Por defecto, esta matriz contiene los elementos GET, POST, HEAD, PUT, DELETE, PATCH, lo que significa que estos métodos están permitidos.

Si desea permitir adicionalmente el método OPTIONS, puede conseguirlo de la siguiente manera:

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

Es crucial enfatizar que si permite el método OPTIONS, también debe manejarlo adecuadamente dentro de su presentador. Este método se utiliza a menudo como una solicitud de comprobación previa, que los navegadores envían automáticamente antes de la solicitud real cuando es necesario determinar si la solicitud está permitida desde el punto de vista de la política CORS (Cross-Origin Resource Sharing). Si permite este método pero no implementa una respuesta adecuada, puede provocar incoherencias y posibles problemas de seguridad.

Lecturas complementarias

versión: 4.0