Symfony: Factory of controllers
Asked Answered
D

3

11

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

Dareece answered 23/8, 2016 at 9:11 Comment(10)
Is this something you need to do on a per request basis? Or is it something that could get be done once via a command?Canova
What do you refer to? If you ask about the event of creating controllers - it should be done only once when building the container and routes. Controllers (as services) and routes should be then cached by the Symfony as usual.Sedda
Sorry, I meant "If you ask about the event of creating definitions of controllers".Sedda
Have you considered writing a command which would generate all your controllers, controller service definitions and routes? And then let Symfony run as usual?Canova
I didn't consider this, but it looks like a workaround rather than the real solution, however I will think about it. However, I would like to publish this bundle, so I'm looking for the "best" solution if it exists :) .Sedda
Fair enough. Commands like doctrine:generate:entities work well. But I get the feeling that you are looking for something completely different. Maybe somebody else will come by with a better understanding of your question.Canova
The problem with your approach is that there is no editable class for the end user to fine grain. Rather than creating controllers at compile time, you would be better off either offering a command or simply creating a single class and then creating service id's for it. As for the routes, They should be created at creation. It requires the use of a custom route loader.Curkell
I was considering a custom route loader, but I couldn't justify it enough. I mean, why not simply adding routes to router on controller creation? Custom route loader seemed like over-engineering, because it would require several more layers of abstraction, which seemed to be not needed in any case. I might be wrong though.Sedda
without a route loader, your routes will be hard coded, in PHP. It is possible to configure your routes in your .yml files, but to add them to the symfony router output a loader is required.Curkell
Now, back to your main problem. Have a look at the "easy admin bundle", it seems to do everything you are trying.Curkell
J
6

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

Joanejoanie answered 15/5, 2017 at 16:25 Comment(2)
So, if I would like to create controllers based on some configuration, I would need to pass this configuration to route loader? I didn't like this approach at first because I was thinking how can I create routes and controllers in the same place somehow (because they are useless without each other), but I might be thinking too deep into this. Thanks for this detailed description :) .Sedda
Routing and handling a request (controller) are 2 different operations that should be decoupled. If you really want to create only one class, your controller factory can also implement the LoaderInterface. It should work, but it breaks SOLID principles.Marmite
A
2

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.

Angelicaangelico answered 9/5, 2017 at 15:57 Comment(0)
E
0

You can use setContainer method for checking user access control. MySolution:

class AuthBaseController extends Controller{
    /**
    * @var \stdClass
    */
    protected $user = null;

    /**
    * this is a function for any role. For example, edit posts
    * @var int
    */
    protected $functionId=null;

    // this is initilizer function for all controllers. If any controller access to this controller then set $systemAccess to true 
    public function setContainer(ContainerInterface $container = null, $systemAccess= false) {
        parent::setContainer($container);
        if($systemAccess) return;
        $session = $this->get("session");
        if($session->has('YOUR_USER_KEY')){
            $this->user = json_decode($session->get('YOUR_USER_KEY'));
            if(!in_array($this->functionId,$this->user->userFunctions) && !is_null($this->functionId)){
                // if user havn't access to this controller
                throw new AccessDeniedException("You can not access to this page!");
            } 
         }else{
            header("Location:".$this->generateUrl("user_login"));
         }
     }
 }

class TaskManagementController extends AuthBaseController {
     /**
     * @var int
     */
     protected $functionId=24;

     public function indexAction(Request $request){
        //your action codes
      }
}
Evulsion answered 13/12, 2016 at 17:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.