Skip to content

heartofphp/cohete

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

521 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cohete

./logo.png

Tu plataforma de lanzamiento hacia la programación asíncrona en PHP

https://img.shields.io/badge/license-MIT-blue.svg https://img.shields.io/badge/PHP-8.3-777BB4.svg https://img.shields.io/badge/ReactPHP-async-4F5B93.svg https://img.shields.io/badge/RxPHP-reactive-B7178C.svg https://img.shields.io/badge/Google_Code_Wiki-indexed-4285F4.svg

Qué es Cohete

Cohete es un framework PHP asíncrono construido sobre ReactPHP y RxPHP, con arquitectura Domain-Driven Design (DDD). No es otro framework más: es una declaración de principios.

“Menos es más. El código que no escribes no tiene bugs.”

Un solo proceso PHP que maneja todas las conexiones de forma asíncrona. Sin Apache. Sin nginx. Sin PHP-FPM. PHP es el servidor.

Mientras Symfony levanta una instancia PHP por cada request (un tren regional para cada pasajero), Cohete mantiene un único proceso con event loop que gestiona miles de conexiones concurrentes (cuatro trenes de alta velocidad en la misma vía).

Motivación

Después de años trabajando con Symfony y otros frameworks, me di cuenta de algo: usaba herramientas sin entender realmente qué me estaban dando. Capas y capas de abstracción, magia negra por todos lados, y cuando algo fallaba… a rezar.

Así que decidí hacer lo que cualquier developer curioso haría: construir mi propio framework. No para reinventar la rueda, sino para entenderla. Y de paso, hacerlo mejor: más simple, más rápido, más transparente.

El resultado es Cohete: todo lo bueno de Symfony (DDD, CQRS, inyección de dependencias) pero sin la complejidad innecesaria. Y con un bonus: programación asíncrona de verdad, no como afterthought.

Por qué Cohete

Mientras otros frameworks crecen en complejidad, Cohete apuesta por:

  • ~3,000 líneas de código - todo el framework
  • 100% non-blocking I/O - un solo proceso, miles de conexiones
  • DDD real - no carpetas vacías con nombres bonitos
  • Cero magia negra - entiendes cada línea
  • Dos caras: API REST + servidor de páginas con el mismo proceso

En producción en pascualmg.dev, sirviendo portfolio, blog y API. Indexado en Google Code Wiki.

Stack Tecnológico

ComponenteTecnologíaPropósito
Event LoopReactPHPNon-blocking I/O
Reactive StreamsRxPHPComposición de operaciones async
HTTP Serverreact/httpServidor HTTP nativo
MySQL Clientreact/mysqlQueries asíncronas
DI ContainerPHP-DIAuto-wiring de dependencias
RouterFastRouteRouting ultra-rápido
MiddlewaresPSR-15Pipeline de request/response
WebSocketsRatchetComunicación bidireccional
TestingPHPUnit + BehatUnit + BDD
MigrationsPhinxControl de esquema BD
Org-modePandocConversión org → HTML server-side

Arquitectura

Cohete tiene dos caras: es un backend API y un servidor de páginas. Ambos corren en el mismo proceso PHP asíncrono.

                 HTTP Request
                      │
      ┌───────────────▼───────────────┐
      │     Middleware Chain (PSR-15)  │
      │  ClientIP → RequestDumper → …  │
      └───────────────┬───────────────┘
                      │
            ┌─────────▼─────────┐
            │   Kernel (Core)    │
            │  Async Dispatcher  │
            └─────────┬─────────┘
                      │
           ┌──────────▼──────────┐
           │  FastRoute Router   │
           │   routes.json       │
           └──────┬───────┬──────┘
                  │       │
       ┌──────────▼──┐ ┌──▼──────────┐
       │  BACKEND    │ │  FRONTEND   │
       │  API REST   │ │  HTML/JS    │
       │  (JSON)     │ │  (Streams)  │
       └──────┬──────┘ └──────┬──────┘
              │               │
┌─────────────▼─────┐  ┌──────▼──────────┐
│  Application      │  │  HtmlController  │
│  Commands/Queries │  │  ReadableStream  │
└─────────┬─────────┘  │  + MIME types    │
          │            └─────────────────┘
┌─────────▼─────────┐
│  Domain Layer     │
│  Entities + VOs   │
└─────────┬─────────┘
          │
