Skip to content

Repository files navigation

kore/framework

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.

Why another one

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.

Install

composer require kore/framework

The 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.

Bootstrap

<?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.

What is in the box

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.

Extension points

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 needs process().
  • ErrorReporter — receive errors caught by the dispatcher and view layers.
  • Psr\Log\LoggerInterface — everything that logs takes one, and defaults to NullLogger.
  • 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.

Documentation

  • 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.

Development

make test      # phpunit
make analyse   # phpstan, level 5 on src
make fix       # php-cs-fixer, PSR-12
make validate  # all three, as CI runs them

guidelines.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.

Licence

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.

About

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.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages