Symfony2 Bundle,提供利用 Symfony 的分发流程实现快速分层模型视图控制器 (HMVC) 解决方案

安装: 28

依赖: 0

建议者: 0

安全: 0

星星: 7

关注者: 2

分支: 3

开放问题: 0

类型:symfony-bundle

0.2.2 2013-02-11 19:06 UTC

This package is not auto-updated.

Last update: 2024-09-28 14:46:26 UTC


README

Build Status

为什么外部应用程序调用使用 RESTful 接口,而内部应用程序调用由许多自定义模型方法组成?内部服务与外部服务有何不同?它们不应该如此。当前的互联网是一个服务云,现在您的应用程序也可以如此。

HMVCBundle 是一个 Symfony2 Bundle,提供快速分层模型视图控制器 (HMVC) 解决方案,利用 Symfony 的分发流程以与外部调用相同的方式调用内部服务。它与 FOSRestBundle、KnpRadBundle 和原生 Symfony 的 RESTful 和非 RESTful 控制器完全集成。

概念

已经有很多关于模型视图控制器模式以及所有业务逻辑都应该放在模型层的讨论。从概念上讲,这是完全正确的,但在实际应用中可能会很痛苦。很多开发时间都花在将控制器操作映射到模型对象和方法上。花费了大量时间来决定代码应该放在哪里以及如何很好地结构化它。我的表单应该放在哪里?我不应该在模型层进行验证吗?如果我使用注解来验证我的 GET 和 POST 值,我不应该在模型层再次验证实体吗?为什么我的控制器有创建/读取/更新/删除方法,映射到我的模型层的创建/读取/更新/删除方法?

我们的观点是,模型视图控制器对于现代网络开发来说并不合适,它是过时的。问题 #1 是视图层没有真正的事件触发,这使得每个请求都只是单个应用程序流程。问题 #2 是我们更喜欢约定而不是配置,这使得我们的控制器层非常可预测且重复。

我们旨在采用网络开发中的表现抽象控制 (PAC) 方法。将控制器视为应用程序中的一个服务的入口。将其视为一个处理流程的控制元素,而不是应用程序逻辑。您的控制层可以直接调用表单或 Doctrine 存储库,可以执行验证,并且可以有一些业务逻辑。当它变得复杂时,您自然会编写抽象类来解决这个问题。PAC 鼓励这样做。您的表现层将用于模板渲染或序列化(例如 JSON/XML)。表现层中的模板可以或不可以调用另一个控制元素来呈现额外的内容,例如页面上方的已登录用户。

每个服务都是一个 MVC/PAC '岛屿' 或三联体。您可能有一个用户服务和产品服务,它们总是通过服务的控制元素相互调用。由于控制层是以 Symfony2 控制器实现的,因此它们也可以从外部调用。使用 FOSRestBundle 或 KnpRadBundle 等RESTful Bundle,您可以自动为所有服务创建内部和外部 RESTful API。

Presentation Abstraction Controller image

HMVCBundle 的工作原理

HMVCBundle 使用 Symfony 的正常请求处理流程来进行内部调用。它返回控制器返回的数据,并使其在其他地方可用。它与 Symfony 的转发方法非常相似,但没有整个 HTML/JSON/XML 渲染过程。目前使用 HMVC 特性(PHP 5.4+)为控制器添加一个方法

$this->call($route, $attributes = array(), $data = array(), $query = array(), $rawResponse = false)

您仍然可以使用您自己的控制器、事件监听器、视图、模板和序列化器。HMVCBundle 不会影响正常行为,它只会为内部请求添加功能。

PHP 5.3: morket_hmvc.agent

您可以直接调用服务 morket_hmvc.agent,它具有与上文所述相同的call()方法。它的工作方式与README中描述的特质完全相同。很快将添加一些示例。

kernel.view事件

