cohete/skeleton

Skeleton project for Cohete async PHP framework

Maintainers

Package info

github.com/pascualmg/cohete-skeleton

Type:project

pkg:composer/cohete/skeleton

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-03-18 14:14 UTC

README

Starter project for Cohete async PHP.

Quick Start

git clone <repository-url>
cd cohete-skeleton
composer install
make run
curl localhost:8080/health

Sin dependencias externas. Arranca con almacenamiento en memoria y bus de eventos local. Listo para desarrollar.

Endpoints

Method Path Description
GET /health Health check
GET /todos List all todos
POST /todos Create a new todo
GET /todos/{id} Get a todo by ID
PUT /todos/{id} Update a todo
DELETE /todos/{id} Delete a todo

Architecture

El skeleton demuestra como construir una app async con infraestructura intercambiable. El dominio no sabe que base de datos ni que bus de mensajes usa -- todo se decide en bootstrap.php via variables de entorno.

                     ┌─────────────────────────────┐
                     │          Domain              │
                     │  Todo, TodoId, TodoRepository│
                     │  TodoCreated (domain event)  │
                     └──────────┬──────────────────┘
                                │ interfaces
                ┌───────────────┼───────────────┐
                │               │               │
     ┌──────────▼───┐  ┌───────▼──────┐  ┌─────▼──────────┐
     │  Repository   │  │  Message Bus  │  │   Controller   │
     │  (storage)    │  │  (events)     │  │   (HTTP)       │
     └──────┬───────┘  └──────┬────────┘  └────────────────┘
            │                 │
     ┌──────┴───────┐  ┌─────┴──────────┐
     │ InMemory     │  │ ReactMessageBus│ ← default (framework)
     │ MySQL        │  │ BunnieMessageBus│ ← RabbitMQ
     └──────────────┘  └────────────────┘

Infrastructure Switching

Todo se controla con variables de entorno. Sin ellas, todo corre in-memory:

Variable Que activa
MYSQL_HOST MysqlTodoRepository en lugar de InMemoryTodoRepository
RABBITMQ_HOST BunnieMessageBus en lugar de ReactMessageBus

Ejemplo: solo MySQL, bus in-memory:

echo "MYSQL_HOST=127.0.0.1" > .env
echo "MYSQL_USER=cohete" >> .env
echo "MYSQL_PASSWORD=cohete" >> .env
echo "MYSQL_DATABASE=cohete_skeleton" >> .env
make run

Ejemplo: MySQL + RabbitMQ:

cp .env.example .env
# descomenta las lineas de RABBITMQ
make run

Sin .env: todo in-memory, zero dependencias externas.

Message Bus

El bus de mensajes transporta domain events. Cuando un Todo se crea, el repository publica un TodoCreated event. Los subscribers reaccionan (logear, notificar, lo que sea).

Interfaz comun

Ambas implementaciones cumplen la misma interfaz del framework:

interface MessageBus
{
    public function publish(Message $message): void;
    public function subscribe(string $messageName, callable $listener): void;
}

ReactMessageBus (default, in-memory)

Viene con cohete/framework. Usa EventEmitter + futureTick(). Los eventos viajan dentro del mismo proceso. Si el proceso muere, se pierden. Perfecto para desarrollo y apps simples.

No necesita configuracion. El ContainerFactory del framework lo registra automaticamente.

BunnieMessageBus (RabbitMQ)

Los eventos viajan por AMQP a traves de RabbitMQ. Varios procesos pueden subscribirse al mismo exchange. Si un consumer muere, los mensajes esperan en la cola. Para produccion real.

Se activa poniendo RABBITMQ_HOST en el entorno. El bootstrap sobreescribe MessageBus::class en el container:

if ($useRabbit) {
    $definitions[MessageBus::class] = static fn () => new BunnieMessageBus([
        'host'     => getenv('RABBITMQ_HOST'),
        'port'     => (int)(getenv('RABBITMQ_PORT') ?: 5672),
        'user'     => getenv('RABBITMQ_USER') ?: 'guest',
        'password' => getenv('RABBITMQ_PASSWORD') ?: 'guest',
        'vhost'    => getenv('RABBITMQ_VHOST') ?: '/',
    ]);
}

Como funciona el async (bunny 0.6)

Bunny 0.6 usa React\Socket\ConnectionInterface internamente. El API parece sincrono pero por debajo usa Fibers y el event loop de ReactPHP:

// Parece bloqueante, pero NO lo es:
$client = new Client($options);
$client->connect();           // internamente: await(promesa del handshake AMQP)
$channel = $client->channel();

Cuando llamas a connect():

  1. Abre un socket TCP via ReactPHP (no-bloqueante)
  2. await() suspende la Fiber actual
  3. El event loop procesa el handshake AMQP
  4. La Fiber se resume y connect() retorna

