gyro/mvc-bundle

针对 Symfony 应用程序的多种改进

安装数: 39,981

依赖项: 0

建议者: 0

安全: 0

星标: 16

关注者: 2

分支: 2

开放问题: 6

类型:symfony-bundle

v1.0.0-beta6 2023-09-25 18:44 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

有时返回数组从控制器返回的灵活性不足,出现以下两种情况

  1. 使用不同的操作名称渲染模板。
  2. 向响应对象添加标题。

对于这种情况,可以将之前的示例更改为返回一个 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();
        }
    }
}

getCurrentUsergetToken方法期望将具体类名字符串作为第一个参数,在这个例子中是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());
    }
}