spiral/framework
Spiral is a high-performance, long-living full-stack PHP framework built on RoadRunner. It ships 60+ PSR-compatible components with resident-memory workers, native queue support, GRPC, WebSockets, and anti-memory-leak tools.
Frame it correctly first. spiral/framework is not a Laravel package you composer require into an existing app — it is a competing full-stack framework with its own container, HTTP kernel, PSR-7/15 stack, ORM bridge, and queue/worker model. The realistic day-to-day use is: you are starting a new service (API, gRPC, queue consumer, CLI worker) and choosing Spiral instead of Laravel/Symfony. If you try to install it alongside Laravel you will fight duplicate PSR implementations and two containers.
Minimal path to a running app:
composer create-project spiral/app my-service
cd my-service
vendor/bin/rr get-binary # fetch the RoadRunner application server
./rr serve # or: php app.php serve
php app.php # lists console commands (no artisan here)
Use spiral/app (web/api/cli skeleton variants) rather than hand-assembling spiral/framework — the skeleton wires the Kernel, RoadRunner config (.rr.yaml), dotenv, and bootloader discovery for you.
Where to look first, in order:
app/src/Kernel.php — extends Spiral\Framework\Kernel; this is where app bootloaders are declared (defineAppBootloaders()).app/src/Bootloader/ — your ServiceProvider equivalent. Everything (config, bindings, routes, middleware, commands) is registered from a bootloader.app/config/*.php — plain PHP arrays consumed via Spiral\Config\ConfigManager; env vars come in through the DotEnv bootloader and AppEnvironment enum.vendor/spiral/framework/src/ for a component you don't understand — the source is the real documentation for anything not covered on spiral.dev/docs.First use case — ship one JSON endpoint end to end. This exercises the four things you'll do daily: route → filter (request validation) → injected service → response.
#[Route(uri: '/api/users', methods: ['POST'])]
final class CreateUserAction
{
public function __invoke(CreateUserFilter $filter, UserService $users): array
{
return ['id' => $users->create($filter)->id];
}
}
final class CreateUserFilter extends Filter
{
#[NotBlank] public string $name = '';
#[Email] public string $email = '';
}
Register UserService in a bootloader, run php app.php route:list to confirm the route, and curl it. Once that round trip works, the rest of the framework's surface area is variations on the same theme.
Map your Laravel reflexes onto Spiral's vocabulary before writing code:
| Laravel | Spiral |
|---|---|
| ServiceProvider | Bootloader (defineBindings, defineSingletons, init, boot, start) |
Facade / app() |
Constructor & method injection from the PSR-11 container |
| Artisan | php app.php (Spiral\Console, #[AsCommand]) |
| Eloquent | Cycle ORM via spiral/cycle-bridge (data mapper, explicit mapping) |
| Blade | Stempler or Twig bridge |
php artisan make:* |
Scaffolder commands (create:controller, create:bootloader, …) |
| HTTP middleware | PSR-15 middleware, registered per route group or globally |
| Horizon / queue workers | RoadRunner workers + spiral/queue (AMQP, SQS, Beanstalk, Kafka) |
Bootloader-centric composition. Bindings, config, routes, middlewares, and console commands all get declared in bootloaders rather than in a monolithic provider. Prefer defineSingletons() over the class constants when possible; use bootstrap/init for ordering-sensitive work. Since 3.10 you have BootloaderRegistryInterface when you need explicit ordering control instead of relying on discovery order.
Interceptors over base-controller inheritance. Since 3.14 the spiral/interceptors package replaces the deprecated spiral/hmvc. If you're following an older tutorial that has controllers extending CoreController for before/after hooks, translate that into a route interceptor — it composes better and works identically for HTTP, console, and queue targets.
Request-scoped values via method injection, not constructors. ServerRequestInterface, auth context, session, and input manager live in the http scope. Inject them into the controller action or middleware process() signature; injecting into a singleton's constructor is the number-one source of "works in dev, throws in prod" scope errors.
Queue-oriented design. Jobs are handlers with an attribute and a typed payload; dispatch by name/payload through QueueManagerInterface. Since 3.13 there's Spiral\Queue\TaskInterface / Task carrying the envelope, and RetryPolicyInterceptor (3.9) gives you configurable retries without hand-rolling a supervisor. Build queue consumers as separate RoadRunner workers in .rr.yaml — that's the framework's real performance story.
Cycle ORM discipline. It's a data mapper, not an Active Record. You define entities + mapping (#[Entity], #[Column], relations), generate migrations from the mapping diff, and drive persistence through EntityManagerInterface (persist / run / clean) with explicit load()/select() for relations. No lazy magic means fewer N+1 surprises and more upfront mapping work. Budget a real learning day here; it's the biggest delta from Eloquent.
Cross-cutting concerns. Telemetry (OpenTelemetry spans) and Monolog are first-class; DebugConfig (3.11) lets you register tags/collectors without touching the debug bootloader. MONOLOG_FORMAT configures log format via env. Treat observability as an install-time decision, not a retrofit.
ServerRequestInterface only resolves inside the http scope (since 3.14.6 it's always a proxy there). Use method injection, or Spiral\Core\Scope (public since 3.15.3) for manual scope boundaries in workers and CLI. Introspector and proxy recursion have real failure modes — read the RecursiveProxyException message rather than guessing.allowSingletonsRebinding; setting it to false today avoids a migration later when the default flips. Likewise, SingletonInterface is deprecated in favor of #[Singleton] (3.12) and will be removed in v4 — don't write new code with the interface.Spiral\Http\Pipeline is deprecated (3.15.0) in favor of LazyPipeline, which resolves middleware inside the correct scope. If middlewares behave differently from what you expect on older tutorials, this is why.Path attribute (3.17.0), auth config replacement (3.14.7), singleton rebinding in telemetry (3.15.8). Pin a minor and upgrade deliberately.rr binary, spiral/roadrunner-bridge, and the framework all move independently. Use rr get-binary per project and commit .rr.yaml; a mismatched server binary produces confusing boot errors rather than clean messages.php app.php route:list / the scaffolder info command / Spiral\Debug collectors, plus reading vendor/spiral/framework/src and the container's resolving trace (improved in 3.15.6–3.15.7 — the traces are genuinely useful, so read them fully).route:list crashed on LINK/UNLINK verbs until 3.17.0 — if you're on an older minor and your introspection commands misbehave, check the changelog before debugging your own routes.create:* commands generate controllers, bootloaders, middleware, and commands directly into app/src. Run them on a clean git working tree, and read the output — they encode the current idioms, which doubles as the fastest way to learn them.How can I help you explore Laravel packages today?