Ir al contenido principal

Manejo de recursos HTML para tus páginas web con PHP

Déjame saber si te resulta familiar esta situación: páginas web que descargan el mismo recurso (sean estilos CSS o código Javascript) más de una vez o incluyen recursos remotos que tardan una eternidad en cada descarga. Yo lo he visto en más de una ocasión y no es difícil imaginar el porqué ocurre. Un desarrollador incluye el recurso de estilos que necesita su segmento de código y otro hace lo mismo, sin reparar (o sin que siquiera importe) que comparten el mismo recurso. En otro escenario muy común, acostumbran incluir muchos recursos remotos, con lo que el rendimiento de la página depende de lo rápido que responda dicho recurso. ¿Puede hacerse algo al respecto?

Claro que si.

Vamos a crear una clase en PHP que se encargue de administrar estos recursos y que nos facilite su despliegue en la página sin repeticiones. ¿Y respecto a la demora en la carga de recursos remotos? Atendamos una cosa por vez, porque como dicen por ahí: «Vísteme despacio, que tengo prisa».

Administrando los recursos CSS

Primero, debemos asegurarnos que la clase de soporte para HTML que vamos a implementar tenga memoria de cada recurso que sea administrado por ella durante cada consulta, es decir, durante el tiempo que el script o programa tome para construir la página web que el usuario espera. En este escenario, será necesario que sin importar en qué momento o desde qué bloque de código sea invocada por el programa, mantenga el historial de uso, por tanto necesitamos que sea una clase de tipo global y como vimos antes (véase Manejo de clases globales únicas en PHP) podemos realizarlo mediante una extensión de nuestra clase Singleton:

class HTMLSupport extends Singleton { ... }

Se adiciona también un helper para poder acceder al objeto global desde cualquier punto en nuestro código de forma rápida y directa:

function miframe_html() : HTMLSupport {
    return HTMLSupport::getInstance();
}

$html = miframe_html();

Entrando ya en materia, para prevenir que se dupliquen recursos, debemos llevar un listado con los recursos pendientes de publicar ($resources) y también de los ya publicados ($published). A continuación definiremos los métodos públicos que queremos o necesitaremos al usar este objeto. ¿Y qué hay de los métodos privados?, se preguntarán. Bueno, estos últimos usualmente se crean en la medida en que se vea su necesidad al momento ya de codificar los métodos públicos, lo importante inicialmente es determinar aquellos métodos que habremos de usar para lograr nuestro objetivo.

Se sugieren entonces los siguientes métodos:

// Adiciona un archivo CSS existente en disco. Intentará referirlo de forma
// remota, si no es posible, publicará su contenido en línea.
$html->cssLocal($filename);

// Adiciona en línea el contenido de un archivo CSS existente en disco.
$html->cssLocal($filename, true);

// Adiciona un recurso CSS indicando su URL, será referido siempre
// apuntando a su ubicación remota.
$html->cssRemote($url);

// Adiciona un recurso CSS directamente en línea.
$html->cssInLine($styles);

Una vez adicionados a la lógica del programa los recursos que habremos de usar, será necesario publicarlos, esto es, incluirlos en nuestra página web para que sean procesados por el navegador. Esto se hará mediante el siguiente método:

// Retorna estilos previamente adicionados.
echo $html->cssExport(); 

Si lo hacemos bien, no importará si se indica al programa el cargue del mismo recurso varias veces, incluso después de publicados.

Finalmente, se recomienda incluir un método que permita remover el listado de recursos pendientes por publicar (después de publicados no hay cómo removerlos de la página), para aquellos casos en los que por alguna razón, dentro de la lógica del programa, puedan ya no ser necesarios.

$html->cssClear();

De esta forma, la clase sugerida para esta implementación es la siguiente:

class HTMLSupport extends Singleton {

    /**
     * @var array $resources    Listado de recursos CSS no publicados.
     */
    private array $resources = [];

    /**
     * @var array $published    Listado de recursos CSS ya publicados.
     */
    private array $published = [];

    /**
     * @var string $last_key    Última llave adicionada a recursos.
     */
    private string $last_key = '';

    /**
     * Inicialización de la clase Singleton.
     */
    protected function singletonStart() {
        $this->cssClear();
    }

