## 如何使用Presenter模式
若將顯示邏輯都寫在 View,會造成 View 代碼臃腫而難以維護,基于 SOLID 原則,應該使用 Presenter 模式輔助 View,將相關的顯示邏輯封裝在不同的 Presenter ,方便中大型項目的維護。
### 版本
Lararvel 5.4.17
## 顯示邏輯
在實際開發中,顯示邏輯常見的如下:
* 將資料顯示不同資料: 如 `性別字段為 M,就顯示 Mr.,若性別字段為 F,就顯示 Mrs.`
* 是否顯示某些資料:如 `根據字段值是否等于 T,要不要顯示改字段`
* 依需求顯示不同格式:如 `依不同的語系,顯示不同的日期格式`
## Presenter
### 將資料顯示不同資料
如 `性別字段為 M,就顯示 Mr.,若性別字段為 F,就顯示 Mrs.`,我們可能會直接用 blade 寫在 view 里,如下:
```
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@foreach($users as $user)
<div>
<h2>@if($user->gender == 'm'){{ "Mr." }} @else {{ "Mrs." }} @endif {{$user->name}}</h2>
<h2>{{ $user->email }}</h2>
</div>
@endforeach
</div>
</body>
</html>
```
在中大型項目中,會有幾個問題:
* 由于 Blade 與 Html 夾雜,不太適合寫太復雜的代碼,只適合做一些簡單的 binding ,否則很容易寫成傳統的 PHP 的意大利面代碼
* 無法對顯示邏輯做重構與物件導向
比較好的方式是使用 Presenter,具體步驟如下:
* 將相依無間注入到 Presenter
* 在 presenter 內寫格式轉換
* 將 Presenter 注入到 View
#### 定義UserPresenter
`app\Presenters\UserPersenter.php` 代碼如下:
```
<?php
namespace App\Presenters;
/**
* Class UserPresenter
*
* @package App\Presenters
*/
class UserPresenter
{
/**
* @param string $gender
* @param string $name
*
* @return string
*/
public function getFullName($gender, $name)
{
return $gender == 'M' ? 'Mr. ' . $name : 'Mrs. ' . $name;
}
}
```
將原本在 blade 中用 `@if(){ .. }@else .. @endif` 寫的邏輯改寫在 Presenter 中。
#### 視圖中使用UserPresenter
使用 `@inject()` 注入 `UserPresenter`,讓 View 可以如 Controller 一樣使用注入的物件。
將來如亂顯示邏輯怎么修改,都不用改到 Blade ,直接在相關 Presenter 中修改即可。
```
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@inject('userPresenter','App\Presenters\UserPresenter')
@foreach($users as $user)
<div>
{{--<h2>@if($user->gender == 'm'){{ "Mr." }} @else {{ "Mrs." }} @endif {{$user->name}}</h2>--}}
<h2>{{ $userPresenter->getFullName($user->gender,$user->name) }}</h2>
<h2>{{ $user->email }}</h2>
</div>
@endforeach
</div>
</body>
</html>
```
改用這種重寫,有幾個優點:
* 將資料顯示不同個格式的顯示邏輯改寫在 presenter,解決了 blade 不容易維護的問題
* 可以顯示邏輯做重構于物件導向
## 是否顯示某些資料
如 `根據字段值是否為 T ,要不要顯示該字段`,我們常常會直接用 blade 寫在 View 中。
```
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@foreach($users as $user)
<h2>{{ $user->name }}</h2>
@if($user->is_hidden == 'F')
<h2>{{ $user->email }}</h2>
@endif
@endforeach
</div>
</body>
</html>
```
在中大型項目中,會有幾個問題:
* 由于 blade 與 HTML 夾雜,不太適合寫太復雜的業務代碼,只適合做一些簡單的 binding,否則很容易寫成傳統的 PHP 的意大利面代碼
* 無法對顯示邏輯做重構與物件導向
比較好的方式是使用 Presenter,具體步驟如下:
* 將相依無間注入到 Presenter
* 在 presenter 內寫格式轉換
* 將 Presenter 注入到 View
`app\Presenters\UserPresenter.php` 代碼:
```
<?php
namespace App\Presenters;
use App\User;
/**
* Class UserPresenter
*
* @package App\Presenters
*/
class UserPresenter
{
/**
* @param \App\User $user
*
* @return string
*/
public function showEmail(User $user)
{
if ($user->is_hidden == 'F') {
return '<h2>' . $user->email '</h2>';
}
return '';
}
}
```
將 `@if() .. @endif` 的 boolean 判斷封裝在 Presenter 內,改由 Presenter 負責輸出 HTML。
```
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@inject('userPresenter','App\Presenters\UserPresenter')
@foreach($users as $user)
<h2>{{ $user->name }}</h2>
{!! $userPresenter->showEmail($user) !!}
@endforeach
</div>
</body>
</html>
```
使用 `@inject()` 注入 `UserPresenter`,讓 View 也可以如 Controller 一樣使用注入的物件。
`{!! !!}` 會保留原來的 HTML 格式。
將來無論顯示邏輯怎么修改,都不用改到 Blade ,直接在 Presenter 內修改。
改用這種寫法,有幾個優點:
* `是否顯示某些資料` 的顯示邏輯改為在 Presenter,解決寫在 Blade 不容易維護的問題
* 可對顯示邏輯做重構與物件導向
### 依需求顯示不同格式
如 `按照不同的語系,顯示不同的日期格式`,我們常常會直接用 Blade 寫在 View 里。 如下:
```
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@foreach($users as $user)
<div>
@if(App::getLocale() == 'uk')
<h2>{{ $user->created_at->format('d M, Y') }}</h2>
@elseif(App::getLocale() == 'tw')
<h2>{{ $user->creaetd_at->format('Y/m/d') }}</h2>
@else
<h2>{{ $user->created_at->formate('M d, Y') }}</h2>
@endif
</div>
@endforeach
</div>
</body>
</html>
```
在中大型的醒目中,會有幾個問題:
* 由于 Blade 與 HTML 夾雜,不太適合寫太復雜的代碼,只適合做一些簡單的 binding,否則很容易寫成傳統的 PHP 的意大利面代碼
* 無法對顯示邏輯做重構與物件導向
比較好的方式是使用 Presenter,具體步驟如下:
* 將相依無間注入到 Presenter
* 在 presenter 內寫不同的日期格式轉換邏輯
* 將 Presenter 注入到 View
#### 定義接口
定義接口代碼 `app\Presenters\DataFormatPresenterInterface.php` ,具體代碼如下:
```
<?php
namespace App\Presenters;
use Carbon\Carbon;
/**
* Interface DateFormatPresenterInterface
*
* @package App\Presenters
*/
interface DateFormatPresenterInterface
{
/**
* 顯示日期格式
*
* @param \Carbon\Carbon $data
*
* @return string
*/
public function showDateFormat(Carbon $data);
}
```
定義了 `showDateFormat()`,各語言必須在 `showDateFormat()` 使用 Carbon 的 `format()` 去轉換日期格式。
#### 一些Presenter
`app\Presenters\DateFormatPresenterTW.php`,具體代碼內容如下:
```
<?php
namespace App\Presenters;
use Carbon\Carbon;
/**
* Class DateFormatPresenterTw
*
* @package \App\Presenters
*/
class DateFormatPresenterTw implements DateFormatPresenterInterface
{
/**
* @param \Carbon\Carbon $date
*
* @return string
*/
public function showDateFormat(Carbon $date)
{
return $date->format('Y/m/d');
}
}
```
`app\Presenters\DateFormatPresenterUk.php`,具體代碼內容如下:
```
<?php
namespace App\Presenters;
use Carbon\Carbon;
/**
* Class DateFormatPresenterUk
*
* @package \App\Presenters
*/
class DateFormatPresenterUk implements DateFormatPresenterInterface
{
/**
* @param \Carbon\Carbon $data
*
* @return string
*/
public function showDateFormat(Carbon $data)
{
return $data->format('d M, Y');
}
}
```
`app\Presenters\DateFormatPresenterUs.php`,具體代碼內容如下:
```
<?php
namespace App\Presenters;
use Carbon\Carbon;
/**
* Class DateFormatPresenterUs
*
* @package \App\Presenters
*/
class DateFormatPresenterUs implements DateFormatPresenterInterface
{
/**
* @param \Carbon\Carbon $date
*
* @return string
*/
public function showDateFormat(Carbon $date)
{
return $date->format('M d,Y');
}
}
```
以上類都實現了 `DateFormatPresenterInterface` 接口,并將轉換成相對應國家日期格式的 Carbon 的 `format()` 寫在 `showDateFormat()` 內。
#### Presenter 工廠
由于每個語言的日期格式都是一個 presenter 物件,那勢必遇到一個最基本的問題: `我們必須根據不同的語言去實例化不同的 Presenter 物件`,我們可能會在 Controller 中去 實例化。如下:
```
/**
* @param \Illuminate\Http\Request $request
*
* @return int
*/
public function index(Request $request)
{
$locate = 'hk';
switch ($locate){
case 'uk':
$presenter = new DateFormatPresenterUk();
break;
case 'tw':
$presenter = new DateFormatPresenterTw();
break;
default:
$presenter = new DateFormatPresenterUs();
}
return $presenter;
}
```
這種寫法雖然可行,但是有如下問題:
* 違反了 SOLID 的開放封閉原則:若將來有新的語言需求,只能不斷去修改 `index()` ,然后不斷的新增 `elseif()` ,計算改用 `switch{ .. }` 也是一樣
* 違反了 SOLID 的依賴反轉原則:Controller 直接根據語言去實例化對應的 Class ,高層直接相依于底層,直接將實例化對象寫死在代碼里
* 無法單元測試:由于 Presenter 直接 New 在 Controller ,因此要測試時,無法對 Presenter 做 mock
##### 定義工廠
比較好的解決方式是使用 **Factory Pattern**
`app/Presenters/DateFormatPresenterFactory.php` 內容如下:
```
<?php
namespace App\Presenters;
/**
* Class DateFormatPresenterFactory
*
* @package \App\Presenters
*/
class DateFormatPresenterFactory
{
/**
* @param $locale
*
* @return \Illuminate\Foundation\Application|mixed
*/
public static function bind($locale)
{
return app()->singleton(DateFormatPresenterInterface::class, 'App\Presenters\DateFormatPresenter' . ucwords($locale));
}
}
```
使用 **Presenter Factory** 的 `create()` 去取代 new 建立物件。
這里當然可以在 `create()` 里去寫 `if () { ... } else { ... }` 去建立 Presenter 物件,不過這樣會違反 SOLID 的開放封閉原則,比較好的方式是改用 `App::bind()`,直接根據 `$locale` 去 binding 相對應的 Class,這樣無論再怎么新增語言與日期格式, Controller 與 Presenter Factory 都不用做任何修改,完全符合開放封閉原則。
##### 控制器調用
`app\Http\Controllers\UserController.php` 中的內容,如下:
```
public function index(Request $request, DateFormatPresenterFactory $dataFormatPresenterFactory)
{
$locate = 'uk';
$presenter = $dataFormatPresenterFactory::bind($locate);
dd($presenter->showDateFormat(Carbon::now()));
return $presenter;
}
```
使用 `$dataFormatPresenterFactory::bind()` 切換 `app()` 的 Presenter 物件,如此 Controller 將開放封閉,將來有新的語言新增或者修改需求,也不用修改 Controller
##### Blade 調用
```
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@inject('dateFormatPresenter','App\Presenters\DateFormatPresenterInterface')
@foreach($users as $user)
<div>
<h2><?php print_r($dateFormatPresenter->showDateFormat($user->created_at)); ?></h2>
</div>
@endforeach
</div>
</body>
</html>
```
使用 `@inject()` 注入 Presenter ,讓 View 也可以如 Controller 一樣使用注入的物件
使用 Presenter 的 `showDateFormate()` 將日期轉換成預計的格式
使用這種寫法有幾個優點
* 將 `依需求顯示不同的格式` 的顯示邏輯寫在 Presenter ,解決寫在 Blade 不容易維護的問題
* 可對顯示邏輯做重構與物件導向
* 符合 SOLID 的開放閉合原則:將來若有新的語言,對于拓展是開放的,只要新增 Class 實現 `DateFormatPresenterInterface` 接口即可;對于修改是封閉的, Controller、FactoryInterface、Factory 與 View 都不用做任何修改
* 不單只有 PHP 可以使用 Service Container,連 Blade 也可以使用 Service Container,甚至搭配 Service Provider
* 可單獨對 Presenter 的顯示邏輯做單元測試
* 若使用了 Presenter 輔助 Blade ,在搭配 `@inject()` 注入到 View,View就會非常干凈,可專心處理 `將資料binding到HTML`的職責
* 將來只有 Layout 改變才會動到 Balde ,若是顯示邏輯改變都是修改 Presenter
### 最后
Presenter 使得顯示邏輯從Blade 中解放,不僅更容易維護、更容易擴展、更容易重復使用且更容易測試
- 介紹
- Laravel5發送郵件使用Service隔離業務
- 如何使用Repository模式
- 如何使用Service模式
- 如何使用Presenter模式
- Laravel 5.* 執行遷移文件報錯:Specified key was too long error
- EloquentORM關聯關系
- EloquentORM關聯關系之一對一
- EloquentORM關聯關系之一對多
- EloquentORM關聯關系之遠層一對多
- EloquentORM關聯關系之多對多
- EloquentORM關聯關系之多態關聯
- EloquentORM關聯關系之多對多多態關聯
- Laravel測試
- Laravel中涉及認證跳轉地址的修改的地方
- Laravel中Collection的基本使用
- all
- avg
- chuck
- collapse
- combine
- contains
- containsStrict
- count
- diff
- diffAssoc
- diffKeys
- each
- every
- except
- filter
- first
- flatMap
- flatten
- flip
- forget
- forPage
- get
- groupBy
- has
- implode
- intersect
- intersectKey
- isEmpty
- isNotEmpty
- keyBy
- keys
- last
- map
- mapWithKeys
- max
- median
- merge
- min
- mode
- nth
- only
- partition
- pipe
- pluck
- pop
- prepend
- pull
- push
- put
- random
- reduce
- reject
- reverse
- search
- shift
- shuffle
- slice
- sort
- sortBy
- sortByDesc
- splice
- split
- sum
- take
- tap
- times
- toArray
- toJson
- transform
- union
- unique
- uniqueStrict
- values
- when
- where
- whereStrict
- whereIn
- whereInStrict
- whereNotIn
- whereNotInStrict
- zip
- Laravel中Collection的實際使用
- collection中sum求和
- collection格式化計算數據
- collection格式化計算數據計算github事件得分總和
- collection格式化markdown數據列表
- collection格式化計算兩個數組的數據
- collection中reduce創建lookup數組
- TODO