Guía 97 – Utilizar WEB Components en PHPRunner

En artículos anteriores hemos tratado:

En este artículo se explicará cómo utilizar los WEB Components en desarrollos PHPRunner.

Objetivo

  • Cómo podemos  utilizar esto WEB Components en desarrollos en PHPRunner.
  • Cómo construir el Backend SLIM4 – PHP, para comunicarnos con las variables de sesión del desarrollo PHPRunner y así poder utilizar la identificación del usuario para los 2 procesos de PHP (PHPRunner y SLIM4).

DEMO: https://fhumanes.com/pc_001

(1) es la integración de una App de Svelte 5 en PHPRuynner
(2) es la integración de un WEB Components hecho en Svelte 5 en PHPRunner.
(3) es la consola del navegador y las trazas que ven dejando los ejemplos para que se vean su funcionamiento.

Solución Técnica

En el artículo S-023 he explicado a través de los ficheros «index.html» cómo se debe integrar estos componentes en páginas HTML, como es el caso de PHPRunner.

En este artículo, voy a explicar cómo he construido el ejemplo, para que podáis apreciar lo sencillo que es.

Voy a repetir un conjunto de WEB donde vais a poder obtener Web Components gratuitos.

Repositorios de Web Components gratuitos

  • WebComponents.org — El mayor catálogo público de Web Components. Reúne componentes de Google, Vaadin, Shoelace, FAST y cientos de desarrolladores independientes.
  • Shoelace — Biblioteca moderna de componentes UI (botones, modales, tabs, formularios). 100% Web Components, sin framework y altamente personalizable.
  • Vaadin Web Components — Componentes empresariales de alta calidad: grids, formularios, layouts, pickers. Muy usados en aplicaciones corporativas.
  • FAST Components — Componentes de Microsoft, rápidos, accesibles y con diseño adaptable. Ideales para apps modernas.
  • Elix — Componentes avanzados de UI (carousels, date pickers, menús complejos) con un enfoque en accesibilidad y rendimiento.
  • PatternFly Elements — Componentes creados por Red Hat para interfaces profesionales y dashboards.
  • Lion Web Components — Componentes minimalistas y accesibles creados por ING, perfectos para formularios y validación.

En estos ejemplos no será habitual que requieran de desarrollo back-End (acceso a datos persistentes en bases de datos), pero en los ejemplos que he hecho, si es así, porque creo que el compartir las variables de sesión entre la aplicación PHPRunner y estos componentes, dan muchas más posibilidades y seguridad en los procesos.

Explicación del funcionamiento en PHPRunner

Los Web Components son en realidad JavaScript y también HTML y CSS. Así pues como JavaScript su funcionamiento se establece en 2 fases:

  • La definición de la librería de JavaScript que hay que cargar y la sustanciación del código. Esto se realiza con un Snippet.
  • La iteración de estos componentes con el resto del programa, tanto en la recepción de datos, como en el envío de los mismos evento JavaScript OnLoad Event.
Snippet1 -app de Svelte 5Snippet2 - Web Comonents Svelte 5JavaScript OnLoad Event - app de SvelteJavaScript OnLoad event - Web Components
$html = <<<EOT

<link rel="stylesheet" crossorigin href="svelte-plugins/plugin_pc_001/index-DOEafSBl.css">
<div id="plugin_pc_001"      
  data-usuario="$time"
  data-color="blue">
</div>

<script type="module" crossorigin src="./svelte-plugins/plugin_pc_001/index-F3UxNtCp.js"></script>

EOT;
echo $html;
$html = <<<EOT
<!-- Si Vite genera CSS separado, lo enlazas aquí -->
<link rel="stylesheet" crossorigin href="svelte-plugins/plugin_pc_002/index-PYdKYWpm.css">

<!-- Múltiples instancias del mismo plugin -->
<pc-plugin id="n1" usuario="Fernando" color="red"></pc-plugin>
<pc-plugin id="n2" usuario="Ana" color="blue"></pc-plugin>
<pc-plugin id='n3' usuario="Luis" color="green"></pc-plugin>

