mikecao/flight
Flight is a fast, simple, extensible micro-framework for PHP. It offers a lightweight routing core with a RESTful router, view rendering, and optional extendable components, making it a good fit for small apps and APIs that don't need a full-stack framework.
Flight (flightphp/core) is a micro-framework, not a Laravel component — treat it as the thing you reach for when a Laravel app is overkill (a webhook receiver, an internal tool, a tiny JSON API, a queue-adjacent side service). It has no container magic, no facades beyond a static Flight class, and no ORM.
Minimal setup:
composer require flightphp/core
// public/index.php
require __DIR__ . '/../vendor/autoload.php';
Flight::route('GET /health', fn() => Flight::json(['ok' => true]));
Flight::route('GET /users/@id', function ($id) {
Flight::render('users/show', ['id' => $id]);
});
Flight::start();
Where to look first:
Flight::route() / Flight::start() — the entire request lifecycle lives here.Flight::set('flight.views.path', ...) and flight.views.extension — the only view config most projects touch.Flight::render($template, $data, $key) — PHP-file templates by default, rendered into $data[$key] for your layout.Your first real use case is almost always rendering a template from a controller-ish closure. In v3.19.4 that path resolution gained an opt-in security boundary:
Flight::set('flight.views.path', __DIR__ . '/../views');
Flight::set('flight.views.restrict_to_path', true); // new in 3.19.4
Turn it on the day you create the project, not later — see Gotchas.
Keep routing closures thin. Flight has no autowiring, so the idiomatic pattern is a small bootstrap.php that wires services into Flight::set() / Flight::register() and closures that pull them back out. Pair this with PSR-4 autoloading for your own classes; Flight deliberately gives you nothing here.
View rendering. Default engine is plain PHP:
Flight::set('flight.views.path', __DIR__ . '/../views');
Flight::set('flight.views.extension', '.phtml');
Flight::route('GET /orders/@id', function ($id) {
Flight::render('orders/show', ['order' => fetch_order($id)], 'content');
});
// views/orders/show.phtml receives $order AND $content (nested render target)
Security boundary is a config flag, so make it part of your app skeleton. The v3.19.4 addition is best consumed as: set flight.views.restrict_to_path = true in your bootstrap alongside flight.views.path, and write an integration test that asserts a traversal attempt (Flight::render('../../composer.json')) fails. That test is your regression guard if the flag is ever dropped during a refactor.
Validate template names at the edge when they're user-influenced. If you do Flight::render("emails/$type") where $type comes from a query string, restrict_to_path is now a second line of defence — but an allowlist is still the first:
$allowed = ['welcome', 'reset', 'receipt'];
if (!in_array($type, $allowed, true)) { Flight::halt(404); }
Integration with a Laravel codebase. The realistic pattern is a sub-application: mount Flight behind a Laravel route or deploy it as a separate vhost sharing a database. Flight boots its own error handler and output buffering, so don't call Flight::start() inside a Laravel controller without ob_start() and careful teardown — you'll fight Laravel's middleware and whoops handler. Prefer a separate deployable, or a Laravel Route::any('/micro/{any?}')->where('any', '.*') passthrough only if you genuinely need URL continuity.
Testing. No test harness ships. Use PHPUnit with Flight::set('flight.engine', ...) overrides, or just boot the app in a subprocess and hit it with curl/HttpClient. For view tests, assert on rendered output of a known template rather than on Flight internals.
restrict_to_path is opt-in and defaults to off. This is the single most important thing about 3.19.4: existing apps are unaffected, which also means nothing is fixed until you flip the flag. Audit any app where template names are partially user-derived.
Enabling it can break legitimate layouts. If your skeletons share templates via absolute paths, symlinked directories, or a views/shared outside flight.views.path, those renders will now fail. Common escape hatches: keep everything under one views root, or resolve shared partials by absolute path through a path you explicitly add to the views path. Test render-heavy pages immediately after enabling.
Symlinks are the classic surprise. With restriction on, a views directory that is a symlink or that contains symlinked subdirectories may not resolve the way you expect depending on how the realpath comparison is done. If templates disappear, readlink -f your views path first.
Config keys are namespaced strings, no typo protection. Flight::set('flight.views.restrict_to_path', true) silently does nothing if misspelled — there's no schema. Keep a single config.php returning an array and loop it into Flight::set() so typos surface in one place.
render() vs view()->render() — pick one. Flight::render() echoes/assigns; Flight::view()->render($template, $data) returns a string. Mixing them inside a layout is a common source of double output.
Error handling differs from Laravel. Flight::halt() and Flight::error() replace exceptions-as-control-flow. Register Flight::map('error', ...) early, and remember Flight's default error page is verbose — set flight.debug to false in production or you'll leak stack traces.
Extension points worth knowing: Flight::map() to override built-ins (e.g. replace the view engine with Twig/Blade via Flight::register('view', ...)), Flight::before()/Flight::after() for cross-cutting concerns, and Flight::register() for a poor-man's container. If you find yourself building more than two of these, that's the signal to use Laravel instead.
Version pinning. Flight is pre-1.0-style in its 3.x line with frequent point releases; pin with ^3.19.4 and read the changelog before bumping — there is no deprecation policy equivalent to Laravel's.
How can I help you explore Laravel packages today?