Skip to content

Using a service as a router resource #15742

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

Closed
wants to merge 18 commits into from

Conversation

weaverryan
Copy link
Member

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? almost
Fixed tickets n/a
License MIT
Doc PR not yet...

Hi guys!

This adds the ability to use a service as a routing resource. In other words, instead of loading routing.yml, you could load my_route_loader, and then a method would be called on your service to return a RouteCollection.

Specifically, I'm interested in this because it would allow a user to point their main router resource to the kernel itself, making it possible to load routes inside the kernel (making a single-file full-stack app more possible).

Thanks!

*/
public function supports($resource, $type = null)
{
return $type == 'service';
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be return 'service' === $type.

*
* @return RouteCollection
*/
public function getRouteCollection(Loader $loader);
Copy link
Member

Choose a reason for hiding this comment

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

please don't typehint an implementation

Copy link
Member Author

Choose a reason for hiding this comment

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

My problem is that the Loader has import on it, which is really what the user will want. The LoaderInterface doesn't have this. What do you think about creating a new interface for loaders with import to avoid type-hinting the implementation?

@weaverryan
Copy link
Member Author

@stof I decided to type-hint against the existing LoaderInterface, as it's the smallest change. I'd still love to have import guaranteed, but I believe you can "import" manually by calling getResolver()->resolve(...)->load() on the LoaderInterface. If there's support for adding some sort of an ImportingLoaderInterface with the import() method, I would be very happy to add that.

I've taken all the feedback into account. This is still ready for review :)

*
* @return RouteCollection
*/
public function getRouteCollection(LoaderInterface $loader);
Copy link
Member Author

Choose a reason for hiding this comment

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

I've type-hinted the interface, but in reality, I'm passing in the concrete ServiceRouteLoader itself. The difference is that ServiceRouteLoader (like all loaders), has a really handy import() method, while LoaderInterface does not. I'd prefer to type-hint something with the import() method - either the concrete Loader or a new interface.

@weaverryan
Copy link
Member Author

Ping @symfony/deciders

@dunglas
Copy link
Member

dunglas commented Sep 14, 2015

Great! Would allow us to remove some magic regarding the router in API platform as well.

@lsmith77
Copy link
Contributor

interesting. would also allow for simple integration of dynamic resources.

/cc @dbu

@sstok
Copy link
Contributor

sstok commented Sep 14, 2015

👍 for this feature!

@dupuchba
Copy link
Contributor

👍 really great

@weaverryan
Copy link
Member Author

The test failure is because FrameworkBundle now depends on a new class inside symfony/router that's also included in this PR (but the tests only pull committed code to symfony/router). Does anyone know if that test is safe to ignore - or if there's a way to fix it?

Apart from fixing or finding out about the test, this is ready for feedback or voting.

/**
* A route loader that executes a service to load the routes.
*
* The depends on the DependencyInjection component
Copy link
Contributor

Choose a reason for hiding this comment

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

typo: "The depends on"

@dbu
Copy link
Contributor

dbu commented Sep 18, 2015

indeed, good idea!

private function addClassResource(\ReflectionClass $class, RouteCollection $collection)
{
do {
$collection->addResource(new FileResource($class->getFileName()));
Copy link
Member

Choose a reason for hiding this comment

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

you should check for $class->getFileName() not being false (or null for HHVM) to handle the case of classes extending a class defined in a PHP extension IMO.

Copy link
Member Author

Choose a reason for hiding this comment

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

This part was stolen from ContainerBuilder: https://github.com/symfony/symfony/blob/2.8/src/Symfony/Component/DependencyInjection/ContainerBuilder.php#L275. Is your comment still true? If so - that would be a problem to fix in both places.

@xabbuh
Copy link
Member

xabbuh commented Sep 23, 2015

@weaverryan The failure for the low tests looks strange. Does it make a difference when you mock the ContainerInterface instead?

@xabbuh
Copy link
Member

xabbuh commented Sep 23, 2015

What I do not understand is the reason to have a container aware routing loader. Wouldn't it be easier if this loader were more like a registry where you could register RouteLoaderInterface instances with arbitrary keys (the FrameworkBundle could ship with a compiler pass so that you can just tag your services and they would be registered automatically)?

@weaverryan
Copy link
Member Author

@xabbuh I understand your thought, but the purpose of this is different. I'm not aiming to make it possible to have RouteLoaderInterface services automatically called because they're registered+tagged. I want to keep loading routes explicit, just add one more way to load routes - via PHP in a real method+service (currently, you can only add routes via PHP in a flat-file).

@weaverryan
Copy link
Member Author

@xabbuh yes - you were right about one of the tests - I've fixed that now.

I believe the last failing test should be ignored: the functional tests in FrameworkBundle are not able to fetch the router service because it depends on the ServiceRouteLoader, which isn't included in the Symfony's core (since it's part of this PR). I don't think there's way to fix that (someone let me know if I'm wrong), but the tests should pass fine once merged.

Thanks!

@xabbuh
Copy link
Member

xabbuh commented Sep 24, 2015

@weaverryan I just mean that it would only be possible to use these services in your routing configuration if they have been registered that way before. The workflow would still be the same then with your approach. The only difference is that you would not try to locate the service in the container (the drawback might be that you then won't be able to lazy load the routing loader services though).

@weaverryan
Copy link
Member Author

@xabbuh Ah, I understand better now. I'd still prefer to keep it this way - it feels logical to me that I should only need to register a class as a service and point to it in the config - the tagging seems unnecessary.

@weaverryan weaverryan force-pushed the service_route_loader branch 2 times, most recently from 608f304 to 764722d Compare September 26, 2015 20:20
@xabbuh
Copy link
Member

xabbuh commented Sep 27, 2015

👍 (being able to load services lazily and not having to tag your services are good reasons for keeping the container awareness)

@weaverryan
Copy link
Member Author

Adding a comment that got covered up:

We could remove the RouteLoaderInterface and instead have this method return a callable.

The difference would then be that we'd probably need to include the method name in the "resource" when using this:

admin_routes:
    resource: 'admin_route_loader::loadRoutes'
    type: service

That would solve the need for the RouteLoaderInterface on the "Micro" kernel. It's also consistent with how event listeners, for example, work.

@fabpot
Copy link
Member

fabpot commented Oct 1, 2015

@weaverryan Indeed, that would be much better. IIRC, when using a service name, the convention is to use only one colon: admin_router_loader:loadRoutes

@fabpot
Copy link
Member

fabpot commented Oct 1, 2015

#15990 has been updated to work with the new service loader.

@weaverryan
Copy link
Member Author

@fabpot I've already done work locally on this PR for the kernel:methodName syntax - I'll be able to push it within the next 2 hours.

@weaverryan weaverryan force-pushed the service_route_loader branch from cd40cad to 6b0a5c6 Compare October 1, 2015 18:10
@weaverryan
Copy link
Member Author

@fabpot a little later, but I just updated the PR:

This uses the service_name:method notation and is enforced in the ObjectRouteLoader, instead of allowing for more generic usage, where sub-classes could resolve strings of any format. I was trying to limit it to work just one way. For example, if you're not worried about the cached container and want to load routes via some "Callable", you cannot do it via the ObjectLoader: use the ClosureLoader.

ping also @symfony/deciders

@aitboudad
Copy link
Contributor

👍 , can you update the CHANGELOG.

@weaverryan
Copy link
Member Author

done!

$loaderObject = $this->getServiceObject($serviceString);

if (!is_object($loaderObject)) {
throw new \LogicException(sprintf('%s:getServiceObject() must return an object: %s returned', get_class($this), $loaderObject));
Copy link
Member

Choose a reason for hiding this comment

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

Here, $loaderObject can be anything; I would return the type instead.

Copy link
Member Author

Choose a reason for hiding this comment

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

very good catch - that's what I had meant to do. Fixed now!

@fabpot
Copy link
Member

fabpot commented Oct 1, 2015

I made a small comment; 👍

@weaverryan
Copy link
Member Author

comment addressed!

@fabpot
Copy link
Member

fabpot commented Oct 1, 2015

I think this one is ready now, ping @symfony/deciders

fabpot added a commit that referenced this pull request Oct 1, 2015
This PR was squashed before being merged into the 2.8 branch (closes #15778).

Discussion
----------

Fluid interface for building routes in PHP

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | not yet...

This - along with #15742 - attempts to making adding routes in PHP (via an actual class+method) not only possible, but also useful.

The two classes - `Route` and `RouteCollectionBuilder` are based off of Silex's `Controller` and `ControllerCollection`. The `RouteCollectionBuilder` is basically a `RouteCollection` that's able to import other resources. Here are the goals:

A) Import routes easily

```php
$routes->import('routing.yml');
```

B) Fluid addition of routes into the collection

```php
$routes->add('/admin', 'AppBundle:Admin:index', 'admin_index')
    ->setMethods(['GET']);
```

C) Ability to create routes with auto-generating names

D) Ability to add a "sub-collection" (kind of like an import, without pointing to another file). Included is the ability to set the controller class:

```php
$blogRoutes = $routes->createBuilder('/blog')
   ->setControllerClass('AppBundle\Controller\BlogController');
$blogRoutes->add('/', 'indexAction');
$blogRoutes->add('/{id}', 'editAction');
$routes->addBuilder($blogRoutes);
```

E) The collection options can be set before or after the routes. With `RouteCollection`, if you set something - e.g. a prefix or a default - and THEN add more routes, those options are not passed to those routes. This is by design, but not ideal for building routes (e.g. in the previous code example, the controllerClass would not be applied using the opposite logic, since it's set before adding the routes).

Thanks!

Commits
-------

15ba2e8 Fluid interface for building routes in PHP
@fabpot
Copy link
Member

fabpot commented Oct 1, 2015

Thank you @weaverryan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.