Tu codigo escribe como si fuera sincrono. El event loop sigue vivo procesando HTTP requests mientras tanto.

Para consume(): registra un callback en la conexion. Cada vez que llega un mensaje por el socket, el event loop lo lee, bunny lo parsea, y ejecuta tu callback. No hay polling. El mismo loop que sirve HTTP sirve AMQP.

Flujo de un domain event

POST /todos
    → CreateTodoController
    → Todo::create() graba TodoCreated event en el aggregate
    → Repository::save() hace pullDomainEvents()
    → MessageBus::publish(TodoCreated)
    → [ReactMessageBus]  EventEmitter::emit() en el proximo tick
      [BunnieMessageBus] channel->publish() al exchange "cohete_events"
                         routing key: "domain_event.todo_created"
    → RabbitMQ rutea al queue del subscriber
    → consume() callback → TodoCreatedSubscriber
    → Logger: "Todo created {id, title}"

Gotcha: queueBind en bunny 0.6

La firma es queueBind($exchange, $queue, $routingKey) -- exchange primero. En la mayoria de clientes AMQP es al reves. Si los inviertes, RabbitMQ dice NOT_FOUND - no exchange 'amq.gen-xxxxx'.

MySQL Mode

Se activa con MYSQL_HOST. El bootstrap crea un MysqlClient (react/mysql, async) y registra MysqlTodoRepository:

$definitions[MysqlClient::class] = static fn () => new MysqlClient(
    sprintf('%s:%s@%s:%s/%s', $user, $pass, $host, $port, $db)
);
$definitions[TodoRepository::class] = static function (ContainerInterface $c) {
    return new MysqlTodoRepository(
        $c->get(MysqlClient::class),
        $c->get(MessageBus::class),
    );
};

El schema se crea con schema.sql (auto-loaded por docker compose).

Docker Compose

Levanta la app con MySQL y RabbitMQ:

cp .env.example .env
docker compose up -d

Servicios incluidos:

  • cohete: la app PHP (puerto 8080)
  • mysql: MySQL 8.0 (puerto 3306, schema auto-loaded)
  • rabbitmq: RabbitMQ 3 + management UI (puertos 5672/15672)

MCP (Model Context Protocol)

The skeleton includes MCP so AI agents can interact with your app from day one.

Local (stdio) -- for development, your agent calls your domain directly:

php src/mcp-server.php
Tool Description
list_todos List all todos
get_todo Get a todo by UUID
create_todo Create a new todo
update_todo Update title/completed
delete_todo Delete a todo

Tools live in src/MCP/TodoToolHandlers.php. Add your own by adding methods with #[McpTool] attribute.

Remote (SSE/HTTP) -- integrated into the HTTP server. Same process, same state:

# Connect Claude Code
claude mcp add my-app --transport sse http://localhost:8080/mcp/sse

Create data via MCP, see it in the browser. Same memory, same event loop.

The skeleton ships with batteries included. If you don't need MCP, MySQL, or RabbitMQ, remove them. If you leave them, unused features don't affect performance -- they only load when activated via env vars.

Project Structure

.
├── config/
│   └── routes.json              # HTTP routing
├── public/
│   ├── index.html               # Frontend entry point
│   └── js/components/           # Web Components (vanilla JS, Shadow DOM)
├── src/
│   ├── Bus/
│   │   └── BunnieMessageBus.php # RabbitMQ message bus
│   ├── Controller/              # HTTP request handlers
│   ├── Domain/                  # Entities, Value Objects, interfaces, Events
│   ├── MCP/
│   │   └── TodoToolHandlers.php # MCP tool handlers (shared by all transports)
│   ├── Repository/
│   │   ├── InMemoryTodoRepository.php  # Default (no deps)
│   │   └── MysqlTodoRepository.php     # Async MySQL
│   ├── Subscriber/
│   │   └── TodoCreatedSubscriber.php   # Event handler
│   ├── bootstrap.php            # HTTP server entry point
│   └── mcp-server.php           # MCP stdio server (local dev)
├── schema.sql                   # MySQL schema
├── .env.example                 # Config template
├── docker-compose.yml           # App + MySQL + RabbitMQ
├── Dockerfile                   # Multi-stage production image
├── Makefile                     # Common tasks
└── composer.json                # Dependencies

How to add a new endpoint

  1. Create a Controller in src/Controller/ implementing Cohete\HttpServer\HttpRequestHandler.
  2. Register the Route in config/routes.json.
  3. Register Dependencies in ContainerFactory::create() call in src/bootstrap.php.

Links

License

MIT License - see LICENSE.