雖然Laravel 4.2沒有@inject,可是依然可以使用Presenter

Presenter模式讓我們將顯示邏輯從blade中解放,將顯示邏輯封裝在presenter中,配合interface,可以使用App::bind()加以切換,但這一切的關鍵就是Laravel 5.1在blade中所提供的@inject,讓我們可以對view做依賴注入,但是在Laravel 4.2,我們該如何使用presenter呢?

Version


PHP 7.0.0
Laravel 4.2.17

日期格式


本文將如何依各種語言顯示不同日期格式的presenter範例,全部用Laravel 4.2改寫。

面臨的挑戰


相對於Laravel 5.1,Laravel 4.2少了以下幾項武器 :

  1. Method Injection
  2. Blade的@inject

Method injection倒是小事,反正最少還有constructor injection可以用。

@inject就是致命傷了,沒有@inject使的我們沒有辦法對view做依賴注入

Presenter


由於有多國語言的需求,我們直接建立presenter interface,各語言可實作此interface,使用compact()將presenter與collection打包在一起傳到view,並透過App::bind()去切換物件,相當於切換語言。1 1實務上並不是一開始就會去開presenter interface,而是先建立presenter物件,然後透過重構的方式產生interface,在此是因為講解的原因,所以從interface開始講起。

Presenter Interface

app/MyBlog/Presenters/DateFormatPresenterInterface.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace MyBlog\Presenters;

use Carbon\Carbon;

interface DateFormatPresenterInterface
{

/**
* 顯示日期格式
*
* @param Carbon $date
* @return string
*/

public function showDateFormat(Carbon $date) : string;
}

定義了showDateFormat(),各語言必須在showDateFormat()使用Carbon的format()去轉換日期格式。2 2此部分與Laravel 5.1的寫法完全一樣。

Presenter

app/MyBlog/Presenters/DateFormatPresenter_uk.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace MyBlog\Presenters;

use Carbon\Carbon;

class DateFormatPresenter_uk implements DateFormatPresenterInterface
{

/**
* 顯示日期格式
*
* @param Carbon $date
* @return string
*/

public function showDateFormat(Carbon $date) : string
{

return $date->format('d M, Y');
}
}

DateFormatPresenter_uk實現了DateFormatPresenterInterface,並將轉換成英國日期格式的Carbon的format()寫在showDateFormat()內。3 3此部分與Laravel 5.1的寫法完全一樣。

app/MyBlog/Presenters/DateFormatPresenter_tw.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace MyBlog\Presenters;

use Carbon\Carbon;

class DateFormatPresenter_tw implements DateFormatPresenterInterface
{

/**
* 顯示日期格式
*
* @param Carbon $date
* @return string
*/

public function showDateFormat(Carbon $date) : string
{

return $date->format('Y/m/d');
}
}

DateFormatPresenter_tw實現了DateFormatPresenterInterface,並將轉換成台灣日期格式的Carbon的format()寫在showDateFormat()內。4 4此部分與Laravel 5.1的寫法完全一樣。

app/MyBlog/Presenters/DateFormatPresenter_us.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace MyBlog\Presenters;

use Carbon\Carbon;

class DateFormatPresenter_us implements DateFormatPresenterInterface
{

/**
* 顯示日期格式
*
* @param Carbon $date
* @return string
*/

public function showDateFormat(Carbon $date) : string
{

return $date->format('M d, Y');
}
}

DateFormatPresenter_us實現了DateFormatPresenterInterface,並將轉換成美國日期格式的Carbon的format()寫在showDateFormat()內。5 5此部分與Laravel 5.1的寫法完全一樣。

Presenter Factory

由於每個語言的日期格式都是一個presenter物件,那勢必遇到一個最基本的問題 : 我們必須根據不同的語言去new不同的presenter物件,直覺我們可能會在controller去new presenter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function index()
{

$users = $this->userRepository->getAgeLargerThan(10);

$locale = Input::get('lang');

if ($locale === 'uk') {
$presenter = new DateFormatPresenter_uk();
} elseif ($locale === 'tw') {
$presenter = new DateFormatPresenter_tw();
} else {
$presenter = new DateFormatPresenter_us();
}

return View::make('users.index', compact('users'));
}

這種寫法雖然可行,但有幾個問題 :

  1. 違反SOLID開放封閉原則 : 若將來有新的語言需求,只能不斷去修改index(),然後不斷的新增elseif,就算改用switch也是一樣。6 6開放封閉原則 : 軟體中的類別、函式對於擴展是開放的,對於修改是封閉的。
  2. 違反SOLID依賴反轉原則 : controller直接根據語言去new相對應的class,高層直接相依於低層,直接將實作寫死在程式中。7 7依賴反轉原則 : 高層不應該依賴於低層,兩者都應該要依賴抽象;抽象不要依賴細節,細節要依賴抽象。
  3. 無法做unit test : 由於presenter直接new在controller,因此要測試時,無法對presenter做mock。

比較好的方式是使用Factory Pattern

app/MyBlog/Presenters/DateFormatPresenterFactory.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace MyBlog\Presenters;

use Illuminate\Support\Facades\App;

