HTTP kernel
Application, Router, Container, HTTP-Emitter and Contracts: a PSR-15 pipeline, compiled routes and a PSR-11 container with autowiring.
Kernel packages
A router, a container, request DTOs, repositories and more, each one independent and replaceable. Use one, or all of them together.
Start from one of the two starters. Each is a working application with a module to copy, tests and static analysis.
composer create-project solophp/skeleton my-app
cd my-app
composer serve # http://127.0.0.1:8000composer create-project solophp/inertia-svelte-skeleton my-app
cd my-app
npm install
composer serve # http://127.0.0.1:8000
npm run dev # in a second terminal: Vite with hot reloadBoth need PHP 8.4 and Composer, and the Inertia starter also needs Node.js. Getting Started walks through the JSON API starter, from the install to a module of your own with its tests; Inertia Starter covers the other one.
Or take a single package into any project:
composer require solophp/routerNot a framework. There is no core that every package depends on. Each package does one job and installs on its own. The starters are ordinary applications that put the packages together, and their code is yours to change.
PSR at the seams. The packages meet at PSR interfaces: PSR-7 and PSR-17 for HTTP messages, PSR-11 for the container, PSR-15 for middleware, PSR-3 for logging, PSR-14 for events, PSR-16 for caching and PSR-18 for HTTP clients. Where PSR has no interface, solophp/contracts adds one, such as a writable container or a validator. Any package can be replaced by another implementation of the same interface, and any PSR library works beside them.
Explicit code. Providers, middleware and routes are plain PHP files you read from top to bottom. A request becomes a typed, immutable DTO, a repository returns your own readonly models, and the starters pass PHPStan at level max.
One request through the JSON API starter:
public/index.php
└─ Application PSR-15 kernel; service providers fill the Container
├─ ErrorMiddleware your code: every exception below becomes a JSON error
├─ JsonBodyMiddleware your code: a JSON body becomes getParsedBody()
└─ RoutingMiddleware Router matches the route, or throws a 404 / 405
└─ TaskController built by the Container, dependencies injected by type
├─ InputFactory Input + Validator: the request becomes a TaskStoreInput, or throws a 422
└─ TaskRepository Repository on Doctrine DBAL: rows become Task models
→ the PSR-7 response, sent by HTTP-EmitterEvery name in that tree is a class you can open: the middleware and the controller live in the application, the rest in the packages.