如何在Laravel 4.2使用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少了以下幾項武器 :
- Method Injection
- 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
1 | namespace MyBlog\Presenters; |
定義了showDateFormat()
,各語言必須在showDateFormat()
使用Carbon的format()
去轉換日期格式。2 2此部分與Laravel 5.1的寫法完全一樣。
Presenter
1 | namespace MyBlog\Presenters; |
DateFormatPresenter_uk
實現了DateFormatPresenterInterface
,並將轉換成英國
日期格式的Carbon的format()
寫在showDateFormat()
內。3 3此部分與Laravel 5.1的寫法完全一樣。
1 | namespace MyBlog\Presenters; |
DateFormatPresenter_tw
實現了DateFormatPresenterInterface
,並將轉換成台灣
日期格式的Carbon的format()
寫在showDateFormat()
內。4 4此部分與Laravel 5.1的寫法完全一樣。
1 | namespace MyBlog\Presenters; |
DateFormatPresenter_us
實現了DateFormatPresenterInterface
,並將轉換成美國
日期格式的Carbon的format()
寫在showDateFormat()
內。5 5此部分與Laravel 5.1的寫法完全一樣。
Presenter Factory
由於每個語言的日期格式都是一個presenter物件,那勢必遇到一個最基本的問題 : 我們必須根據不同的語言去new不同的presenter物件
,直覺我們可能會在controller去new presenter。
1 | public function index() |
這種寫法雖然可行,但有幾個問題 :
- 違反SOLID的開放封閉原則 : 若將來有新的語言需求,只能不斷去修改
index()
,然後不斷的新增elseif
,就算改用switch
也是一樣。6 6開放封閉原則 : 軟體中的類別、函式對於擴展是開放的,對於修改是封閉的。 - 違反SOLID的依賴反轉原則 : controller直接根據語言去new相對應的class,高層直接相依於低層,直接將實作寫死在程式中。7 7依賴反轉原則 : 高層不應該依賴於低層,兩者都應該要依賴抽象;抽象不要依賴細節,細節要依賴抽象。
- 無法做unit test : 由於presenter直接new在controller,因此要測試時,無法對presenter做mock。
比較好的方式是使用Factory Pattern。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17namespace 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 Factory
的create()
去取代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
1 | use Illuminate\Http\Request; |
第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;
}
將相依的UserRepository
與DateFormatPresenterFactory
注入到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是users
與dateFormatPresenter
,而users
的資料就是collection,而dateFormatPresenter
的資料就是presenter物件。
- 符合SOLID的開放封閉原則 : 若將來有新的語言需求,controller完全不用做任何修改。
- 符合SOLID的依賴反轉原則 : controller不再直接相依於presenter,而是改由factory去建立presenter。
- 可以做unit test : 由於將factory依賴注入進controller,因此要測試時,只要將要mock的presenter factory注入進controller即可。
View
除了沒有@inject
外,其他寫法與Laravel 5.1完全一樣。
使用presenter的showDateFormat()
將日期轉成想要的格式。
- Blade非常乾淨,完全沒有
@if
、@elseif
與@endif
。 - 由於邏輯都寫在PHP中,將來可以繼續對顯示邏輯做重構與物件導向。
- Presenter只會影響目前的view,不像mutator會有side effect。
- 符合SOLID的單一職責原則 : 顯示邏輯被封裝在presenter內,不像mutor將顯示邏輯寫在model內。
- 符合SOLID的開放封閉原則 : 將來若有新的語言,對於擴展是開放的 : 只要新增class實踐
DateFormatPresenterInterface
即可;對於修改是封閉的 : controller、factory interface、factory與view都不用做任何修改。 - 可單獨對presenter的顯示邏輯做unit test。
Conclusion
- Laravel 4.2的blade雖然沒有
@inject
,但只要透過compact()
,我們依然可以將presenter打包進view,這樣就可以不用在blade直接寫顯示邏輯,而將其封裝在presenter物件內。
Sample Code
完整的範例可以在我的GitHub上找到。