mezzio-tooling
mezzio-tooling copied to clipboard
Add a command to print the application's routing table
Q | A |
---|---|
Documentation | yes |
Bugfix | no |
BC Break | no |
New Feature | yes |
RFC | no |
QA | no |
Description
This PR creates a new command that prints the application's routing table. For each route, it prints its name, path, middleware, and any additional options, in a tabular format, to the terminal.
The motivation for the change was wanting to quickly look up this information while developing Mezzio-based applications, but having to trawl through configuration files to find it. It would be easier to have a command that collated and printed the information to the terminal, helping to save time and avoid missing any listed routes.
12fc77a feels out of place - please rebase instead
I'm not sure how to finish the PR because of how the routes are typically loaded in an application generated with the Mezzio Skeleton. What I mean is that the routing table is typically loaded in public/index.php, using the following code:
(function () {
/** @var \Psr\Container\ContainerInterface $container */
$container = require 'config/container.php';
/** @var \Mezzio\Application $app */
$app = $container->get(\Mezzio\Application::class);
$factory = $container->get(\Mezzio\MiddlewareFactory::class);
// Execute programmatic/declarative middleware pipeline and routing
// configuration statements
(require 'config/pipeline.php')($app, $factory, $container);
(require 'config/routes.php')($app, $factory, $container);
$app->run();
})();
So, how should the code load the routes so that it can get access to the information?
So, how should the code load the routes so that it can get access to the information?
Since commands are typically run in the project root, you can generally assume that you can load the routes file using require 'config/routes.php';
. This file, and the config/pipeline.php
file, returns an anonymous function, which accepts three arguments:
- A
Mezzio\Application
instance - A
Mezzio\MiddlewareFactory
instance - The PSR-11 container instance
Once that function has been called, the Mezzio\Router\RouteCollector
instance should be fully populated, and you can loop over its getRoutes()
method to introspect the various Mezzio\Router\Route
instances.
Your command will need to either accept all three of the above objects to its constructor, or you could have it accept just the container, and then pull the other two from the container when you are ready to create the routing table.
As a minimal example:
class RoutingTableCommand extends Commands
{
public function __construct(private ContainerInterface $container)
{
parent::__construct();
}
protected function configure(): void
{
// setup command information here, such as options or arguments
}
protected function execute(InputInterface $input, OutputInterface $outptu): int
{
// create routing table
(require 'config/routes.php')(
$this->container->get('Mezzio\Application'),
$this->container->get('Mezzio\MiddlewareFactory'),
$this->container
);
$routes = $container->get('Mezzio\Router\RouteCollector');
// iterate over the routes
foreach ($routes->getRoutes() as $route) {
}
return 0;
}
}
Can we make this configurable? We do have a routes.php in combination with the https://github.com/mezzio/mezzio/blob/3.12.x/src/Container/ApplicationConfigInjectionDelegator.php If you now expect all routes.php to return a callables, that would be too specific. At least check if the config has the delegator registered and if so, do not require the routes.php.
edit: could be a routeloaderinterface which is either a "nooploader" or a "callables routes.php loader" for example.
Can we make this configurable? We do have a routes.php in combination with the https://github.com/mezzio/mezzio/blob/3.12.x/src/Container/ApplicationConfigInjectionDelegator.php If you now expect all routes.php to return a callables, that would be too specific. At least check if the config has the delegator registered and if so, do not require the routes.php.
edit: could be a routeloaderinterface which is either a "nooploader" or a "callables routes.php loader" for example.
Mind if we have a call to talk about this further?
Sure! I have to travel to munich this evening. Maybe we find some time later or on Sunday/next week?
Since we did not made a call yet, I took some time to looked into the actual PR.
IMHO, the command itself is properly implemented. Thats actually the exact same way as I would've done that. The commands factory receives the route collector. So thats already nice.
When I see this correct, we need the following tasks finished so that we can merge this:
- [ ] getting rid of the psalm issues, some of them can be fixed by adding this psalm plugin
- [x] replacement of prophecy with mocks
- [x] proper namespaces (unit tests are failing as the namespace of some classes is incorrect)
Almost every check is red here, so maybe getting them green would be fine.
Another thing is - I already provided a command in laminas-cache. This command is kinda related to mezzio-router
, so is there a reason why we do not put it in that library?
There are other commands in this library which do not properly fit into a specific laminas/mezzio component as they do not provide anything for a specific component (CreateHandler
/CreateMiddleware
do generate code for PSR-15, Migrate*
commands are related to PSR-15 as well).
The Module stuff could be part of mezzio
itself since we migrated this to laminas-cli
.
Just curious - no need to change this right now, but I would prefer having stuff in the components they belong to. That would prevent having to install mezzio-tooling just for the route table command.
Oh, there comes to my mind, that there are multiple ways to configure the app to have routes...
This is another way:
(function () {
/** @var \Psr\Container\ContainerInterface $container */
$container = require 'config/container.php';
/** @var \Mezzio\Application $app */
$app = $container->get(\Mezzio\Application::class);
$factory = $container->get(\Mezzio\MiddlewareFactory::class);
// Execute programmatic/declarative middleware pipeline and routing
// configuration statements
(require 'config/pipeline.php')($app, $factory, $container);
(require 'config/routes.php')($app, $factory, $container);
$app->get('/i-am-route', [IAmRouteRequestHandler::class], 'i-am-route');
$app->run();
})();
So how should we receive that route which is being added via $app->get()
? Imho, the way how mezzio is "configurable" in that way, its impossible to provide a proper command for all kind of project configurations.
So to summarize, there are:
- applications using
routes.php
which returns an array (afair that was the way how expressive was configured in either its 1st version or in even older versions) and/orConfigProvider
(thats actually how we do it in our company, so we have bothroutes.php
and some routes are part ofConfigProvider
) - applications using
routes.php
which returns a callable (consuming application, container, etc.) - applications using
ConfigProvider
along with delegators forApplication
IMHO, thats quite a lot of ways. Does anyone have an idea on how to be compatible with all these types of configurations?
FWIW, I normally setup routes by delegating around RouteCollector
so that on the cli, if I need to generate routes, I can just grab the router from the container in the knowledge that all my routes are configured without needing to bootstrap the application - typically, I delete routes.php
. In the past I've also used delegators on Application
to inject routes
@gsteel thats exactly the point, there are multiple ways of providing routes.
Since it is impossible to actually catch index.php routes (be it callables router.php or direct Application#get), I would try to Focus on just loading the route collector.
I like the idea of this command, but I wouldnt pay attention to the bunch of ways to provide a route.
Adding some description to the command might be good enough. Routes can be still passed to a delegator if some1 wants to use the command.
Sorry about dropping this. Back on it now.
So, how should the code load the routes so that it can get access to the information?
Since commands are typically run in the project root, you can generally assume that you can load the routes file using
require 'config/routes.php';
. This file, and theconfig/pipeline.php
file, returns an anonymous function, which accepts three arguments:* A `Mezzio\Application` instance * A `Mezzio\MiddlewareFactory` instance * The PSR-11 container instance
Once that function has been called, the
Mezzio\Router\RouteCollector
instance should be fully populated, and you can loop over itsgetRoutes()
method to introspect the variousMezzio\Router\Route
instances.Your command will need to either accept all three of the above objects to its constructor, or you could have it accept just the container, and then pull the other two from the container when you are ready to create the routing table.
As a minimal example:
class RoutingTableCommand extends Commands { public function __construct(private ContainerInterface $container) { parent::__construct(); } protected function configure(): void { // setup command information here, such as options or arguments } protected function execute(InputInterface $input, OutputInterface $outptu): int { // create routing table (require 'config/routes.php')( $this->container->get('Mezzio\Application'), $this->container->get('Mezzio\MiddlewareFactory'), $this->container ); $routes = $container->get('Mezzio\Router\RouteCollector'); // iterate over the routes foreach ($routes->getRoutes() as $route) { } return 0; } }
Thanks for this, @weierophinney. Getting back in to this to bring it to a close.
@gsteel thats exactly the point, there are multiple ways of providing routes.
Since it is impossible to actually catch index.php routes (be it callables router.php or direct Application#get), I would try to Focus on just loading the route collector.
I like the idea of this command, but I wouldnt pay attention to the bunch of ways to provide a route.
Adding some description to the command might be good enough. Routes can be still passed to a delegator if some1 wants to use the command.
So, it seems that the command needs to be configurable by the user, so that they tell it where to look?
@settermjd no I would simply load the route collector and extract all known methods from there. if a project adds routes via index.php, these projects cant use this command until they move routes to configuration or to delegators. But let that be the problem of those projects. Id say focus on runtime extraction.
So, how should the code load the routes so that it can get access to the information?
Since commands are typically run in the project root, you can generally assume that you can load the routes file using
require 'config/routes.php';
. This file, and theconfig/pipeline.php
file, returns an anonymous function, which accepts three arguments:* A `Mezzio\Application` instance * A `Mezzio\MiddlewareFactory` instance * The PSR-11 container instance
Once that function has been called, the
Mezzio\Router\RouteCollector
instance should be fully populated, and you can loop over itsgetRoutes()
method to introspect the variousMezzio\Router\Route
instances.Your command will need to either accept all three of the above objects to its constructor, or you could have it accept just the container, and then pull the other two from the container when you are ready to create the routing table.
As a minimal example:
class RoutingTableCommand extends Commands { public function __construct(private ContainerInterface $container) { parent::__construct(); } protected function configure(): void { // setup command information here, such as options or arguments } protected function execute(InputInterface $input, OutputInterface $outptu): int { // create routing table (require 'config/routes.php')( $this->container->get('Mezzio\Application'), $this->container->get('Mezzio\MiddlewareFactory'), $this->container ); $routes = $container->get('Mezzio\Router\RouteCollector'); // iterate over the routes foreach ($routes->getRoutes() as $route) { } return 0; } }
I know it's been 2 years since I last got on this, but that worked a treat, @mwop.
This PR has (to me) become a bit of a mess. What is the best way to finish it? Should I close it and start over on a newer branch?
IMO a new PR on a different branch - this PR/branch has been through a lot over the past couple of years.
Btw, we also have this feature in Dotkernl API, pretty much the same logic, slightly different implementation.
IMO a new PR on a different branch - this PR/branch has been through a lot over the past couple of years.
Btw, we also have this feature in Dotkernl API, pretty much the same logic, slightly different implementation.
Thanks @alexmerlin. I'm looking to close this PR and create a new one (hopefully getting it merged quickly).
Closing in favour of a newer, cleaner PR.