┌─────────▼─────────┐
│  Infrastructure   │
│  MySQL Async      │
│  MessageBus       │
└───────────────────┘

Parte 1: Backend (API REST)

El backend sigue CQRS estricto: Commands para escritura, Queries para lectura. Todo devuelve PromiseInterface.

Los endpoints están definidos en routes.json. Cada ruta apunta a un handler invocable (__invoke) que el contenedor resuelve con auto-wiring.

El flujo:

Request → Router → Handler(Request) → Command/Query → Domain → Repository → Promise<Response>

Parte 2: Frontend (Servidor de páginas)

Cohete no es solo una API que tira JSONs. El HtmlController demuestra que un servidor async puede servir páginas completas con la misma eficiencia:

// GET /html/{path} → sirve cualquier fichero estático con streaming
$html = new ReadableResourceStream(fopen($filename, 'rb'));
return new Response(200, ['Content-Type' => $mimeType], $html->pipe($throughStream));

Esto hace streaming del fichero sin cargarlo entero en memoria. Detecta MIME types automáticamente (JS, CSS, HTML, imágenes…). Es el mismo PHP que maneja la API pero sirviendo tu SPA completa.

El frontend usa Web Components nativos con Atomic Design:

  • Sin npm, sin webpack, sin build steps
  • Shadow DOM para encapsulación
  • CSS Custom Properties para temas
  • RxJS para reactividad
  • Cero frameworks

Estructura del Proyecto

src/
├── bootstrap.php                    # Entry point (40 líneas)
└── ddd/
    ├── Domain/                      # Lógica de negocio pura
    │   ├── Entity/
    │   │   ├── Post.php             # Agregado con Value Objects
    │   │   └── PostRepository.php   # Interface (contrato)
    │   ├── ValueObject/
    │   │   ├── PostId.php           # UUID v4
    │   │   ├── HeadLine.php         # Max 256 chars
    │   │   ├── Slug.php             # Auto-generado, transliteración UTF-8
    │   │   ├── ArticleBody.php      # Contenido
    │   │   ├── Author.php
    │   │   └── DatePublished.php    # ISO8601/ATOM
    │   ├── Bus/
    │   │   └── MessageBus.php       # Interface eventos de dominio
    │   └── Service/
    │       └── PostCreator.php      # Orquesta creación + eventos
    │
    ├── Application/                 # Casos de uso (CQRS)
    │   └── Post/
    │       ├── CreatePostCommand.php
    │       ├── CreatePostCommandHandler.php
    │       ├── FindAllPostsQuery.php
    │       ├── FindAllPosts.php
    │       ├── FindPostByIdQuery.php
    │       └── FindPostByIdQueryHandler.php
    │
    └── Infrastructure/              # Detalles técnicos
        ├── HttpServer/
        │   ├── Kernel/Kernel.php    # NÚCLEO (103 líneas)
        │   ├── ReactHttpServer.php  # Inicialización servidor
        │   ├── Router/
        │   │   ├── Router.php       # FastRoute wrapper
        │   │   └── routes.json      # Definición de rutas
        │   └── RequestHandler/      # Controllers
        │       ├── FindAllPostsController.php
        │       ├── FindPostByIdController.php
        │       ├── CreatePostController.php
        │       ├── UploadOrgController.php   # POST /post/org
        │       ├── HtmlController.php        # Servidor de páginas
        │       ├── HealthController.php
        │       └── EchoController.php
        ├── Repository/
        │   ├── ObservableMysqlPostRepository.php  # RxPHP + MySQL async
        │   ├── AsyncMysqlPostRepository.php       # Promise puro
        │   └── FilePostRepository.php             # JSON para dev
        ├── Service/
        │   └── OrgToHtmlConverter.php  # Pandoc integration
        ├── Bus/
        │   └── ReactMessageBus.php     # EventEmitter + futureTick
        ├── PSR11/
        │   └── ContainerFactory.php    # PHP-DI auto-wiring
        └── webserver/html/             # Frontend (Web Components)
            ├── index.html
            ├── pascualmgPorfolio.html
            └── js/atomic/
                ├── atom/               # Componentes básicos
                ├── molecule/           # Composiciones
                └── organism/           # Páginas completas

Instalación

Requisitos

  • PHP 8.3+ con Composer
  • MySQL o MariaDB
  • Pandoc (opcional, para publicar posts en org-mode)

Quick Start

