Getting Started

Get up and running with Waypoint in under 5 minutes.

Table of contents

Installation

Install Waypoint via Composer:

composer require ascetic-soft/waypoint

Requirements:

  • PHP >= 8.4
  • ext-mbstring

Optional PSR packages

The core matching engine works as pure PHP. For PSR-15 request handling (middleware, DI, Router::handle()), install the PSR packages:

composer require psr/http-message psr/http-server-handler psr/http-server-middleware psr/container

Quick Start

Waypoint separates route registration (RouteRegistrar) from request dispatching (Router).

Step 1: Register routes

use AsceticSoft\Waypoint\RouteRegistrar;

$registrar = new RouteRegistrar();

$registrar->get('/hello/{name}', function (string $name) use ($responseFactory) {
    $response = $responseFactory->createResponse();
    $response->getBody()->write("Hello, {$name}!");
    return $response;
});

Step 2: Create the router

use AsceticSoft\Waypoint\Router;

$router = new Router($container, $registrar->getRouteCollection());

Step 3: Handle requests

use Nyholm\Psr7\ServerRequest;

$request  = new ServerRequest('GET', '/hello/world');
$response = $router->handle($request);

Waypoint automatically extracts {name} from the URL and injects it into your handler. No manual parsing required.


Using Controllers

For real applications, use controller classes instead of closures:

$registrar = new RouteRegistrar();

$registrar->get('/users',             [UserController::class, 'list']);
$registrar->post('/users',            [UserController::class, 'create']);
$registrar->get('/users/{id:\d+}',    [UserController::class, 'show']);
$registrar->put('/users/{id:\d+}',    [UserController::class, 'update']);
$registrar->delete('/users/{id:\d+}', [UserController::class, 'destroy']);

Controller methods receive route parameters automatically with type coercion:

class UserController
{
    public function show(int $id, UserRepository $repo): ResponseInterface
    {
        // $id is automatically cast to int
        // $repo is resolved from the PSR-11 container
        $user = $repo->find($id);
        // ...
    }
}

Route parameters are matched by name and automatically cast to the type declared in the method signature (int, float, bool). Container services are resolved by type-hint.


Using Attributes

Instead of manual registration, declare routes directly on your controllers:

use AsceticSoft\Waypoint\Attribute\Route;

#[Route('/api/users', middleware: [AuthMiddleware::class])]
class UserController
{
    #[Route('/', methods: ['GET'], name: 'users.list')]
    public function list(): ResponseInterface { /* ... */ }

    #[Route('/{id:\d+}', methods: ['GET'], name: 'users.show')]
    public function show(int $id): ResponseInterface { /* ... */ }

    #[Route('/', methods: ['POST'], name: 'users.create')]
    public function create(ServerRequestInterface $request): ResponseInterface { /* ... */ }
}

Then load them:

$registrar = new RouteRegistrar();

// Load specific classes
$registrar->loadAttributes(UserController::class, PostController::class);

// Or scan an entire directory
$registrar->scanDirectory(__DIR__ . '/Controllers', 'App\\Controllers');

// Optionally filter by filename pattern
$registrar->scanDirectory(__DIR__ . '/Controllers', 'App\\Controllers', '*Controller.php');

A Complete Bootstrap Example

<?php
// bootstrap.php

use AsceticSoft\Waypoint\Cache\RouteCompiler;
use AsceticSoft\Waypoint\RouteRegistrar;
use AsceticSoft\Waypoint\Router;
use AsceticSoft\Waypoint\Exception\RouteNotFoundException;
use AsceticSoft\Waypoint\Exception\MethodNotAllowedException;

require_once __DIR__ . '/vendor/autoload.php';

$cacheFile = __DIR__ . '/cache/routes.php';
$compiler  = new RouteCompiler();

// In production: load from cache; otherwise register and compile
if ($compiler->isFresh($cacheFile)) {
    $router = new Router($container);
    $router->loadCache($cacheFile);
} else {
    $registrar = new RouteRegistrar();
    $registrar->scanDirectory(__DIR__ . '/Controllers', 'App\\Controllers');

    // Compile for next request
    $compiler->compile($registrar->getRouteCollection(), $cacheFile);

    $router = new Router($container, $registrar->getRouteCollection());
}

// Global middleware
$router->addMiddleware(CorsMiddleware::class);

// Handle request
try {
    $response = $router->handle($request);
} catch (RouteNotFoundException $e) {
    // 404 Not Found
} catch (MethodNotAllowedException $e) {
    // 405 Method Not Allowed
    $allowed = $e->getAllowedMethods();
}

What’s Next?

  • Routing — Route registration, parameters, groups, and attribute-based routing
  • Middleware — Global and per-route PSR-15 middleware
  • Advanced — Dependency injection, URL generation, caching, and diagnostics
  • Internals — Algorithms, data structures, and architecture diagrams
  • API Reference — Complete reference for all public classes and methods