En artículos anteriores hemos tratado:
- Plugins jQuery vs Web Components donde he explicado que los WEB Components, aún siendo cosas diferentes, sustituyen a los plugins de Jquery, que son los actuales plugins de PHPRunner.
- S-023 – Construir Web Components con Svelte 5, en este artículo explico otro poco más lo que son los WEB Components y cómo se construyen utilizando Svelte 5.
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.
$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í):
<?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.