controllerroutessymfonyfactory-pattern

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 factory, 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 set up 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 factory 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. So, maybe in the compiler pass? However the compiler pass doesn't have access to the configuration. I mean in fact, it has, if I will just load the 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 the 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 a considerable lack of information regarding this particular pattern - factory of controllers :).


Solution

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