Laravel中如何使用Presenter模式

  • 原创
  • 发布时间: 2个月前
  • 收藏数: 0 / 点赞数: 0 / 阅读数: 70

若将显示逻辑都写在 View,会造成 View 代码臃肿而难以维护,基于 SOLID 原则,应该使用 Presenter 模式辅助 View,将相关的显示逻辑封装在不同的 Presenter ,方便中大型项目的维护。

显示逻辑

在实际开发中,显示逻辑常见的如下:

  • 将数据显示不同数据: 如 性别字段为 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 不容易维护的问题

  • 可以显示逻辑做重构于presenter

是否显示某些数据

如 根据字段值是否为 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 中解放,不仅更容易维护、更容易扩展、更容易重复使用且更容易测试

评论