在内部调用时,Bundle将很可能并且应该阻止您的kernel.view(onKernelView())事件。这是确保不会为内部(HMVC)请求渲染任何HTML/JSON/XML所必需的。kernel.view事件定义的优先级为128,因此它会阻止FOSRestBundle和KnpRadBundle的kernel.view事件,但您可以将其优先级更高。这是Symfony的本地行为,Symfony将始终只允许每个(子)请求一个kernel.view,因为输出只能渲染一次。(注意:当控制器返回的不是Response对象时,如数据数组或视图对象,将触发kernel.view)

异常

在执行内部HMVC调用时,任何抛出的异常都会被传递,这意味着您可以在执行调用的控制器中捕获它们。请参见下面的示例。

如何使用它

只需在每个希望从中进行调用的控制器中使用Morket\Bundle\HMVCBundle\Controller\HMVC特质即可。就是这样。HMVC不会以任何方式影响现有软件。您可以部分使用HMVC,或者包含Bundle而不使用它。它不会破坏任何东西。

无依赖关系的简单示例

<?php
namespace Acme\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

use Morket\Bundle\HMVCBundle\Controller\HMVC;

class ProductsController extends Controller
{
    use HMVC;

    public function getProductsAction()
    {
        try {
            $user = $this->call('get_user', ['id' => $this->getRequest()->get('user_id')]);
            // do something
        } catch (NotFoundHttpException $e) {
            // user not found
        }
    }
}

使用提供的RadRestController的实际示例,依赖于FOSRestBundle和KnpRadBundle

<?php

namespace Acme\Controller;

use Acme\Entity\User;
use FOS\RestBundle\Controller\Annotations\View;
use FOS\RestBundle\Controller\Annotations\RequestParam;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Request\ParamFetcher;
use Morket\Bundle\HMVCBundle\Controller\RadRestController;
use Symfony\Component\HttpKernel\Exception\HttpException;

class UsersController extends RadRestController
{
    /**
     * @View
     */
    public function getUserAction($id)
    {
        return array('user' => $this->findOr404('App:User', ['id' => $id]));
    }

    /**
     * @QueryParam(name="filter", requirements="[a-z]+", strict=true)
     * @View
     */
    public function getUsersAction(ParamFetcher $paramFetcher)
    {
        return ['users' => $this->getRepository('App:User')
                                ->search($paramFetcher->get('filter'))];
    }

    /**
     * @RequestParam(name="username", requirements="[a-z0-9]+")
     * @RequestParam(name="something", requirements="[a-z]+")
     */
    public function postUsersAction($username, $something)
    {
        // We're illustrating the use of @RequestParam here, not best practices in saving a user entity

        $user = new User;
        $user->setUsername($username);
        $user->setSomething($something);
        $this->persist($user, true);

        return $this->routeRedirectView('get_user', ['id' => $user->getId()], 201);
    }

    /**
     * @View
     */
    public function putUserAction($id)
    {
        $user = $this->call('get_user', ['id' => $id]);
        $user->setUsername($this->getRequest()->get('username'));
        $user->setSomething($this->getRequest()->get('something'));
        $this->persist($user, true);

        return $this->routeRedirectView('get_user', ['id' => $user->getId()], 200);
    }

    /**
     * @View
     */
    public function deleteUserAction($id)
    {
        throw new HttpException(500);
    }
}

应用的其他地方

<?php
namespace Acme\Controller;

use Morket\Bundle\HMVCBundle\Controller\HMVC;
class InsaneController
{
    use HMVC;

    public function insaneAction()
    {
        $user = $this->call('get_user', ['id' => 1]); // get a user
        $users = $this->call('get_users', [], [], ['filter' => 'mor']); // get users by filter

        $this->call('post_users', ['username' => 'morket', 'something' => 'abc']); // add a user
        $this->call('put_user', ['id' => 1], ['username' => 'morket', 'something' => 'cba']); // update user
        $this->call('delete_user', ['id' => 1]); // delete user
    }
}

如您所见,所有来自FOSRestBundle和KnpRadBundle的酷炫功能都可以使用,包括参数规范这样的注解。如果您查看上面的UsersController,应用程序将可以内部和外部调用如下

