Symfony 2.x 中的所有东西都应该是一个捆绑包吗?

Posted

技术标签:

【中文标题】Symfony 2.x 中的所有东西都应该是一个捆绑包吗?【英文标题】:Should everything really be a bundle in Symfony 2.x? 【发布时间】:2012-04-17 11:18:36 【问题描述】:

我知道像this 这样的问题,人们倾向于讨论一般的 Symfony 2 捆绑概念。

问题是,在一个特定的应用程序中,例如一个类似推特的应用程序,是否真的应该把所有东西都放在一个通用包中,比如official docs 说的?

我问这个的原因是因为当我们开发应用程序时,一般来说,我们不希望将我们的代码高度耦合到一些全栈胶水框架。

如果我开发了一个基于 Symfony 2 的应用程序,并且在某个时候,我认为 Symfony 2 并不是真正继续开发的最佳选择,这对我来说会是个问题吗? p>

所以一般的问题是:为什么所有东西都是一个捆绑包是件好事?

编辑#1

自从我提出这个问题已经快一年了,我写了一封 article 来分享我对这个主题的了解。

【问题讨论】:

这只是评论,不是答案。我个人认为,在开始项目之前,我们应该仔细选择框架。每个框架都有自己的做事方式,所以它会提供工具来最好地支持这种方式。如果我们喜欢这种方式,我们就会跟随。还有其他选择。我们不想用刀代替锯来切割木头。但这是你提出的一个非常有趣的问题:) 【参考方案1】:

当然,您可以解耦您的应用程序。只需将其开发为库并将其集成到 symfony vendor/-文件夹中(使用 depscomposer.json,取决于您使用 Symfony2.0 还是 Symfony2.1)。但是,您至少需要一个包,它充当库的“前端”,Symfony2 可以在其中找到控制器(等等)。

【讨论】:

由于标签symfony-2.0 我假设您使用当前的2.0 版本。在这种情况下,在任何你喜欢的地方创建一个 git 存储库并将所有内容放入其中,你想要独立于 symfony 开发的东西。在你的 symfony 项目中更新你的 deps-file 就像这里提到的 symfony.com/doc/current/cookbook/workflow/… 然后只需为特定于 symfony 的东西创建一个(或多个)应用程序包(php app/console generate:bundle)。【参考方案2】:

您可以使用KnpRadBundle,它试图简化项目结构。

另一种方法是使用src/Company/Bundle/FrontendBundle,例如用于包,src/Company/Stuff/Class.php 用于独立于 symfony 并且可以在框架之外重用的类

【讨论】:

但是我会将应用程序耦合到 KnpRadBundle... 在这个问题上没有更简单的方法吗? 依赖于 symfony 的部分(控制器、模型、模板等...)将始终与 symfony 耦合,因为您正在使用它(扩展类、使用助手等...) .单独工作的类将位于 Company 命名空间中,您可以使用依赖容器加载它们。这些类可以独立于框架。 问题是,Bundle 的概念直接用于公开分享。当我编写一些应用程序时,我不想分享我的代码,除了那些我有意构建为社区驱动模块的部分。我错了吗? 您不必共享捆绑包。将捆绑包视为具有某些配置的一组类。在每个项目中,您可以拥有不同的捆绑包。 你应该阅读symfony book【参考方案3】:

我已经写了一篇关于这个主题的更全面和更新的博客文章:http://elnur.pro/symfony-without-bundles/


不,并非所有东西都必须捆绑在一起。你可以有这样的结构:

src/Vendor/Model — 用于模特, src/Vendor/Controller — 用于控制器, src/Vendor/Service — 用于服务, src/Vendor/Bundle — 用于捆绑包,例如 src/Vendor/Bundle/AppBundle, 等

这样,您将只在AppBundle 中添加真正特定于 Symfony2 的内容。如果您决定稍后切换到另一个框架,您将摆脱 Bundle 命名空间并将其替换为所选框架的内容。

请注意,我在这里建议的是app 特定代码。对于可重复使用的包,我仍然建议使用the best practices。

将实体排除在包之外

为了将src/Vendor/Model 中的实体保留在任何捆绑包之外,我已将config.yml 中的doctrine 部分从

doctrine:
    # ...
    orm:
        # ...
        auto_mapping: true

doctrine:
    # ...
    orm:
        # ...
        mappings:
            model:
                type: annotation
                dir: %kernel.root_dir%/../src/Vendor/Model
                prefix: Vendor\Model
                alias: Model
                is_bundle: false