<!-- Carga del bundle Svelte (application mode) -->
<script type="module" crossorigin src="svelte-plugins/plugin_pc_002/index-B1lkW1v-.js"></script>

EOT;

echo $html;
// Interface para los WEB Component Plugin_pc_001
window.addEventListener("svelteChange", (e) => {
   console.log("Valor recibido desde Svelte:", e.detail);
});

// Dialogar con la APP de Svelte 5
setTimeout(() => {
   window.plugin_pc_001.setUsuario("PHPRunner");
}, 4000);
setTimeout(() => {
   window.plugin_pc_001.reset();
}, 8000);
// Interface para los WEB Component Plugin_pc_002
document.getElementById("n1").addEventListener("contadorChange", (e) => {
    console.log("Actualización N1:",  e.detail);
});

document.getElementById("n2").addEventListener("contadorChange", (e) => {
    console.log("Actualización N2:",  e.detail);
});

document.getElementById("n3").addEventListener("contadorChange", (e) => {
    console.log("Actualización N3:",  e.detail);
});

// Dialogar con los WEB Components
setTimeout(() => {
     const n1 = document.getElementById("n1");
     const n2 = document.getElementById("n2"); 
     const n3 = document.getElementById("n3"); 
     n1.setUsuario("PHPRunner_1");
     n2.setUsuario("PHPRunner_2");
     n3.setUsuario("PHPRunner_3");
  }, 5000);

Tenéis que observar que se define un «id» en la instanciación del objeto y después vamos a utilizar ese «id», para los eventos o funciones que ejecutemos.

En el JavaScript incluyo un «setTimeout», para retrasar la ejecución de las acciones, para que se pueda ver cambios en el interfaz del ejemplo.

Tengo intención de analizar algún Web Components de esas librerías que he puesto, para hacer un ejemplo de su utilización en PHPRunner y si es posible utilizar el código de los plugins de PHPRunner con estos Web Components. Teóricamente es posible, pero como no existe ninguna documentación técnica al respecto, hay que realizar pruebas.

Aplicación Back-End en SLIM4 para compartir la sesión con un desarrollo PHPRunner.

Lo primero que tenemos que hacer es leer o definir, cual es el código de sesión de nuestra aplicación

Una vez que tenemos ese dato, ya podemos escribir el código ( en el blog hay múltiples ejemplos de este tipo de desarrollo, por eso no lo explico con detalle aquí):

v1/index.phpv1/.htaccessinclude/Config.phpinclude/ExternalSessionMiddleware.phpinclude/DBFunctions.phpInclude/DB_session.phpinclude/Functions.php
<?php
/**
 *
 * @About:      API Interface
 * @File:       index.php
 * @Date:       $Date:$ Sep 2025
 * @Version:    $Rev:$ 1.0
 * @Developer:  Federico Guzman || Modificado por Fernando Humanes para PHP 8.3
 **/



include_once '../include/Config.php';       // Configuration Rest Api
include_once '../include/Function.php';     // Funciones generales
include_once '../include/ExternalSessionMiddleware.php';

// require_once("../../include/dbcommon.php"); // DataBase PHPRunner

// Debug
$debugCode = false; // On | Off, de depuración y volcado en el fichero "error.log"
// custom_error(1,"URL ejecutada: ".$_SERVER["REQUEST_URI"]);          // To debug
// custom_error(2,"Método de petición: ".$_SERVER['REQUEST_METHOD']);  // to Debug
// $body = $body = file_get_contents('php://input');// 
// custom_error(3,"Body: ".$body);                                    // to Debug
//  custom_error(4,"Campos POST: ".print_r($_POST,true));               // to Debug
$debugCode = false;

// use App\Models\Db;  // Utilizamos la conexión de PHPRunner
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;

use Slim\Middleware\ErrorMiddleware;
use Slim\Exception\HttpNotFoundException;
// use DI\Container;
use Slim\Routing\RouteCollectorProxy;
use Slim\Middleware\BodyParsingMiddleware;


