AutoRoute
Automatically maps HTTP requests to PHP action classes.
Install / Use
/learn @pmjones/AutoRouteREADME
AutoRoute
AutoRoute automatically maps incoming HTTP requests (by verb and path) to PHP action classes in a specified namespace, reflecting on a specified action method within that class to determine the dynamic URL argument values. Those parameters may be typical scalar values (int, float, string, bool), or arrays, or even value objects of your own creation. AutoRoute also helps you generate URL paths based on action class names, and checks the dynamic argument typehints for you automatically.
Install AutoRoute using Composer:
composer require pmjones/auto-route ^2.0
AutoRoute is low-maintenance. Merely adding a class to your source code, in the recognized namespace and with the recognized action method name, automatically makes it available as a route. No more managing a routes file to keep it in sync with your action classes!
AutoRoute is fast. In fact, it is roughly 2x faster than FastRoute in common cases -- even when FastRoute is using cached route definitions.
Note:
When comparing alternatives, please consider AutoRoute as being in the same category as AltoRouter, FastRoute, Klein, etc., and not of Aura, Laminas, Laravel, Symfony, etc.
Contents
- Motivation
- Examples
- How It Works
- Usage
- Generating Route Paths
- Custom Configuration
- Dumping All Routes
- Creating Classes From Routes
- Questions and Recipes
Motivation
Regular-expression (regex) routers generally duplicate important information that can be found by reflection instead. If you change the action method parameters targeted by a route, you need to change the route regex itself as well. As such, regex router usage may be considered a violation of the DRY principle. For systems with only a few routes, maintaining a routes file as duplicated information is not such a chore. But for systems with a hundred or more routes, keeping the routes in sync with their target action classes and methods can be onerous.
Similarly, annotation-based routers place routing instructions in comments, often duplicating dynamic parameters that are already present in explicit method signatures.
As an alternative to regex and annotation-based routers, this router implementation eliminates the need for route definitions by automatically mapping the HTTP action class hierarchy to the HTTP method verb and URL path, reflecting on typehinted action method parameters to determine the dynamic portions of the URL. It presumes that the action class names conform to a well-defined convention, and that the action method parameters indicate the dynamic portions of the URL. This makes the implementation both flexible and relatively maintenance-free.
Examples
Given a base namespace of Project\Http and a base url of /, this request ...
GET /photos
... auto-routes to the class Project\Http\Photos\GetPhotos.
Likewise, this request ...
POST /photo
... auto-routes to the class Project\Http\Photo\PostPhoto.
Given an action class with method parameters, such as this ...
namespace Project\Http\Photo;
class GetPhoto
{
public function __invoke(int $photoId)
{
// ...
}
}
... the following request will route to it ...
GET /photo/1
... recognizing that 1 should be the value of $photoId.
AutoRoute supports static "tail" parameters on the URL. If the URL ends in a path segment that matches the tail portion of a class name, and the action class method has the same number and type of parameters as its parent or grandparent class, it will route to that class name. For example, given an action class with method parameters such as this ...
namespace Project\Http\Photo\Edit;
class GetPhotoEdit // parent: GetPhoto
{
public function __invoke(int $photoId)
{
// ...
}
}
... the following request will route to it:
GET /photo/1/edit
Finally, a request for the root URL ...
GET /
... auto-routes to the class Project\Http\Get.
Tip:
Any HEAD request will auto-route to an explicit
Project\Http\...\Head*class, if one exists. If an explicitHeadclass does not exist, the request will implicitly be auto-routed to the matchingProject\Http\...\Get*class, if one exists.
How It Works
Class File Naming
Action class files are presumed to be named according to PSR-4 standards; further:
-
The class name starts with the HTTP verb it responds to;
-
Followed by the concatenated names of preceding subnamespaces;
-
Ending in
.php.
Thus, given a base namespace of Project\Http, the class Project\Http\Photo\PostPhoto
will be the action for POST /photo[/*].
Likewise, Project\Http\Photos\GetPhotos will be the action class for GET /photos[/*].
And Project\Http\Photo\Edit\GetPhotoEdit will be the action class for GET /photo[/*]/edit.
An explicit Project\Http\Photos\HeadPhotos will be the action class for
HEAD /photos[/*]. If the HeadPhotos class does not exist, the action class
is inferred to be Project\Http\Photos\HeadPhotos instead.
Finally, at the URL root path, Project\Http\Get will be the action class for GET /.
Dynamic Parameters
The action method parameter typehints are honored by the Router. For example, the following action ...
namespace Project\Http\Photos\Archive;
class GetPhotosArchive
{
public function __invoke(int $year = null, int $month = null)
{
// ...
}
}
... will respond to the following:
GET /photos/archive
GET /photos/archive/1970
GET /photos/archive/1970/08
... but not to the following ...
GET /photos/archive/z
GET /photos/archive/1970/z
... because z is not recognized as an integer. (More finely-tuned validations
of the method parameters must be accomplished in the action method itself, or
more preferably in the domain logic, and cannot be intuited by the Router.)
The Router can recognize typehints of int, float, string, bool, and
array.
For bool, the Router will case-insensitively cast these URL segment values
to true: 1, t, true, y, yes. Similarly, it will case-insensitively cast
these URL segment values to false: 0, f, false, n, no.
For array, the Router will use str_getcsv() on the URL segment value to
generate an array. E.g., an array typehint for a segment value of a,b,c will
receive ['a', 'b', 'c'].
Finally, trailing variadic parameters are also honored by the Router. Given an action method like the following ...
namespace Project\Http\Photos\ByTag;
class GetPhotosByTag
{
public function __invoke(string $tag, string ...$tags)
{
// ...
}
}
... the Router will honor this request ...
GET /photos/by-tag/foo/bar/baz/
... and recognize the method parameters as __invoke('foo', 'bar', 'baz').
Extended Example
By way of an extended example, these classes would be routed to by these URLs:
App/
Http/
Get.php GET / (root)
Photos/
GetPhotos.php GET /photos (browse/index)
Photo/
DeletePhoto.php DELETE /photo/1 (delete)
GetPhoto.php GET /photo/1 (read)
PatchPhoto.php PATCH /photo/1 (update)
PostPhoto.php POST /photo (create)
Add/
GetPhotoAdd.php GET /photo/add (form for creating)
Edit/
GetPhotoEdit.php GET /photo/1/edit (form for updating)
HEAD Requests
RFC 2616 requires that "methods GET and HEAD must be supported by all general-purpose servers".
As such, AutoRoute will automatically fall back to a Get* action class if a
relevant Head* action class is not found. This keeps you from having to create
a Head* class for every possible Get* action.
However, you may still define any Head* action class you like, and AutoRoute
will use it.
Usage
Instantiate the AutoRoute container class with the top-level HTTP action namespace and the directory path to classes in that namespace:
use AutoRoute\AutoRoute;
$autoRoute = new AutoRoute(
'Project\Http',
dirname(__DIR__) . '/src/Project/Http/'
);
You may use named constructor parameters if you wish:
use AutoRoute\AutoRoute;
$autoRoute = new AutoRoute(
namespace: 'Project\Http',
directory: dirname(__DIR__) . '/src/Project/Http/',
);
Then pull the Router out of the container, and call route() with the HTTP
request method verb and the path string to get back a Route:
$router = $autoRoute->getRouter();
$route = $router->route($request->method->name, $request->url->path);
You can then dispatch to the action class method using the returned Route information, or handle errors:
use AutoRoute\Exception;
switch ($route->error) {
case null:
// no errors! create the action class instance
// ... and call it with the method and arguments.
$action = Factory::newInstance($route->class);
$method = $route->method;
$arguments = $route->arguments;
$response = $action->$method(...$arguments);
break;
case Exception\InvalidArgument::CLASS:
$response = /* 400 Bad Request */;
break;
case Ex
