juliangut / slim-routing
Slim framework routing
Fund package maintenance!
juliangut
Installs: 4 488
Dependents: 0
Suggesters: 0
Security: 0
Stars: 19
Watchers: 4
Forks: 5
Open Issues: 2
Requires
- php: ^8.0
- juliangut/mapping: ^1.4.2
- slim/slim: ^4.7
- symfony/polyfill-php81: ^1.27
Requires (Dev)
- ext-dom: *
- ext-json: *
- doctrine/annotations: ^2.0
- infection/infection: ~0.25|~0.27
- juliangut/easy-coding-standard-config: ^1.14
- juliangut/phpstan-config: ^1.1
- laminas/laminas-diactoros: ^3.2
- mikey179/vfsstream: ^1.6
- overtrue/phplint: ^9.0
- phpcompatibility/php-compatibility: ^9.3
- phpmd/phpmd: ^2.14
- phpunit/phpunit: ^9.6|^10.4
- povils/phpmnd: ^3.2
- roave/security-advisories: dev-master
- slim/twig-view: ^3.3
- spatie/array-to-xml: ^3.2
- symfony/console: ^6.0|^7.0
- symfony/yaml: ^6.0|^7.0
Suggests
- slim/twig-view: In order to return Twig rendered responses (>=3.3)
- spatie/array-to-xml: In order to return XML responses (>=3.2)
- symfony/console: In order to use console commands (>=6.0)
- symfony/yaml: In order to load routing from YAML files (>=6.0)
README
slim-routing
A replacement for Slim's router that adds Attribute and configuration based routing as well as expands the possibilities of your route callbacks by handling different response types
Thanks to this library, instead of configuring routes by hand one by one and including them into Slim's routing you can create mapping files that define and structure your routes and let them be included automatically instead
Additionally, if you're familiar with Symfony's definition of routes through Attributes you'll feel at home with slim-routing because route mappings can be defined the same way as well
- On class Attributes (i.e. controller classes)
- In routing definition files, currently supported in PHP, JSON, XML and YAML
Route gathering and compilation can be quite a heavy process depending on how many classes/files and routes are defined, specially in the case of attributes. For this reason it's strongly advised to always use route collector's cache and Slim's route expression caching on production applications and invalidate cache on deployment
Thanks to slim-routing route callbacks can now return \Jgut\Slim\Routing\Response\ResponseType
objects that will be ultimately transformed into the mandatory Psr\Message\ResponseInterface
in a way that lets you fully decouple view from the route
Installation
Composer
composer require juliangut/slim-routing
symfony/yaml to parse yaml routing files
composer require symfony/yaml
spatie/array-to-xml to return XML responses
composer require spatie/array-to-xml
slim/twig-view to return Twig rendered responses
composer require slim/twig-view
Usage
Require composer autoload file
require './vendor/autoload.php';
use Jgut\Slim\Routing\AppFactory; use Jgut\Slim\Routing\Configuration; use Jgut\Slim\Routing\Response\PayloadResponse; use Jgut\Slim\Routing\Response\RedirectResponse; use Jgut\Slim\Routing\Response\ResponseType; use Jgut\Slim\Routing\Response\Handler\JsonResponseHandler; use Jgut\Slim\Routing\Response\Handler\RedirectResponseHandler; use Jgut\Slim\Routing\Strategy\RequestHandler; use Psr\Http\Message\ServerRequestInterface; $configuration = new Configuration([ 'sources' => ['/path/to/routing/files'], ]); AppFactory::setRouteCollectorConfiguration($configuration); // Instantiate the app $app = AppFactory::create(); $routeCollector = $app->getRouteCollector(); $responseFactory = $app->getResponseFactory(); // Register custom invocation strategy to handle ResponseType objects $invocationStrategy = new RequestHandler( [ RedirectResponse::class => new RedirectResponseHandler($responseFactory, $routeCollector), // Handlers can be pulled from the container PayloadResponse::class => JsonResponseHandler::class, ], $responseFactory, $app->getContainer() ); $routeCollector->setDefaultInvocationStrategy($invocationStrategy); $cache = new PSR16Cache(); $routeCollector->setCache($cache); // Recommended if you want to add more routes manually $routeCollector->registerRoutes(); // Additional routes if needed $app->get('/', function(ServerRequestInterface $request): ResponseType { return new PayloadResponse(['param' => 'value'], $request); }); $app->run();
Configuration
sources
must be an array containing arrays of configurations to create MappingDriver objects:type
one of \Jgut\Slim\Routing\Mapping\Driver\DriverFactory constants:DRIVER_ATTRIBUTE
,DRIVER_PHP
,DRIVER_JSON
,DRIVER_XML
,DRIVER_YAML
orDRIVER_ANNOTATION
if no driver, defaults to DRIVER_ATTRIBUTEpath
a string path or array of paths to where mapping files are located (files or directories) REQUIRED if no driverdriver
an already created \Jgut\Slim\Routing\Mapping\Driver\DriverInterface object REQUIRED if no type AND path
trailingSlash
boolean, indicates whether to append a trailing slash on route pattern (true) or remove it completely (false), by default. False by defaultplaceholderAliases
array of additional placeholder aliases. There are some default aliases already available:- numeric =>
\d+
- alpha =>
[a-zA-Z]+
- alnum =>
[a-zA-Z0-9]+
- slug ->
[a-zA-Z0-9-]+
- uuid ->
[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
- mongoid ->
[0-9a-f]{24}
- any =>
[^}]+
- numeric =>
namingStrategy
, instance of \Jgut\Slim\Routing\Naming\Strategy (\Jgut\Slim\Routing\Naming\SnakeCase by default)
In the case of Attributes mapping all classes in source paths will be traversed in search of routing definitions
Response handling
Ever wondered why you should encode output or call template renderer in every single route? or even why respond with a ResponseInterface object in the end?
$app->get('/hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { return $this->view->render( $response, 'greet.html', [ 'name' => $args['name'] ] ); })->setName('greet');
Route callbacks normally respond with a Psr\Message\ResponseInterface
object, but thanks to slim-routing they can now respond with a string, null or even better with a more intent expressive ResponseType object that will be handled afterward
$app->get( '/hello/{name}', fn (array $args): string => 'Hello ' . $args['name'], )->setName('greet');
Of course normal ResponseInterface responses from route callback will be treated as usual
ResponseType aware invocation strategies
For the new response handling to work you need to register a new invocation strategy provided by this library, there are four strategies provided out of the box that plainly mimic the ones provided by Slim
Jgut\Slim\Routing\Strategy\RequestHandler
Jgut\Slim\Routing\Strategy\RequestResponse
Jgut\Slim\Routing\Strategy\RequestResponseArgs
Jgut\Slim\Routing\Strategy\RequestResponseNamedArgs
Response type
Response types are Value Objects with the needed data to later produce a ResponseInterface object. This leaves the presentation logic out of routes allowing for cleaner routes and easy presentation logic reuse
use Jgut\Slim\Routing\Response\ResponseType; use Jgut\Slim\Routing\Response\ViewResponse; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; $app->get( '/hello/{name}', fn (ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseType => new ViewResponse('greet.html', ['name' => $args['name']], $request, $response), )->setName('greet');
If a route returns an instance of \Jgut\Slim\Routing\Response\ResponseType
it will be passed to the corresponding handler according to configuration
There are three response types already provided:
RedirectResponse
Slim's route aware redirection, can redirect to a Slim route or an external locationPayloadResponse
stores simple payload data to be later transformed for example into JSON or XMLViewResponse
keeps agnostic template parameters, so they can be rendered in a handler
You can easily create your own.
Response type handler
Mapped on invocation strategy, a response handler will be responsible for returning a Psr\Message\ResponseInterface
from the received \Jgut\Slim\Routing\Response\ResponseType
Typically, they will agglutinate presentation logic: how to represent the data contained in the response type, such as transform it into JSON, XML, etc. or render it with a template engine such as Twig
Register response type handlers on invocation strategy creation or
use Jgut\Slim\Routing\Response\PayloadResponse; use Jgut\Slim\Routing\Response\Handler\JsonResponseHandler; $invocationStrategy->setResponseHandler(PayloadResponse::class, JsonResponseHandler::class);
Provided response types handlers:
RedirectResponseHandler
receives a RedirectResponse type and returns the corresponding PSR7 redirect responseJsonResponseHandler
receives a PayloadResponse type and returns a PSR7 JSON responseXmlResponseHandler
receives a PayloadResponse type and returns a PSR7 XML response (requires spatie/array-to-xml)TwigViewResponseHandler
receives a generic ViewResponse type and returns a template rendered PSR7 response thanks to slim/twig-view
You can create your own response type handlers to compose specifically formatted response (JSON:API, ...) or use another template engines (Plates, Blade, ...), or craft any other response
Parameter transformation
Route parameters can be transformed before arriving to route callable. The most common use of this feature would be to transform IDs into actual object/entity used in the callable
To achieve this you need to provide a \Jgut\Slim\Routing\Transformer\ParameterTransformer
instance
For example, you would want to transform parameters into Doctrine entities
use Jgut\Slim\Routing\Transformer\ParameterTransformer; use Slim\Exception\HttpNotFoundException; final class UserEntityTransformer implements ParameterTransformer { public function __construct( private EntityManager $entityManager, ) {} protected function supports(string $parameter, string $type) : bool { return $parameter === 'user' && $type === UserEntity::class; } protected function transform(string $parameter, string $type, mixed $value): mixed { $user = $this->entityManager->getRepository($type)->find($value); if ($user === null) { throw new HttpNotFoundException('User not found'); } return $user; } }
Mind that a single transformer can transform one or several parameters
Console commands
use Symfony\Component\Console\Application; use Jgut\Slim\Routing\Console\ListCommand; use Jgut\Slim\Routing\Console\MatchCommand; /** @var \Slim\App $app */ $routeResolver = $app->getRouteResolver(); $cli = new Application('Slim CLI'); $cli->add(new ListCommand($routeResolver)); $cli->add(new MatchCommand($routeResolver)); $app->run();
List routing
List defined routes supporting searching and sorting
php -f cli.php slim:routing:list --help
Match routing
Match routes with Slim's route resolver, so results will be exactly the same as through HTTP requests
php -f cli.php slim:routing:match --help
Route mapping
Routes can be defined in two basic ways: by writing them down in definition files of various formats or directly in classes with Attributes
Attributes
Group (Class level)
Defines a group in which routes may reside. It is not mandatory but useful when you want to do route grouping or apply middleware to several routes at the same time
use Jgut\Slim\Routing\Mapping\Attribute\Group; #[Group( prefix: 'routePrefix', parent: Area::class, pattern: 'section/{section}', placeholders: ['section': 'alpha'], arguments: ['scope' => 'public'], )] class Section {}
prefix
, optional, prefix to be prepended to route namesparent
, optional, references a parent group namepattern
, optional, path pattern, prepended to route patterns. Do not use placeholders in the patternplaceholders
, optional, array of regex/alias for pattern placeholders,arguments
, optional, array of arguments to attach to final route
Route (Method level)
Defines the final routes added to Slim
use Jgut\Slim\Routing\Mapping\Attribute\Route; class Section { #[Route( name: 'routeName', methods: ['GET', 'POST'], pattern: 'user/{user}', placeholders: ['user': 'alnum'], arguments: ['scope': 'admin.read'] xmlHttpRequest: true, priority: -10, )] public function userAction() {} }
name
, optional, route name so it can be referenced in Slimmethods
, optional, one or a list of accepted HTTP route methods. ºANY" is a special method that transforms to[GET, POST, PUT, PATCH, DELETE]
, if ANY is used no other method is allowed in the list (defaults to GET)pattern
, optional, path pattern (defaults to '/'). Do not use placeholders in the patternplaceholders
, optional, array of regex/alias for pattern placeholdersarguments
, optional, array of arguments to attach to the routexmlHttpRequest
, request should be AJAX, false by defaultpriority
, optional, integer for ordering route registration. The order is global among all loaded routes. Negative routes get loaded first (defaults to 0)
Middleware (Class and Method level)
Defines middleware to apply. Multiple Attributes can be added
use Jgut\Slim\Routing\Mapping\Attribute\Group; use Jgut\Slim\Routing\Mapping\Attribute\Middleware; use Jgut\Slim\Routing\Mapping\Attribute\Route; use Psr\Http\Message\ResponseInterface; #[Group()] #[Middleware(GroupMiddleware::class)] #[Middleware(AdditionalMiddleware::class)] class Section { #[Route(methods: ['GET'], pattern: 'user')] #[Middleware(RouteMiddleware::class)] public function userAction(): ResponseInterface {} }
middleware
, MiddlewareInterface class that will be extracted from the container
Transformer (Class and Method level)
Defines route transformers. Multiple Attributes can be added
use Jgut\Slim\Routing\Mapping\Attribute\Group; use Jgut\Slim\Routing\Mapping\Attribute\Route; use Jgut\Slim\Routing\Mapping\Attribute\Transformer; use Psr\Http\Message\ResponseInterface; #[Group()] #[Transformer(transformer: SectionEntityTransfomer::class)] class Section { #[Route(methods: ['GET'], pattern: 'user/{user}')] #[Transformer( transformer: UserEntityTransfomer::class), parameters: ['user': User::class], )] public function userAction($request, $response, $user): ResponseInterface {} }
transformer
, ParameterTransformer class that will be extracted from the containerparameters
, optional, array of definitions of parameters
Definition files
PHP
return [ [ // Group 'prefix' => 'prefix', 'pattern' => 'group-pattern', 'placeholders' => [ 'group-placeholder' => 'type', ], 'arguments' => [ 'group-argument' => 'value', ], 'parameters' => [ 'group-parameters' => 'type', ], 'transformers' => ['group-transformer'], 'middlewares' => ['group-middleware'], 'routes' => [ [ // Route 'name' => 'routeName', 'xmlHttpRequest' => true, 'methods' => ['GET', 'POST'], 'priority' => 0, 'pattern' => 'route-pattern', 'placeholders' => [ 'route-placeholder' => 'type', ], 'parameters' => [ 'route-parameters' => 'type', ], 'transformers' => ['route-transformer'], 'arguments' => [ 'route-argument' => 'value', ], 'middlewares' => ['route-middleware'], 'invokable' => 'callable', ], [ // Subgroup 'pattern' => 'subgroup-pattern', 'placeholders' => [ 'subgroup-placeholder' => 'type', ], 'arguments' => [ 'subgroup-argument' => 'value', ], 'middlewares' => ['subgroup-middleware'], 'routes' => [ // Routes/groups ... ], ], // Routes/groups ... ], ], // Routes/groups ... ];
XML
<?xml version="1.0" encoding="utf-8"?> <root> <group1 prefix="prefix" pattern="group-pattern"> <placeholders> <group-placeholder1>type</group-placeholder1> </placeholders> <arguments> <group-argument1>value</group-argument1> </arguments> <parameters> <group-parameter1>type</group-parameter1> </parameters> <transformers> <transformer1>group-transformer</transformer1> </transformers> <middlewares> <middleware1>group-middleware</middleware1> </middlewares> <routes> <route1 name="routeName" priority="0" pattern="route-pattern"> <xmlHttpRequest>true</xmlHttpRequest> <methods> <method1>GET</method1> <method2>POST</method2> </methods> <placeholders> <route-placeholder1>type</route-placeholder1> </placeholders> <parameters> <route-parameter1>type</route-parameter1> </parameters> <transformers> <transformer1>route-ransformer</transformer1> </transformers> <arguments> <route-argument1>value</route-argument1> </arguments> <middlewares> <middleware1>route-middleware</middleware1> </middlewares> <invokable>callable</invokable> </route1> <subgroup1 prefix="prefix" pattern="subgroup-pattern"> <placeholders> <subgroup-placeholder1>type</subgroup-placeholder1> </placeholders> <argument> <subgroup-argument1>value</subgroup-argument1> </argument> <middlewares> <middleware1>subgroup-middleware</middleware1> </middlewares> <routes> <!-- Routes/groups... --> </routes> </subgroup1> <!-- Routes/groups... --> </routes> </group1> <!-- Routes/groups... --> </root>
JSON
Mind comments are not valid standard JSON
[ { // Group "prefix": "prefix", "pattern": "group-pattern", "placeholders": [{ "group-placeholder": "type" }], "arguments": [{ "group-argument": "value" }], "parameters": [{ "group-parameter": "type" }], "transformers": ["group-transformer"], "middlewares": ["group-middleware"], "routes": [ { // Route "name": "routeName", "xmlHttpRequest": true, "methods": ["GET", "POST"], "priority": 0, "pattern": "route-pattern", "placeholders": [{ "route-placeholder": "type" }], "parameters": [{ "route-parameter": "type" }], "transformers": ["route-transformer"], "arguments": [{ "route-argument": "value" }], "middlewares": ["route-middleware"], "invokable": "callable" }, { // Subgroup "pattern": "subgroup-pattern", "placeholders": [{ "subgroup-placeholder": "type" }], "arguments": [{ "subgroup-argument": "value" }], "middlewares": ["subgroup-middleware"], "routes": [ // Routes/groups ... ] } // Routes/groups ... ] } // Routes/groups ... ]
YAML
# Group - prefix: prefix pattern: group-pattern placeholders: - group-placeholder: type arguments: - group-argument: value parameters: - group-parameter: type transformers: [group-ransformer] middlewares: [group-middleware] routes: # Route - name: routeName xmlHttpRequest: true methods: [GET, POST] priority: 0 pattern: route-pattern placeholders: - route-placeholder: type parameters: - route-parameter: type transformers: [route-ransformer] arguments: - route-argument: value middlewares: [route-middleware] invokable: callable # Subgroup - pattern: subgroup-pattern placeholders: - subgroup-placeholder: type arguments: - subgroup-argument: value middlewares: [subgroup-middleware] routes: # Routes/groups ... # Routes/groups ... # Routes/groups ...
Group
Defines a group in which routes may reside
routes
, array of routes and/or subgroups (this key identifies a group)prefix
, optional, prefix to be prepended to route namespattern
, optional, path pattern, prepended to route patterns. Do not use placeholders in the patternplaceholders
, optional, array of regex/alias for pattern placeholders,parameters
, optional, array of definitions of parameters, to be used in transformertransformers
, optional, array of ParameterTransformer class that will be extracted from the containerarguments
, optional, array of arguments to attach to final routemiddlewares
, optional, array of MiddlewareInterface class to be added to all group routes
Route
Defines a route added to Slim
invokable
, callable to be invoked on route match. Can be a container entry, class name or an array of [class, method]name
, optional, route name so it can be referenced in Slimpattern
, optional, path pattern (defaults to '/'). Do not use placeholders in the patternplaceholders
, optional, array of regex/alias for pattern placeholdersxmlHttpRequest
, request should be AJAX, false by defaultmethods
, optional, one or a list of accepted HTTP route methods. "ANY" is a special method that transforms to[GET, POST, PUT, PATCH, DELETE]
, if ANY is used no other method is allowed (defaults to GET)parameters
, optional, array of definitions of parameters, to be used in transformertransformers
, optional, array of ParameterTransformer class that will be extracted from the containerarguments
, optional, array of arguments to attach to the routemiddlewares
, optional, array of MiddlewareInterface class to be added to the routepriority
, optional, integer for ordering route registration. The order is global among all loaded routes. Negative routes get loaded first (defaults to 0)
Annotations
Annotations are deprecated and will be removed eventually. Use Attribute mapping when possible.
You need to require Doctrine's annotation package
composer require doctrine/annotations
Group (Class level)
Defines a group in which routes may reside. It is not mandatory but useful when you want to do route grouping or apply middleware to several routes at the same time
use Jgut\Slim\Routing\Mapping\Annotation as JSR; /** * @JSR\Group( * prefix="routePrefix", * parent=Area::class, * pattern="section/{section}", * placeholders={"section": "[a-z]+"}, * arguments={"scope": "public"} * parameters={"section": "\Namespace\To\Section"}, * transformers={"\Namespace\To\GroupTransformer"} * middleware={"\Namespace\To\GroupMiddleware"} * ) */ class Section {}
prefix
, optional, prefix to be prepended to route namesparent
, optional, references a parent group namepattern
, optional, path pattern, prepended to route patternsplaceholders
, optional, array of regex/alias for pattern placeholders,parameters
, optional, array of definitions of parameters, to be used in route transformertransformers
, optional, array of ParameterTransformer class that will be extracted from the containerarguments
, optional, array of arguments to attach to final routemiddlewares
, optional, array of MiddlewareInterface class to be added to all group routes
Route (Method level)
Defines the final routes added to Slim
use Jgut\Slim\Routing\Mapping\Annotation as JSR; class Section { /** * @JSR\Route( * name="routeName", * xmlHttpRequest=true, * methods={"GET", "POST"}, * pattern="user/{user}", * placeholders={"user": "[a-z0-9]+"}, * arguments={"scope": "admin.read"} * parameters={"user": "\Namespace\To\User"}, * transformers={"\Namespace\To\RouteTransformer"}, * middleware={"\Namespace\To\RouteMiddleware"}, * priority=-10 * ) */ public function userAction() {} }
name
, optional, route name so it can be referenced in Slimpattern
, optional, path pattern (defaults to '/')placeholders
, optional, array of regex/alias for pattern placeholdersxmlHttpRequest
, request should be AJAX, false by defaultmethods
, optional, list of accepted HTTP route methods. "ANY" is a special method that transforms to[GET, POST, PUT, PATCH, DELETE]
, if ANY is used no other method is allowed in the list (defaults to GET)parameters
, optional, array of definitions of parameters, to be used in transformertransformers
, optional, array of ParameterTransformer class that will be extracted from the containerarguments
, optional, array of arguments to attach to the routemiddlewares
, optional, array of MiddlewareInterface class to be added to the routepriority
, optional, integer for ordering route registration. The order is global among all loaded routes. Negative routes get loaded first (defaults to 0)
Route composition
Using grouping with juliangut/slim-routing is a little different to how default Slim's router works
Groups are never really added to the router (in the sense you can add them in Slim with $app->group(...)
), instead, routes are a composition of definitions that makes the final route
Name
Final route name is composed of the concatenation of group prefixes followed by route name according to configured route naming strategy
Pattern
Resulting route pattern is composed of the concatenation of group patterns if any and finally route pattern
Placeholders
Resulting placeholders list is composed of all group placeholders if any and route placeholders
It is important to pay attention not to duplicate placeholder names in the resulting pattern as this can't be handled by FastRoute. Check group tree patterns for placeholder names
Arguments
Resulting route arguments is composed of all group arguments if any and route arguments
Middlewares
Resulting middlewares added to a route will be the result of combining group middleware and route middleware and are applied to the route in the following order, so that final middleware execution order will be the same as expected in any Slim app:
- Firstly route middleware will be set to the route in the order they are defined
- Then group middleware (if any) are to be set into the route in the same order they are defined
- If group has a parent then parent's middleware are set in the order they are defined, and this goes up until no parent group is left
Transformers and parameters
Resulting transformers and parameters list are a combination of all group transformers and parameters if any and route transformers and parameters
As with placeholders, it is important to pay attention not to duplicate parameter names as child groups and routes will replace previous parameters
Migration from 2.x
- Minimum PHP version is now 8.0
- Minimum Slim version is now 4.7
- PHP8 Attributes have been introduced for route mapping
- ParameterTransformer methods and signatures have changed
- AbstractTransformer has been removed, simply implement ParameterTransformer
- Some internal methods have changed their signatures or have been made final
Annotations
Annotations have been deprecated and its use is highly discouraged in favor of Attributes
- @Router Annotation optional and triggers an E_USER_DEPRECATED
- @Group and @Route Annotations "middleware" parameter have changed to "middlewares" and accepts a list or a single middleware
- @Route Annotation transformers now accepts a list or a single transformer
- @Group Annotation now supports transformers as well
Contributing
Found a bug or have a feature request? Please open a new issue. Have a look at existing issues before.
See file CONTRIBUTING.md
License
See file LICENSE included with the source code for a copy of the license terms.