gyro / mvc-bundle
针对 Symfony 应用程序的多种改进
Requires
- composer-runtime-api: *
- symfony/http-kernel: ~4.4|~5.4|~6.0
Requires (Dev)
- php: ~7.4|~8.0
- doctrine/coding-standard: ^8.2.1
- nikic/php-parser: ^4.4
- phake/phake: ^4.2.0
- phpunit/phpunit: ^9.4
- symfony/symfony: ~4.4|~5.4|~6.0
- twig/twig: ^1.44|^2.14|^3.1
- vimeo/psalm: 4.15.0
This package is auto-updated.
Last update: 2024-08-27 17:15:47 UTC
README
在 Symfony 之上构建的小型框架,引入了一组约定,面向希望从 LTS 升级到 LTS 的用户。
MVCBundle 通过添加各种抽象来解耦和简化 Symfony 控制器,避免在控制器中使用 Symfony 服务或类。
此包继承自 "QafooLabsNoFrameworkBundle" 包。
目标
这允许编写仅依赖于领域/模型的控制器,并使其作为真正的“应用程序服务”运行,易于测试。
Gyro 的目标是创建注册为服务的轻量级控制器(通过 YML 或 XML)。任何控制器中所需的服务数量应非常小(2-4)。我们相信上下文应显式传递给控制器,以避免将其隐藏在服务中。
最终,这应使控制器可以通过轻量级的单元和集成测试进行测试。通过构建不依赖于 Symfony 的控制器(除了可能请求/响应类之外),无需将 Symfony 与业务逻辑进行详细的分离。
安装
通过 Packagist 和 Composer
composer require gyro/mvc-bundle
将包添加到您的应用程序内核
$bundles = [ // ... new Gyro\Bundle\MVCBundle\GyroMVCBundle(), ];
从控制器返回视图数据
返回数组
此包替换了 Sensio FrameworkExtraBundle 的 @Extra\Template()
注解支持,无需将注解添加到控制器操作中。
您可以直接从控制器返回数组,模板名称将根据控制器+操作-方法名称推断。
如果您从 App\Controller
默认命名空间返回,则模板将从 ':Ctrl:action.html.twig' 中获取。
<?php # src/App/Controller/DefaultController.php namespace App\Controller; class DefaultController { public function helloAction($name = 'Fabien') { return ['name' => $name]; // :Default:hello.html.twig } }
返回 TemplateView
有时返回数组从控制器返回的灵活性不足,出现以下两种情况
- 使用不同的操作名称渲染模板。
- 向响应对象添加标题。
对于这种情况,可以将之前的示例更改为返回一个 TemplateView
实例
<?php # src/App/Controller/DefaultController.php namespace App\Controller; use Gyro\MVC\TemplateView; class DefaultController { public function helloAction($name = 'Fabien') { return new TemplateView( ['name' => $name], 'hallo', // :Default:hallo.html.twig instead of hello.html.twig 201, ['X-Foo' => 'Bar'] ); } }
注意:与默认 Symfony 基础控制器中的 render()
方法不同,此处交换了视图参数和模板名称。这是因为除了视图参数之外,所有内容都是可选的。
返回 ViewModel
通常控制器会快速收集与视图相关的逻辑,因为这些数据转换方法不太重要,因此没有正确提取到 Twig 扩展中。这就是为什么除了返回数组支持之外,您还可以使用 ViewModel 并从您的操作中返回它们。
每个 ViewModel 都是一个类,它映射到恰好一个模板,并可以包含在 view
模板名称下在 Twig 中使用的属性和方法,其解析机制与返回数组时相同。
ViewModel 可以是任何类,只要它不扩展 Symfony Response 类。
<?php # src/App/View/Default/HelloView.php namespace App\View\Default; class HelloView { public $name; public function __construct($name) { $this->name = $name; } public function getReversedName() { return strrev($this->name); } }
在您的控制器中,您只需返回 ViewModel
<?php # src/App/Controller/HelloController.php namespace App\Controller; class HelloController { public function helloAction($name) { return new HelloView($name); } }
它将被渲染为 :Hello:hello.html.twig
,其中 ViewModel 可用作为 view
twig 变量
Hello {{ view.name }} or {{ view.reversedName }}!
您可以选择从 Gyro\MVC\ViewStruct
扩展。每个 ViewStruct
实现都有一个构造函数,接受并设置在 ViewModel 类上存在的键值对属性。
重定向路由
在Symfony中进行重定向更可能发生在内部,针对给定的路由。您可以从控制器返回Gyro\MVC\RedirectRoute
,然后监听器将其转换为适当的Symfony RedirectResponse
<?php # src/App/Controller/DefaultController.php namespace App\Controller; use Gyro\MVC\RedirectRoute; class DefaultController { public function redirectAction() { return new RedirectRoute('hello', ['name' => 'Fabien']); } }
如果您想设置头部或不同的状态码,可以将Response
作为第三个参数传递,这将用于创建新的响应。
添加Cookies、Flash消息、缓存头部
当从控制器返回视图模型、数组或重定向路由,且没有直接访问响应时,无法轻松地添加响应头。这正是PHP生成器发挥作用的地方,您可以通过yield
添加额外的响应元数据。
<?php # src/App/Controller/DefaultController.php namespace App\Controller; use Gyro\MVC\Headers; use Gyro\MVC\Flash; use Symfony\Component\HttpFoundation\Cookie; class DefaultController { public function helloAction($name) { yield new Cookie('name', $name); yield new Headers(['X-Hello' => $name]); yield new Flash('warning', 'Hello ' . $name); return ['name' => $name]; } }
在响应发送后执行代码
为了简单地将控制器中的工作延迟到Symfony的kernel.terminate
事件,Gyro的yield applier抽象处理一个AfterResponseTask
,该任务接受一个闭包,在通过事件订阅者调用Response::send
后执行。
public function registerAction($request): RedirectRoute { $user = $this->createUser($request); $this->entityManager->persist($user); yield new AfterResponseTask(fn () => $this->sendEmail($user)); return new RedirectRoute('home'); }
将TokenContext注入到操作中
在Symfony中,通过security.context
服务可以访问与安全相关的信息。从设计角度来看,这不好,因为它在需要访问与安全相关的信息时引入了一个有状态的服务。
为了避免从服务中访问安全状态,需要将其作为参数传递,从控制器操作开始。
这就是TokenContext
类的作用。只需为任何操作添加一个类型提示即可,MVCBundle将传递此对象到您的操作中。通过它,您可以访问各种与安全相关的功能。
<?php # src/App/Controller/DefaultController.php namespace App\Controller; use Gyro\MVC\TokenContext; class DefaultController { public function redirectAction(TokenContext $context) { if ($context->hasToken()) { $user = $context->getCurrentUser(MyUser::class); } else if ($context->hasAnonymousToken()) { // do anon stuff } if ($context->isGranted('ROLE_ADMIN')) { // do admin stuff echo $context->getCurrentUserId(); echo $context->getCurrentUsername(); } } }
getCurrentUser
和getToken
方法期望将具体类名字符串作为第一个参数,在这个例子中是MyUser::class
。这用于与PSalm模板注释一起使用,以改进静态分析。
在单元测试中,如果您想测试控制器,可以使用MockTokenContext
。它不适用于复杂的isGranted()
检查或令牌,但如果您只使用用户对象,它允许非常简单的测试设置。
处理FormRequest
在Symfony中处理表单通常会导致复杂、不可测试的控制器操作,这些操作非常紧密地耦合到各种Symfony服务。为了避免在控制器内部处理form.factory
,我们引入了一个专门的请求对象,它隐藏了所有这些。
<?php # src/App/Controller/ProductController.php namespace App\Controller; use Gyro\MVC\FormRequest; use Gyro\MVC\RedirectRoute; class ProductController { private $repository; public function __construct(ProductRepository $repository) { $this->repository = $repository; } public function editAction(FormRequest $formRequest, $id) { $product = $this->repository->find($id); if (!$formRequest->handle(new ProductEditType(), $product)) { return ['form' => $formRequest->createFormView(), 'entity' => $product]; } $product = $formRequest->getValidData(); $this->repository->save($product); return new RedirectRoute('Product.show', ['id' => $id]); } }
在测试中,您可以使用new Gyro\MVC\Form\InvalidFormRequest()
和new Gyro\MVC\Form\ValidFormRequest($validData)
来处理控制器测试中的表单。
会话的参数转换器
您可以将会话作为参数传递给控制器
public function indexAction(Session $session) { }
转换异常
通常,您使用的库或您自己的代码抛出的异常可以转换为HTTP错误,而不仅仅是500服务器错误。为了防止在控制器中反复进行此操作,您可以在监听器中配置转换这些异常。
# config/packages/gyro_mvc.yml gyro_mvc: convert_exceptions: Doctrine\ORM\EntityNotFoundException: Symfony\Component\HttpKernel\Exception\NotFoundHttpException Doctrine\ORM\ORMException: 500
关于转换的显著事实
- 可以指定目标异常类或HTTP状态码
- 还会检查子类。
- 如果您没有定义转换,则不会注册监听器。
- 如果转换了异常,则会在转换之前专门记录原始异常。这意味着当异常发生时,它会被记录两次。
以下异常默认注册
事件分派器适配器
Symfony事件分派器的API在版本3和4之间发生了特殊变化,并在5中再次变化。您不再传递事件名称作为必需的第一个参数,而是现在可以将其作为可选的第二个参数传递。这样做是为了使Symfony与PSR-14 (Event-Dispatcher)保持一致。
此代码的迁移路径有点令人讨厌,并且当使用PSalm时会导致需要抑制的违规。
陀螺仪公司为EventDispatcher提供了一种适配器,以避免这个问题。其API与PSR-14 API兼容,但不实现该接口。然后它正确地委托给Symfony事件调度器。
请注入服务gyro_mvc.event_dispatcher
而不是服务event_dispatcher
。
use Gyro\MVC\EventDispatcher\EventDispatcher; class MyEvent { } class MyService { private EventDispatcher $eventDispatcher; public function __construct(EventDispatcher $eventDispatcher) { $this->eventDispatcher = $eventDispatcher; } public function performMyOperation() { // .... $this->eventDispatcher->dispatch(new MyEvent()); } }