pinatra/framework
Pinatra Framework is a Laravel-based foundation for building modular PHP applications. It offers reusable components and conventions for structuring code, extending functionality, and maintaining web projects, helping teams develop faster with a consistent architecture.
Pinatra is a single-class, static-DSL micro-framework. If you think "Laravel route closures without the container," you have the right mental model.
Install:
composer require pinatra/framework
Minimal app (front controller index.php):
require __DIR__ . '/vendor/autoload.php';
use Pinatra\Pinatra as P;
P::get('/', function () {
return 'Hello world';
});
P::get('/hello/:name', function ($name) {
return "Hello $name";
});
P::run();
Point your web server's rewrite rules at index.php (Apache .htaccess or an nginx try_files $uri /index.php?$query_string;), and you have a working app. P::run() must be the last statement — it dispatches the current request and emits the response.
Where to look first:
README.md on the repo — the canonical syntax reference; it is short and worth reading end-to-end.src/Pinatra/Pinatra.php — the whole framework is essentially this one file. Skim it once; you'll understand every behavior from it.tests/ — the most reliable documentation of edge cases (route params, filters, view rendering).First useful use case: a small JSON endpoint or internal webhook receiver. Pinatra is strongest when you need 50–500 lines of routing and a couple of templates, deployed as a standalone vhost, without pulling in a full framework.
Day-one checklist:
P::config('views', __DIR__ . '/views') before your first P::render().P::config('environment', 'development') or 'production') so error output is sane.P::notFound() and P::error() handlers immediately — leaving them default is the fastest way to leak stack traces in production.Verbs and parameters. Standard Sinatra-style routing:
P::get('/users/:id', function ($id) { /* ... */ });
P::post('/users', function () { /* ... */ });
P::put('/users/:id', function ($id) { /* ... */ });
P::delete('/users/:id', function ($id) { /* ... */ });
Matched segments are passed positionally to the closure. Wildcards (/files/*) pass the remainder.
Filters as middleware. Pinatra has no middleware stack; before/after filters are the substitute. Use them for auth, headers, and request logging:
P::before(function () {
header('X-App: my-app');
});
P::before('/admin/*', function () {
if (!isset($_SESSION['user'])) {
P::halt(401, 'Unauthorized');
}
});
P::after(function () {
// runs after the route; good for cleanup/logging
});
Filters can be pattern-scoped (P::before('/admin/*', ...)) and run in registration order — treat that ordering as your middleware pipeline.
Control-flow primitives. These are the three you'll reach for daily:
P::halt($status, $body) — stop immediately and emit a response. This is your abort().P::redirect($url, $status = 302) — must be followed by return; if anything comes after it.P::pass() — decline the current route and let routing continue. Useful for conditional/fallback handlers.Views. Enable rendering by configuring a views directory, then render templates with data:
P::config('views', __DIR__ . '/views');
P::get('/profile/:id', function ($id) {
return P::render('profile.php', ['id' => $id]);
});
Return the rendered string from the route closure (or echo it) — Pinatra does not auto-wrap output. There is no Blade; templates are plain PHP. Escape output yourself (htmlspecialchars($x, ENT_QUOTES, 'UTF-8')); there is no auto-escaping safety net.
Config and settings. P::config($key, $value) sets values consumed by the framework (views path, environment). Keep app-specific constants in your own config.php required at bootstrap rather than overloading Pinatra's config namespace — it's untyped and string-keyed.
Custom helpers. Register reusable closures and call them through the facade:
P::helper('json', function ($data, $status = 200) {
http_response_code($status);
header('Content-Type: application/json');
return json_encode($data);
});
P::get('/api/ping', function () {
return P::json(['ok' => true]);
});
Helpers are the idiomatic extension point — use them instead of global functions.
Error handling. Map these once at bootstrap:
P::notFound(function () {
return P::render('404.php');
});
P::error(function ($e) {
error_log($e->getMessage());
return P::render('500.php');
});
Embedding in a larger app. Because Pinatra uses static global state and dispatches the request itself, the practical integration is a separate front controller (a subdirectory or dedicated vhost) rather than trying to mount it inside an existing Laravel/Symfony kernel. If you must share auth, share the session and a small user-lookup helper, not the frameworks' internals.
Testing. There is no request/response object to inject, so test by driving the front controller. A pragmatic pattern is to extract route registration into routes.php, require it in both index.php and tests, then exercise endpoints over HTTP via curl/Guzzle against a PHP built-in server, or capture P::run() output with output buffering and set $_SERVER['REQUEST_METHOD'] / REQUEST_URI before dispatch.
Suggested project layout:
public/index.php # bootstrap, config, P::run()
app/routes.php # all route registrations
app/helpers.php # P::helper definitions
app/filters.php # P::before / P::after
views/ # plain PHP templates
each()-era idioms may surface as deprecations or warnings. Pin it in composer.json, run the test suite on your target PHP version before committing, and be prepared to patch Pinatra.php locally if needed.Pinatra is a static facade with process-wide state. You cannot run two apps in one process, and tests can bleed configuration between cases. Isolate tests in separate processes or reset static state in tearDown by reflecting on the class if you need to reconfigure.P::run() is terminal and non-idempotent. Call it once, last. Calling it twice, or calling it from inside a route closure, will produce confusing behavior./users/new) before parameterized ones (/users/:id) or the parameterized route will swallow them.P::config vs P::set vs P::helper. The README uses config for framework settings. Don't assume any of these are validated — typos in keys fail silently. Confirm the exact method names for your pinned version against Pinatra.php rather than trusting blog posts, since forks of the original Breeze API drifted.return) and enforce it in code review.redirect() does not exit. Always return; after it, otherwise subsequent code in the closure keeps running.halt() inside a before filter is the correct way to reject a request. Do not exit() — you'll skip after filters and your cleanup.development so errors are visible; in production, register P::error() and log via error_log() or your PSR-3 logger, because Pinatra has no built-in logging channel. When routing "fails," check the rewrite rules and REQUEST_URI before suspecting the framework — misconfigured path info is the most common cause of 404s.session_start() before P::run() if you need sessions. Because routes can emit output, any code that sets headers must run before any echo.How can I help you explore Laravel packages today?