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:
redirect()
,redirectPermanent()
,redirectUrl()
yforward()
redireccionaerror()
sale del presentador debido a un errorsendJson($data)
sale del presentador y envía los datos en formato JSONsendTemplate()
abandona el presentador y renderiza inmediatamente la plantillasendResponse($response)
abandona el presentador y envía su propia respuestaterminate()
abandona el presentador sin respuesta
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.
Creación de enlaces
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 LanguageAware
{
#[Persistent]
public string $lang;
}
class ProductPresenter extends Nette\Application\UI\Presenter
{
use LanguageAware;
}
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:
- Nette\Application\Responses\CallbackResponse – envía una devolución de llamada
- Nette\Application\Responses\FileResponse – envía el archivo
- Nette\Application\Responses\ForwardResponse – envía ()
- Nette\Application\Responses\JsonResponse – envía JSON
- Nette\Application\Responses\RedirectResponse – redirige
- Nette\Application\Responses\TextResponse – envía texto
- Nette\Application\Responses\VoidResponse – respuesta en blanco
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));
Restricción de acceso mediante #[Requires]
El atributo #[Requires]
ofrece opciones avanzadas para restringir el acceso a los presentadores y sus métodos.
Puede utilizarse para especificar métodos HTTP, requerir solicitudes AJAX, limitar el acceso al mismo origen y restringir el
acceso sólo al reenvío. El atributo puede aplicarse a clases de presentadores, así como a métodos individuales como
action<Action>()
, render<View>()
, handle<Signal>()
y
createComponent<Name>()
.
Puede especificar estas restricciones
- en los métodos HTTP:
#[Requires(methods: ['GET', 'POST'])]
- que requieren una petición AJAX:
#[Requires(ajax: true)]
- acceso sólo desde el mismo origen:
#[Requires(sameOrigin: true)]
- acceso sólo mediante reenvío:
#[Requires(forward: true)]
- restricciones sobre acciones específicas:
#[Requires(actions: 'default')]
Para obtener más información, consulte Cómo utilizar el atributo Requires atributo.
Comprobación del método HTTP
En Nette, los presentadores verifican automáticamente el método HTTP de cada solicitud entrante, principalmente por razones
de seguridad. Por defecto, se permiten los métodos GET
, POST
, HEAD
, PUT
,
DELETE
, PATCH
.
Si desea habilitar métodos adicionales como OPTIONS
, puede utilizar el atributo #[Requires]
(a
partir de 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á incluido en el array $presenter->allowedMethods
. Añade un método como este
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.