A small HTTP framework for PHP 8.4+. It hands you a Request, routes it to a
callable, and renders whatever that callable returns. Roughly 5,000 lines,
no compile step, no configuration DSL, no annotations.
It exists to fill exactly one box in the architecture described in Nothing Shared, Everything Gained — the box labelled "The HTTP Framework":
HTTP ──► Controller ──► Service ──► Gateway ──► Storage
(translate) (logic) (access)
▲
└── this package stops here
Your Controllers translate requests into Data Objects and results into responses. Your Services hold logic and no state. Your Gateways hide storage. None of them need to know this package exists — only the thin Controller layer touches it. That is the whole design goal: the framework is replaceable, because almost none of your code imports it.
Because the Pareto point is much earlier than most frameworks assume.
$request->json is a decoded object. $request->variables is an array of
route matches and query parameters. There is no HeaderBag, no
ParameterBag, no InputBag. Repeated HTTP headers are not modelled, because
almost nobody needs them, and modelling them would cost every reader of every
controller a layer of indirection forever. When one choice is simpler and
covers the cases you actually have, the simpler choice wins. Where the
simple choice turns out to be wrong for you, the abstract base classes below
let you replace the piece rather than fight it.
The result is a framework you can read end to end in an afternoon, and whose overhead per request is a handful of object constructions, or about 2ms.
The idea is not new: a predecessor was
Qafoo/REST-Micro-Framework,
and the Request, Router, Dispatcher and View split here is a
descendant of the one demonstrated there.
composer require kore/frameworkThe core needs only psr/log. The optional pieces — Database needs
ext-pdo, Mailer\Lettermint needs the Lettermint client — declare their
own dependencies under suggest; install those only if you use them.
<?php
use Kore\Framework;
require __DIR__ . '/../vendor/autoload.php';
$container = new Framework\Container();
$container->register('config', fn () => new Framework\Configuration(__DIR__ . '/../'));
$container->register('taskManager', fn () => new Framework\DeferredTaskManager());
$container->register('userController', fn ($c) => new My\Controller\User($c->get('userService')));
// Decides what an error may tell the client: everything in development,
// only the framework exceptions everywhere else.
$errorMapper = Framework\ErrorMapper::fromConfiguration($container->get('config'));
$router = new Framework\Router\Regexp([
'(^/users$)' => [
'GET' => $container->lazy('userController', 'list'),
'POST' => $container->lazy('userController', 'create'),
],
'(^/users/(?P<id>[^/]+)$)' => [
'GET' => $container->lazy('userController', 'get'),
],
]);
$dispatcher = new Framework\Dispatcher\Simple(
$router,
new Framework\View\Json($errorMapper),
$container->get('taskManager')
);
$dispatcher->dispatch(new Framework\Request());A controller is a plain class. It receives the Request and returns a Data
Object — the view turns that into a response:
namespace My\Controller;
class User
{
public function __construct(
private \My\Service\User $users,
) {}
public function get(\Kore\Framework\Request $request): \My\Domain\User
{
return $this->users->load($request->variables['id']);
}
}Throwing Kore\Framework\Exceptions\NotFoundException produces a 404,
BadRequestException a 400, and so on. There is no error handling in the
controller because there is nothing useful for it to do.
| Component | What it does |
|---|---|
Request |
Struct-like access to the request. Properties resolve lazily through Request\PropertyHandler implementations. |
Router |
Abstract. Router\Regexp maps method + path regexp to a callable; Router\AuthenticatedRegexp adds middleware groups. |
ErrorMapper |
Maps errors to a status code and to the message the client may see. Opt in to full detail for development. |
Dispatcher |
Abstract. Dispatcher\Simple routes, calls, renders; Dispatcher\ErrorReporting adds error reporting. |
View |
Abstract. View\Json, View\Html, View\XML, and View\AcceptHeaderViewDispatcher to pick one by Accept. |
Response |
For the cases where a controller must control the response itself. |
Container |
A lazy service registry. lazy() defers controller construction until a route actually matches. |
Configuration |
Layered INI files: .env, overridden by .env.prod or .env.local. Read lazily, read-only, any section your files define. |
Database |
A thin PDO wrapper returning iterable statements. Your Gateways use this; your Services do not. |
Middleware |
Bearer-token authentication, roles, shared link tokens, CORS. |
Mailer |
Abstract, with a Lettermint and a Mock implementation. |
DeferredTaskManager |
Runs work after the response has been sent to the client. |
The framework describes what it needs as an interface or an abstract class and lets you supply the implementation — the ports of a (very modest) hexagon:
Middleware\TokenVerifier— verify a bearer token. Issuing, refreshing and revoking tokens is your application's business, not the framework's.Middleware\SharedTokenVerifier— verify the token of a shareable link. Which resource it unlocks, and where those tokens live, is yours.Middleware\AuthMiddleware— authenticate however you like: a session, an OAuth redirect, a signature. The router only needsprocess().ErrorReporter— receive errors caught by the dispatcher and view layers.Psr\Log\LoggerInterface— everything that logs takes one, and defaults toNullLogger.Router,Dispatcher,View,Mailer,Request\PropertyHandler— abstract classes with usable implementations included. Replace one without touching the rest.
Nothing here uses a service locator, a compiler pass or reflection to find your code. You wire it up in the bootstrap, in plain PHP, where you can read it. And nothing here defaults to somebody else's infrastructure: buckets, endpoints, origins, template directories and mail variables are required arguments, so a missing value is a startup error rather than a surprise.
- Bootstrap — a complete container, routes and front controller at the size a real application reaches, plus deferred work and profiling.
- Authentication — bearer tokens, roles, shareable link tokens, and writing your own session or OAuth middleware.
- Views — one format or several, deriving templates from the route, and owning the response.
- Errors — what the client is told, how to see everything in development, and where the detail goes instead.
- Configuration — layered INI files, your own sections, and wiring mail.
- Development guidelines — what belongs in this package, what shape it takes, and what we refuse to do. Read this before opening a pull request.
make test # phpunit
make analyse # phpstan, level 5 on src
make fix # php-cs-fixer, PSR-12
make validate # all three, as CI runs themguidelines.md describes how this package is built — the rules from Nothing Shared, Everything Gained applied to the framework layer itself, including the deviations we know about.
EUPL-1.2. Copyright the contributors.
The EUPL is a copyleft licence: if you distribute a modified version, or provide it as a network service, the source must be available under the EUPL or a compatible licence (GPL, AGPL, MPL, LGPL, EPL and others are listed in the appendix). Using the framework in your own application does not make your application a derivative work.