A replacement for Slim's router that adds annotation 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
If you're familiar with how Doctrine defines entities mappings you'll feel at home with slim-routing because route mappings are defined the same way
- On class annotations (in 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 annotations. For this reason it's strongly advised to always use this library route collector cache and Slim's route collector caching on production applications and invalidate cache on deployment
Route callbacks can now thanks to slim-routing 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
composer require juliangut/slim-routing
doctrine/annotations to parse routing annotations
composer require doctrine/annotations
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
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\Handler\JsonResponseHandler;
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();
/** @var \Psr\SimpleCache\CacheInterface $cache */
$cache = new CacheImplementation();
// Register custom invocation strategy to handle ResponseType objects
$invocationStrategy = new RequestHandler(
[
PayloadResponse::class => JsonResponseHandler::class,
],
$app->getResponseFactory(),
$app->getContainer()
);
$routeCollector = $app->getRouteCollector();
$routeCollector->setDefaultInvocationStrategy($invocationStrategy);
$routeCollector->setCache($cache);
// Not mandatory but recommended if more routes are added, see below
$routeCollector->registerRoutes();
// Set other routes
$app->get('/', function(ServerRequestInterface $request) {
return new PayloadResponse(['param' => 'value'], $request);
});
$app->run();
sources
must be an array containing arrays of configurations to create MappingDriver objects:type
one of \Jgut\Slim\Routing\Mapping\Driver\DriverFactory constants:DRIVER_ANNOTATION
,DRIVER_PHP
,DRIVER_JSON
,DRIVER_XML
orDRIVER_YAML
defaults to DRIVER_ANNOTATION if no driverpath
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\NamingInterface (\Jgut\Slim\Routing\Naming\SnakeCase by default)
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 ($request, $response, $args) {
return $this->view->render(
$response,
'profile.html',
[
'name' => $args['name']
]
);
})->setName('profile');
Route callbacks normally respond with a Psr\Message\ResponseInterface
object, but thanks to slim-routing they can now respond with a string, null or event better, with a more intent expressive ResponseType object that will be handled afterwards
Of course normal ResponseInterface responses from route callback will be treated as usual
For the new response handling to work you need to register a new invocation strategy provided by this library, there are three strategies provided out of the box that plainly mimic the defaults provided by Slim itself
Jgut\Slim\Routing\Strategy\RequestHandler
Jgut\Slim\Routing\Strategy\RequestResponse
Jgut\Slim\Routing\Strategy\RequestResponseArgs
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\ViewResponse;
$app->get('/hello/{name}', function ($request, $response, $args) {
return new ViewResponse('profile.html', ['name' => $args['name']], $request, $response);
})->setName('profile');
If a route returns an instance of \Jgut\Slim\Routing\Response\ResponseType
it will be passed to the corresponding handler according to configuration
Provided response types:
PayloadResponse
stores simple payload data to be later transformed for example into JSON or XMLViewResponse
keeps agnostic template parameters so it can be rendered in a handler
Mapped on invocation strategy, a response handler will be responsible of 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\XmlResponseHandler;
$invocationStrategy->setResponseHandler(PayloadResponse::class, XmlResponseHandler::class);
Provided response types handlers:
JsonResponseHandler
receives a PayloadResponse and returns a JSON responseXmlResponseHandler
receives a PayloadResponse and returns a XML response (requires spatie/array-to-xml)TwigViewResponseHandler
receives a generic ViewResponse and returns a template rendered 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, ...)
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 defined in the route itself. There is an abstract class \Jgut\Slim\Routing\Transformer\AbstractTransformer
to easily implement parameters transformation
For example, you would want to transform parameters into Doctrine entities
use Jgut\Slim\Routing\Transformer\AbstractTransformer;
class CustomTransformer extends AbstractTransformer
{
protected $entityManager;
public function __construct(EntityManager $entityManager) {
$this->entityManager = $entityManager;
}
protected function transformParameter(string $parameter, string $type)
{
return $this->entityManager->getRepository($type)->find($parameter);
}
}
Routes can be defined in two basic ways: by setting them in definition files of various formats or directly defined in annotations on controller classes
Just to identify classes defining routes. Its presence is mandatory on each routing class either way the rest of the annotations won't be read
use Jgut\Slim\Routing\Mapping\Annotation as JSR;
/**
* @JSR\Router
*/
class Home
{
}
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\Router
* @JSR\Group(
* prefix="routePrefix",
* parent="parentGroupClassName",
* pattern="section/{name}",
* placeholders={"name": "[a-z]+"},
* parameters={"action": "\My\Entity"},
* arguments={"scope": "public"}
* middleware={"groupMiddlewareName"}
* )
*/
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 path placeholders,parameters
, optional, array of definitions of parameters, to be used in route transformerarguments
, optional, array of arguments to attach to final routemiddleware
, optional, array of middleware to be added to all group routes
Defines the final routes added to Slim
use Jgut\Slim\Routing\Mapping\Annotation as JSR;
/**
* @JSR\Router
*/
class Section
{
/**
* @JSR\Route(
* name="routeName",
* xmlHttpRequest=true,
* methods={"GET", "POST"},
* pattern="do/{action}",
* placeholders={"action": "[a-z0-9]+"},
* transformer="CustomTransformer",
* parameters={"action": "\My\Entity"},
* arguments={"scope": "admin.read"}
* middleware={"routeMiddlewareName"},
* priority=-10
* )
*/
public function doSomething()
{
}
}
name
, optional, route name so it can be referenced in Slimpattern
, optional, path pattern (defaults to '/')xmlHttpRequest
, 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)placeholders
, optional, array of regex/alias for path placeholdersparameters
, optional, array of definitions of parameters, to be used in transformertransformer
, optional, reference to a ParameterTransformer instance that will be extracted from the containerarguments
, optional, array of arguments to attach to the routemiddleware
, optional, array of middleware 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)
return [
[
// Group
'prefix' => 'prefix',
'pattern' => 'group-pattern',
'placeholders' => [
'group-placeholder' => 'type',
],
'arguments' => [
'group-argument' => 'value',
],
'middleware' => ['group-middleware'],
'routes' => [
[
// Route
'name' => 'routeName',
'xmlHttpRequest' => true,
'methods' => ['GET', 'POST'],
'priority' => 0,
'pattern' => 'route-pattern',
'placeholders' => [
'route-placeholder' => 'type',
],
'parameters' => [
'route-parameters' => 'type',
],
'transformer' => 'customTransformer',
'arguments' => [
'route-argument' => 'value',
],
'middleware' => ['route-middleware'],
'invokable' => 'callable',
],
[
// Subgroup
'pattern' => 'subgroup-pattern',
'placeholders' => [
'subgroup-placeholder' => 'type',
],
'arguments' => [
'subgroup-argument' => 'value',
],
'middleware' => ['subgroup-middleware'],
'routes' => [
// Routes/groups ...
],
],
// Routes/groups ...
],
],
// Routes/groups ...
];
[
{
// Group
"prefix": "prefix",
"pattern": "group-pattern",
"placeholders": [{
"group-placeholder": "type"
}],
"arguments": [{
"group-argument": "value"
}],
"middleware": ["group-middleware"],
"routes": [
{
// Route
"name": "routeName",
"xmlHttpRequest": true,
"methods": ["GET", "POST"],
"priority": 0,
"pattern": "route-pattern",
"placeholders": [{
"route-placeholder": "type"
}],
"parameters": [{
"route-parameter": "type"
}],
"transformer": "customTransformer",
"arguments": [{
"route-argument": "value"
}],
"middleware": ["route-middleware"],
"invokable": "callable"
},
{
// Subgroup
"pattern": "subgroup-pattern",
"placeholders": [{
"subgroup-placeholder": "type"
}],
"arguments": [{
"subgroup-argument": "value"
}],
"middleware": ["subgroup-middleware"],
"routes": [
// Routes/groups ...
]
}
// Routes/groups ...
]
}
// Routes/groups ...
]
<?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>
<middleware>
<middleware1>group-middleware</middleware1>
</middleware>
<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>
<transformer>CustomTransformer</transformer>
<arguments>
<route-argument1>value</route-argument1>
</arguments>
<middleware>
<middleware1>route-middleware</middleware1>
</middleware>
<invokable>callable</invokable>
</route1>
<subgroup1 prefix="prefix" pattern="subgroup-pattern">
<placeholders>
<subgroup-placeholder1>type</subgroup-placeholder1>
</placeholders>
<argument>
<subgroup-argument1>value</subgroup-argument1>
</argument>
<middleware>
<middleware1>subgroup-middleware</middleware1>
</middleware>
<routes>
<!-- Routes/groups... -->
</routes>
</subgroup1>
<!-- Routes/groups... -->
</routes>
</group1>
<!-- Routes/groups... -->
</root>
# Group
- prefix: prefix
pattern: group-pattern
placeholders:
- group-placeholder: type
arguments:
- group-argument: value
middleware: [group-middleware]
routes:
# Route
- name: routeName
xmlHttpRequest: true
methods: [GET, POST]
priority: 0
pattern: route-pattern
placeholders:
- route-placeholder: type
parameters:
- route-parameter: type
transformer: CustomTransformer
arguments:
- route-argument: value
middleware: [route-middleware]
invokable: callable
# Subgroup
- pattern: subgroup-pattern
placeholders:
- subgroup-placeholder: type
arguments:
- subgroup-argument: value
middleware: [subgroup-middleware]
routes:
# Routes/groups ...
# Routes/groups ...
# Routes/groups ...
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 patternsplaceholders
, optional, array of regex/alias for path placeholders,parameters
, optional, array of definitions of parameters, to be used in route transformerarguments
, optional, array of arguments to attach to final routemiddleware
, optional, array of middleware to be added to all group routes
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 '/')xmlHttpRequest
, 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 (defaults to GET)placeholders
, optional, array of regex for path placeholdersparameters
, optional, array of definitions of parameters, to be used in transformertransformer
, optional, reference to a ParameterTransformer instance that will be extracted from the containerarguments
, optional, array of arguments to attach to the routemiddleware
, optional, array of middleware 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)
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(...)
) but routes are a composition of definitions that makes the final route
Final route name is composed of the concatenation of group prefixes followed by route name according to configured route naming strategy
Resulting route pattern is composed of the concatenation of group patterns if any and finally route pattern
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
Resulting route arguments is composed of all group arguments if any and route arguments
Resulting middleware 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 route group (if any) middleware 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
- Minimum Slim version is now 4.2
- ResponseType handling is NOT automatically available due to how Slim 4 handles routing, it has been moved into custom RequestHandlerInvocationStrategyInterface implementations and thus must be registered on RouteCollector
- Response handlers list configuration have been moved to each RequestHandlerInvocationStrategyInterface implementation
Found a bug or have a feature request? Please open a new issue. Have a look at existing issues before.
See file CONTRIBUTING.md
See file LICENSE included with the source code for a copy of the license terms.