require_once __DIR__ . '/../libs/autoload.php';   // Library SLIM v4

$app = AppFactory::create();

// Middleware de CORS
$app->add(function ($request, $handler) {
    $response = $handler->handle($request);
    return $response
            ->withHeader('Access-Control-Allow-Origin', '*')
            ->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, authorization, Authorization, token-user')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
            ->withHeader('Content-Type', 'application/json');
});

// Gestión de OPTIONS
$app->options('/{routes:.+}', function ($request, $response, $args) {
    return $response;
});

// Activar middleware de sesión externa
$app->add(new ExternalSessionMiddleware());

$app->addRoutingMiddleware();
// $app->add(new BasePathMiddleware($app)); // No usar si se ejecuta en subdirectorio
$app->addErrorMiddleware(true, true, true);
$app->addBodyParsingMiddleware();


$app->setBasePath(SCRIPTS_DIR);             // Indica el directorio desde donde está trabajando

require_once '../include/DbFunctions.php';
$db = new DbFunctions();



$app->get('/session', function (Request $request, Response $response, $args) use ($db) {         // sessión 
         return $db->createSession($request, $response, $args);
    });
    
$app->get('/updateSession', function (Request $request, Response $response, $args) use ($db) {         // sessión 
         return $db->updateSession($request, $response, $args);
    });
    
$app->get('/resetSession', function (Request $request, Response $response, $args) use ($db) {         // sessión 
         return $db->resetSession($request, $response, $args);
    });

$app->run();
RewriteEngine On 
RewriteCond %{REQUEST_FILENAME} !-f 
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ %{ENV:BASE}index.php [QSA,L]
<?php

// URL de ejecución de la aplicación
define('SCRIPTS_DIR', '/pc_001-server/v1'); 

define('NAME_SESSION','p'.'test_pc_001');  // En PHPRunner se define sin el prefijo "p"

// Tiempo de sesión
define('TIME_OUT','+30 minutes');

$server = 'test';  // 'test' or 'production'
// Configuración del entorno de Desarrollo "test" o de Producción
if ($server == 'test' ) {
// Path root of Directory files in File System or Disc
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASSWORD', 'XXXXXXX'); 
define('DB_NAME', 'pruebas');
// Config PHPMailer
define('EMAIL_HOST', 'smtp.hostinger.com');
define('EMAIL_USER', '[email protected]');
define('EMAIL_PASSWORD', 'XXXXXXXXXXX');
define('EMAIL_PORT', 465);

} else {
// In Server Linux
define('DB_HOST', 'localhost');
define('DB_USER', 'u637977917_XXXXXX' );
define('DB_PASSWORD', 'XXXXXXXXXXX'); 
define('DB_NAME', 'u637977917_factu'); 
// Config PHPMailer
define('EMAIL_HOST', 'smtp.hostinger.com');
define('EMAIL_USER', '[email protected]');
define('EMAIL_PASSWORD', 'XXXXXXXXXXX');
define('EMAIL_PORT', 465);
} 

//referencia generado con MD5(uniqueid(<some_string>, true))
define('API_KEY','61437cfc-caa5-4cf9-9bee-85fe47efb09a');

/**
 * API Response HTTP CODE
 * Used as reference for API REST Response Header
 *
 */
/*
200 	OK
201 	Created
304 	Not Modified
400 	Bad Request
401 	Unauthorized
403 	Forbidden
404 	Not Found
409     Conflict
422 	Unprocessable Entity
500 	Internal Server Error
*/

// Error messages to facilitate their translation

