Worker Asíncrono Python & Graphviz (Framedev Architect)
En sistemas empresariales de gran escala, restringir la totalidad del ciclo de procesamiento a un único lenguaje de programación representa un cuello de botella arquitectónico. Si bien PHP 8.5 (mediante OPcache y el compilador JIT) ofrece un rendimiento sobresaliente en el manejo de peticiones I/O concurrentes y renderizado web, tareas como el análisis sintáctico de código, procesamiento matricial masivo, inteligencia artificial o generación de grafos vectoriales encuentran su entorno óptimo en otros ecosistemas.
Mañuti implementa una arquitectura políglota por diseño. A través de un puente de orquestación de procesos de bajo nivel (Subprocess / Inter-Process Communication - IPC), el framework delega cargas pesadas de cómputo a entornos externos (como Python 3, Go o Rust) sin intermediación de redes lentas ni sobrecargas de serialización HTTP.
El módulo Framedev Architect es la materialización en producción de este paradigma: una herramienta de ingeniería inversa y observabilidad viva que utiliza Python 3 y Graphviz para auditar, mapear y renderizar vectorialmente la arquitectura del aplicativo en tiempo real.
1. El Paradigma Políglota de Mañuti: Más Allá de las APIs REST
Tradicionalmente, para conectar Laravel con lenguajes como Python, las arquitecturas recurren a microservicios desacoplados expuestos mediante APIs REST (FastAPI, Flask). Aunque viable en ciertos escenarios, este enfoque introduce fricción operativa severa:
- Sobrecarga de Red (Network Overhead): Latencia acumulada por resolución DNS, handshakes TLS y apertura de sockets TCP para tareas internas.
- Complejidad de Serialización: Empaquetar y desempaquetar peticiones HTTP complejas satura la memoria.
- Puntos Únicos de Fallo: Obliga a mantener, escalar y monitorear múltiples capas de servidores web independientes.
flowchart TD
subgraph Tradicional ["Arquitectura Tradicional por API REST (Alta Latencia)"]
T_WEB["[Laravel Web]"]
T_RED["[Red Interna]"]
T_API["[FastAPI Daemon]"]
T_RESP["[Respuesta HTTP]"]
T_WEB -->|"HTTP/JSON"| T_RED
T_RED -->|"TLS Handshake"| T_API
T_API -->|"Cómputo"| T_RESP
end
subgraph Manuti ["Arquitectura Mañuti IPC Worker (Latencia Cero / Directo en Kernel)"]
M_WORKER["[Laravel / Horizon Worker]"]
M_IPC["[Process IPC (POSIX Pipe / Stdio)]"]
M_PYTHON["[Python 3 Engine]"]
M_MINIO["[Salida Directa a MinIO S3]"]
M_WORKER --> M_IPC
M_IPC --> M_PYTHON
M_PYTHON -->|"Memoria Compartida / FS"| M_MINIO
end
El Enfoque del Puente Nativo de Mañuti
Mañuti opta por la orquestación asíncrona en el nodo de cómputo (Worker-Level IPC):
- Invocación en Kernel: Laravel utiliza el Facade
Illuminate\Support\Facades\Processpara comunicarse directamente con el intérprete a través de flujos estándar (stdin,stdout,stderr). - Aislamiento en Colas: La ejecución se transfiere a un proceso de Laravel Horizon en Redis, evitando cualquier bloqueo del hilo principal de atención al usuario (Nginx/PHP-FPM).
- Transporte de Datos Agnóstico: Los parámetros se transfieren mediante estructuras JSON ligeras o rutas de memoria compartida, garantizando que el motor de destino (sea un script de Python, un binario compilado de Go o una utilidad de consola C++) opere con el máximo rendimiento del sistema operativo.
2. Caso de Uso en Producción: Framedev Architect
Framedev Architect es la prueba de concepto y herramienta activa del framework que ilustra este puente. Su objetivo es generar automáticamente el mapa de navegación, lógica y persistencia de toda la plataforma:
- Escaneo Estático: Python recorre de forma recursiva los directorios de Laravel.
- Extracción de Dependencias: Mapea rutas HTTP, controladores inyectados, llamadas a vistas Blade y modelos de base de datos con sus relaciones Eloquent (
hasMany,belongsTo,belongsToMany). - Compilación Vectorial: Compila la topología en un grafo dirigido utilizando la biblioteca Graphviz.
- Desacoplamiento en la Nube: Transfiere el resultado vectorial final (SVG) a un almacenamiento distribuido compatible con S3 (MinIO), eliminando archivos temporales locales.
3. Preparación del Entorno en el Host / Worker
La integración requiere que el nodo donde corre Horizon posea el runtime de Python 3 y las librerías nativas del sistema operativo:
# 1. Binarios del Sistema y Graphviz
sudo apt update
sudo apt install python3 python3-pip graphviz -y
# 2. Dependencias de Python (en Host o Virtualenv)
sudo apt install python3-graphviz python3-pathspec -y
# O mediante gestor de paquetes de Python:
pip3 install graphviz boto3 python-dotenv
4. El Motor Autónomo de Python (full_flow_mapper.py)
El script se diseñó de forma monolítica y auto-contenida en python/scripts/full_flow_mapper.py, evitando dependencias rotas entre módulos internos y garantizando compatibilidad con la ejecución desatendida:
import os
import re
import sys
import json
from graphviz import Digraph
def find_controller_file(project_path, controller_name):
"""Localiza un controlador en cualquier nivel de subcarpeta dentro de app/Http/Controllers"""
search_path = os.path.join(project_path, 'app/Http/Controllers')
if not os.path.exists(search_path): return None
for root, _, files in os.walk(search_path):
if f"{controller_name}.php" in files:
return os.path.join(root, f"{controller_name}.php")
return None
def analyze_model_relations(project_path, model_name, dot, processed_models):
"""Audita los modelos Eloquent para extraer relaciones hasMany, belongsTo, etc."""
if model_name in processed_models: return
processed_models.add(model_name)
model_path = os.path.join(project_path, 'app/Models', f"{model_name}.php")
if not os.path.exists(model_path): return
with open(model_path, 'r', encoding='utf-8') as f:
content = f.read()
# Expresión regular que captura la anatomía de relaciones en Laravel
rel_pattern = r"return\s+\$this->(hasMany|belongsTo|hasOne|belongsToMany)\s*\(\s*(?:['\"]?([a-zA-Z0-9\\]+)['\"]?|([a-zA-Z0-9]+)::class)"
relations = re.findall(rel_pattern, content)
for rel_type, rel_name_str, rel_name_class in relations:
related_model = (rel_name_str or rel_name_class).split('\\')[-1]
dot.node(related_model, f"Modelo:\n{related_model}", shape='database', fillcolor='#f3f0ff', color='#7c3aed')
dot.edge(model_name, related_model, label=rel_type, style='dotted', color='#a855f7', arrowhead='none', fontsize='8')
def parse_full_flow(project_path):
"""Analiza rutas, controladores, vistas y modelos para compilar el SVG"""
dot = Digraph(comment='Framedev Architect v12', format='svg')
dot.attr(rankdir='LR', nodesep='0.5', ranksep='1.2', fontname='Verdana', bgcolor='#ffffff')
dot.attr('node', style='filled, rounded', fontname='Verdana', fontsize='10', penwidth='2')
routes_file = os.path.join(project_path, 'routes/web.php')
if not os.path.exists(routes_file): return {"error": "No se localizó routes/web.php"}
with open(routes_file, 'r', encoding='utf-8') as f:
content = f.read()
# Mapeo de rutas estándar de Laravel
pattern = r"Route::(get|post|put|delete)\('([^']+)',\s*\[([a-zA-Z0-9\\]+)::class,\s*'([a-zA-Z0-9]+)'\]\)"
matches = re.findall(pattern, content)
processed_models = set()
for http_method, route, controller_raw, class_method in matches:
controller_name = controller_raw.split('\\')[-1]
route_id = f"route_{route}"
color = '#ea580c' if http_method == 'get' else '#dc2626'
# Nodo de Entrada: URL
dot.node(route_id, f"{http_method.upper()}: {route}", shape='cds', fillcolor='#fff7ed', color=color, URL=route)
file_path = find_controller_file(project_path, controller_name)
if file_path:
# Nodo de Lógica: Controlador
dot.node(controller_name, f"Controlador:\n{controller_name}@{class_method}", shape='ellipse', fillcolor='#eff6ff', color='#2563eb')
dot.edge(route_id, controller_name)
with open(file_path, 'r', encoding='utf-8') as cp:
c_content = cp.read()
# Nodos de Salida: Vistas Blade renderizadas
for view_name in re.findall(r"(?:view|View::make)\s*\(\s*['\"]([^'\"]+)['\"]", c_content):
dot.node(f"v_{view_name}", f"Vista:\n{view_name}.blade.php", shape='note', fillcolor='#f1f5f9', color='#475569')
dot.edge(controller_name, f"v_{view_name}", style='dashed')
# Nodos de Datos: Modelos Eloquent consultados
for model in re.findall(r"use App\\Models\\([a-zA-Z0-9]+);", c_content):
dot.node(model, f"Modelo:\n{model}", shape='database', fillcolor='#f3f0ff', color='#7c3aed')
dot.edge(controller_name, model, label='usa')
analyze_model_relations(project_path, model, dot, processed_models)
output_path = os.path.join(project_path, 'storage/app/public/full_flow_map')
dot.render(output_path, format='svg', cleanup=True)
return {"status": "success", "file": output_path + ".svg"}
if __name__ == "__main__":
if len(sys.argv) < 2:
print(json.dumps({"error": "Falta la ruta base del proyecto"}))
sys.exit(1)
print(json.dumps(parse_full_flow(sys.argv[1])))
5. El Job de Orquestación en Horizon (GenerateFullArchitectureMap)
El Job se encarga de aislar la llamada, monitorear la salida en el disco del worker y subir el archivo al almacenamiento en la nube MinIO mediante el disco s3:
// app/Jobs/GenerateFullArchitectureMap.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
class GenerateFullArchitectureMap implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public $timeout = 180; // 3 minutos de tolerancia máxima
public function handle(): void
{
$pythonBin = '/usr/bin/python3';
$scriptPath = base_path('python/scripts/full_flow_mapper.py');
$localPath = storage_path('app/public/full_flow_map.svg');
// 1. Invocación limpia y segura mediante Process IPC
$process = Process::run("{$pythonBin} {$scriptPath} " . escapeshellarg(base_path()));
if ($process->failed()) {
Log::error("Fallo en ejecución de Python: " . $process->errorOutput());
return;
}
// 2. Persistencia en MinIO S3 si el archivo fue generado
if (file_exists($localPath)) {
Storage::disk('s3')->put(
'architecture/full_flow_map.svg',
fopen($localPath, 'r+'),
[
'visibility' => 'public',
'ContentType' => 'image/svg+xml' // Impide que el navegador fuerce la descarga
]
);
// 3. Purga del archivo temporal en el worker local
unlink($localPath);
Log::info("Mapa de arquitectura sincronizado con éxito en MinIO.");
}
}
}
6. Integración en el Frontend: Inyección Directa y Pan-Zoom
Para erradicar problemas de CORS, incompatibilidades con la etiqueta <object> o desajustes del evento load, el cliente web descarga el archivo SVG de MinIO mediante una llamada fetch e inyecta el árbol XML directamente en el DOM:
// resources/views/admin/architecture/index.blade.php
async function loadAndInjectSvg(url) {
const container = document.getElementById('canvas-container');
const loader = document.getElementById('loader');
loader.classList.remove('hidden');
try {
// Rompemos caché de MinIO con parámetro temporal
const response = await fetch(url + '?t=' + new Date().getTime());
if (!response.ok) throw new Error("No se pudo obtener el SVG de MinIO");
const svgText = await response.text();
// Inyección atómica en el DOM
container.innerHTML = svgText;
const svgElement = container.querySelector('svg');
if (svgElement) {
svgElement.style.width = '100%';
svgElement.style.height = '100%';
svgElement.removeAttribute('width');
svgElement.removeAttribute('height');
// Habilitación de Zoom y Arrastre Infinito
svgPanZoom(svgElement, {
zoomEnabled: true,
controlIconsEnabled: true,
fit: true,
center: true,
minZoom: 0.01,
maxZoom: 100,
mouseWheelZoomEnabled: true,
preventMouseEventsDefault: true
});
}
} catch (e) {
console.error("Error al renderizar arquitectura:", e);
} finally {
loader.classList.add('hidden');
}
}
7. Extensibilidad del Modelo: Futuros Motores
La técnica de proceso IPC utilizada en Framedev Architect establece un estándar reutilizable para incorporar otros ecosistemas dentro de Mañuti:
- Machine Learning & NLP (Python): Procesamiento de documentos legales y detección de anomalías en logs de auditoría mediante
scikit-learnoPyTorch. - Procesamiento Concurrente Ultrasónico (Go): Generación de reportes tabulares de millones de registros o hashing criptográfico intensivo.
- Seguridad de Memoria y Criptografía (Rust): Implementación de módulos nativos compilados con
ext-php-rspara firmas digitales de alta velocidad.