git clone https://github.com/pascualmg/cohete.git
cd cohete
composer install
cp .env.example .env
# Editar .env: ROUTES_PATH debe ser ruta absoluta a routes.json
php src/bootstrap.php

El servidor estará en http://localhost:8000. Eso es todo. Un proceso PHP que es tu servidor.

Con base de datos

# Configurar MySQL en .env, después:
make migrate     # Crea las tablas
make fixtures    # Carga datos de prueba (opcional)
make run         # Arranca el servidor

Con Nix (opcional)

Si usas Nix, el proyecto incluye un flake.nix con todo el entorno de desarrollo:

nix develop      # PHP 8.3 + Composer + PHPUnit + PHPStan + Psalm
make run

Configuración

Crea un archivo .env basándote en .env.example:

APP_ENV=dev
HTTP_SERVER_HOST=0.0.0.0
HTTP_SERVER_PORT=8000
ROUTES_PATH=/ruta/absoluta/a/routes.json

MYSQL_HOST=127.0.0.1
MYSQL_USER=cohete
MYSQL_PASSWORD=secret
MYSQL_DATABASE=cohete
MYSQL_PORT=3306

Importante: ROUTES_PATH requiere ruta absoluta.

API Endpoints

MétodoRutaDescripciónHandler
GET/Redirect al portfolioRedirectController
GET/healthHealth checkHealthController
GET/postLista todos los postsFindAllPostsController
GET/post/{id}Obtiene post por UUIDFindPostByIdController
POST/postCrea post (JSON)CreatePostController
POST/post/orgPublica post org-modeUploadOrgController
GET/html/{path}Sirve ficheros estáticosHtmlController
GET,PUT,…/echoEcho request (debug)EchoController

Ejemplos

Listar posts

curl https://pascualmg.dev/post | jq

Crear post (JSON)

curl -X POST https://pascualmg.dev/post \
  -H "Content-Type: application/json" \
  -d '{
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "headline": "Mi primer post",
    "articleBody": "<p>Contenido HTML</p>",
    "author": "pascualmg",
    "datePublished": "2026-02-15T10:00:00+00:00"
  }'

Publicar post desde org-mode

curl -X POST https://pascualmg.dev/post/org --data-binary @mi-post.org

Ver el portfolio

Abre https://pascualmg.dev/html/pascualmgPorfolio.html en el navegador.

Blog con Org-Mode

El blog es un caso de uso real que demuestra las dos caras de Cohete trabajando juntas: el backend recibe y almacena contenido, el frontend lo presenta.