$errorMessages = array(
    "001" => "Falta Token de Autorización o ha expirado",
    "002" => "El Token de autorización es incorrecto",
    "003" => "Campo(s) Requerido(s) o atributo(s) {1} faltan o están vacíos",
    "004" => "Usuario o Password no válida",
<?php

    use Psr\Http\Message\ServerRequestInterface as Request;
    use Psr\Http\Message\ResponseInterface as Response;

class ExternalSessionMiddleware
{
    private string $cookieName = NAME_SESSION;
    private string $sessionSavePath;

    public function __construct()
    {
        $this->sessionSavePath = ini_get('session.save_path') ?: sys_get_temp_dir();
    }

    public function __invoke($request, $handler)
    {
        
        $cookies = $request->getCookieParams();
        
        // custom_error(62,"Cookies enviadas : ".print_r($cookies, true)); 
        // custom_error(63,"Cookies buscada : ".$this->cookieName); 
        
        $origin = $request->getHeaderLine('Origin');
        
        if ( $_SERVER['REQUEST_METHOD'] <> 'OPTIONS') { // Si es Options, no hacer el proceso
        
            // 1. Verificar que la cookie existe
           if ($origin === 'http://localhost:5173') {
                $sessionId = 'testplugins'; // Para cuando se ejecuta desde entrono de desarrollo

            } else {
                if (!isset($cookies[$this->cookieName])) {
                    return $this->error("Cookie de sesión no encontrada");
                }

                $sessionId = $cookies[$this->cookieName];
            }

            // 3. Cargar la sesión externa
            session_id($sessionId);

            session_start([
                'use_cookies' => false,
                'read_and_close' => false
            ]);

            // custom_error(20,"DEBUG SESSION:  ".print_r($_SESSION, true));  // to Debug


            /*
            // 3. Verificar contenido de sessión y verificar que ususario tiene Login
            if (empty($_SESSION['UserID'])) {
                return $this->error("La sesión existe, pero no tiene Login");
            }
            */
        }
        
        // Procesar la petición
        $response = $handler->handle($request);

        // 4. Guardar y cerrar la sesión
        session_write_close();

        return $response;
    }

    private function error(string $msg)
    {
        $response = new \Slim\Psr7\Response();
        
        // custom_error(61,"Middleware error : ".$msg);  
        
        $responseBody = [];
        $responseBody["error"] = true;
        $responseBody["message_num"] = '002';
        $responseBody["message"] = $msg;
        
        $response->getBody()->write(json_encode($responseBody));
         return $response
                ->withHeader('content-type', 'application/json')
                ->withStatus(401);   
        }
}
<?php

include_once __DIR__.'/Config.php';                 // Configuration Rest Api
include_once __DIR__.'/Function.php';               // General Function

require_once __DIR__.'/DBF_session.php';            // Funciones específicas según tablas

/**
 *
 * @About:      Prueba de Concepto 001 - pc_001
 * @File:       DbFunctions
 * @Date:       $Date:$ sep 2025
 * @Version:    $Rev:$ 1.0
 * @Developer:  fernando humanes
 **/

/*
    use Psr\Http\Message\ResponseInterface as Response;
    use Psr\Http\Message\ServerRequestInterface as Request;
    
    use PHPMailer\PHPMailer\PHPMailer;   // PHPMailer
    use PHPMailer\PHPMailer\SMTP;
    use PHPMailer\PHPMailer\Exception;
 */   

class DbFunctions
{

    private $db;

    function __construct()
    {
        $dbHost = DB_HOST;
        $dbName = DB_NAME;
        /*
        $dsn = "mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4";
        $options = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ];
        try {
            $pdo = new PDO($dsn, DB_USER, DB_PASSWORD, $options);
            $this->db = $pdo;
        } catch (PDOException $e) {
            custom_error(1000,"Error en DbFunction: ".print_r($e, true));  // to Debug
            throw new PDOException($e->getMessage(), (int)$e->getCode());
        }
        */
         
    }

     use Session;                     // Grupo de funcionaes de Gestión de la Sesión "particular" del sistema
}
<?php
    
    use Psr\Http\Message\ServerRequestInterface as Request;
    use Psr\Http\Message\ResponseInterface as Response;

trait Session {
    /*
     * Muestra los datos de la Sesión ya existente
     */
    public function createSession(Request $request, Response $response)   // , array $args)
     {   
        global $errorMessages; 
        
        // custom_error(71,"Get session: ".print_r($_SESSION,true)); 
        
        $responseBody = [];
        $responseBody["error"] = false;
        $responseBody["message_num"] = '000';
        $responseBody["message"] = 'ok';
        $responseBody["SESSION"] = $_SESSION;
        
        $response->getBody()->write(json_encode($responseBody));
         return $response
                ->withHeader('content-type', 'application/json')
                ->withStatus(200);   
        }
        
    /*
     * Muestra los datos de la Sesión ya existente
     */
    public function updateSession(Request $request, Response $response)   // , array $args)   
        
    {  
        global $errorMessages; 
        
        $_SESSION['ultimo_acceso'] = date('Y-m-d H:i:s');
        $_SESSION['contador'] = ($_SESSION['contador'] ?? 0) + 1;
        
        $responseBody = [];
        $responseBody["error"] = false;
        $responseBody["message_num"] = '000';
        $responseBody["message"] = 'ok';
        $responseBody["SESSION"] = $_SESSION;
        
        $response->getBody()->write(json_encode($responseBody));
         return $response
                ->withHeader('content-type', 'application/json')
                ->withStatus(200);   
        }
        
        /*
        * Muestra los datos de la Sesión ya existente
        */
       public function resetSession(Request $request, Response $response)   // , array $args)   

       {  
            global $errorMessages; 

            unset($_SESSION['ultimo_acceso']) ;
            unset($_SESSION['contador']);

            $responseBody = [];
            $responseBody["error"] = false;
            $responseBody["message_num"] = '000';
            $responseBody["message"] = 'ok';
            $responseBody["SESSION"] = $_SESSION;

            $response->getBody()->write(json_encode($responseBody));
             return $response
                    ->withHeader('content-type', 'application/json')
                    ->withStatus(200);   
        }

    }
<?php

/*********************** USEFULL FUNCTIONS **************************************/

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Selective\BasePath\BasePathMiddleware;
use Slim\Factory\AppFactory;


/**
 * Verificando los parametros requeridos en el método
 */
function verifyRequiredParams($required_fields, $request_params
        // ,  Request  $request, Response $response
        )
{
    global $errorMessages;
    $error = false;
    $error_fields = "";

    // $request_params = $request->getParsedBody();

    foreach ($required_fields as $field) {
        // if (!isset($request_params[$field]) || strlen(trim($request_params[$field])) <= 0) {
        if (!isset($request_params[$field])) {
            $error = true;
            $error_fields .= $field . ', ';
        }
    }

    if ($error) {
        // Required field(s) are missing or empty
        // echo error json and stop the app
        $responseBody = array();
        $responseBody["error"] = true;
        $responseBody["message_num"] = '003';
        $responseBody["message"] = str_replace("{1}",substr($error_fields, 0, -2),$errorMessages['003']);
        $responseBody["error_status"] = 400; 
        return $responseBody;
    }
    $responseBody = array();
    $responseBody["error"] = false;
    return $responseBody;
}

/**
 * Revisa si la consulta contiene un Header "Authorization" para validar
 */
function authenticate(Request $request, Response $response)
{
    global $errorMessages;
    // Getting request headers
    $headers = $request->getHeaders();
    // Verifying Authorization || authorization Header
    if (isset($headers['Authorization'])|| isset($headers['authorization']) ) {
        // get the api key
        if (isset($headers['Authorization']) ) $token = $headers['Authorization'];
        if (isset($headers['authorization']) ) $token = $headers['authorization'];
        
        // validating api key
        if (!($token[0] == API_KEY)) { //API_KEY declarada en Config.php

            // api key is not present in users table
            $responseBody["error"] = true;
            $responseBody["message_num"] = '002';
            $responseBody["message"] = $errorMessages['002'];
            $responseBody["error_status"] = 401; 
            return $responseBody;
        } else {
            //procede utilizar el recurso o metodo del llamado
            $responseBody = array();
            $responseBody["error"] = false;
            return $responseBody;
        }
    } else {
        // api key is missing in header
        $responseBody["error"] = true;
        $responseBody["message_num"] = '001';
        $responseBody["message"] = $errorMessages['001'];
        $responseBody["error_status"] = 400; 
        return $responseBody;
    }
}

// Generate 16 bytes (128 bits) of random data or use the data passed into the function.
   function guidv4($data = null) {
        
        $data = $data ?? random_bytes(16);
        assert(strlen($data) == 16);
    
        // Set version to 0100
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
        // Set bits 6-7 to 10
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80);
    
        // Output the 36 character UUID.
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
    } 

    // To normalize filer name
    function RemoveSpecialCharFile($str) // To normalize filer name
{
    $res = preg_replace('([^A-Za-z0-9_. ])', ' ', $str);
    $res = str_replace(' ','_',$res);
    return $res;
}

