Five minutes to set expectations. After this chapter you have not written any code yet. You have a sketch of the destination, a list of the rails you'll ride on, and a small to-do list to align your local Scriptor install with the tutorial.

What "done" looks like

At the end of chapter 6 you will be able to:

  1. Drop a composer require bigins/scriptor-simple-router into any Scriptor install,
  2. write a tiny routes.php somewhere your site already loads (a theme bootstrap, the _ext.php, or a one-off boot snippet; chapter 5 covers the wiring options),
  3. add one line to your theme's _ext.php,
  4. and reach /api/users/123 to get JSON back, with no Scriptor page resolution involved.

The finished plugin is publishable too: a tagged Composer package that someone else can composer require from their own Scriptor site.

The target API

Here is what the reader will be writing by the end of chapter 4. Read it like a screenshot, not a spec; we'll arrive at every line.

use Bigins\ScriptorSimpleRouter\Router;
use Bigins\ScriptorSimpleRouter\Request;
use Bigins\ScriptorSimpleRouter\Response;

$router = Router::instance();

// 1. JSON endpoint with a path parameter
$router->get('/api/users/{id}', function (Request $req): Response {
    return Response::json([
        'id'   => (int) $req->param('id'),
        'name' => 'Ada Lovelace',
    ]);
});

// 2. Webhook receiver. Handler is a controller class string
$router->post('/webhook/stripe', StripeWebhookController::class);

// 3. Plain-text response with a custom status code
$router->get('/healthz', fn() => Response::text('OK')->status(200));

And here is the one line of theme glue that activates it (chapter 5 unpacks every word):

// In your theme's _ext.php, before $site = new MyTheme(...):
if (\Bigins\ScriptorSimpleRouter\Router::handle()) return;

That is the entire surface. No middleware chains, no route groups, no per-route dependency injection. Three handler shapes (Closure, controller-class string, [Class, method] pair), four HTTP verbs, path parameters with {name} syntax. Small on purpose.

The four moving parts

Every chapter touches one of these four boxes. By chapter 5 they all click together:

  • composer.json with a PSR-4 autoload entry pointing at src/, plus a require on scriptor/scriptor. Chapter 2 walks the file line by line.
  • A Plugin class implementing Scriptor\Boot\Plugin\Plugin. Plugin Discovery picks it up automatically once the package is installed. The class registers the Router singleton in the container during register().
  • A Router class that holds the route registry and runs the match algorithm. Chapter 3 builds it; it has no Scriptor dependencies at all and is unit-testable in isolation.
  • One line in the theme's _ext.php. That is the only piece the site operator (not the plugin author) has to touch. Chapter 5 explains why this line is enough and where it has to sit relative to caching.

If any of those phrases feel new, the relevant Concept chapter is linked at the top of the chapter where it shows up.

The rules of the road

Three guard-rails to keep the result honest:

  1. Routes are defined once, at boot. No runtime route mutation from inside handlers, no lazy "register if not already there". A static, fully-known route table is the only thing that makes the matcher predictable and the plugin debuggable.
  2. Handlers return a Response. They do not echo, do not set headers directly, do not call exit. The Router takes the Response and sends it. That contract is what lets chapter 6 ship a tested plugin: tests assert on the Response, not on captured output buffers.
  3. The Router short-circuits BEFORE page resolution. Once Router::handle() returns true, Scriptor's normal pipeline (PSR-14 events, page rendering, template loading) never runs. That is the whole point of hooking into _ext.php instead of subscribing to PageResolving: a JSON endpoint has no business being a fake Page.

What you will not find in this chapter (or this track)

  • Comparison to FastRoute / league/route / Symfony Routing. Those libraries are excellent and solve more than Simple Router ever will. The Cookbook covers how to wrap one of them inside a Scriptor plugin if you need the extra power.
  • Authentication middleware. Real APIs need it; Simple Router doesn't ship it. The Cookbook has a recipe.
  • OpenAPI / spec generation. Out of scope.

To-do before chapter 2

  • Working Scriptor install: composer install clean and php bin/scriptor install ran successfully (see docs/install.md if you have not done this yet).
  • /editor/pages/ accessible with the admin credentials you set during install.
  • A theme with an _ext.php (the bundled basic theme already has one). If your current theme has no _ext.php, chapter 5 shows how to add one. Three lines, no dependencies. You do not need to do this yet.
  • Pick an empty directory for the plugin to live in outside the Scriptor install. We'll use ~/code/scriptor-simple-router/ throughout; substitute your own path. Chapter 2 walks composer init from inside it and shows how to point a local Scriptor at the in-progress plugin via a path repository, so you can iterate without publishing anything.

When all four boxes are ticked, open chapter 2 and write your first composer.json.


Behind the scenes. The plugin discovery + lifecycle contract Simple Router rides on is documented in the Concepts → Plugin Discovery chapter. The ScriptorPlugin interface itself lives at boot/Plugin/Plugin.php. The _ext.php request-pipeline hook that chapter 5 leans on lives at public/index.php lines 53-67.