如何依各種語言顯示不同日期格式 ?
由於多國語言的需求,不只有語言部分必須翻譯,連日期格式也必須符合當地習慣,最典型的如美國習慣Dec 25, 2015,而英國卻習慣25 Dec, 2015,台灣則習慣2015/12/25,不過在資料庫存的卻是同一份日期,只是因為顯示邏輯的不同,在view必須用不一樣的格式呈現。
Version
PHP 7.0.0
Laravel 5.1.22
日期格式
PHP談到日期格式,就一定要想到Carbon,Laravel已經內建Carbon,且只要資料庫是date、datetime或timestamp型態,model的該欄位已經自動轉成Carbon物件,也就是說,可以直接使用Carbon的format()去做任何日期格式的轉換。
雖然Carbon可以幫我們轉換日期格式,但該在哪裡使用format()呢?本文提供3種寫法,並深入討論3種寫法的優缺點 :
- Blade
- Mutator
- Presenter
Blade
由於日期格式為顯示邏輯,最直覺的寫法就是在blade透過Carbon去轉換日期格式。

由於Laravel已經將created_at欄位轉成Carbon物件,所以自帶format(),我們可以透過format()得到我們要的日期格式。
使用blade寫法的優缺點如下 :
優點 :
- 將顯示邏輯寫在blade,非常直覺。
缺點 :
@if、@elseif與@ednif與HTML夾雜,不易維護。- 由於直接寫在blade,無法對顯示邏輯做重構與物件導向。
- 違反SOLID的開放封閉原則 : 將來若有新的語言,只能不斷的加上
@elseif判斷。
Mutator
Laravel為model欄位提供了mutator與accessor,簡單的說,就是model欄位的getter與setter,因為欲顯示的日期格式與資料庫存的日期格式不同,所以我們只要在mutator使用Carbon的format(),就可以得到我們要的日期格式。
1 | namespace MyBlog; |
40行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* [created_at]的mutator
*
* @param $value
* @return string
*/
public function getCreatedAtAttribute($value): string
{
$locale = App::getLocale();
$date = $this->asDateTime($value);
if ($locale === 'uk') {
return $date->format('d M, Y');
} elseif ($locale === 'tw') {
return $date->format('Y/m/d');
} else {
return $date->format('M d, Y');
}
}
在model加上getCreatedAtAttribute(),其中get為規定的prefix,而Attribute為規定的postfix,中間為為欄位名稱created_at,改用CamelCase表示。
App::getLocale()會傳回由controller所設定的語系,如us、tw、uk…等。
$value為mutator所傳進的資料,其中model的asDateTime()可將mutator的$value轉成Carbon物件。
依Carbon的format()轉成各國所慣用的日期格式。
使用mutator寫法的優缺點如下 :
優點 :
- 將邏輯寫在PHP,沒有寫在blade,程式較容易維護,且可以重構與物件導向。
- 只要在model寫一次,就可以套用在所有的view。
缺點 :
- 將顯示邏輯寫在model,違反SOLID的單一職責原則,model就算要寫,也應該寫資料庫邏輯,而不適合寫顯示邏輯。
- 若不是每個view都套用此顯示邏輯,使用mutator會對其他view產生side effect。
- 若要對mutator做unit test,需要碰到資料庫。1 1雖然可以使用SQLite In-Memory Database使測試速度加快,但還是會比使用Presenter沒碰資料庫慢很多。
Presenter
由於有多國語言的需求,我們直接建立presenter interface,各語言可實作此interface,使用@inject將presenter注入到view,並透過App::bind()去切換物件,相當於切換語言。2 2實務上並不是一開始就會去開presenter interface,而是先建立presenter物件,然後透過重構的方式產生interface,在此是因為講解的原因,所以從interface開始講起。
Presenter Interface
1 | namespace MyBlog\Presenters; |
定義了showDateFormat(),各語言必須在showDateFormat()使用Carbon的format()去轉換日期格式。
Presenter
1 | namespace MyBlog\Presenters; |
DateFormatPresenter_uk實現了DateFormatPresenterInterface,並將轉換成英國日期格式的Carbon的format()寫在showDateFormat()內。
1 | namespace MyBlog\Presenters; |
DateFormatPresenter_tw實現了DateFormatPresenterInterface,並將轉換成台灣日期格式的Carbon的format()寫在showDateFormat()內。
1 | namespace MyBlog\Presenters; |
DateFormatPresenter_us實現了DateFormatPresenterInterface,並將轉換成美國日期格式的Carbon的format()寫在showDateFormat()內。
Presenter Factory
由於每個語言的日期格式都是一個presenter物件,那勢必遇到一個最基本的問題 : 我們必須根據不同的語言去new不同的presenter物件,直覺我們可能會在controller去new presenter。
1 | public function index(Request $request) |
這種寫法雖然可行,但有幾個問題 :
- 違反SOLID的開放封閉原則 : 若將來有新的語言需求,只能不斷去修改
index(),然後不斷的新增elseif,就算改用switch也是一樣。 - 違反SOLID的依賴反轉原則 : controller直接根據語言去new相對應的class,高層直接相依於低層,直接將實作寫死在程式中。
- 無法做unit test : 由於presenter直接new在controller,因此要測試時,無法對presenter做mock。
比較好的方式是使用Factory Pattern。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15namespace 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);
}
}
使用Presenter Factory的create()去取代new建立物件。
這裡當然可以在create()去寫if...elseif去建立presenter物件,不過這樣會違反SOLID的開放封閉原則,比較好的方式是改用App::bind(),直接根據$locale去binding相對應的class,這樣無論在怎麼新增語言與日期格式,controller與Presenter Factory都不用做任何修改,完全符合開放封閉原則。
Controller
1 | namespace App\Http\Controllers; |
11行1
2
3
4
5
6
7
8
9
10
11
12/** @var UserRepository 注入的UserRepository */
protected $userRepository;
/**
* UserController constructor.
*
* @param UserRepository $userRepository
*/
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
將相依的UserRepository注入到UserController。
24行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* Display a listing of the resource.
*
* @param Request $request
* @param DateFormatPresenterFactory $dateFormatPresenterFactory
* @return \Illuminate\Http\Response
*/
public function index(Request $request,
DateFormatPresenterFactory $dateFormatPresenterFactory)
{
$users = $this->userRepository->getAgeLargerThan(10);
$locale = ($request['lang']) ? $request['lang'] : 'us';
$dateFormatPresenterFactory->create($locale);
return view('users.index', compact('users'));
}
index()會相依於DateFormatPresenterFactory,當然也可以在constructor一併將DateFormatPresenterFactory注入,但是考慮到DateFormatPresenterFactory只有index()使用,因此改用Method Injection的方式,只將DateFormatPresenterFactory注入到index()即可,這樣可以減輕constructor的壓力,不必所有相依物件都寫在constructor。
- 符合SOLID的開放封閉原則 : 若將來有新的語言需求,controller完全不用做任何修改。
- 符合SOLID的依賴反轉原則 : controller不再直接相依於presenter,而是改由factory去建立presenter。
- 可以做unit test : 由於將factory依賴注入進controller,因此要測試時,只要將要mock的presenter factory注入進controller即可。
- 若該相依物件,有很多method要使用,可考慮使用Constructor Injection,如repository、service。
- 若該相依物件,只有單個method自己使用,可考慮使用Method Injection,如factory。
View

使用@inject注入presenter,讓view也可以如controller一樣使用注入的物件。
使用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
- Blade : 最直覺,但與HTML夾雜,不適合寫太複雜的邏輯,且違反SOLID原則。
- Mutator : 很方便,但要小心對其他view或controller的side effect,unit test會直接碰到資料庫,速度較慢。
- Presenter : 較複雜,適合複雜的邏輯,容易維護,符合SOLID原則,unit test不會碰到資料庫,速度非常快。
Sample Code
完整的範例可以在我的GitHub上找到。