Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony: Factory of controllers

I'm making a custom user bundle, allowing for defining multiple user types, with their own repositories, managers, providers etc. So, I decided, instead of creating the limited set of controllers, to create a controller factories, which would produce controllers based on the defined user types and configuration. But this raises the important question - where, and how should those factories operate?

Now, mind you that it doesn't suffice to create a controller in the factory, we also have to setup all routes for it, somewhere.

The question is - what would be the best architecture for this?

When it comes to choosing a layer where I will place my code, I was considering, among others:

  1. Loading factories definitions in Extension's load method, and creating all of the controllers there. The problem: Router is not available there, because it happens before container building, so I couldn't create routes in the same place.

  2. Sooo... maybe in the compiler pass? But compiler pass doesn't have access to configuration... I mean... in fact it has, if I will just load configuration and process it manually, but I'm still not sure if this is a good place, but I'm leaning towards this solution right now.

When it comes to creating routes:

  1. Should I place routes creation logic in the controller factory? But I'm creating controllers as services and factory doesn't have access to the serviceId of the created controller, and serviceId is required for creating a route, so nope.

  2. In the controller itself? I mean, that's how annotation routes work, so it might be viable. Controller would have to implement something like my own ControllerInterface with the method getRoutes, and the external service/compiler pass would need to create a controller as a service first, and then get routes from the said controller, modify them, so they would refer this controller's serviceId and add them to the router... regardless of how messy this looks like.

  3. Is there any other option?

There is considerable lack of information regarding this particular pattern - factory of controllers :) .

like image 357
Łukasz Zaroda Avatar asked Aug 23 '16 09:08

Łukasz Zaroda


2 Answers

The first version of API Platform was using a similar technique.

The first step is to register routes. A route maps an URL pattern with a controller defined under the _controller route's attribute. It's how the Routing component and the HttpKernel components are linked together (there is no strong coupling between those 2 components). Routes can be registered by creating a RouteLoader: http://symfony.com/doc/current/routing/custom_route_loader.html

It's how API Platform, Sonata and Easy Admin work for instance.

At runtime, the callable specified under the _controller attributes will be executed. It will receive the HTTP request in parameter and should return a HTTP response. It may access to other services (and even to the container) if needed.

A controller can be any callable (method, function, invokable class...), but it can also be a service thanks to the following syntax my_controller_service:myAction (see http://symfony.com/doc/current/controller/service.html).

The DependencyInjection component allows to build services using a factory: http://symfony.com/doc/current/service_container/factories.html. Factory method can receive other services or parameters (config).

To sum up:

1/ Register a service definition for your controller using your factory to build it, like the following:

# app/config/services.yml
services:
    # ...

    app.controller_factory:
        class: AppBundle\Controller\ControllerFactory
        arguments: ['@some_service', '%some_parameter%]

    app.my_controller:
        class:     AppBundle\Controller\ControllerInterface
        factory:   'app.controller_factory:createController'
        arguments: ['@some_service', '%some_parameter%]

Of course, if you need to, create your controller definitions programmatically in the AppBundle\DependencyInjection\AppBundleExtension class. You may also use an abstract service definition to avoid code duplication (http://symfony.com/doc/current/service_container/parent_services.html).

2/ Create a RouteLoader service registering your Route instances. You can take a look to this example: https://github.com/api-platform/core/blob/1.x/Routing/ApiLoader.php

Then, register this route loader as a service:

# app/config/services.yml
services:
    app.routing_loader:
        class: AppBundle\Routing\MyLoader
        arguments: ['@some_service', '%some_parameter%]
        tags:
            - { name: routing.loader }

3/ Tell the router to execute this RouteLoader:

# app/config/routing.yml
app:
    resource: . # Omitted
    type: mytype # Should match the one defined in your loader's supports() method

All done!

(I'm a Symfony Core Team member but also the API Platform creator, so this is an opinionated answer.)

like image 133
Kévin Dunglas Avatar answered Nov 13 '22 08:11

Kévin Dunglas


To operate that factories, first you need to define some rules to create the routes using a custom route loader in the compilation pass, and I guess you should also need to customize the routing matching and resolution procedure in order to check the route received, then the rules that defines the relation between the route pattern or value with the concrete router created by the factory and finally pass the request to the function within the concrete router.

I have read your question several times, and I still don't see the advantages of this approach. Are you going to create the routers by inheritance or composition? The ruleset to define the concrete (even if the contains parameters and are not completely "concrete") routes needs to go until function level and even that this can be solved by a good naming convention, I still see to many difficulties.

Just an opinion, of course.

like image 2
Carlos Avatar answered Nov 13 '22 08:11

Carlos