¿Qué es lo que toda persona quiere al programar? Bueno, aparte de una compensación económica y la satisfacción del trabajo bien hecho, yo diría que ver el resultado de ese trabajo en pantalla, ¿verdad? Así que vamos precisamente a eso. Y no, no se trata de realizar un simple echo, vamos a ponerle un poco más de empeño para llegar a un modelo que sea funcional, práctico y sencillo (o algo medianamente sencillo), alineado con algunas de las recomendaciones que usualmente se hacen cuando se aborda el tema de “construir” las salidas a pantalla.
Si bien es cierto que PHP nos permite enviar texto a pantalla en cualquier momento, puede ser valido hacerlo en la medida que nuestro script progresa (en especial cuando requieren unos pocos archivos para su ejecución), pero para aplicaciones más grandes, como aquellas que requieren registro de usuarios y consultas a bases de datos, se recomienda que todo el proceso de dar forma a lo que se va a mostrar en pantalla se realice en scripts separados de aquellos encargados de la lógica de procesamiento pesado, es decir, de la lógica de negocio, de la que tiene acceso directo a los datos. Esto tiene entre otras ventajas:
- Facilitar el mantenimiento y especialización, tanto de scripts como de los grupos de desarrolladores que les dan soporte, diferenciando entre back-end (procesamiento de datos) y front-end (salida a pantalla e interacción con el usuario).
- Permitir que pueda cambiarse el look de las páginas sin tener que modificar los scripts de procesamiento de datos.
A esta separación o especialización de tareas es a lo que en diseño de Software se refiere como vistas en los desarrollos tipo MVC (Modelo-Vista-Controlador).
Procedamos entonces a desarrollar una clase que nos permita implementar este entorno de vistas para nuestros desarrollos en PHP.
Nuestra primera vista
Para comenzar, partimos del supuesto que tenemos un proceso que al final entrega una serie de datos que necesitamos mostrar en pantalla. Es importante que en este punto, los datos estén lo más “masticados” (procesados) posible, de forma que el script que dará salida a pantalla no tenga que realizar cálculos o procesos adicionales, diferentes al de dar formato a esos datos. Por ejemplo, decidir si mostrar un dato tipo fecha como “2024-12-24” o “Dic. 24 / 2024” es una de esas cosas que debería decidirse en la vista.
function processA() { $dato1 = 'Hola mundo'; $dato2 = 1234; // Para mostrar en pantalla echo miframe_view('process-a', compact('dato1', 'dato2')); }
En este ejemplo la función miframe_view() será la encargada de generar el texto a enviar a pantalla. Ahora, probablemente te preguntarás: ¿necesitamos el “echo”, no podría quedar directamente incluido en la función? La respuesta es “si podría”, pero al dejarlo separado abonamos el terreno para permitir el uso de esta misma función para la creación de correos, documentos u otro tipo de aplicaciones que requieran generar una salida formateada HTML.
Esta función “miframe_view” ha de proveer un atajo para el manejo de la clase global que en últimas será la encargada de hacer la magia de capturar el texto renderizado. Siguiendo los lineamientos establecidos previamente para el manejo de clases globales, la definiremos como una extensión de la clase Singleton:
Si requieres más información sobre el tipo de manejo dado aquí a las clases globales y el uso de patrones Singleton, te invito a consultar el artículo Manejo de clases globales únicas en PHP.
class RenderView extends Singleton { public function view(string $viewname, array $params = []): string { ... } }
Para facilitar la consulta de esta clase, se proveerá el helper miframe_render() que dará acceso a dicha clase global:
function miframe_render(): RenderView { return RenderView::getInstance(); }
De esta forma, la implementación de “miframe_view” sería algo como esto:
function miframe_view(string $viewname, array $params = []): string { return miframe_render()->view($viewname, $params); }
¿Y cómo sería el script que construirá la salida a pantalla o lo que es lo mismo (para el caso de consultas web por lo menos), que construirá la página HTML que necesitamos para mostrar los datos procesados? Siguiendo con el ejemplo dado, nuestra vista deberá corresponder a un script PHP con nombre process-a.php, cuyo contenido puede ser uno como el siguiente:
<div> <p>Texto: <span><?= trim($dato1) ?></span></p> <p>Número: <span><?= number_format($dato2) ?></span></p> </div>
Si se preguntan porqué no se incluyen los tags “<html>” y “<body>”, como debe corresponder a una página HTML debidamente formateada. Se debe a que dichos tags, seguramente serán comunes a muchas otras páginas a usar en la solución global (con la inclusión de recursos de estilos CSS y Javascript que puedan necesitarse para dar soporte a la interfaz gráfica deseada) y por tanto vamos a incorporarlos en otra vista, una que automáticamente incorporará el texto producido en la vista “process-a” y que llamaremos “layout”.
Manejo e implementación del Layout
Este es un ejemplo sencillo de lo que la vista de layout va a contener:
<html> <head> <title><?= $title ?></title> </head> <body> <h1><?= $title ?></h1> <?= miframe_render()->contentView() ?> </body> </html>
En este caso, deberá declararse la variable $title e implementar el método contentView(), que retornará el texto renderizado (generado) por las vistas previamente ejecutadas.
Debido a su característica de “vista padre” y para efectos de garantizar el comportamiento deseado, tendremos que configurar el layout antes de invocar miframe_view() por primera vez. En primer lugar, es necesario indicar a la clase RenderView dónde encontrar los archivos de layout y demás vistas, para evitar el dar paths completos en cada llamado a miframe_view() y facilitar la legibilidad del código. Para esto usamos este método:
miframe_render()->location('path/to/dir');
A continuación, indicamos ahora si el nombre de la vista a usar como layout y declaramos las variables que habrá de usar, lo que para nuestro ejemplo corresponde a la variable $title:
miframe_render()->layout('layout-base') ->globals(['title' => 'Ejemplo de manejo de vistas']);
A diferencia de cuando invocamos la vista, el nombre del layout se ingresa previamente como un elemento de configuración ya que deberá ser aplicado de forma “automática” al generar la vista. ¿Y cómo se asignan los valores de las variables usadas en el layout? Como vemos en el ejemplo, se realiza por medio de un arreglo pasado como argumento del método globals().
Nótese también que tanto al invocar las vistas como al indicar el layout a usar, no se hace necesario indicar la extensión “.php” del archivo script. El sistema deberá asumir que lo es y adicionar la extensión si hace falta, aunque si se usa una extensión diferente o se requiere indicarla por alguna razón, esta podrá indicarse o podrá incluirse también el path del script, sin que ello conduzca a un fallo en el sistema. Por ejemplo:
echo miframe_view('/path/alterno/a/location/process-a.vista', compact('dato1', 'dato2'));
Como curiosidad, la implementación de las propiedades usadas para gestionar el layout se realiza de la siguiente manera:
$this->layout = new class { // Archivo public string $filename = ''; // Valores a usar en el Layout public array $params = []; // Contenido de vistas previas public string $contentView = ''; // TRUE para indicar que el Layout está en ejecución public bool $isRunning = false; };
Una primer versión usaba un arreglo de datos para este fin, pero dado que PHP soporta el uso de clases anónimas (desde la versión 7), es mejor aprovechar esta capacidad para efectos principalmente de legibilidad del código, ya que toda vez es más práctico (y minimiza errores al escribir) invocar o asignar un valor usando la sintaxis $this->layout->filename que su equivalente en arreglo, que sería $this->layout['filename'].
¿Cómo se captura el texto generado por la vista?
Vimos en el ejemplo inicial que la vista debe retornar el texto HTML, pero no es necesario que los scripts usados para las vistas retornen una cadena de texto como tal, simplemente basta con que se ejecuten de la forma tradicional, enviando el texto directo a pantalla (o al navegador en este caso) y dejar que la clase RenderView se encargue de capturar ese texto. Veamos cómo es que la clase puede realizar esta captura.
Tradicionalmente, cuando se quiere incluir un script en otro, basta con hacer algo como:
include “layout-base.php”;
Sin embargo, cuando se incluye un script dentro de un método, el código de dicho script queda dentro del contexto del método y puede por tanto acceder a las propiedades y métodos de la clase por medio del operador $this. Esto podría ser de utilidad pero también puede causar que por accidente se modifique el comportamiento esperado de la clase. Para prevenir que esto suceda, en lugar de simplemente usar include, se recurre a una función static para que lo aísle del entorno en que se encuentra, haciendo que $this quede fuera de su alcance.
Es así como queda entonces la función que realiza la inclusión de las vistas:
$fun = static function (string $view_filename, array &$view_args) { extract($view_args, EXTR_SKIP | EXTR_REFS); include $view_filename; };
Nótese adicionalmente, que el argumento $view_args (que contiene el arreglo con los valores a usar) se exporta al entorno de la función para facilitar su uso en la vista. Por tanto y para prevenir colisiones (en cuyo caso el valor contenido en el arreglo será ignorado por cuenta del calificador EXTR_SKIP de la función PHP extract()), se sugiere no usar dichos nombres en los valores asignados a la vista o layout. También se aplica el calificador EXTR_REFS para que los valores exportados sean referencias a los valores en el arreglo $arg_views y evitar duplicar valores en memoria.
Es finalmente en este contexto que le indicamos a la clase capturar el texto que habrá de generarse en la vista, usando las funciones PHP para control de salida ob_start() y ob_get_clean():
protected function evalTemplate(string $filename, array $params): string { $fun = static function (string $view_filename, array &$view_args) { extract($view_args, EXTR_SKIP | EXTR_REFS); include $view_filename; }; // Bloquea salida a pantalla ob_start(); // Ejecuta $fun($filename, $params); // Recupera contenido $content = ob_get_clean(); return $content; }
Para más información sobre el manejo de los controles de salida en PHP, puedes consultar la documentación de estas funciones en el manual de PHP.
Control de las vistas
La gestión interna de las vistas es algo que también debemos considerar con cuidado, especialmente cuando consideramos que:
- Una vista puede ejecutar otras vistas, por ejemplo, para el renderizado de componentes específicos o cajas con texto informativo.
- Si en una vista usada para mostrar errores se presentara un error y se llamara a si misma, podría repetir este comportamiento una y otra vez, hasta el infinito y más allá (menos mal que X-Debug intercepta estos bucles y previene que se bloquee la página, prueba usarlo en tus desarrollos).
Para el control de las vistas se usará un arreglo que llevará una llave basada en el nombre de cada vista, de forma que se evalué su existencia antes de asignar la nueva secuencia de vista y si existe, cancelar el proceso. Se deberá preservar también el identificador de la vista desde donde se ejecuta esta nueva, es decir, su vista “padre”. De esta forma podemos llevar un control mínimo de la secuencia en que se ejecutan o invocan las vistas.
protected function newTemplate(string $viewname, bool $only_validate = false): bool { $reference = md5($viewname); if (isset($this->views[$reference])) { return false; } if (!$only_validate) { // No solo valida, debe crear la referencia $this->views[$reference] = ['name' => $viewname, 'parent' => $this->currentView]; // Actualiza identificador de vista actual $this->currentView = $reference; } return true; } private function removeTemplate() { $reference = $this->currentView; if (isset($this->views[$reference])) { // Restablece la vista anterior (o false si no existe) $this->currentView = $this->views[$reference]['parent']; unset($this->views[$reference]); } }
Y si agrupamos todo, el método de captura que obtenemos es:
public function view(string $viewname, array $params): string { $content = ''; if ($this->newTemplate($viewname)) { // Valida nombre de la vista y recupera nombre de archivo asociado $filename = $this->checkFile($viewname); // Ejecuta vista $content = $this->evalTemplate($filename, $params); // Restablece vista previa $this->removeTemplate(); // Valida si se incluye layout en esta vista $this->includeLayout($content); } return $content; }
El método includeLayout se encarga de incluir el contenido en el layout cuando la vista ejecutada sea la última en fila. Como es posible que el layout pueda a su vez realizar la inclusión de nuevas vistas, se debe prevenir que se invoque nuevamente a si mismo, lo que se consigue mediante el uso de una propiedad de control (en este caso, $this->layout->isRunning):
protected function includeLayout(string &$content): bool { // Ejecuta layout (si alguno) si no hay vistas pendientes. $result = ( !$this->layout->isRunning && $this->currentView == '' ); if ($result && !empty($this->layout->filename)) { // Protege la ejecución del Layout $this->layout->isRunning = true; // Preserva el contenido previamente renderizado $this->layout->contentView = $content; // Ejecuta vista $content = $this->evalTemplate( $this->layout->filename, $this->layout->params ); // Libera memoria $this->layout->contentView = ''; // Habilita de nuevo la ejecución del Layout $this->layout->isRunning = false; } return $result; }
Aunque no es lo ideal, puede ocurrir que el layout se ejecute varias veces desde la aplicación, lo que no se considera como un error, porque puede que efectivamente eso sea lo que se quiere. En todo caso, su debido uso y cuidado queda a criterio del desarrollador.
Ya tenemos las partes ¿qué hay de la implementación completa?
Como dijo una mente curiosa, “el diablo está en los detalles”. Todas las consideraciones aquí planteadas y algunas más, pueden verse en la implementación detallada de la clase RenderView que está disponible en este repositorio de GitHub y que puedes consultar libremente.
También puedes encontrar una demo funcional de estas librerías en:
En la demo encontrarás una versión extendida donde se incluyen funcionalidades adicionales como:
- Adición de filtros para depuración del texto renderizado antes de mostrarlo en pantalla, que puede usarse por ejemplo para remover información sensible.
- Facilitar el mantenimiento y la depuración de código mediante una interfaz para volcado de datos y permitir el manejo de vista en modo Desarrollo.
- Ayudas visuales para identificar cada vista en pantalla en modo Desarrollo.
- Método alternativo para permitir texto alternativo al invocar vistas que ya están en ejecución.
¿Qué te ha parecido el desarrollo de este recurso?¿Crees que pueda servirte para tus propias aplicaciones? Quedo atento a tus comentarios y sugerencias en la sección de Comentarios.
Hasta la próxima.
Imagen de portada por Denise Henze en Pixabay
Comentarios
Publicar un comentario