实体的名称——从 Doctrine 存储库中访问——在这种情况下以 Model 开头,例如,Model:User

您可以使用子命名空间将相关实体组合在一起,例如src/Vendor/User/Group.php。在这种情况下,实体的名称是Model:User\Group

让控制器远离捆绑包

首先,您需要告诉JMSDiExtraBundle 扫描src 文件夹中的服务,方法是将其添加到config.yml

jms_di_extra:
    locations:
        directories: %kernel.root_dir%/../src

然后你 define controllers as services 并将它们放在 Controller 命名空间下:

<?php
namespace Vendor\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\SecurityExtraBundle\Annotation\Secure;
use Elnur\AbstractControllerBundle\AbstractController;
use Vendor\Service\UserService;
use Vendor\Model\User;

/**
 * @Service("user_controller", parent="elnur.controller.abstract")
 * @Route(service="user_controller")
 */
class UserController extends AbstractController

    /**
     * @var UserService
     */
    private $userService;

    /**
     * @InjectParams
     *
     * @param UserService $userService
     */
    public function __construct(UserService $userService)
    
        $this->userService = $userService;
    

    /**
     * @Route("/user/add", name="user.add")
     * @Template
     * @Secure("ROLE_ADMIN")
     *
     * @param Request $request
     * @return array
     */
    public function addAction(Request $request)
    
        $user = new User;
        $form = $this->formFactory->create('user', $user);

        if ($request->getMethod() == 'POST') 
            $form->bind($request);

            if ($form->isValid()) 
                $this->userService->save($user);
                $request->getSession()->getFlashBag()->add('success', 'user.add.success');

                return new RedirectResponse($this->router->generate('user.list'));
            
        

        return ['form' => $form->createView()];
    

    /**
     * @Route("/user/profile", name="user.profile")
     * @Template
     * @Secure("ROLE_USER")
     *
     * @param Request $request
     * @return array
     */
    public function profileAction(Request $request)
    
        $user = $this->getCurrentUser();
        $form = $this->formFactory->create('user_profile', $user);

        if ($request->getMethod() == 'POST') 
            $form->bind($request);

            if ($form->isValid()) 
                $this->userService->save($user);
                $request->getSession()->getFlashBag()->add('success', 'user.profile.edit.success');

                return new RedirectResponse($this->router->generate('user.view', [
                    'username' => $user->getUsername()
                ]));
            
        

        return [
            'form' => $form->createView(),
            'user' => $user
        ];
    

请注意,我使用 ElnurAbstractControllerBundle 来简化将控制器定义为服务的过程。

剩下的最后一件事是告诉 Symfony 寻找没有捆绑包的模板。我通过覆盖模板猜测器服务来做到这一点,但由于 Symfony 2.0 和 2.1 之间的方法不同,我为它们提供了两个版本。

覆盖 Symfony 2.1+ 模板猜测器

我已经创建了一个 bundle 来为你做这件事。

覆盖 Symfony 2.0 模板监听器

首先,定义类:

<?php
namespace Vendor\Listener;

use InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Sensio\Bundle\FrameworkExtraBundle\EventListener\TemplateListener as FrameworkExtraTemplateListener;
use JMS\DiExtraBundle\Annotation\Service;

class TemplateListener extends FrameworkExtraTemplateListener

    /**
     * @param array   $controller
     * @param Request $request
     * @param string  $engine
     * @throws InvalidArgumentException
     * @return TemplateReference
     */
    public function guessTemplateName($controller, Request $request, $engine = 'twig')
    
        if (!preg_match('/Controller\\\(.+)Controller$/', get_class($controller[0]), $matchController)) 
            throw new InvalidArgumentException(sprintf('The "%s" class does not look like a controller class (it must be in a "Controller" sub-namespace and the class name must end with "Controller")', get_class($controller[0])));

        

        if (!preg_match('/^(.+)Action$/', $controller[1], $matchAction)) 
            throw new InvalidArgumentException(sprintf('The "%s" method does not look like an action method (it does not end with Action)', $controller[1]));
        

        $bundle = $this->getBundleForClass(get_class($controller[0]));

        return new TemplateReference(
            $bundle ? $bundle->getName() : null,
            $matchController[1],
            $matchAction[1],
            $request->getRequestFormat(),
            $engine
        );
    

    /**
     * @param string $class
     * @return Bundle
     */
    protected function getBundleForClass($class)
    
        try 
            return parent::getBundleForClass($class);
         catch (InvalidArgumentException $e) 
            return null;
        
    

