Routing

Basics

Routing is the process of mapping HTTP requests to actions. You can check out what makes Aphiria's routing library different here as well as the server configuration necessary to use it.

Let's look at how to register a route in a module (or view its attribute-based alternative). Routing is performed for you automatically, and there's nothing more to do besides actually defining your controller:

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Routing\RouteCollectionBuilder;

final class BookModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRoutes($appBuilder, function (RouteCollectionBuilder $routes): void {
            $routes
                ->get('/books/:bookId')
                ->mapsToMethod(BookController::class, 'getBookById')
                ->withMiddleware(Authorization::class)
        });
    }
}

You can use a fluent syntax for configuring your routes. Let's look at a complete example that includes actually performing the routing:

use Aphiria\Routing\Matchers\TrieRouteMatcher;
use Aphiria\Routing\RouteCollectionBuilder;
use Aphiria\Routing\UriTemplates\Compilers\Tries\TrieFactory;
use App\Books\Api\{Authorization, BookController};

// Register the routes
$routes = new RouteCollectionBuilder();
$routes
    ->get('/books/:bookId')
    ->mapsToMethod(BookController::class, 'getBookById')
    ->withMiddleware(Authorization::class);

// Set up the route matcher
$routeMatcher = new TrieRouteMatcher(new TrieFactory($routes->build())->createTrie());

// Finally, let's find a matching route
$result = $routeMatcher->matchRoute(
    $_SERVER['REQUEST_METHOD'],
    $_SERVER['HTTP_HOST'],
    $_SERVER['REQUEST_URI']
);

Let's say the request was GET /books/123. You can check if a match was found by calling:

if ($result->matchFound) {
    // ...
}

Grabbing the matched controller info is as simple as:

$result->route->action->controllerName; // "BookController"
$result->route->action->methodName; // "getBooksById"

To get the route variables, call:

$result->routeVariables; // ["bookId" => "123"]

To get the middleware bindings, call:

foreach ($result->route->middlewareBindings as $middlewareBinding) {
    $middlewareBinding->className; // "Authorization"
    $middlewareBinding->parameters; // ["role" => "admin"]
}

You can configure your app to return a 405 response using the result's allowed methods:

header('Allow', implode(', ', $result->allowedMethods));

Route Variables

Aphiria provides a simple syntax for your URIs. To capture variables in your route, use :varName, eg users/:userId/profile.

You can also add constraints to your variables.

Optional Route Parts

If part of your route is optional, then surround it with brackets. For example, the following will match both archives/2017 and archives/2017/7: archives/:year[/:month]. Optional route parts can be nested: archives/:year[/:month[/:day]]. This would match archives/2017, archives/2017/07, and archives/2017/07/24.

Route Groups

Often times, a lot of your routes will share similar properties, such as hosts and paths to match on, or middleware. Route groups can even be nested. Learn more how to add them as attributes or route builders.

Middleware

Middleware are a great way to modify both the request and the response on an endpoint. Aphiria lets you define middleware on your endpoints without binding you to any particular library/framework's middleware implementations. Learn how to add them as attributes or route builders.

Middleware Parameters

Some frameworks, such as Aphiria and Laravel, let you bind parameters to middleware. For example, if you have an Authorization middleware, but need to bind the user role that's necessary to access that route, you might want to pass in the required user role. Learn more about how to specify middleware parameters as attributes or route builders.

Route Constraints

Sometimes, you might find it useful to add some custom logic for matching routes. This could involve enforcing anything from only allowing certain HTTP methods for a route (eg HttpMethodRouteConstraint) or only allowing HTTPS requests to a particular endpoint. Learn more how to add them as attributes or route builders.

Route Attributes

Aphiria provides the optional functionality to define your routes via attributes if you so choose. A benefit to defining your routes this way is that it keeps the definition of your routes close (literally) to your controller methods, reducing the need to jump around your code base.

Example

Let's actually define a route:

use Aphiria\Api\Controllers\Controller;
use Aphiria\Authentication\Attributes\Authenticate;
use Aphiria\Routing\Attributes\{Get, Middleware};
use App\Books\Book;

final class BookController extends Controller
{
    #[Get('/books/:bookId'), Authenticate]
    public function getBookById(int $bookId): Book
    {
        // ...
    }
}

Note: Controllers must either extend Aphiria\Api\Controllers\Controller or use the #[Controller] attribute.

