Search by

sapling / core

phil-venter

A small PHP framework for prototypes, one-off projects and internal tools, with no dependencies and a core you can read in a day.

dev-main 2026-11-01 20:11 UTC

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 __invoke method. 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 in bootstrap.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.