alexbilbie/proton
Proton is a lightweight PHP micro-framework by Alex Bilbie for building small web applications and APIs. It provides simple routing and a minimal, extensible structure, making it a handy Composer-installable starting point for tiny projects.
Proton is a micro framework in the truest sense: a thin Application object that wires together Symfony's HttpFoundation, FastRoute, a Pimple-style DI container, and StackPHP middleware. There is no CLI, no generator, no config DSL — the framework is a handful of classes you can read end-to-end in under an hour. That is the first thing to do.
Minimal bootstrap:
composer require alexbilbie/proton
// public/index.php
require __DIR__ . '/../vendor/autoload.php';
$app = new Proton\Application();
$app->get('/', function ($request, $response) {
$response->setContent('Hello world');
return $response;
});
$app->run();
Point nginx/Apache at public/index.php as the single front controller. run() builds a Request from globals, dispatches through your middleware stack, executes the matched handler, and send()s the Response.
Where to look first, in order:
src/Proton/Application.php — routing methods (get, post, put, delete, any), register(), addMiddleware(), run()./users/{id}) and how parameters reach you ($request->getAttribute('id')).First real use case: a small JSON API. One route, one closure, return a Symfony\Component\HttpFoundation\JsonResponse (or $response->headers->set('Content-Type', 'application/json') plus setContent(json_encode(...))). That gets you from zero to a working endpoint in about ten lines, which is exactly the niche this package fills.
Routing. Verb methods take a pattern and a callable. Handlers receive (Request $request, Response $response) and must return a Response. Named placeholders are read from request attributes rather than method arguments, so controllers stay decoupled from the router. Group route definitions into routes/api.php files that accept $app and are required from the bootstrap — the framework gives you no structure, so impose your own early.
Controllers as invokables. Closures stop scaling at roughly five routes. Move each handler into an invokable class (__invoke(Request $request, Response $response): Response), register it in the container, and pass the service name or a [$app->container, 'name']-style callable to the route. This is where you get testability and constructor injection for free.
Container registration is your service layer. Register the database connection, Twig, Monolog, and any repositories as lazy closures. Because resolution is lazy, a closure that captures $app is fine as long as it doesn't touch services at registration time. Override services in tests by re-registering the same key before dispatching.
Middleware is StackPHP. $app->addMiddleware(new SomeMiddleware) wraps the kernel; signature is (Request $request, callable $next) returning a Response, with $next($request) continuing the chain. Order matters — the first-added middleware is the outermost layer. Use it for:
Throwable and converts it to a 500 JSON/HTML response (do this first, before anything else can throw);Keep middleware single-purpose; there is no priority system, only insertion order.
Config. Pass an array to the Application constructor (or set container parameters). Read environment from getenv() in the bootstrap and pick config/dev.php vs config/prod.php. Never hardcode credentials in route files.
Testing. Because everything is HttpFoundation, you can skip HTTP entirely: build a Request::create('/users/1', 'GET') and invoke the kernel's handle() (or call the app's dispatch method) directly inside PHPUnit. Assert on status code and body. This is the cheapest win available — do it before writing a single integration test through a web server.
Splitting the app. A common shape: bootstrap.php returns a configured $app; public/index.php requires it and calls run(); tests/ requires the same bootstrap and calls handle() instead. One wiring path, two entry points.
print, or falling off the end of a handler yields a blank or fatal response with no useful error. Every branch of every handler needs an explicit return.run() obviously does nothing; registering services after they are first resolved also does nothing, because Pimple caches instances once created.var_dump() inside a handler writes before output buffering kicks in and can corrupt the response. Prefer error_log(), or a Monolog-backed logger pulled from the container. The Xdebug stack trace from an uncaught exception is more informative than anything the framework prints.Application to add your own route helpers or default middleware, decorate the container, and swap the dispatcher. Do not patch files in vendor/ — wrap or subclass instead, or you lose the ability to update at all (which, given the project's state, may be the only update you ever get).composer audit and composer outdated yourself on a schedule. Treat every transitive dependency as yours to maintain.How can I help you explore Laravel packages today?