关联分离

3.3 version
维护中的版本

框架的一个欠点,目前看来是在创建一个新网站时,我们每次都要从front.php中复制和粘贴代码。60行的代码并不多,但若能将其打包到一个类中显然更好。这能带来更好的复用性 而且更易测试,好处很多。

若你已近观代码,front.php有一个输入点(input),即Request,以及一个输出点(output),即Response。我们的框架类将遵循以下简单原则:程序的逻辑就是要创建“与请求相关联”的响应。

我们先为框架创建一个非常自我的命名空间:Simplex。再将处理请求的逻辑,移至它自己的Simplex\Framework类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// example.com/src/Simplex/Framework.php
namespace Simplex;
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcher;
 
class Framework
{
    protected $matcher;
    protected $controllerResolver;
    protected $argumentResolver;
 
    public function __construct(UrlMatcher $matcher, ControllerResolver $controllerResolver, ArgumentResolver $argumentResolver)
    {
        $this->matcher = $matcher;
        $this->controllerResolver = $controllerResolver;
        $this->argumentResolver = $argumentResolver;
    }
 
    public function handle(Request $request)
    {
        $this->matcher->getContext()->fromRequest($request);
 
        try {
            $request->attributes->add($this->matcher->match($request->getPathInfo()));
 
            $controller = $this->controllerResolver->getController($request);
            $arguments = $this->argumentResolver->getArguments($request, $controller);
 
            return call_user_func_array($controller, $arguments);
        } catch (ResourceNotFoundException $e) {
            return new Response('Not Found', 404);
        } catch (\Exception $e) {
            return new Response('An error occurred', 500);
        }
    }
}

接着更新相应的example.com/web/front.php程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// example.com/web/front.php
 
// ...
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
 
$context = new Routing\RequestContext();
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
 
$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();
 
$framework = new Simplex\Framework($matcher, $controllerResolver, $argumentResolver);
$response = $framework->handle($request);
 
$response->send();

为了完成对重构的“打包”,我们把除了路由定义之外的每一样东西,从example.com/src/app.php转移到另外一个命名空间:Calendar

为了让定义在SimplexCalendar命名空间下的类能够被自动加载,更新composer.json文件:

1
2
3
4
5
6
{
    "...": "...",
    "autoload": {
        "psr-4": { "": "src/" }
    }
}

Composer的自动加载器也需要被更新,请运行composer dump-autoload

把控制器转移到Calendar\Controller\LeapYearController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// example.com/src/Calendar/Controller/LeapYearController.php
namespace Calendar\Controller;
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Calendar\Model\LeapYear;
 
class LeapYearController
{
    public function indexAction(Request $request, $year)
    {
        $leapyear = new LeapYear();
        if ($leapyear->isLeapYear($year)) {
            return new Response('Yep, this is a leap year!');
        }
 
        return new Response('Nope, this is not a leap year.');
    }
}

再把is_leap_year()函数也转移到它自己的类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// example.com/src/Calendar/Model/LeapYear.php
namespace Calendar\Model;
 
class LeapYear
{
    public function isLeapYear($year = null)
    {
        if (null === $year) {
            $year = date('Y');
        }
 
        return 0 == $year % 400 || (0 == $year % 4 && 0 != $year % 100);
    }
}

别忘了更新相应的example.com/src/app.php文件:

1
2
3
4
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
    'year' => null,
    '_controller' => 'Calendar\\Controller\\LeapYearController::indexAction',
)));

总结一下,以下是全新的文件体系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
example.com
├── composer.json
├── composer.lock
├── src
│   ├── app.php
│   └── Simplex
│       └── Framework.php
│   └── Calendar
│       └── Controller
│       │   └── LeapYearController.php
│       └── Model
│           └── LeapYear.php
├── vendor
│   └── autoload.php
└── web
    └── front.php

就是这样!现在我们的程序有四个不同的层,每一个层都有一个组织良好的目标:

  • web/front.php:前端控制器。这是唯一要与客户端互动而暴露的PHP代码(它得到Request并返回Response),它为我们的框架和程序提供了一个“用于初始化”的样板代码(boiler-plate code);

  • src/Simplex:可复用的框架代码。抽象出对进入的请求的处理(顺带一提,它令你的控制器/模板易于测试——后续文章有进一步阐述);

  • src/Calendar:我们程序的特定代码(主要是控制器和model);

  • src/app.php:程序配置/框架自定义的部分。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)