    /**
     * Adiciona un archivo CSS existente en disco.
     *
     * @param string $filename Archivo CSS.
     * @param bool $inline     TRUE para publicar contenido, FALSE para indicar URL al
     *                         archivo local (solo si es posible, de lo contrario publica
     *                         el contenido directamente).
     * @return bool            TRUE si pudo adicionar el recurso, FALSE si hubo error.
     */
    public function cssLocal(string $filename, bool $inline = false) : bool {
        $filename = trim($filename);
        if ($filename !== '' && is_file($filename)) {
            // Se asegura siempre de registrar correctamente el path fisico
            $filename = realpath($filename);
            $src = 'locals';
            if (!$inline) {
                $server = miframe_server();
                // En este caso, debe intentar incluirlo como remoto
                $path = $server->removeDocumentRoot($filename);
                if ($path !== false) {
                    // Pudo obtener la URL
                    // Adiciona a listado de publicados el path real
                    $key = $this->keyPath($filename, $src);
                    $this->published['css'][$key] = true;
                    // Asegura formato URL remoto
                    $filename = '/' . $server->purgeURLPath($path);
                    // Modifica el calificador
                    $src = 'remote';
                }
            }
            $this->addResourceCSS($filename, $src);
        }
        elseif ($filename !== '') {
            // No pudo acceder al archivo
            $info = "Recurso CSS \"{$filename}\" no es un archivo valido";
            // Genera mensaje de error
            trigger_error($info, E_USER_WARNING);
            return false;
        }

        return true;
    }

    /**
     * Adiciona un recurso CSS indicando su URL, se publica
     * apuntando a su ubicación remota.
     *
     * @param string $path URL al archivo de CSS remoto.
     */
     public function cssRemote(string $path) {
        $path = trim($path);
        if ($path !== '') {
            $this->addResourceCSS($path, 'remote');
        }
    }

    /**
     * Adiciona un recurso CSS directamente en línea
     *
     * @param string $styles Estilos CSS.
     */
    public function cssInLine(string $styles) {
        $styles = $this->cleanCode($styles);
        if ($styles !== '') {
            // Se asegura siempre de registrar correctamente el path fisico
            $this->addResourceCSS($styles, 'inline');
        }
    }

    /**
     * Adiciona recurso CSS
     *
     * @param string $filename Ubicación del archivo de recurso.
     * @param string $dest     Uso del recurso (local, enlínea, etc.)
     */
    private function addResourceCSS(string $filename, string $dest) {
        $this->addResource($filename, 'css', $dest);
    }

    /**
     * Adiciona recurso
     *
     * @param string $filename Ubicación del archivo de recurso.
     * @param string $type     Tipo de recurso (css, script, etc.)
     * @param string $dest     Uso del recurso (local, enlínea, etc.)
     */
    private function addResource(string $filename, string $type, string $dest) {
        $key = $this->keyPath($filename, $dest);
        if (!isset($this->published[$type]) ||
            !array_key_exists($key, $this->published[$type])
            ) {
            $this->resources[$type][$key] = $filename;
        }
    }

    /**
     * Genera identificador asociado a un recurso.
     *
     * @param string $filename Ubicación del archivo de recurso.
     * @param string $prefix   Prefijo asociado al recurso.
     * @return string          Llave asociada al recurso.
     */
    private function keyPath(string $filename, string $prefix) : string {
        $this->last_key = $prefix . ':' . md5(strtolower($filename));
        return $this->last_key;
    }

    /**
     * Último identificador adicionado al listado de recursos.
     *
     * @return string Llave.
     */
    public function lastKey() {
        return $this->last_key;
    }

    /**
     * Genera código con los estilos CSS no publicados, para su uso en páginas web.
     *
     * @param  bool $inline TRUE retorna los estilos, FALSE genera tag link al archivo CSS indicado.
     * @return string       HTML con estilos a usar.
     */
    public function cssExport() : string {
        return $this->cssMake($this->resources['css']);
    }

    /**
     * Estilos CSS para usar en páginas HTML.
     *
     * Una vez procesados, los recursos se remueven del listado $data y
     * se adicionan al listado de recursos ya publicados ($this->published).
     *
     * @param  array $data Arreglo con listado de recursos.
     * @return string      HTML con estilos a usar.
     */
    private function cssMake(array &$data) {

        // Codigo remoto se almacena aqui
        $text = '';

        // Estilos en linea se almacenan aqui
        $code = '';

        foreach ($data as $key => $filename) {
            $src = substr($key, 0, 6);
            $local_inline = false;

            switch ($src) {

                case 'locals': // Local, en linea
                    $code .= '/* ' . $src . ':' . basename($filename) . ' */' . PHP_EOL .
                        $this->cleanCode(file_get_contents($filename)) .
                        PHP_EOL;
                    break;

                case 'remote': // Remoto siempre
                    $text .= '<link rel="stylesheet" href="' . $filename . '" />' . PHP_EOL;
                    break;

                case 'inline': // En línea siempre
                    $code .= '/* ' . $key . ' */' . PHP_EOL .
                            $filename .
                            PHP_EOL;
                    break;

                default:
            }

            // Adiciona a listado de publicados
            $this->published['css'][$key] = true;
            // Remueve de listado de pendientes
            unset($this->resources['css'][$key]);
        }

        if ($code !== '') {
            $code = '<style>' . PHP_EOL . $code . '</style>' . PHP_EOL;
        }

        return $text . $code;
    }