The following HTTP methods have route attributes:

Each attribute takes in the same parameters:

use Aphiria\Routing\Attributes\Get;

#[Get(
    path: '/courses/:courseId',
    host: 'api.example.com',
    name: 'getCourse',
    isHttpsOnly: true,
    parameters: ['role' => 'admin']
)]

You can read more about how request parameters are resolved in your controller methods here.

Route Groups

You can apply route groups, constraints, and middleware to all endpoints in a controller using the #[Controller] attribute.

use Aphiria\Api\Controllers\Controller as BaseController;
use Aphiria\Authentication\Attributes\Authenticate;
use Aphiria\Routing\Attributes\{Controller, Get, RouteConstraint};
use App\Courses\Course;

#[Controller(
    path: '/courses/:courseId',
    host: 'api.example.com',
    isHttpsOnly: true
)]
#[RouteConstraint(MyConstraint::class)]
#[Authenticate]
final class CourseController extends BaseController
{
    #[Get('')]
    public function getCourseById(int $courseId): Course
    {
        // ...
    }
    
    #[Get('/professors')]
    public function getCourseProfessors(int $courseId): array
    {
        // ...
    }
}

When our routes get compiled, the route group path will be prefixed to the path of any route within the controller. In the above example, this would create a route with path /courses/:courseId and another with path /courses/:courseId/professors.

Middleware

Middleware are a separate attribute:

use Aphiria\Routing\Attributes\Middleware;

#[Middleware(Authorization::class, parameters: ['role' => 'admin'])]

Note: You can also use the nearly identical Aphiria\Middleware\Attributes\Middleware attribute instead of the routing library's. The two are interchangeable.

You can also add middleware to a controller class to indicate that it applies to all routes in that controller.

Route Constraints

You can specify the name of the route constraint class and any primitive constructor parameter values:

use Aphiria\Routing\Attributes\{Get, RouteConstraint};
use App\Users\User;

final class UserController extends Controller
{
    #[Get('/users/:userId')]
    #[RouteConstraint(MyConstraint::class, constructorParameters: ['param1'])]
    public function getUserById(int $userId): User
    {
        // ...
    }
}

Similar to middleware, you can add route constraints to a controller class to apply it to all routes in that controller.

Scanning For Attributes

Before you can use attributes, you'll need to configure Aphiria to scan for them.

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;

final class GlobalModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRouteAttributes($appBuilder);
    }
}

You can manually configure the router to scan for attributes:

use Aphiria\Routing\Attributes\AttributeRouteRegistrant;
use Aphiria\Routing\Matchers\TrieRouteMatcher;
use Aphiria\Routing\RouteCollection;
use Aphiria\Routing\UriTemplates\Compilers\Tries\TrieFactory;

$routes = new RouteCollection();
$routeAttributeRegistrant = new AttributeRouteRegistrant(['PATH_TO_SCAN']);
$routeAttributeRegistrant->registerRoutes($routes);
$routeMatcher = new TrieRouteMatcher(new TrieFactory($routes)->createTrie());

// Find a matching route
$result = $routeMatcher->matchRoute(
    $_SERVER['REQUEST_METHOD'],
    $_SERVER['HTTP_HOST'],
    $_SERVER['REQUEST_URI']
);

Route Builders

Route builders are an alternative to attributes that give you a fluent syntax for mapping your routes to controller methods. They also let you bind any middleware classes and properties to the route. The following methods are available to create routes:

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Routing\RouteCollectionBuilder;

final class FooModule extends AphiriaModule
{
   public function configure(IApplicationBuilder $appBuilder): void
   {
       $this->withRoutes($appBuilder, function (RouteCollectionBuilder $routes) {
           $routes->delete('/foo');
           $routes->get('/foo');
           $routes->options('/foo');
           $routes->patch('/foo');
           $routes->post('/foo');
           $routes->put('/foo');
       });
   }
}
$routes->delete('/foo');
$routes->get('/foo');
$routes->options('/foo');
$routes->patch('/foo');
$routes->post('/foo');
$routes->put('/foo');

Let's look at the different parameters route builders accept:

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Routing\RouteCollectionBuilder;

