Tu plataforma de lanzamiento hacia la programación asíncrona en PHP
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).
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.
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.
| Componente | Tecnología | Propósito |
|---|---|---|
| Event Loop | ReactPHP | Non-blocking I/O |
| Reactive Streams | RxPHP | Composición de operaciones async |
| HTTP Server | react/http | Servidor HTTP nativo |
| MySQL Client | react/mysql | Queries asíncronas |
| DI Container | PHP-DI | Auto-wiring de dependencias |
| Router | FastRoute | Routing ultra-rápido |
| Middlewares | PSR-15 | Pipeline de request/response |
| WebSockets | Ratchet | Comunicación bidireccional |
| Testing | PHPUnit + Behat | Unit + BDD |
| Migrations | Phinx | Control de esquema BD |
| Org-mode | Pandoc | Conversión org → HTML server-side |
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 │
└───────────────────┘
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>
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
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
- PHP 8.3+ con Composer
- MySQL o MariaDB
- Pandoc (opcional, para publicar posts en org-mode)
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.phpEl servidor estará en http://localhost:8000. Eso es todo. Un proceso PHP que es tu servidor.
# Configurar MySQL en .env, después:
make migrate # Crea las tablas
make fixtures # Carga datos de prueba (opcional)
make run # Arranca el servidorSi usas Nix, el proyecto incluye un flake.nix con todo el entorno de desarrollo:
nix develop # PHP 8.3 + Composer + PHPUnit + PHPStan + Psalm
make runCrea 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=3306Importante: ROUTES_PATH requiere ruta absoluta.
| Método | Ruta | Descripción | Handler |
|---|---|---|---|
| GET | / | Redirect al portfolio | RedirectController |
| GET | /health | Health check | HealthController |
| GET | /post | Lista todos los posts | FindAllPostsController |
| GET | /post/{id} | Obtiene post por UUID | FindPostByIdController |
| POST | /post | Crea post (JSON) | CreatePostController |
| POST | /post/org | Publica post org-mode | UploadOrgController |
| GET | /html/{path} | Sirve ficheros estáticos | HtmlController |
| GET,PUT,… | /echo | Echo request (debug) | EchoController |
curl https://pascualmg.dev/post | jqcurl -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"
}'curl -X POST https://pascualmg.dev/post/org --data-binary @mi-post.orgAbre https://pascualmg.dev/html/pascualmgPorfolio.html en el navegador.
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.
- Escribes tu post en org-mode (el formato de Emacs)
- Lo envías al endpoint
POST /post/org - Cohete extrae metadatos (
#+TITLE,#+AUTHOR,#+DATE) - Pandoc convierte el org a HTML server-side
- Se almacena en MySQL con UUID, slug auto-generado, y fecha
- El frontend (Web Component
PascualmgBlog) lo muestra
#+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
curl -X POST https://pascualmg.dev/post/org \
--data-binary @mi-post.orgRespuesta:
{
"id": "5e529ca2-1746-4cd6-a302-99a7ac476013",
"headline": "Mi artículo sobre PHP asíncrono",
"author": "pascualmg",
"datePublished": "2026-02-15T00:00:00+00:00"
}# Publicar en producción
publish-org.sh mi-post.org
# Publicar en local
publish-org.sh mi-post.org http://localhost:8000Entra a pascualmg.dev, navega al Blog, y usa el formulario de subida para seleccionar tu fichero .org.
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')
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)
);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.
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");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...
});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.
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
5 temas disponibles con CSS Custom Properties, cambiables en runtime:
spacemacs-dark- Inspirado en el editorspacemacs-light- Versión clarasolarized-dark- El clásicosolarized-light- Para los que prefieren luzlinkedin- Profesional
Los temas usan variables CSS (--base, --head1, --head2, --bg2, --border…) que todos los componentes respetan via Shadow DOM.
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.
nix develop --command bash -c 'vendor/bin/phpunit'# 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 postsmake migrate # Ejecutar migraciones
make fixtures # Cargar datos de prueba
vendor/bin/phinx status # Ver estado
vendor/bin/phinx rollback # Rollback últimamake 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 NixCohete 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=5En pascualmg.dev usamos Cloudflare Tunnel para HTTPS sin abrir puertos:
Internet → Cloudflare (pascualmg.dev) → tunnel → localhost:80 → Cohete
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.
No es DDD académico. Es DDD práctico:
- El código es la documentación - Si necesitas un diagrama para entenderlo, está mal diseñado
- Menos capas, más claridad - Solo 3 capas porque solo necesitas 3
- Async por defecto - No como afterthought, como fundamento
- Nix para reproducibilidad - Mismo entorno en desarrollo y producción
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.
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.
Este es un proyecto abierto para aprender y compartir. Las contribuciones son bienvenidas:
- Fork el repositorio
- Crea tu rama (
git checkout -b feature/nueva-feature) - Commit tus cambios (
git commit -m 'Add: nueva feature') - Push a la rama (
git push origin feature/nueva-feature) - Abre un Pull Request
MIT License - Haz lo que quieras con esto.
- Producción: https://pascualmg.dev
- GitHub: https://github.com/pascualmg/cohete
- Google Code Wiki: https://codewiki.google/github.com/pascualmg/cohete
- Autor: Pascual Muñoz Galián
—
Cohete: Porque PHP puede volar.
