decodelabs / greenleaf
Super-fast directory based HTTP router
Requires
- php: ^8.1
- decodelabs/archetype: ^0.3
- decodelabs/coercion: ^0.2.7
- decodelabs/dictum: ^0.6
- decodelabs/exceptional: ^0.4.4
- decodelabs/glitch-support: ^0.4.5
- decodelabs/harvest: ^0.3
- decodelabs/singularity: ^0.2.5
- decodelabs/slingshot: ^0.1.1
- decodelabs/veneer: ^0.11.6
- psr/container: ^2.0
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- decodelabs/guidance: ^0.1.8
- decodelabs/pandora: ^0.2.12
- decodelabs/phpstan-decodelabs: ^0.6.7
Suggests
- decodelabs/guidance: Uuid route support
README
Super-fast directory based HTTP router
Greenleaf provides a simple, fast and flexible way to route HTTP requests to controllers and actions based on the URL path.
Get news and updates on the DecodeLabs blog.
Installation
Install via Composer:
composer require decodelabs/greenleaf
Usage
Greenleaf provides a PSR-15 middleware that can be used with any PSR-15 compatible framework. It will parse the request path and attempt to match it against a set of configured routes.
The heart of Greenleaf is a directory based class mapping that enables loading Generators
, Routes
and Actions
from a directory tree that closer matches the structure of most web apps from a logical perspective.
You will need to register at least one namespace with Greenleaf to allow it to load classes from your configured directory tree.
use DecodeLabs\Greenleaf; Greenleaf::$namespaces->add('MyApp\\Greenleaf');
Dispatcher
Greenleaf provides its Dispatcher under the Harvest Middleware namespace to allow for easy integration and automated class resolution with Harvest.
You can however instantiate the Dispatcher directly and treat it as a standard PSR HTTP Handler.
use DecodeLabs\Greenleaf; use DecodeLabs\Harvest; $dispatcher = Greenleaf::createDispatcher(); $request = Harvest::createRequestFromEnvironment(); $response = $dispatcher->handle($request);
Generators
Generators are used to load and configure routes. They are simple classes that implement the Generator
interface.
By default, Greenleaf will load a Scanner
Generator which in turn will scan the configured directory tree for other Generators and load the Actions they provide.
To define Routes in your directory tree, you can start with a Generic Routes Generator.
namespace MyApp\Greenleaf; use DecodeLabs\Greenleaf; use DecodeLabs\Greenleaf\Generator; use DecodeLabs\Greenleaf\GeneratorTrait; class Routes implements Generator { use GeneratorTrait; public function generateRoutes(): iterable { // Basic route yield Greenleaf::route('/', 'home'); // Basic route with parameter yield Greenleaf::route('test/{slug}', 'test') // Route with inset parameters yield Greenleaf::route('test-{slug}/', 'test?hello') ->with('slug', validate: 'slug'); // Route with multi-part path parameters yield Greenleaf::route('assets/{path}', 'assets') ->with('path', validate: 'path'); // Redirect yield Greenleaf::redirect('old/path', 'new/path'); } }
Router
When the Dispatcher runs, it loads an appropriate Router to take care of matching the Request to the configured Routes.
At this early stage, Greenleaf provides a reference Matching implementation that just brute forces its way through the list of Routes until it finds a match. This implementation is not optimised for speed and will be replaced shortly with a high performance compiled router system that will be able to handle thousands of routes with ease.
When a Router implementation finds a match, it transforms the Route pattern into a Greenleaf custom URI and a set of parameters that are then passed to the Action (if relevant).
Greenleaf URI
The URI format is mostly just a subset of HTTP URLs, with leaf
as the scheme, standard path, query and fragment components, and one notable addition: areas.
Denoted by a ~ as the first element of the path, an area allows the app to delineate segregated areas (such as the front end and admin) with ease.
The default area is "front" and is used when no area is specified.
For example:
// Route Greenleaf::route('test/{slug}', 'test?hello'); // Creates URI Greenleaf::uri('leaf://~front/test?hello'); // $params = ['slug' => 'value-of-slug-in-request'] // Resolves to: $actionClass = MyApp\Greenleaf\Front\TestAction::class; // -------------------------- // Or Greenleaf::route('admin/blog/articles', '~admin/blog/articles'); // Creates URI Greenleaf::uri('leaf://~admin/blog/articles'); // Resolves to: $actionClass = MyApp\Greenleaf\Admin\Blog\ArticlesAction::class;
Actions
Once loaded, an Action must only implement an execute($request, $uri, $params)
method, however Greenleaf provides a number of traits that can be used to add additional functionality.
The ByMethodTrait
for example will attempt to invoke a method on the Action based on the HTTP method of the request.
Note that most traits that work in this fashion will use Slingshot
to invoke the method with deep dependency injection support. In this example, the slug from the matched route request URL is passed as a string to the action handlers.
namespace MyApp\Greenleaf\Front; use DecodeLabs\Harvest; use DecodeLabs\Harvest\Response; use DecodeLabs\Greenleaf; use DecodeLabs\Greenleaf\Action; use DecodeLabs\Greenleaf\Action\ByMethodTrait; class TestAction implements Action { use ByMethodTrait; public function get(string $slug): Response { return Harvest::text('Get response'); } public function post(string $slug): Response { return Harvest::text('Post response'); } }
URLs
One of the main benefits of Greenleaf is that it allows you to generate URLs for your routes in a simple and flexible way by creating leaf URIs with many of the required URL constructs already in place.
The router will then be able to match these URIs to the correct route and pass the parameters to the URL generator.
use DecodeLabs\Greenleaf; // route pattern: test/{slug} $url = Greenleaf::createUrl( 'test?hello#fragment', ['slug' => 'my-slug'] ); // https://mydomain.localtest.me/test/my-slug?hello#fragment
Licensing
Greenleaf is licensed under the MIT License. See LICENSE for the full license text.