class DateFormatPresenterFactory
{

/**
* @param string $locale
*/

public function create(string $locale)
{

App::bind(DateFormatPresenterInterface::class,
'MyBlog\Presenters\DateFormatPresenter_' . $locale);

return App::make(DateFormatPresenterInterface::class);
}
}

使用Presenter Factorycreate()去取代new建立物件。

這裡當然可以在create()去寫if...elseif去建立presenter物件,不過這樣會違反SOLID開放封閉原則,比較好的方式是改用App::bind(),直接根據$locale去binding相對應的class,這樣無論在怎麼新增語言日期格式,controller與Presenter Factory都不用做任何修改,完全符合開放封閉原則

由於Laravel 4.2沒有@inject,所以我們必須提早處理,App::make()相當於@inject,會根據App::bind()的設定new出指定的物件。

Controller

app/controllers/UserController.php
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
use Illuminate\Http\Request;
use MyBlog\Presenters\DateFormatPresenterFactory;
use MyBlog\Repositories\UserRepository;

class UserController extends \BaseController
{

/** @var UserRepository */
protected $userRepository;
/** @var DateFormatPresenterFactory */
protected $dateFormatPresenterFactory;

/**
* UserController constructor.
* @param UserRepository $userRepository
* @param DateFormatPresenterFactory $dateFormatPresenterFactory
*/

public function __construct(UserRepository $userRepository,
DateFormatPresenterFactory $dateFormatPresenterFactory)

{

$this->userRepository = $userRepository;
$this->dateFormatPresenterFactory = $dateFormatPresenterFactory;
}

/**
* Display a listing of the resource.
*
* @return Response
*/

public function index()
{

$users = $this->userRepository->getAgeLargerThan(10);

$locale = (Input::get('lang')) ? Input::get('lang') : 'us';

$dateFormatPresenter = $this->dateFormatPresenterFactory->create($locale);

return View::make('users.index', compact('users', 'dateFormatPresenter'));
}
}

第7行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** @var UserRepository */
protected $userRepository;
/** @var DateFormatPresenterFactory */
protected $dateFormatPresenterFactory;

/**
* UserController constructor.
* @param UserRepository $userRepository
* @param DateFormatPresenterFactory $dateFormatPresenterFactory
*/

public function __construct(UserRepository $userRepository,
DateFormatPresenterFactory $dateFormatPresenterFactory)

{

$this->userRepository = $userRepository;
$this->dateFormatPresenterFactory = $dateFormatPresenterFactory;
}

將相依的UserRepositoryDateFormatPresenterFactory注入到UserController

由於Laravel 4.2沒有支援method injection,只能將相依物件全部由constructor injection注入。

24行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Display a listing of the resource.
*
* @return Response
*/

public function index()
{

$users = $this->userRepository->getAgeLargerThan(10);

$locale = (Input::get('lang')) ? Input::get('lang') : 'us';

$dateFormatPresenter = $this->dateFormatPresenterFactory->create($locale);

return View::make('users.index', compact('users', 'dateFormatPresenter'));
}

關鍵在這兩行 :

1
2
3
$dateFormatPresenter = $this->dateFormatPresenterFactory->create($locale);

return View::make('users.index', compact('users', 'dateFormatPresenter'));

之前在Laravel 5.1的presenter factory的create()只做App::bind(),但現在Laravel 4.2的create()是實際傳回presenter物件。

這個presenter物件要怎麼傳進view呢?關鍵就在compact(),它將collection與presenter打包成一個陣列,傳進view。

這個陣列的key是usersdateFormatPresenter,而users的資料就是collection,而dateFormatPresenter的資料就是presenter物件。

使用factory pattern之後,controller有了以下的優點 :
  1. 符合SOLID開放封閉原則 : 若將來有新的語言需求,controller完全不用做任何修改。
  2. 符合SOLID依賴反轉原則 : controller不再直接相依於presenter,而是改由factory去建立presenter。
  3. 可以做unit test : 由於將factory依賴注入進controller,因此要測試時,只要將要mock的presenter factory注入進controller即可。

View

blade很乾淨

除了沒有@inject外,其他寫法與Laravel 5.1完全一樣。

使用presenter的showDateFormat()將日期轉成想要的格式。

View搭配presenter之後,有以下優點 :
  1. Blade非常乾淨,完全沒有@if@elseif@endif
  2. 由於邏輯都寫在PHP中,將來可以繼續對顯示邏輯做重構物件導向
  3. Presenter只會影響目前的view,不像mutator會有side effect。
  4. 符合SOLID單一職責原則 : 顯示邏輯被封裝在presenter內,不像mutor將顯示邏輯寫在model內。
  5. 符合SOLID開放封閉原則 : 將來若有新的語言,對於擴展是開放的 : 只要新增class實踐DateFormatPresenterInterface即可;對於修改是封閉的 : controller、factory interface、factory與view都不用做任何修改。
  6. 可單獨對presenter的顯示邏輯做unit test。

Conclusion


  • Laravel 4.2的blade雖然沒有@inject,但只要透過compact(),我們依然可以將presenter打包進view,這樣就可以不用在blade直接寫顯示邏輯,而將其封裝在presenter物件內。

Sample Code


完整的範例可以在我的GitHub上找到。

2015-12-24