然后通过将其添加到 config.yml 来告诉 Symfony 使用它:

parameters:
    jms_di_extra.template_listener.class: Vendor\Listener\TemplateListener

使用没有包的模板

现在,您可以使用捆绑包中的模板。将它们保存在app/Resources/views 文件夹下。例如,上面示例控制器中这两个操作的模板位于:

app/Resources/views/User/add.html.twig app/Resources/views/User/profile.html.twig

引用模板时,只需省略捆绑部分:

% include ':Controller:view.html.twig' %

【讨论】:

这实际上是一种非常有趣的方法。有了它,我还可以开发包含社区可以使用的特定功能集的真正捆绑包,而几乎不需要将我的应用程序与框架本身耦合。 为了使您与社区共享的代码也不与 Symfony2 耦合,您可以将一般的东西放入一个库中,然后创建一个将该库与 Symfony2 集成的包。 这是一个有趣的想法,只要您不依赖任何代码生成命令。例如,generate:doctrine:crud 期望实体(在 elnur 的情况下为模型)位于包中才能工作。 使用这种方法有没有办法重新获得 CLI 应用程序/控制台界面的功能?我喜欢将模型保存在任何捆绑包之外的位置,但我希望保留对 CLI 功能的访问权限。 这应该放在一个包里:)【参考方案4】:

一个普通的 symfony 发行版可以在没有任何额外(应用程序)包的情况下工作,这取决于你想从完整的堆栈框架中使用多少功能。

例如,您的控制器可以是任何可调用的,只要它们被自动加载,就可以放在项目结构中的任何位置。

在路由定义文件中,可以使用:

test:
    pattern:   /test
    defaults:   _controller: Controller\Test::test 

它可以是任何普通的旧 php 对象,只是因为它必须返回一个 Symfony\Component\HttpFoundation\Response 对象而与框架绑定。

您的树枝模板(或其他模板)可以像 app/Resources/views/template.html.twig 一样放置,并且可以使用 ::template.html.twig 逻辑名称呈现。

所有 DI 服务都可以在 app/config/config.yml 中定义(或者从 app/config/services.yml 导入,所有服务类也可以是任何普通的旧 php 对象。完全不依赖于框架。

所有这些都是 symfony 全栈框架默认提供的。

当您想要使用翻译文件(如 xliff)时,您会遇到问题,因为它们是通过包发现的。

symfony-light 分发旨在通过发现通常只能通过捆绑包发现的所有内容来解决此类问题。

【讨论】:

【参考方案5】:

Symfony 框架非常适合快速启动概念验证,所有代码都可以在 src/ 中的默认捆绑应用程序中输入

在此捆绑包中,您可以根据需要构建代码。

之后,如果您想使用其他技术来开发您的 POC,您可以轻松地转换它,因为您不会在捆绑概念中构建所有代码。

对于所有概念,您并没有极端化这一点。捆绑好,但捆绑一切,每天都不好。

也许您可以使用 Silex(Symfony 微框架)来开发您的概念证明,以减少捆绑第三方的影响。

【讨论】:

【参考方案6】:

因为已经过去了 5 年,这里还有几篇关于 Symfony Bundles 的文章。

    What are Bundles in Symfony? by Iltar van der Berg。

TLDR:

您是否需要在您的应用程序中直接使用多个捆绑包?最有可能的 不是。你最好写一个 AppBundle 来防止意大利面 依赖关系。您只需关注best practices 即可 工作正常。

    Symfony: How to Bundle Toni Uebernickel。

TLDR:

只为您的应用程序逻辑创建一个名为 AppBundle 的包。 一个 AppBundle - 但请不要将您的应用程序逻辑放在那里!

【讨论】:

以上是关于Symfony 2.x 中的所有东西都应该是一个捆绑包吗?的主要内容,如果未能解决你的问题,请参考以下文章

symfony2 实体、存储库、服务混乱

如何销毁 Symfony 2 中的所有会话

Symfony 3,一个应用程序中的 2 个防火墙

luogu P4156 [WC2016]论战捆竹竿

Symfony2 包继承丢失父包路由

Symfony LTS:如何从 2.8 升级到 3.4?