sapling / core
A small PHP framework for prototypes, one-off projects and internal tools, with no dependencies and a core you can read in a day.
Requires
- php: >=8.4
Requires (Dev)
- carthage-software/mago: 1.57.0
- pestphp/pest: ^5.3
Suggests
- ext-curl: for Sapling\Client\HttpClient
- ext-pdo: for Sapling\Db and the migrations commands, with your database's driver
- ext-pdo_sqlite: for Sapling\Db::sqlite()
- ext-session: for Sapling\Http\Session and the Csrf middleware
- ext-sodium: for Sapling\Crypto
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-01 20:14:31 UTC
README
Sapling is a small PHP framework for prototypes, one-off projects and internal tools. It has no runtime dependencies, and you can read the whole core, about 2,100 lines of code, in a day.
Each action or command is one class in one file, with an attribute that declares its route or command name:
namespace App; use Sapling\Attributes\Route; use Sapling\Http\Request; use Sapling\Http\Response; #[Route('GET', '/hello/:name')] final class Hello { public function __invoke(Request $request): Response { return Response::text("Hello {$request->param('name')->string()}"); } }
Status
Sapling hasn't been tested in production environments yet. Use it at your own risk.
Sapling was built with AI assistance, and its author reviewed all of the code.
Getting started
Start a new app from the skeleton:
composer create-project sapling/skeleton:dev-main my-app
cd my-app
composer serve
It has the four files a Sapling app needs (bootstrap.php, config.php, public/index.php and bin/console), an app/ folder for your actions, a tests/ folder that composer test runs with Pest, and a .env for development with a fresh APP_KEY. Errors are plain text until you set an error handler. A small app can also fit in one file, without the skeleton.
To add your first action, save the Hello class above as app/Hello.php and open http://localhost:8080/hello/Ann. The guides below are in reading order, starting with the app.
Why Sapling
I like Symfony, Laravel and Slim, and I use them. For a prototype or a slapped-together internal tool, they bring more dogma and ceremony than the project needs.
I've been fiddling with my own micro frameworks since early 2023, and Sapling is roughly my tenth. Some of the earlier ones were quick experiments I never came back to. Others got too clever, with interfaces nobody needed, a template compiler, a pile of global helpers, or packages and a build step just to boot.
Design
- Actions, middleware and commands are classes with a single
__invokemethod. Closures work too, for small apps. - Sapling only checks what PHP can't. When you make a mistake, PHP's own error usually names the class at fault.
- No static state, magic methods, generated code or caches. The few global functions are small helpers that work without an app.
- Settings live in
config.php, secrets in.env, and the wiring and middleware inbootstrap.php, which the web server, commands and tests all load.
Guides
| Guide | What's in it |
|---|---|
| The app | App, bootstrap.php, public/index.php, boot() and scan() |
| Configuration | config.php, .env and the settings Sapling reads |
| The container | How classes get what their constructors ask for, and #[Config] |
| Routing and actions | Actions, #[Route], #[Middleware], the router and how routes match |
| Requests and responses | The request lifecycle, Request, Response and downloads |
| Values | Reading outside data safely with Value, from requests, commands, sessions and replies |
| Middleware | Writing middleware, and the built-in SecurityHeaders, Csrf and Cors |
| When things go wrong | HttpException, crashes, the error handler and logging |
| Sessions | Session values, flash values, signing in and out, and how long a session lasts |
| Templates | Partials, components, layouts and escaping |
| htmx | Fragments, redirects, errors, scripts and sessions with htmx |
| Database | Db, SQLite and migrations |
| Encrypting secrets | Crypto and app_key |
| Commands | #[Command], Console and the built-in commands |
| Talking to other services | HttpClient, ClientRequest and ClientResponse, one request at a time or several at once |
| Converting data | JSON and form data with Data |
| Helpers | abort(), e(), attrs() and the other global functions |
| Testing | Requests, signing in, flash values, faked services and commands in tests |
| Running in production | A checklist for the server |
Not included, on purpose
There's no ORM, query builder, validation, caching or route groups, and there are no events, queues, long-running workers, mail or PSR interfaces. When your app needs one of these, you write the code for it. Without route groups, every route spells out its full path and you can grep for it.
Requirements
PHP 8.4 or newer. On PHP 8.5, Sapling also catches an ignored result from withHeader().
Some parts need extensions that most PHP installs already have:
| Part | Extension |
|---|---|
Db |
pdo, plus the driver for your database (pdo_sqlite for Db::sqlite()) |
Session and Csrf |
session |
Value |
filter |
HttpClient |
curl |
Crypto |
sodium |
Working on Sapling
| Command | What it does |
|---|---|
composer test |
Runs the tests through the app, a real php -S server, and commands in a subprocess |
composer format |
Formats the code with mago, as set in mago.toml |
composer check |
Checks formatting, runs mago's linter and static analyzer, then the tests |
composer mutate |
Runs mutation testing, to find code the tests don't check (needs Xdebug or PCOV) |
Core never calls the global helpers, since an app can replace them.
Tests mirror src/, as tests/Http/RouterTest.php does for src/Http/Router.php, with one expect() per line.
The analyzer skips the tests, since it can't follow Pest's closures. baseline-analyzer.toml and baseline-linter.toml list the findings that have been reviewed, and only new ones are reported. After a fix, regenerate them with vendor/bin/mago analyze --generate-baseline and vendor/bin/mago lint --generate-baseline and diff them before committing.
Each guide in docs/ has a table of contents, an introduction, sections named after tasks, and a method reference. Update a guide in the same change as the behaviour it describes. Code for a job Sapling leaves to apps, such as retrying a request, is a short example in the guide it belongs to.
License
Sapling is 0BSD. You can use it for anything. See LICENSE.