final class UserModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRoutes($appBuilder, function (RouteCollectionBuilder $routes) {
            $routes
                ->get(path: '/user', host: 'api.example.com', isHttpsOnly: true)
                ->mapsToMethod(UserController::class, 'getUserById');
        });
    }
}
$routes
    ->get(path: '/user', host: 'api.example.com', isHttpsOnly: true)
    ->mapsToMethod(UserController::class, 'getUserById');

You can also call RouteCollectionBuilder::route() and pass in the HTTP method(s) you'd like to map to.

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Routing\RouteCollectionBuilder;

final class UserModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRoutes($appBuilder, function (RouteCollectionBuilder $routes) {
            $routes->route(['GET'], path: '/user', host: 'api.example.com', isHttpsOnly: true);
        });
    }
}
$routes->route(['GET'], path: '/user', host: 'api.example.com', isHttpsOnly: true);

Route Groups

Route groups let you logically group routes with shared parameters, eg path prefixes and middleware.

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Routing\Middleware\MiddlewareBinding;
use Aphiria\Routing\RouteCollectionBuilder;
use Aphiria\Routing\RouteGroupOptions;

final class CourseModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRoutes($appBuilder, function (RouteCollectionBuilder $routes) {
            $routes->group(
                new RouteGroupOptions(
                    path: '/courses/:courseId',
                    host: 'api.example.com',
                    isHttpsOnly: true,
                    constraints: [new MyConstraint()],
                    middlewareBindings: [new MiddlewareBinding(Authentication::class)],
                    parameters: ['role' => 'admin']
                ),
                function (RouteCollectionBuilder $routes) {
                    // This route's path will be "courses/:courseId"
                    $routes
                        ->get('')
                        ->mapsToMethod(CourseController::class, 'getCourseById');
            
                    // This route's path will be "courses/:courseId/professors"
                    $routes
                        ->get('/professors')
                        ->mapsToMethod(CourseController::class, 'getCourseProfessors');
                }
            );
        });
    }
}
use Aphiria\Routing\Middleware\MiddlewareBinding;
use Aphiria\Routing\RouteCollectionBuilder;
use Aphiria\Routing\RouteGroupOptions;

$routes->group(
    new RouteGroupOptions(
        path: '/courses/:courseId',
        host: 'api.example.com',
        isHttpsOnly: true,
        constraints: [new MyConstraint()],
        middlewareBindings: [new MiddlewareBinding(Authentication::class)],
        parameters: ['role' => 'admin']
    ),
    function (RouteCollectionBuilder $routes) {
        // This route's path will be "courses/:courseId"
        $routes
            ->get('')
            ->mapsToMethod(CourseController::class, 'getCourseById');

        // This route's path will be "courses/:courseId/professors"
        $routes
            ->get('/professors')
            ->mapsToMethod(CourseController::class, 'getCourseProfessors');
    }
);

Middleware

To bind a single middleware class to your route, call:

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Routing\RouteCollectionBuilder;

final class FooModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRoutes($appBuilder, function (RouteCollectionBuilder $routes) {
            $routes
                ->get('foo')
                ->mapsToMethod(MyController::class, 'myMethod')
                ->withMiddleware(FooMiddleware::class);
        });
    }
}
$routes
    ->get('foo')
    ->mapsToMethod(MyController::class, 'myMethod')
    ->withMiddleware(FooMiddleware::class);

To bind many middleware classes, call:

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Routing\RouteCollectionBuilder;

final class FooModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRoutes($appBuilder, function (RouteCollectionBuilder $routes) {
            $routes
                ->get('foo')
                ->mapsToMethod(MyController::class, 'myMethod')
                ->withManyMiddleware([
                    FooMiddleware::class,
                    BarMiddleware::class
                ]);
        });
    }
}
$routes
    ->get('foo')
    ->mapsToMethod(MyController::class, 'myMethod')
    ->withManyMiddleware([
        FooMiddleware::class,
        BarMiddleware::class
    ]);

Under the hood, these class names get converted to instances of MiddlewareBinding.

You can also add parameters to your middleware:

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Routing\Middleware\MiddlewareBinding;
use Aphiria\Routing\RouteCollectionBuilder;

final class FooModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRoutes($appBuilder, function (RouteCollectionBuilder $routes) {
            $routes
                ->get('foo')
                ->mapsToMethod(MyController::class, 'myMethod')
                ->withMiddleware(Authorization::class, ['role' => 'admin']);
            
            // Or
            
            $routes
                ->get('foo')
                ->mapsToMethod(MyController::class, 'myMethod')
                ->withManyMiddleware([
                    new MiddlewareBinding(Authorization::class, ['role' => 'admin']),
                    // Other middleware...
                ]);
        });
    }
}
use Aphiria\Routing\Middleware\MiddlewareBinding;