Cómo funciona

  1. Escribes tu post en org-mode (el formato de Emacs)
  2. Lo envías al endpoint POST /post/org
  3. Cohete extrae metadatos (#+TITLE, #+AUTHOR, #+DATE)
  4. Pandoc convierte el org a HTML server-side
  5. Se almacena en MySQL con UUID, slug auto-generado, y fecha
  6. El frontend (Web Component PascualmgBlog) lo muestra

Formato del fichero org

#+TITLE: Mi artículo sobre PHP asíncrono
#+AUTHOR: pascualmg
#+DATE: 2026-02-15

* Introducción

Aquí va el contenido con toda la potencia de org-mode:
listas, código, tablas, enlaces...

** Ejemplo de código

#+begin_src php
$promise = $httpClient->get('https://api.example.com')
    ->then(fn($response) => json_decode($response->getBody()));
#+end_src

Publicar un post

Opción 1: curl directo

curl -X POST https://pascualmg.dev/post/org \
  --data-binary @mi-post.org

Respuesta:

{
  "id": "5e529ca2-1746-4cd6-a302-99a7ac476013",
  "headline": "Mi artículo sobre PHP asíncrono",
  "author": "pascualmg",
  "datePublished": "2026-02-15T00:00:00+00:00"
}

Opción 2: Script publish-org.sh

# Publicar en producción
publish-org.sh mi-post.org

# Publicar en local
publish-org.sh mi-post.org http://localhost:8000

Opción 3: Desde el navegador

Entra a pascualmg.dev, navega al Blog, y usa el formulario de subida para seleccionar tu fichero .org.

Pipeline de conversión

Fichero .org
    │
    ▼
UploadOrgController
    │
    ├──► extractMetadata()     # regex: #+TITLE, #+AUTHOR, #+DATE
    │
    ├──► convert()             # pandoc -f org -t html --no-highlight
    │                          # via proc_open (stdin/stdout pipes)
    │
    ├──► CreatePostCommand     # UUID v4, slug auto-generado
    │
    ▼
PostCreator → Repository.save() → MySQL
    │
    ▼
MessageBus.publish('domain_event.post_created')

Patrones Clave

Handlers Invocables

Todos los handlers son __invoke() - funciones, no objetos con métodos:

// En lugar de $handler->handle($command)
($this->handler)($command);

// Query con Promise
($this->findAllPosts)(new FindAllPostsQuery())
    ->then(
        fn($posts) => JsonResponse::withPayload($posts),
        fn($error) => JsonResponse::withError($error)
    );

Observable + Promise

Combinación de RxPHP con React Promises para composición funcional:

public function findAll(): PromiseInterface
{
    return Observable::fromPromise(
        $this->mysqlClient->query('SELECT * FROM post ORDER BY datePublished DESC')
    )
    ->map(fn($result) => array_map([self::class, 'hydrate'], $result->resultRows))
    ->toPromise();
}

Observable envuelve Promise para mapear, filtrar, combinar. Después vuelve a Promise para el controller.

Value Objects Inmutables

Validación en construcción, inmutabilidad garantizada:

// Slug se genera automáticamente con transliteración UTF-8 → ASCII
$slug = Slug::from("Programación Asíncrona en PHP");
// → "programacion-asincrona-en-php"

// UUID validado en construcción
$id = PostId::from("550e8400-e29b-41d4-a716-446655440000");

// Fecha en formato ATOM
$date = DatePublished::from("2024-01-15T10:30:00+00:00");

Bus de Mensajes Reactivo

Eventos de dominio sin acoplamiento, usando futureTick para no bloquear:

// Publicar evento (se ejecuta en el siguiente tick del event loop)
$this->messageBus->publish(
    new Message('domain_event.post_created', [$post])
);

// Suscribirse (en ContainerFactory)
$messageBus->subscribe('domain_event.post_created', function($post) {
    $logger->info("Post creado: {$post->headline}");
    // Notificar, indexar, invalidar cache...
});

Streaming de ficheros

El HtmlController sirve ficheros usando streams reactivos de ReactPHP:

$readable = new ReadableResourceStream(fopen($file, 'rb'));
$through  = new ThroughStream(fn($chunk) => $chunk);
return new Response(200, ['Content-Type' => $mime], $readable->pipe($through));

No carga el fichero en memoria. Lo streamea chunk a chunk por el event loop. Mismo patrón que usaría nginx, pero en PHP.

Frontend: Portfolio + Blog

El frontend demuestra que Cohete sirve páginas tan bien como APIs. Todo con Web Components nativos siguiendo Atomic Design:

html/js/atomic/
├── atom/                       # Componentes básicos
│   ├── ChangingText.js         # Texto animado (Typed.js style)
│   └── YastModal.js            # Modal reutilizable
├── molecule/                   # Composiciones
│   ├── PortfolioHeader.js      # Header con avatar + links
│   ├── ExperienceTimeline.js   # Timeline de experiencia profesional
│   ├── SoftSkills.js           # Skills técnicos
│   ├── SocialLinks.js          # GitHub, LinkedIn, etc.
│   ├── AboutThisPortfolio.js   # Sección "sobre el portfolio"
│   ├── technologiesList.js     # Stack tecnológico
│   └── PostDetail.js           # Vista detalle de un post del blog
└── organism/                   # Páginas completas
    ├── PascualmgSpa.js         # Shell principal de la SPA
    ├── pascualmg-portfolio.js  # Página del portfolio
    ├── PascualmgBlog.js        # Blog: listado + subida org
    ├── CreatePostForm.js       # Formulario de creación
    ├── ChatBox.js              # Chat WebSocket
    ├── themeSwitcher.js        # Selector de tema
    └── ThemeToogler.js         # Toggle tema claro/oscuro

Sistema de Temas

5 temas disponibles con CSS Custom Properties, cambiables en runtime:

  • spacemacs-dark - Inspirado en el editor
  • spacemacs-light - Versión clara
  • solarized-dark - El clásico
  • solarized-light - Para los que prefieren luz
  • linkedin - Profesional

Los temas usan variables CSS (--base, --head1, --head2, --bg2, --border…) que todos los componentes respetan via Shadow DOM.

Brutalismo Web

El portfolio demuestra que puedes construir webs modernas sin:

  • npm install de 500MB
  • Frameworks JavaScript de moda
  • Build steps eternos
  • Dependencias que se rompen cada 6 meses

Solo HTML, CSS y JavaScript vanilla con Web Components estándar del navegador.

Testing

Unit Tests (PHPUnit)

nix develop --command bash -c 'vendor/bin/phpunit'

BDD Tests (Behat)

# El servidor debe estar corriendo
nix develop --command bash -c 'vendor/bin/behat'

Escenarios incluidos:

Scenario: Create a post via API
  Given I am an API client
  And the database is empty
  When I send payload to endpoint "/post" with method "POST"
  Then the response code should be 202
  And the post with id "..." exists

Scenario: Import post from org file
  Given I have an org file with title "Test Post"
  When I upload it to "/post/org"
  Then the response code should be 202
  And the post headline is "Test Post"

Scenario: List all posts
  Given there are 3 posts in the database
  When I send a GET request to "/post"
  Then the response contains 3 posts

Migraciones

make migrate                    # Ejecutar migraciones
make fixtures                   # Cargar datos de prueba
vendor/bin/phinx status         # Ver estado
vendor/bin/phinx rollback       # Rollback última

Makefile

make run          # Arranca el servidor async
make watch        # Servidor con file watcher
make migrate      # Ejecuta migraciones Phinx
make fixtures     # Carga datos de prueba
make mysql        # Docker Compose MySQL (dev)
make rabbitmq     # Docker Compose RabbitMQ
make fix          # php-cs-fixer
make behat        # Tests BDD
make test_ab      # Benchmark con Apache Bench
make nix-install  # Instala Nix

Despliegue en producción

Cohete es un solo proceso PHP. Desplegarlo es tan simple como:

php src/bootstrap.php &

Para algo más robusto, un systemd service básico basta:

[Service]
ExecStart=/usr/bin/php /path/to/cohete/src/bootstrap.php
Restart=always
RestartSec=5

En pascualmg.dev usamos Cloudflare Tunnel para HTTPS sin abrir puertos:

Internet → Cloudflare (pascualmg.dev) → tunnel → localhost:80 → Cohete

Para usuarios NixOS

El repo incluye un módulo NixOS declarativo (modules/services/cohete.nix) con hardening systemd, MariaDB integrada, y pandoc en PATH. Ver docs/setup-local.org para detalles.

Filosofía

DDDD: Domain-Driven Design for Developers

No es DDD académico. Es DDD práctico:

  1. El código es la documentación - Si necesitas un diagrama para entenderlo, está mal diseñado
  2. Menos capas, más claridad - Solo 3 capas porque solo necesitas 3
  3. Async por defecto - No como afterthought, como fundamento
  4. Nix para reproducibilidad - Mismo entorno en desarrollo y producción

PHP puede volar

La industria PHP vive atrapada en el modelo request-response síncrono: cada petición levanta un proceso PHP, carga todo el framework, responde, y muere. Es como encender y apagar el motor del coche en cada semáforo.

ReactPHP cambia el paradigma: un proceso PHP que arranca una vez y se queda vivo, manejando todas las conexiones con un event loop no-bloqueante. El mismo modelo que usa Node.js, pero en PHP.

Cohete demuestra que esto funciona en producción, con DDD, con CQRS, con tests, y sin renunciar a nada.

Dos caras, un servidor

Muchos frameworks async solo sirven APIs (JSONs). Cohete va más allá: el HtmlController demuestra que el mismo servidor puede servir páginas HTML completas usando streams reactivos. No necesitas nginx delante para los estáticos. PHP lo hace igual de bien, con streaming, sin cargar ficheros enteros en memoria.

Esto convierte a Cohete en un servidor completo: API + Frontend + WebSockets, todo en un solo proceso PHP.

Contribuciones

Este es un proyecto abierto para aprender y compartir. Las contribuciones son bienvenidas:

  1. Fork el repositorio
  2. Crea tu rama (git checkout -b feature/nueva-feature)
  3. Commit tus cambios (git commit -m 'Add: nueva feature')
  4. Push a la rama (git push origin feature/nueva-feature)
  5. Abre un Pull Request

Licencia

MIT License - Haz lo que quieras con esto.

Links

Cohete: Porque PHP puede volar.

About

reactive PHP without pain

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • PHP 48.9%
  • HTML 25.3%
  • JavaScript 21.8%
  • Nix 1.7%
  • Haskell 0.8%
  • Gherkin 0.8%
  • Other 0.7%