您是否注意到,在内部PHP调用中提供数据的方式上,post_users和put_user之间的区别?当使用方法参数(如postUsersAction($username, $something))时,您应该考虑这些请求属性而不是POST数据。如果您使用常规方法,调用$request->get('something')或$paramFetcher->get('something'),您应该考虑POST数据。

避免Service Locator + PHP 5.3中的使用

HMVC特质使用Controller中的$this->get(),这是一个Service Locator。您可能认为Service Locator是一种反模式。在这种情况下,您希望将HMVC代理注入到控制器中或自己实例化它。默认的morket_hmvc.agent服务在容器请求范围内定义,这意味着它将为每个新的请求实例再次创建。由于Agent类中的Request参数是可选的,您可以创建自己的服务来执行任何您想做的事情。

以下示例显示了morket_hmvc.agent服务的直接使用,作为PHP 5.4特质的替代。

<?php
namespace Acme\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class ProductsController extends Controller
{
    public function getProductsAction()
    {
        try {
            $user = $this->get('morket_hmvc.agent')
                         ->call('get_user', ['id' => $this->getRequest()->get('user_id')]);
            // do something
        } catch (NotFoundHttpException $e) {
            // user not found
        }
    }
}

神奇的转换

HMVCBundle提供了一个自定义的Response对象,它扩展了Symfony的常规HTTP Response对象。当使用call方法时,HMVCBundle将自动从Response对象获取数据,并直接将数据返回给您。如果控制器响应是一个数组或只有一个数据元素的FOSRestBundle View,它将只返回那个元素。您可以在上面的示例中看到这一点。

当从控制器返回Response对象时,不使用HMVC Response对象。它只会在从控制器返回数组或View对象时使用。因此,如果您在所有地方手动返回Response对象(这也发生在返回$this->render()时),HMVC组件将没有用,因为您将在代码中获得HTML/JSON/XML。

重定向

考虑到HTTP规范和RESTful架构,有很多情况下您可能需要返回Location HTTP头。例如,在POST请求之后,您可能会希望在Location头中返回201 Created状态码以及新的资源。Symfony提供了RedirectResponse对象,而FOSRestBundle提供了RedirectView和RouteRedirectView,这使得这个过程变得非常简单。

目前,HMVCBundle仅支持FOSRestBundle的RouteRedirectView,因为它可以包含数据。上面的示例展示了其用法。在不久的将来,我们将添加对Symfony原生重定向的支持。我们需要将Location头URL映射回路由,并过滤数据。例如,当重定向到/users/1337时,我们希望返回['id' => 1337]。

如果您想撰写这部分内容,请随时贡献。我们使用PHPSpec2来描述行为。

待办事项

  1. 测试
  2. 将重定向转换为数据
  3. 在控制器级别(HTTP和内部)添加默认缓存,并添加@Cached注解
  4. 添加通用库,用于在执行外部REST调用时将表单序列化为正确的数据(当前表单错误序列化不佳)
  5. 在README中添加更多/更好的示例
  6. 为能够使用FOSRestBundle/KnpRadBundle而无需复制代码做出贡献

需要帮助或想要做出贡献吗?

请随时使用GitHub上的问题对HMVCBundle或组合多个bundle的最佳实践进行提问或评论。您也可以发送邮件至erik@morket.com。还可以关注我们的新Twitter账号@MorketDev

MIT许可证

版权所有(c)2013 Morket http://github.com/morket

在此特此免费授予任何人获得本软件及其相关文档文件(“软件”)副本的许可,以便在不受限制的情况下处理软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本,并允许向软件提供的人这样做,前提是遵守以下条件

上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。

软件按“原样”提供,不提供任何形式的质量保证,无论是明示的还是暗示的,包括但不限于适销性、特定目的适用性和非侵权性保证。在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任负责,无论这些责任是因合同、侵权或其他原因而引起的,与软件或其使用或任何其他方式有关。