A Bit of Context
In my experimenting with code, I often find myself trying to simplify things down to the very essence of the problem I'm trying to solve.
In a world where AI agents write (and review) a LOT of code, developers suffer from decision fatigue, and CTOs brag about "written lines of code per dollar" only to cower when token prices spike, I think there's a need for simplicity. Maybe a small side project, something that makes us developers reconnect with the joy of building stuff, for instance.
Just a month ago, I was planning to write a very simple PWA to track family expenses and host it on my home server. I didn't need to write one from scratch, but I wanted to, just to build something that didn't have "AI-Driven/Based/Powered" in its name.
So there I was, writing some PHP.
Not by hand, of course. I'm not a caveman.
I noticed that even though I wanted a basic web app, I had to fight with the AI's need for unnecessary complexity at every step. So I started chopping away useless stuff, explaining that it's just a simple app that would probably fit inside a single index.php of yore with a couple of includes.
Then it struck me. What if I could write the entire app inside a single index.php while maintaining it as extendable and clean as any microframework?
Turns out I can, leveraging PHP 8+ metaprogramming. As any other PHP developer, I love reinventing the wheel, and so I did. Meet Monad, the monofile PHP microframework.
The Philosophy
Monad is built around a few radical choices:
-
Monofile core: the entire engine lives inside
index.php. Copy it orincludeit, and you have a working app. -
Zero dependencies: if you have PHP 8.0+ and PDO, you're ready. If you use Composer you can
composer create-project heraldoffire/monad my-app, andvendor/autoload.phpis auto-detected and loaded. -
HTMX-first: if you've read my post on the HATE stack, you know I love HTMX. Monad knows when a request comes from HTMX and automatically skips the layout, returning only the requested HTML fragment. You can also send commands to the frontend via HTMX response headers directly from your controller.
Of course, you don't need to add code to index.php. Monad's router supports controllers/middlewares, and routes can be registered in a dedicated file and then included in the framework. You can structure your application as you see fit, Monad will not complain.
Monad\MagicObject
Scary name for a class, I know. MagicObject is the heart of Monad: a DI container where every closure is automatically bound to the app context. Every service, $app->db, $app->session, $app->response is a MagicObject. You register a factory and Monad calls it once, caching the result:
$app->bind('notify', fn() => new NotificationService('smtp.example.com'));
// Now you can use it anywhere
$app->notify->send('Welcome!');
All services are singletons by default. If you need to force re-resolution, call $app->reset(). This is especially useful in tests.
For type-safety purists, Monad also exposes $app->get(ClassName::class) with generic support for IDE autocompletion.
Templates with Auto-Escaping
We don't need a templating engine, as PHP already IS a templating engine of sorts. But we need it to be secure: data passed to the view is recursively wrapped in ViewContext, which applies automatic escaping and offers fluent helpers:
// In your controller
$app->response->layout = 'html.layout';
$app->response->render('html.pages.home', [
'user' => ['name' => 'Sam', 'balance' => 1250.50],
'items' => [['name' => 'Pizza', 'price' => 12]]
]);
<!-- In the template -->
<h1>Welcome, <?= $view->data->user->name ?></h1>
<p>Balance: <?= $view->data->user->balance->number(2) ?> €</p>
<ul>
<?php foreach ($view->data->items as $item): ?>
<li><?= $item->name ?>: <?= $item->price->number(2) ?> €</li>
<?php endforeach; ?>
</ul>
No manual escaping: MagicValue's __toString() handles that automatically. And you get helpers like ->number(2), ->date('Y'), ->json(), ->raw(), and ->default($fallback) right in the template.
Routing and Onion Middleware
The router supports dynamic URL parameters (/users/:id/posts/:postId) and grouped middleware:
$app->group('/admin', ['auth'], function ($app) {
$app->addRoute('GET', '/dashboard', [DashboardController::class, 'index']);
$app->addRoute('POST', '/users', [UserController::class, 'store']);
});
Middleware follows the Onion pattern: each middleware wraps the next, executing code both "on the way in" and "on the way out." Here's a simple logger middleware:
$app->addRoute('GET', '/api/data', [ApiController::class, 'index'], [
function ($app, $next) {
$start = microtime(true);
$next($app);
$ms = number_format((microtime(true) - $start) * 1000, 2);
error_log("{$app->request->method} {$app->request->path} → {$app->response->statusCode} ({$ms}ms)");
}
]);
Execution order for a pipeline [mw1, mw2] is: mw1_in → mw2_in → handler → mw2_out → mw1_out.
Database
Monad includes a PDO/SQLite-based database layer. The methods are designed to avoid tedious SQL boilerplate:
// Read
$user = $app->db->fetchOne('SELECT * FROM users WHERE id = :id', ['id' => 1]);
// Insert (returns the ID)
$id = $app->db->insert('users', ['username' => 'mario', 'role' => 'admin']);
// Update
$app->db->update('users', ['role' => 'user'], ['id' => $id]);
// Delete
$app->db->delete('users', ['id' => $id]);
There's also built-in protection against SQL injection in column and table names.
CLI and Commands
Monad isn't just HTTP. If you run php index.php from the terminal, the app switches to CLI mode and routes the requested command:
$app->addCommand('migrate', function ($app, $args) {
echo "Running migrations...\n";
$app->db->execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY)");
echo "Done!\n";
});
php index.php migrate
# or
./monad migrate
Built-in Testing Engine
Monad ships with an extremely simple built-in test runner. Tests run via the CLI and each case executes with a fresh container reset, ensuring complete isolation:
$test->beforeEach(function ($app) {
$app->config->db = ['path' => ':memory:']; // in-memory DB for tests
});
$test->it("The container acts as a singleton", function ($app, $test) {
$app->bind('rand', fn() => rand(1, 1000000));
$test->expect($app->rand)->toEqual($app->rand);
});
The assertion API is fluent: ->toEqual(), ->toBeTrue(), ->toContain(), ->toBeInstanceOf().
You can also simulate in-process HTTP requests:
$response = $test->request('GET', '/users/42');
$test->expect($response->statusCode)->toEqual(200);
Running tests:
./monad test # runs tests.php
./monad test tests/ # recursively finds *Test.php
./monad test tests/UserTest.php # single file
Embedding
This is, to me, a great feature: if you require index.php, Monad does not auto-dispatch. Instead, it returns the $app instance, letting you use the framework as a library or service container in standalone scripts or legacy projects:
// cron.php
$app = require 'index.php';
$inactive = $app->db->fetchAll("SELECT * FROM users WHERE active = 0");
echo "Found " . count($inactive) . " inactive users.";
AI Agent Support
Monad ships with first-class support for AI coding agents. Inside the repo you'll find a monad skill for AI agents that explains how to work with the framework: architecture, conventions, CLI commands, and gotchas.
The AGENTS.md at the root does the same for human-readable context. The idea is that an agent can be dropped into a Monad project and immediately understand how to add routes, write templates, run tests, or debug DI issues without hallucinating Laravel patterns into your code.
Who's this for
Monad is an exercise in minimalism. It proves that with less than 1000 lines of PHP you can have routing, dependency injection, CSRF protection, sessions, auto-escaping templates, database abstraction, and a testing engine all with an ergonomics designed for HTMX. That said, Monad is experimental. It's perfect for:
- Rapid prototyping: a working app in minutes, no build tool configuration or massive vendor directories.
- HTMX apps: if you love the hypermedia-driven approach, Monad gives you a backend that respects it.
- Learning: a great way to study PHP metaprogramming, closures, magic methods, and architectural patterns under the hood.
- Micro-apps and hackathons: when you need a router, a query builder, and a template engine all in one file
I wouldn't use it for enterprise monoliths with thousands of strictly-typed files. MagicObject's dynamic flexibility trades static analysis away, and that's a conscious choice.
Talk is cheap, show me the code
Monad is on GitHub and it's open source (MIT). Have fun using it!