$routes
    ->get('foo')
    ->mapsToMethod(MyController::class, 'myMethod')
    ->withMiddleware(Authorization::class, ['role' => 'admin']);

// Or

$routes
    ->get('foo')
    ->mapsToMethod(MyController::class, 'myMethod')
    ->withManyMiddleware([
        new MiddlewareBinding(Authorization::class, ['role' => 'admin']),
        // Other middleware...
    ]);

Route Constraints

To add a single route constraint to a route, call:

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Routing\RouteCollectionBuilder;

final class PostModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRoutes($appBuilder, function (RouteCollectionBuilder $routes) {
            $routes
                ->get('posts')
                ->mapsToMethod(PostController::class, 'getAllPosts')
                ->withConstraint(new FooConstraint());
        });
    }
}
$routes
    ->get('posts')
    ->mapsToMethod(PostController::class, 'getAllPosts')
    ->withConstraint(new FooConstraint());

To add many route constraints, call:

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Routing\RouteCollectionBuilder;

final class PostModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRoutes($appBuilder, function (RouteCollectionBuilder $routes) {
            $routes
                ->get('posts')
                ->mapsToMethod(PostController::class, 'getAllPosts')
                ->withManyConstraints([new FooConstraint(), new BarConstraint()]);
        });
    }
}
$routes
    ->get('posts')
    ->mapsToMethod(PostController::class, 'getAllPosts')
    ->withManyConstraints([new FooConstraint(), new BarConstraint()]);

Versioned API Example

Let's say your app sends an API version header, and you want to match an endpoint that supports that version. You could do this by using a route "parameter" and a route constraint. Let's create some routes that have the same path, but support different versions of the API:

use Aphiria\Routing\Attributes\{Get, RouteConstraint};

final class CommentController extends Controller
{
    #[Get('/comments', parameters: ['API-VERSION' => 'v1.0'])]
    #[RouteConstraint(ApiVersionConstraint::class)]
    public function getAllComments1_0(): array
    {
        // This route will require an API-VERSION value of 'v1.0'
    }
    
    #[Get('/comments', parameters: ['API-VERSION' => 'v2.0'])]
    #[RouteConstraint(ApiVersionConstraint::class)]
    public function getAllComments2_0(): array
    {
        // This route will require an API-VERSION value of "v2.0"
    }
}

Now, let's add a route constraint to match the "API-VERSION" header to the parameter on our route:

use Aphiria\Routing\Matchers\Constraints\IRouteConstraint;
use Aphiria\Routing\Matchers\MatchedRouteCandidate;

final class ApiVersionConstraint implements IRouteConstraint
{
    public function passes(
        MatchedRouteCandidate $matchedRouteCandidate,
        string $httpMethod,
        string $host,
        string $path,
        array $headers
    ): bool {
        $parameters = $matchedRouteCandidate->route->parameters;

        if (!isset($parameters['API-VERSION'])) {
            return false;
        }

        return \in_array($parameters['API-VERSION'], $headers['API-VERSION'], true);
    }
}

If we hit /comments with an "API-VERSION" header value of "v2.0", we'd match the second route in our example.

Getting Headers in PHP

PHP is irritatingly difficult to extract headers from $_SERVER, which is why the routing library includes HeaderParser:

use Aphiria\Routing\Requests\RequestHeaderParser;

$headers = new RequestHeaderParser()->parseHeaders($_SERVER);
echo $headers['Content-Type']; // "application/json"

Route Variable Constraints

You can enforce certain constraints to pass before matching on a route. These constraints come after variables, and must be enclosed in parentheses. For example, if you want an integer to fall between two values, you can specify a route of

:month(int,min(1),max(12))

Note: If a constraint does not require any parameters, then the parentheses after the constraint slug are optional.

Built-In Constraints

The following constraints are built-into Aphiria:

Name Description
alpha The value must only contain alphabet characters
alphanumeric The value must only contain alphanumeric characters
between The value must fall between a min and max (takes in whether or not the min and max values are inclusive)
date The value must match a date-time format
in The value must be in a list of acceptable values
int The value must be an integer
notIn The value must not be in a list of values
numeric The value must be numeric
regex The value must satisfy a regular expression
uuidv4 The value must be a UUID v4

