class: center, middle # AutoRoute for PHP ## No More Route Files! Paul M. Jones http://paul-m-jones.com/ https://github.com/pmjones/AutoRoute/ --- # Overview - History and review of routers - Problems and desires - AutoRoute solution - Tradeoffs --- # The (Good|Bad) Old Days - URL path mapped directly to docroot file path - Invoke the script at that file location - Parameters ... - via query string: ``` # GET http://example.com/foo/bar.php?id=1 /var/www/html/foo/bar.php $_GET['id'] = '1'; ``` - via path info: ``` # GET http://example.com/foo/bar.php/1 /var/www/html/foo/bar.php $_SERVER['PATH_INFO'] = '/1'; ``` - All HTTP verbs mapped to same script - Web server itself as degenerate form of Front Controller --- # Routers and Dispatchers - Modern applications do not expose scripts in document root - Instead, functionality encapsulated in classes - Formal Front Controller script (index.php or front.php) - Router reads the URL and maps to class/method/params ```php # GET http://example.com/foo/bar/1 class Foo { public function bar(int $id) : Response { } } ``` - Dispatcher invokes an instance method with params - Need a routing file: regular expressions --- # Routes File (Rails) ``` # generic get /:controller/:action/:id # ~^/([^/]+)/([^/]+)/([^/]+)$~ # specific get /foo/bar/:id # ~^/foo/bar/([^/]+)$~ # constraints get /photo/archive/{:year}/{:month}, constraints: { year: /\d+/, month: /\d+/ } # ~^/photo/archive/(\d+)/(\d+)$~ ``` --- # Routes File (PHP FastRoute) ```php $r->addRoute('GET', '/users', ['Users', 'index']); // {id} must be a number (\d+) $r->addRoute('GET', '/user/{id:\d+}', ['Users', 'show']); // The /{title} suffix is optional $r->addRoute('GET', '/articles/{id:\d+}[/{title}]', ['Articles', 'read']); ``` Others:
--- # Callable Routing (PHP Slim) ```php $app->get( '/hello/{name}', function (Request $request, Response $response, array $args) { $name = $args['name']; $response->getBody()->write("Hello, $name"); return $response; } ); ``` --- # Route Grouping (PHP FastRoute) ```php $r->addGroup('/admin', function (RouteCollector $r) { # GET /admin/foo $r->addRoute('GET', '/foo', ['Admin', 'foo']); # GET /admin/bar $r->addRoute('GET', '/bar', ['Admin', 'bar']); # GET /admin/baz $r->addRoute('GET', '/baz', ['Admin', 'baz']); }); ``` --- # Resource Routing (Rails) ``` resources photos ``` ... creates: ``` get /photos, to: photos#index get /photos/new, to: photos#new post /photos, to: photos#create get /photos/:id, to: photos#show get /photos/:id/edit, to: photos#edit patch /photos/:id, to: photos#update put /photos/:id, to: photos#update delete /photos/:id, to: photos#destroy ``` --- # Resource Routing (PHP) - Laravel ```php Route::resource('photos', 'PhotoController'); ``` - FastRoute and others ```php $r->addRoute('GET', '/photos', ['Photos', 'index']); $r->addRoute('GET', '/photos/new', ['Photos', 'new']); $r->addRoute('POST', '/photos', ['Photos', 'create']); $r->addRoute('GET', '/photos/{id:\d+}', ['Photos', 'show']); $r->addRoute('GET', '/photos/{id:\d+}/edit', ['Photos', 'edit']); $r->addRoute('PATCH', '/photos/{id:\d+}', ['Photos', 'update']); $r->addRoute('PUT', '/photos/{id:\d+}', ['Photos', 'update']); $r->addRoute('DELETE', '/photos/{id:\d+}', ['Photos', 'destroy']); ``` --- # Annotation Routing ```php class PhotoController extends AbstractController { /** * @Route("/photo", name="photo_list") */ public function list() { } /** * @Route("/photo/{slug}", name="photo_show") */ public function show($slug) { } } ``` --- # Dispatching (PHP) ```php $route = $router->match($urlPath); $class = $route->getController(); $method = $route->getAction(); $params = $route->getParams(); $object = new $class(); $response = $object->$method(...$params); // or $request ``` --- # The Problem - Volume - Route files become long and difficult to manage - Helpers like `resource photos` are framework-specific - Even with helpers, each added route is one more to process - Non-DRY - If you add a new controller, have to add to routes file - If you modify a signature, have to modify the route spec - Duplication of information found in class/method definitions - True even for annotation-based systems --- # The Desire - Best of the (good|bad) old days: direct mapping of URL ... - ... but to classes instead of file paths - Allowance for different HTTP verbs - Allowance for static tail segments - Automatic discovery of method parameters - Result: no more routing files --- class: center, middle # The Solution ## pmjones/AutoRoute https://github.com/pmjones/AutoRoute --- class: middle > AutoRoute automatically maps HTTP requests by verb and path to PHP classes in > a specified namespace. > > It reflects on a specified action method within that class to determine the > dynamic URL parameters. > > Merely adding a class to your source code, in the specified namespace and > with the specified action method name, automatically makes it available as a > route. --- # Algorithm - AutoRoute walks the URL path segment-by-segment - Looks for a matching namespace as it goes - On a matching namespace, looks for matching verb-prefixed class - On a matching class, looks for matching method parameters - If there are remaining segments, loop over for next namespace --- # Individual Examples `GET /photos` ```php namespace App\Http\Photos; class GetPhotos { public function __invoke() { } } ``` `GET /photo/{id}` ```php namespace App\Http\Photo; class GetPhoto { public function __invoke(int $id) { } } ``` --- # Expanded Example Under the `App\Http\` PSR-4 namespace ... ``` 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) ``` --- # Static Leading Segments Given `POST /photo` and this class: ```php namespace App\Http\Photo; class PostPhoto { public function __invoke() { } } ``` --- # Individual Dynamic Segments Given `GET /photo/1` and this class: ```php namespace App\Http\Photo; class GetPhoto { public function __invoke(int $id) { } } ``` Given `PATCH /photo/1` and this class: ```php namespace App\Http\Photo; class PatchPhoto { public function __invoke(int $id) { } } ``` --- # Static Tail Segments (aka "single nested resource") Given `GET /photo/1/edit` and this class: ```php namespace App\Http\Photo\Edit; class GetPhotoEdit { public function __invoke(int $id) { } } ``` --- # Multiple Dynamic Segments Given `GET /photos/archive/1979/11` and this class: ```php namespace App\Http\Photos\Archive; class GetPhotosArchive { public function __invoke(int $year, int $month) { } } ``` --- # Variadic Segments Given `GET /photos/by-tag/foo/bar/baz` and this class: ```php namespace App\Http\Photos\ByTag; class GetPhotosByTag { public function __invoke(string ...$tags) { } } ``` --- # Recognized Typehints - int - float - string - array - `'foo,bar,baz'` => `['foo', 'bar', 'baz']` - bool - `'1' | 'y' | 'yes' | 't' | 'true'` => `true` --- class: middle, center # Usage --- # Installation and Setup `$ composer require pmjones/auto-route ^1.0` ```php use AutoRoute\AutoRoute; $autoRoute = new AutoRoute( 'App\Http', dirname(__DIR__) . '/src/App/Http/' ); $router = $autoRoute->newRouter(); ``` --- # Routing ```php try { $route = $router->route($request->method, $request->url[PHP_URL_PATH]); } catch (\AutoRoute\InvalidNamespace $e) { // 400 Bad Request } catch (\AutoRoute\InvalidArgument $e) { // 400 Bad Request } catch (\AutoRoute\NotFound $e) { // 404 Not Found } catch (\AutoRoute\MethodNotAllowed $e) { // 405 Method Not Allowed } ``` --- # Dispatch ```php // presuming a DI-based Factory that can create new action class instances: $action = Factory::new($route->class); // call the action instance with the method and params, // presumably getting back an HTTP Response $response = call_user_func($action, $route->method, ...$route->params); ``` --- # Configuration ```php $autoRoute->setSuffix('Action'); // class GetPhotoEditAction $autoRoute->setMethod('exec'); // public function exec(...) $autoRoute->setBaseUrl('/api'); // /api/photo/1/edit $autoRoute->setWordSeparator('_'); // foo_bar $router = $autoRoute()->newRouter(); ``` --- # Dumping Routes `$ php bin/autoroute-dump.php App\\Http ./src/Http` ``` POST /photo App\Http\Photo\PostPhoto GET /photo/add App\Http\Photo\Add\GetPhotoAdd DELETE /photo/{int:id} App\Http\Photo\DeletePhoto GET /photo/{int:id} App\Http\Photo\GetPhoto PATCH /photo/{int:id} App\Http\Photo\PatchPhoto GET /photo/{int:id}/edit App\Http\Photo\Edit\GetPhotoEdit GET /photos/archive[/{int:year}][/{int:month}][/{int:day}] App\Http\Photos\Archive\GetPhotosArchive GET /photos[/{int:page}] App\Http\Photos\GetPhotos ``` --- # Generating Routes Generator instance: ```php $generator = $autoRoute->newGenerator(); ``` Usage: ```php use App\Http\Photo\Edit\GetPhotoEdit; $href = $generator->generate(GetPhotoEdit::CLASS, 1); // => /photo/1/edit ``` --- class: middle, center # Conclusion --- # Tradeoffs - Invokable (single-action) controllers - aka "Actions" from [Action Domain Responder](http://pmjones.io/adr) - Must follow naming convention - URL segment to PHP namespace - URL path matching only - Use `Request` object for headers, hostname, etc. - No "fine-grained" validation/constraints - e.g., only `int` not `\d{4}` - validate in the Domain, not the User Interface --- # Bonus: It's Fast Faster than FastRoute!
(Not that it matters.) --- class: center, middle # Questions? Comments? --- class: center, middle # Thanks! http://paul-m-jones.com/ https://github.com/pmjones/AutoRoute/ ---