Symfony:控制器工厂

Posted

技术标签:

【中文标题】Symfony:控制器工厂【英文标题】:Symfony: Factory of controllers 【发布时间】:2016-12-30 00:52:13 【问题描述】:

我正在制作一个自定义用户包,允许定义多种用户类型,以及他们自己的存储库、管理器、提供者等。因此,我决定创建一个控制器工厂,而不是创建有限的控制器集,它将根据定义的用户类型和配置生成控制器。但这提出了一个重要的问题——这些工厂应该在哪里以及如何运营?

现在,请注意,在工厂中创建控制器是不够的,我们还必须在某个地方为其设置所有路由。

问题是 - 最好的架构是什么?

在选择放置代码的层时,我曾考虑过:

    在 Extension 的 load 方法中加载工厂定义,并在那里创建所有控制器。问题:路由器在那里不可用,因为它发生在容器构建之前,所以我无法在同一个地方创建路由。

    Sooo...也许在编译器通道中?但是编译器通行证无权访问配置...我的意思是...实际上它有,如果我只是加载配置并手动处理它,但我仍然不确定这是否是一个好地方,但我我现在倾向于这个解决方案。

关于创建路线:

    我应该在控制器工厂中放置路由创建逻辑吗?但是我将控制器创建为服务,工厂无权访问创建的控制器的 serviceId,并且创建路由需要 serviceId,所以没有。

    在控制器本身?我的意思是,这就是注释路由的工作方式,所以它可能是可行的。控制器必须使用 getRoutes 方法实现类似于我自己的 ControllerInterface 的东西,并且外部服务/编译器通道需要首先创建一个控制器作为服务,然后从所述控制器获取路由,修改它们,所以他们会引用这个控制器的 serviceId 并将它们添加到路由器......不管这看起来有多乱。

    还有其他选择吗?

关于这种特殊模式的信息相当缺乏 - 控制器工厂:)。

【问题讨论】:

这是您需要根据每个请求执行的操作吗?还是可以通过命令一次完成的事情? 你指的是什么?如果您询问创建控制器的事件 - 在构建容器和路由时应该只做一次。控制器(作为服务)和路由应该像往常一样被 Symfony 缓存。 抱歉,我的意思是“如果您询问创建控制器的定义的事件”。 您是否考虑过编写一个命令来生成所有控制器、控制器服务定义和路由?然后让 Symfony 照常运行? 我没有考虑这一点,但它看起来像是一种解决方法,而不是真正的解决方案,但是我会考虑一下。但是,我想发布此捆绑包,因此我正在寻找“最佳”解决方案(如果存在):)。 【参考方案1】:

您可以使用 setContainer 方法来检查用户访问控制。 我的解决方案:

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
      

【讨论】:

【参考方案2】:

要操作那些工厂,首先你需要在编译过程中定义一些规则来使用自定义路由加载器创建路由,我想你还需要自定义路由匹配和解析过程以检查路由收到,然后定义路由模式或值与工厂创建的具体路由器之间关系的规则,最后将请求传递给具体路由器内的函数。

我已多次阅读您的问题,但我仍然没有看到这种方法的优势。你打算通过继承还是组合来创建路由器?定义具体(即使包含参数并且不是完全“具体”)路由的规则集需要一直到函数级别,即使这可以通过良好的命名约定来解决,我仍然看到很多困难。

当然,只是一个意见。

【讨论】:

【参考方案3】:

The first version of API Platform 使用了类似的技术。

第一步是注册路由。路由将 URL 模式映射到在 _controller 路由属性下定义的控制器。这就是 Routing 组件和 HttpKernel 组件连接在一起的方式(这两个组件之间没有强耦合)。 可以通过创建RouteLoader:http://symfony.com/doc/current/routing/custom_route_loader.html来注册路由

这就是 API Platform、Sonata 和 Easy Admin 的工作方式。

在运行时,_controller 属性下指定的可调用对象将被执行。它将接收参数中的 HTTP 请求,并应返回 HTTP 响应。如果需要,它可以访问其他服务(甚至是容器)。

控制器可以是任何可调用的(方法、函数、可调用的类...),但由于以下语法my_controller_service:myAction(参见http://symfony.com/doc/current/controller/service.html),它也可以是服务。

DependencyInjection 组件允许使用工厂构建服务:http://symfony.com/doc/current/service_container/factories.html。工厂方法可以接收其他服务或参数(config)。

总结一下:

1/ 为您的控制器注册一个服务定义,使用您的工厂来构建它,如下所示:

# 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%]

当然,如果需要,可以在 AppBundle\DependencyInjection\AppBundleExtension 类中以编程方式创建控制器定义。您还可以使用abstract 服务定义来避免代码重复 (http://symfony.com/doc/current/service_container/parent_services.html)。

2/ 创建一个RouteLoader 服务来注册您的Route 实例。你可以看看这个例子:https://github.com/api-platform/core/blob/1.x/Routing/ApiLoader.php

然后,将此路由加载器注册为服务:

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

3/ 告诉路由器执行这个RouteLoader:

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

全部完成!

(我是 Symfony 核心团队的成员,也是 API 平台的创建者,所以这是一个固执己见的答案。)

【讨论】:

那么,如果我想根据一些配置创建控制器,我需要将此配置传递给路由加载器吗?起初我不喜欢这种方法,因为我在想如何以某种方式在同一个地方创建路由和控制器(因为没有彼此它们是无用的),但我可能想得太深入了。感谢您的详细描述:)。 路由和处理请求(控制器)是两个不同的操作,应该分离。如果你真的只想创建一个类,你的控制器工厂也可以实现LoaderInterface。它应该可以工作,但它违反了 SOLID 原则。

以上是关于Symfony:控制器工厂的主要内容,如果未能解决你的问题,请参考以下文章

Symfony 4 不自动装配动态路由控制器

Symfony,在控制器中强制注销

Laravel Post 控制器不工作(Symfony \ Component ...)

控制器的单元测试(Symfony)

Symfony 4:FOSUserBundle:覆盖控制器

Symfony - 如何获取控制器的主要路线?