Making Your Own Custom Constraints

You can register your own constraint by implementing IRouteVariableConstraint. Let's make a constraint that enforces a certain minimum string length:

use Aphiria\Routing\UriTemplates\Constraints\IRouteVariableConstraint;

final class MinLengthConstraint implements IRouteVariableConstraint
{
    public function __construct(private int $minLength) {}

    public static function getSlug(): string
    {
        return 'minLength';
    }

    public function passes($value): bool
    {
        return \mb_strlen($value) >= $this->minLength;
    }
}

Let's register our constraint with the constraint factory. You can use a component:

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Framework\Application\AphiriaModule;

final class GlobalModule extends AphiriaModule
{
    public function configure(IApplicationBuilder $appBuilder): void
    {
        $this->withRouteVariableConstraint(
            $appBuilder,
            MinLengthConstraint::getSlug(),
            fn(int $minLength) => new MinLengthConstraint($minLength)
        );
    }
}

Let's register our constraint with the constraint factory. You can register the constraint manually:

use Aphiria\Routing\UriTemplates\Constraints\RouteVariableConstraintFactory;
use Aphiria\Routing\UriTemplates\Constraints\RouteVariableConstraintFactoryRegistrant;

// Register some built-in constraints to our factory
$constraintFactory = new RouteVariableConstraintFactoryRegistrant()
    ->registerConstraintFactories(new RouteVariableConstraintFactory());

// Register our custom constraint
$constraintFactory->registerConstraintFactory(
    MinLengthConstraint::getSlug(),
    fn(int $minLength) => new MinLengthConstraint($minLength)
);

Finally, register this constraint factory with the trie compiler:

use Aphiria\Routing\Matchers\TrieRouteMatcher;
use Aphiria\Routing\RouteCollectionBuilder;
use Aphiria\Routing\UriTemplates\Compilers\Tries\{TrieCompiler, TrieFactory};

$routes = new RouteCollectionBuilder();
$routes
    ->get('parts/:serialNumber(minLength(6))')
    ->mapsToMethod(PartController::class, 'getPartBySerialNumber');

$trieCompiler = new TrieCompiler($constraintFactory);
$trieFactory = new TrieFactory($routes->build(), null, $trieCompiler);
$routeMatcher = new TrieRouteMatcher($trieFactory->createTrie());

Our route will now enforce a serial number with minimum length 6.

Creating Route URIs

You might find yourself wanting to create a link to a particular route within your app. Let's say you have a route named GetUserById with a URI template of /users/:userId. We can generate a link to get a particular user. The best way is to inject an instance of IRouteUriFactory into your controller:

use Aphiria\Api\Controllers\Controller;
use Aphiria\Net\Http\IResponse;
use Aphiria\Routing\Attributes\{Get, Post};
use Aphiria\Routing\UriTemplates\IRouteUriFactory;

final class UserController extends Controller
{
    public function __construct(private IRouteUriFactory $routeUriFactory) {}
    
    #[Post('/users')]
    public function createUser(User $user): IResponse
    {
        // Create the user...
        
        $location = $this->routeUriFactory->createRouteUri('GetUserById', ['userId' => $user->id]);
        
        return $this->created($location);
    }
    
    #[Get('/users/:userId', name: 'GetUserById')]
    public function getUserById(int $userId): User
    {
        // Get the user...
    }
}

// Assume you've already created your routes
$routeUriFactory = new AstRouteUriFactory($routes);

// Will create "/users/123"
$uriForUser123 = $routeUriFactory->createRouteUri('GetUserById', ['id' => 123]);

Generated URIs will be a relative path unless the URI template specified a host. Absolute URIs are assumed to be HTTPS unless the URI template is specifically set to not be HTTPS-only.

Optional route variables can be specified, too. Let's assume the URI template for GetBooksFromArchive is /archives/:year[/:month]:

final class BookController extends Controller
{
    public function __construct(private IRouteUriFactory $routeUriFactory) {}
    
    #[Get('/books/links')]
    public function getArchiveLinks(): array
    {
        $links = [
            // Will create "/archives/2019"
            $this->routeUriFactory->createRouteUri('GetBooksFromArchive', ['year' => 2019]),
            // Will create "/archives/2019/12"
            $this->routeUriFactory->createRouteUri('GetBooksFromArchive', ['year' => 2019, 'month' => 12]),
        ];
        
        return $links;
    }
}

