mezzio-tooling icon indicating copy to clipboard operation
mezzio-tooling copied to clipboard

Add a command to print the application's routing table

Open settermjd opened this issue 2 years ago • 16 comments

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.

settermjd avatar Jul 08 '22 14:07 settermjd

12fc77a feels out of place - please rebase instead

Ocramius avatar Aug 24 '22 13:08 Ocramius

12fc77a feels out of place - please rebase instead

Could I get an assist with that?

settermjd avatar Sep 02 '22 19:09 settermjd

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?

settermjd avatar Sep 26 '22 19:09 settermjd

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;
    }
}

weierophinney avatar Sep 26 '22 19:09 weierophinney

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.

boesing avatar Sep 26 '22 20:09 boesing

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?

settermjd avatar Oct 06 '22 19:10 settermjd

Sure! I have to travel to munich this evening. Maybe we find some time later or on Sunday/next week?

boesing avatar Oct 07 '22 14:10 boesing

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.

boesing avatar Oct 13 '22 22:10 boesing

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:

  1. 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/or ConfigProvider (thats actually how we do it in our company, so we have both routes.php and some routes are part of ConfigProvider)
  2. applications using routes.php which returns a callable (consuming application, container, etc.)
  3. applications using ConfigProvider along with delegators for Application

IMHO, thats quite a lot of ways. Does anyone have an idea on how to be compatible with all these types of configurations?

boesing avatar Oct 13 '22 22:10 boesing

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 avatar Oct 14 '22 08:10 gsteel

@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.

boesing avatar Oct 16 '22 02:10 boesing

Sorry about dropping this. Back on it now.

settermjd avatar Mar 02 '23 20:03 settermjd

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;
    }
}

Thanks for this, @weierophinney. Getting back in to this to bring it to a close.

settermjd avatar Mar 02 '23 20:03 settermjd

@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 avatar Mar 02 '23 20:03 settermjd

@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.

boesing avatar Apr 30 '23 23:04 boesing

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;
    }
}

I know it's been 2 years since I last got on this, but that worked a treat, @mwop.

settermjd avatar Jul 17 '24 11:07 settermjd

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?

settermjd avatar Oct 12 '24 11:10 settermjd

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.

alexmerlin avatar Oct 12 '24 12:10 alexmerlin

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).

settermjd avatar Oct 12 '24 12:10 settermjd

Closing in favour of a newer, cleaner PR.

settermjd avatar Oct 12 '24 12:10 settermjd