    /**
     * Limpia código, remueve comentarios y líneas en blanco.
     *
     * @param string $content Código a depurar.
     * @return string         Código depurado.
     */
    private function cleanCode(string $content) {
        // Remueve comentarios /* ... */
        // Sugerido en https://stackoverflow.com/a/643136
        $content = preg_replace('!/\*.*?\*/!s', '', $content);
        // Remueve lineas en blanco
        $content = preg_replace('/\n\s*\n/', "\n", $content);
        // Remueve lineas en general para exportar una unica linea
        $content = str_replace(["\n", "\r", "\t"], ['', '', ' '], $content);
        // Adiciona espacios antes y después de los parentesis
        $content = str_replace([ '{', '}' ], [ ' { ', ' } ' ], $content);
        // Remueve espacios dobles
        while (strpos($content, '  ') !== false) {
            $content = str_replace('  ', ' ', $content);
        }

        return trim($content);
    }

    /**
     * Limpia el listado de recursos pendientes por publicar.
     */
    public function cssClear() {
        $this->resources['css'] = [];
    }
}

El código en detalle pueden observarlo y/o descargarlo en github.com. Una demo funcional de esta librería está disponible en lekosdev.com.

Unas palabras finales...

Esta clase puede expandirse para incluir otros elementos de control, como por ejemplo:

  • Agrupar los recursos locales en un único archivo de estilos para reducir la cantidad de referencias desde las páginas web y agilizar su carga.
  • Descargar los recursos remotos para mantenerlos en caché y reducir los tiempos de carga de la página (que pueden ser notorios según el origen del recurso, tal como se menciona al inicio de este artículo).
  • Realizar este mismo control para código Javascript.
  • Gestionar la apertura y cierre de páginas web, algo que puede resultar repetitivo y frustrante a la larga, especialmente cuando debes incluir iconos, cabeceras para SEO, etc.

Quién sabe, quizás en futuros artículos estemos revisando y actualizando esta clase, mantente en sintonía.

¿Te ha resultado útil este artículo? Por favor déjamelo saber en los comentarios, junto con tus sugerencias de otros temas para artículos futuros.

Por lo pronto, hasta una próxima oportunidad y feliz codificación.

Imagen de portada por Chunli Chen en Pixabay

Comentarios

Entradas populares de este blog

Manejo de clases globales únicas en PHP

¿Cómo acceder desde cualquier script en tu proyecto a Clases y/o funciones de uso común? Este puede ser una de las primeras directrices a establecer para cualquier proyecto porque siempre, siempre , sea en  PHP  u otro lenguaje, será necesario usar recursos comunes. En PHP existen diferentes alternativas para su manejo, ya sea por medio de variables globales o de clases/objetos estáticos. A continuación consideraremos una propuesta para este manejo. Creación de recursos globales Para ilustrar esta solución, partimos de la necesidad de implementar una librería para manejo de servicios relacionados con el servidor Web, que de forma amigable nos permita disponer de información como: Valores almacenados de la variable superglobal $_SERVER de PHP. Valores asociados a la consulta realizada por el usuario, por Ej. la dirección IP del usuario o la URL ingresada. Valores asociados al servidor web usado, por Ej. la dirección IP del servidor o la ubicación del script que ej...

¿Qué tan bueno es realmente el “foreach” en PHP?

Como toda buena historia, esta comienza hace algún tiempo. El que fuera mi jefe por allá en la primera década del 2000, realmente odiaba (y mucho) el uso del foreach en el código PHP . Prefería que usáramos alguna alternativa diferente, alguna combinación del  for o del while . ¿Por qué? Ve tú a saber, nunca fue abierto respecto a las razones de su aprensión hacia ese constructor propio del lenguaje. Pero antes de continuar, veamos qué es y para qué nos puede servir. Arreglos, tenían que ser arreglos ¿Qué es foreach ? De acuerdo al manual de PHP , su definición es la siguiente: El constructor foreach proporciona un modo sencillo de iterar sobre arrays . foreach funciona sólo sobre arrays y objetos , y emitirá un error al intentar usarlo con una variable de un tipo diferente de datos o una variable no inicializada. Para su uso correcto existen dos sintaxis validas, a saber: foreach (expresión_array as $value) { ... } foreach (expresión_array as $key => $value) { ....