If you use parameter attributes, Aphiria will respect them when determining where to apply the route variables (eg by putting them in the route path/host or in the query string).

Creating Route Requests

If your routes include a #[Header] variable that you'd like to auto-populate or you want to create an HTTP request for your route and not just a URI, you can use RouteRequestFactory:

use Aphiria\Api\Controllers\Controller;
use Aphiria\Framework\Routing\{IRouteRequestFactory, RouteRequestFactory};
use Aphiria\Routing\Attributes\Get;

final class BookController extends Controller
{
    public function __construct(private IRouteRequestFactory $routeRequestFactory) {}
    
    #[Get('/books/dump-request')]
    public function dumpRequest(): string
    {
        // For demonstration's sake, we'll just dump the raw HTTP request
        return (string)$this->routeRequestFactory->createRouteRequest(
            'GetBooksFromArchive',
            ['year' => 2019]
        );
    }
}

Note: If your route supports multiple HTTP methods, you must specify the HTTP method to use as a third parameter in createRouteRequest(). If your route supports GET requests, it will automatically also support HEAD requests. In this case, the factory will default to creating a GET request unless you specify 'HEAD' as the method.

Caching

The process of building your routes and compiling the trie is a relatively slow process, and isn't necessary in a production environment where route definitions aren't changing. Aphiria provides both the ability to cache the results of your route builders and the compiled trie.

Aphiria will automatically cache important data when APP_ENV equals "production".

Route Caching

To enable caching, pass in an IRouteCache (FileRouteCache is provided) to the first parameter of RouteRegistrantCollection:

use Aphiria\Routing\Caching\FileRouteCache;
use Aphiria\Routing\RouteCollection;
use Aphiria\Routing\RouteRegistrantCollection;

$routes = new RouteCollection();
$routeRegistrant = new RouteRegistrantCollection(new FileRouteCache('/tmp/routeCache.txt'));

// Once you're done configuring your route registrant...

$routeRegistrant->registerRoutes($routes);

Trie Caching

To enable caching, pass in an ITrieCache (FileTrieCache comes with Aphiria) to your trie factory (passing in null will disable caching). If you want to enable caching for a particular environment, you could do so:

use Aphiria\Routing\UriTemplates\Compilers\Tries\Caching\FileTrieCache;
use Aphiria\Routing\UriTemplates\Compilers\Tries\TrieFactory;

// Let's say that your environment name is stored in an environment var named "ENV_NAME"
$trieCache = \getenv('ENV_NAME') === 'production'
    ? new FileTrieCache('/tmp/trieCache.txt')
    : null;
$trieFactory = new TrieFactory($routes, $trieCache);

// Finish setting up your route matcher...

Using Aphiria's Net Library

You can use Aphiria's net library to route the request instead of relying on PHP's superglobals:

use Aphiria\Net\Http\RequestFactory;

$request = new RequestFactory()->createRequestFromSuperglobals($_SERVER);

// Set up your route matcher like before...

$result = $routeMatcher->matchRoute(
    $request->method,
    $request->ur->host,
    $request->uri->path
);

Matching Algorithm

Rather than the typical regex approach to route matching, we decided to go with a trie-based approach. Each node maps to a segment in the path, and could either contain a literal or a variable value. We try to proceed down the tree to match what's in the request URI, always giving preference to literal matches over variable ones, even if variable segments are declared first in the routing config. This logic not only applies to the first segment, but recursively to all subsequent segments. The benefit to this approach is that it doesn't matter what order routes are defined. Additionally, literal segments use simple hash table lookups. What determines performance is the length of a path and the number of variable segments.

The matching algorithm goes as follows:

  1. Incoming request data is passed to TrieRouteMatcher::matchRoute(), which loops through each segment of the URI path and proceeds only if there is either a literal or variable match in the URI tree
    • If there's a match, then we scan all child nodes against the next segment of the URI path and repeat step 1 until we don't find a match or we've matched the entire URI path
    • TrieRouteMatcher::matchRoute() uses generators so we only descend the URI tree as many times as we need to find a match candidate
  2. If the match candidate passes constraint checks (eg HTTP method constraints), then it's our matching route, and we're done. Otherwise, repeat step 1, which will yield the next possible match candidate.