// Validación de estructuras de los array asociativos
function validarFormatoArray(array $datos=[['email'=>'','nombre'=>'']], array $clavesEsperadas = ['email','nombre']) {
    // Definimos las claves exactas que esperamos
    $valido = true;
    $registroInvalido = null;
    
    foreach ($datos as $indice => $registro) {
        // 1. Comprobar que es un array
        if (!is_array($registro)) {
            $valido = false;
            $registroInvalido = $indice;
            break;
        }

        // 2. Comprobar el número exacto de campos
        if (count($registro) !== 2) {
            $valido = false;
            $registroInvalido = $indice;
            break;
        }

        // 3. Comprobar que las claves son exactamente las esperadas
        $clavesActuales = array_keys($registro);
        
        // array_diff() nos dice si hay claves que faltan o sobran
        if (count(array_diff($clavesEsperadas, $clavesActuales)) > 0 || count(array_diff($clavesActuales, $clavesEsperadas)) > 0) {
            $valido = false;
            $registroInvalido = $indice;
            break;
        }
    }
    if ($valido) {
        // echo "✅ El formato de todos los registros es correcto.\n";
        return true;
    } else {
        // echo "❌ Error de formato en el registro con índice **$registroInvalido**.\n";
        return false;
    }
    
}
// Función para añadir trazas de depuración del código PHP
function custom_error($number, $text) {
    global $debugCode;

    if ($debugCode === true) {

        $logFile = __DIR__ . '/../error.log';

        // Si no existe, lo crea vacío
        if (!file_exists($logFile)) {
            // touch() crea el archivo y respeta permisos del sistema
            touch($logFile);
        }

        // Abrir en modo append (crea si no existe, pero ya lo controlamos arriba)
        $ddf = fopen($logFile, 'a');

        fwrite($ddf, "[" . date("r") . "] Error $number: $text\r\n");
        fclose($ddf);
    }
}

Los más relevante es:

  • En el fichero index se define la llemada a la ejecución de un «midleware» para controlar la sesión y cuyo código está en «ExternalSessionMiddleware.php». Veréis que hay muchas líneas comentadas porque en el ejemplo no se utiliza la obligatoriedad de que el usuario previamente esté identificado en PHPRunner, pero se puede controlar que no responda ningún dato si la petición no llega desde el progrma con identificación prevía del ususario.
  • En «DB_session.php», es donde está el código de operaciones sobre los datos de la sesión, que puede acceder el programa PHPRunner y el SLIM4, y por tanto, comunicar cualqueir datos a los JavaScript que se han codificado en Svelte 5.

Creo que todo es muy sencillo, para que os sirva para probar e imaginar el potencial que puede tener estas carcaterísticas para vuestros de sarrollos.

Os dejo este código de forma completa y cualquier duda, os pido que me la comuniquéis a través de email.

Adjuntos

Archivo Tamaño de archivo Descargas
zip pc_001-server 398 KB 1

Blog personal para facilitar soporte gratuito a usuarios de React y PHPRunner