Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine routes list functionality #58

Open
wants to merge 10 commits into
base: 2.10.x
Choose a base branch
from

Conversation

settermjd
Copy link
Contributor

Q A
Documentation yes
Bugfix yes
BC Break no
New Feature no
RFC no
QA yes

Description

This PR implements all but one of the suggestions that @gsteel made in #52. Thank you for the motivation and inspiration to make the changes and improve the original work.

@settermjd settermjd self-assigned this Nov 14, 2024
@settermjd settermjd force-pushed the refine-routes-list-functionality branch from 6baf56f to 49a82a8 Compare November 14, 2024 11:56
Previously, RoutesFilter was taking an array with the filter options
(i.e., method, path, name, etc) but, as @gsteel noted, there was a
perfectly good RouteFilterOptions class available. I don't know why I
developed it, but overlooked it. This change refactors RoutesFilter to
use it instead, then goes one step further and introduces a
RouteFilterOptionsInterface so that, when no filter options are
available, an EmptyRouteFilterOptions can be supplied, hopefully making
the code easier to intuit and maintain.

Signed-off-by: Matthew Setter <[email protected]>
This change stores the filter options as member variables so that
they're more readily reusable and so that it should be less likely to
make a mistake with using them at some point in the future.

Signed-off-by: Matthew Setter <[email protected]>
This change removes the class' default options, replacing them with
unions of non-empty-string|null. In doing so it's explicit that
null means the option is unset and should be ignored.

This change was motivated by @gsteel's comment
mezzio#52 (comment).
I believe that I've implemented it correctly.

Signed-off-by: Matthew Setter <[email protected]>
This is to ensure that any possible type of error is caught and handled.

Signed-off-by: Matthew Setter <[email protected]>
I'm not sure why I didn't do this originally. Perhaps I was
experimenting to learn how routing works in Mezzio more deeply.
Regardless, this change refactors ListRoutesCommand to take a
RouteCollector object instead of a ContainerInterface and from that
ContainerInterface fetching a RouteCollector.

Signed-off-by: Matthew Setter <[email protected]>
Signed-off-by: Matthew Setter <[email protected]>
This change refactors ListRoutesCommandFactory to retrieve a
ConfigLoaderInterface object from the application's DiC instead of
instantiating a RoutesFileConfigLoader object directly. The main reason
for this is to provide flexibility to the user, should they have the
need to retrieve routes in a different way than the default
implementation, which searches in config/routes.php and in any routes
registered through ConfigProvider classes.

Signed-off-by: Matthew Setter <[email protected]>
This change provides a default implementation for retrieving an
application's routes configuration. It uses the stock approach of
searching for routes in config/routes.php and any registered with the
Application object in any of the loaded ConfigProvider classes. It also
provides a simplistic example for building a custom implementation,
should it be required.

Signed-off-by: Matthew Setter <[email protected]>
This change updates the packages ConfigProvider class to register a
ConfigLoaderInterface service with the DI container provided by
DefaultRoutesConfigLoaderFactory. If a user needs to override this
implementation, they can do so as per the additional documentation in
the package's README.

Signed-off-by: Matthew Setter <[email protected]>
@settermjd settermjd force-pushed the refine-routes-list-functionality branch from 49a82a8 to 8629833 Compare November 14, 2024 11:58
@settermjd
Copy link
Contributor Author

settermjd commented Nov 14, 2024

I don't know if I've messed this PR up a little, given the number of commits. Also, the Psalm results I have locally are different from the GitHub check results. Would love some help getting them in sync.

@gsteel gsteel mentioned this pull request Nov 17, 2024
@alexmerlin
Copy link
Member

I don't know if I've messed this PR up a little, given the number of commits. Also, the Psalm results I have locally are different from the GitHub check results. Would love some help getting them in sync.

@settermjd Try running Psalm locally without cache:

vendor/bin/psalm --no-cache

Or explicitly clear cache first:

vendor/bin/psalm --clear-cache

before running Psalm as usually.

Copy link
Member

@gsteel gsteel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @settermjd - I've got quite a lot of feedback here 😬

Overall, I think a lot of this can be simplified.

@@ -66,6 +68,7 @@ public function getDependencies(): array
{
return [
'factories' => [
ConfigLoaderInterface::class => DefaultRoutesConfigLoaderFactory::class,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The factory should be mapped to the implementation, i.e. RoutesFileLoader::class => Factory::class and the interface should be added to aliases, i.e. ConfigLoaderInterface::class => RoutesFileLoader::class,

Users then do the same thing with 'factories' => [MyLoader::class => MyFactory::class], 'aliases' => [ConfigLoaderInterface::class => MyLoader::class]

The difference is that I can ask the container for a specific implementation, or, I can ask it for the configured, default implementation by using the interface

if (is_array($methods)) {
array_walk($methods, fn(string &$value) => $value = strtoupper($value));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
array_walk($methods, fn(string &$value) => $value = strtoupper($value));
$this->methods = array_map(static fn (string $value): string => strtoupper($value), $methods);

Comment on lines +19 to +24
/** @param string|null|array<array-key,string> $methods */
public function __construct(
string $middleware = '',
string $name = '',
string $path = '',
$methods = []
private string|null $middleware,
private string|null $name,
private string|null $path,
private array|string|null $methods
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would drop null|string from $methods and make the argument list<string>, so you always have either list<string> or []

Comment on lines +29 to +32
/**
* Returns any route methods to filter the available routing data by
*/
public function getMethods(): array|string|null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* Returns any route methods to filter the available routing data by
*/
public function getMethods(): array|string|null;
/**
* Returns any route methods to filter the available routing data by
*
* @return list<string>
*/
public function getMethods(): array;


By default, `Mezzio\Tooling\Routes\DefaultRoutesConfigLoaderFactory` registers a `ConfigLoaderInterface` service with the application's DI container, which retrieves the application's routes from two sources:

- _config/routes.php_
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First emphasis style on the page wins, so either change all the others to _ or adjust your changes to use *

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename must be enclosed in backticks (`):

  • for a single file: filename.ext
  • for a file in a directory: directory/filename.ext

ConfigLoaderInterface $configLoader
private RouteCollector $routeCollector,
private ConfigLoaderInterface $configLoader,
private RouteFilterOptionsInterface $filterOptions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The $filterOptions property is redundant - it's a value object created at runtime from a bunch of strings and it's immediately overwritten when calling execute

@@ -0,0 +1,38 @@
<?php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class seems entirely uneccessary

@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface seems unnecessary

Comment on lines -53 to +51
$this->command = new ListRoutesCommand($container, $configLoader);
$this->command = new ListRoutesCommand(
$this->routeCollection,
$configLoader,
new EmptyRouteFilterOptions()
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symfony ships a 'CommandTester' which goes along the lines of:

$this->tester = new CommandTester($command);

You can then execute the command with a route collector instance that contains an array of actual routes and assert that the table output and json output match expectations rather than mocking everything and asserting a bunch of methods are called.

https://symfony.com/doc/current/console.html#testing-commands

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you make the route collector a test property, you can inject routes into it, per test to vary expected output based on options passed to the command and then there's effectively no mocking

@@ -78,7 +75,11 @@ public function toArray(): array
{
$values = [];
foreach (get_object_vars($this) as $key => $value) {
if (! empty($value)) {
if (in_array($key, $this->getFilterOptionsMinusMethods()) && $value !== "") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire toArray() method is unused in the command AFAICT. Can it be dropped?

@gsteel gsteel mentioned this pull request Nov 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants