resources/views/pages/frontend/docs/framedev-architect-worker.md
Última modificación: 27 Oct, 2026

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):

  1. Invocación en Kernel: Laravel utiliza el Facade Illuminate\Support\Facades\Process para comunicarse directamente con el intérprete a través de flujos estándar (stdin, stdout, stderr).
  2. 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).
  3. 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:

  1. Escaneo Estático: Python recorre de forma recursiva los directorios de Laravel.
  2. 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).
  3. Compilación Vectorial: Compila la topología en un grafo dirigido utilizando la biblioteca Graphviz.
  4. 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-learn o PyTorch.
  • 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-rs para firmas digitales de alta velocidad.

Enlaces Relacionados

Edita este documento en >> framedev-architect-worker.md
← Volver a la Landing Page