初学者学习Laravel时分两种,一种是乖乖的将程序填入MVC构架内,导致controller与model异常的肥大,日后一样很难维护;一种是常常不知道程序该写在哪一个class内而犹豫不决,毕竟传统PHP都是一个页面一个档案。本文整理出最适合Laravel的中大型项目构架,兼具容易维护、容易扩充与容易重复使用的特点,并且容易测试。

Controller过于肥大

受RoR的影响,初学者常认为MVC构架就是model,view,controller:

Model就是数据库。
Controller负责与HTTP沟通,调用model与view。
View就是HTML。
假如依照这个定义,以下这些需求该写在哪里呢?

  1. 发送Email,使用外部API。
  2. 使用PHP写的逻辑。
  3. 依需求将显示格式作转换。
  4. 依需求是否显示某些数据。
  5. 依需求显示不同数据。

其中1,2属于商业逻辑,而3,4,5属于显示逻辑,若依照一般人对MVC的定义,model是数据库,而view又是HTML,以上这些需求都不能写在model与view,只能勉强写在controller。
因此初学者开始将大量程序写在controller,造成controller的肥大难以维护。

Model过于肥大

既然逻辑写在controller不方便维护,那我将逻辑都写在model就好了?

当你将逻辑从controller搬到model后,虽然controller变瘦了,但却肥了model,model从原本代表数据库,现在变成还要负担商业逻辑与显示逻辑,结果更惨。
Model代表数据库吗?把它想成是Eloquent class就好,数据库逻辑应该写在repository里,这也是为什么Laravel 5已经没有models目录,Eloquent class仅仅是放在app根目录下而已。

中大型项目构架

那我们该怎么写呢?别将我们的思维局限在MVC内:

  • Model:仅当成Eloquent class。
  • Repository:辅助model,处理数据库逻辑,然后注入到service。
  • Service:辅助controller,处理商业逻辑,然后注入到controller。
  • Controller:接收HTTP request,调用其他service。
  • Presenter:处理显示逻辑,然后注入到view。
  • View:使用blade将数据binding到HTML。

picture

其中蓝色为原本的MVC,而紫色为本文要介绍的的重点:Repository模式,Service模式与Presenter模式。
箭头表示物件依赖注入的方向。
我们可以发现MVC构架还在,由于SOLID的单一职责原则与依赖反转原则:

  1. 我们将数据库逻辑从model分离出来,由repository辅助model,将model依赖注入进repository。
  2. 我们将商业逻辑从controller分离出来,由service辅助controller,将service依赖注入进controller。
  3. 我们将显示逻辑从view分离出来,由presenter辅助view,将presenter依赖注入进view。

建立目录

在 app 目录建立 Repositories,Services 与 Presenters 目录。

别害怕在Laravel预设目录以外建立的其他目录,根据SOLID的单一职责原则,class功能越多,责任也越多,因此越违反单一职责原则,所以你应该将你的程序分割成更小的部分,每个部分都有它专属的功能,而不是一个class功能包山包海,也就是所谓的万能类别,所以整个项目不应该只有MVC三个部分,放手根据你的需求建立适当的目录,并将适当的class放到该目录下,只要我们的class有namespace帮我们分类即可。

Repository

若将数据库逻辑都写在model,会造成model的肥大而难以维护,基于SOLID原则,我们应该使用Repository模式辅助model,将相关的数据库逻辑封装在不同的repository,方便中大型项目的维护。

数据库逻辑

在CRUD中,CUD比较稳定,但R的部分则千变万化,大部分的数据库逻辑都在描述R的部分,若将数据库逻辑写在controller或model都不适当,会造成controller与model肥大,造成日后难以维护。

Model

使用repository之后,model仅当成Eloquent class即可,不要包含数据库逻辑,仅保留以下部分:

__Property__:如$table$fillable...

__Mutator__:包括mutator与accessor.

__Method__:relation类的method,如使用hasMany()belongsTo().

__注释__:因为Eloquent会根据数据库字段动态产生propertymethod,等。若使用Laravel IDE Helper,会直接在model加上@property@method描述model的动态propertymethod

  • User.php
app/User.php
namespace MyBlog;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
/**
* MyBlog\User
*
* @property integer $id
* @property string $name
* @property string $email
* @property string $password
* @property string $remember_token
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereId($value)
* @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereName($value)
* @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereEmail($value)
* @method static \Illuminate\Database\Query\Builder|\MyBlog\User wherePassword($value)
* @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereRememberToken($value)
* @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereCreatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereUpdatedAt($value)
*/
class User extends Model implements AuthenticatableContract,
                                  AuthorizableContract,
                                  CanResetPasswordContract
{
  use Authenticatable, Authorizable, CanResetPassword;

  /**
   * The database table used by the model.
   *
   * @var string
   */
  protected $table = 'users';

  /**
   * The attributes that are mass assignable.
   *
   * @var array
   */
  protected $fillable = ['name', 'email', 'password'];

  /**
   * The attributes excluded from the model's JSON form.
   *
   * @var array
   */
  protected $hidden = ['password', 'remember_token'];
}

Repository

初学者常会在controller直接调用model写数据库逻辑:

public function index()
{
  $users = User::where('age', '>', 20)
              ->orderBy('age')
              ->get();

  return view('users.index', compact('users'));
}

数据库逻辑是要抓20岁以上的数据。在中大型项目,会有几个问题:

  1. 将数据库逻辑写在controller,造成controller的肥大难以维护。
  2. 违反SOLID的单一职责原则:数据库逻辑不应该写在controller。
  3. controller直接相依于model,使得我们无法对controller做单元测试。
  4. 比较好的方式是使用repository:
  5. 将model依赖注入到repository。
  6. 将数据库逻辑写在repository。
  7. 将repository依赖注入到service。
  • UserRepository.php
app/Repositories/UserRepository.php
namespace MyBlog\Repositories;
use Doctrine\Common\Collections\Collection;
use MyBlog\User;
class UserRepository
{
/** @var User 注入的User model */
protected $user;
/**
* UserRepository constructor.
* @param User $user
*/
//将相依的User model依赖注入到UserRepository。
public function __construct(User $user)
{
 $this->user = $user;
}
/**
* 回传大于?年纪的数据
* @param integer $age
* @return Collection
*/
public function getAgeLargerThan($age)
{
 return $this->user
     ->where('age', '>', $age)
     ->orderBy('age')
     ->get();
}
}
  • UserController.php
namespace App\Http\Controllers;
use App\Http\Requests;
use MyBlog\Repositories\UserRepository;
class UserController extends Controller
{
/** @var  UserRepository 注入的UserRepository */
protected $userRepository;

/**
* UserController constructor.
*
* @param UserRepository $userRepository
*/
// 将相依的UserRepository依赖注入到UserController。
public function __construct(UserRepository $userRepository)
{
 $this->userRepository = $userRepository;
}

/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//从原本直接相依的User model,改成依赖注入的UserRepository。
 $users = $this->userRepository
     ->getAgeLargerThan(20);

 return view('users.index', compact('users'));
}
}

用这种写法,有几个优点:

  1. 将数据库逻辑写在repository,解决controller肥大问题。
  2. 符合SOLID的单一职责原则:数据库逻辑写在repository,没写在controller。
  3. 符合SOLID的依赖反转原则:controller并非直接相依于repository,而是将repository依赖注入进controller。

实务上建议repository仅依赖注入于service,而不要直接注入在controller,本示例因为还没介绍到servie模式,为了简化起见,所以直接注入于controller。

是否该建立Repository Interface?

理论上使用依赖注入时,应该使用interface,不过interface目的在于抽象化方便抽换,让代码达到开放封闭的要求,但是实务上要抽换repository的机会不高,除非你有抽换数据库的需求,如从MySQL抽换到MongoDB,此时就该建立repository interface。
不过由于我们使用了依赖注入,将来要从class改成interface也很方便,只要在constructor的type hint改成interface即可,维护成本很低,所以在此大可使用repository class即可,不一定得用interface而造成over design,等真正需求来时再重构成interface即可。

Conclusion

事实上可以一开始1个repository对应1个model,但不用太执着于1个repository一定要对应1个model,可将repository视为逻辑上的数据库逻辑类别即可,可以横跨多个model处理,也可以1个model拆成多个repository,端看需求而定。
Repository使得数据库逻辑从controller或model中解放,不仅更容易维护、更容易扩展、更容易重复使用,且更容易测试。
Sample Code

Service

若将商业逻辑都写在controller,会造成controller肥大而难以维护,基于SOLID原则,我们应该使用Service模式辅助controller,将相关的商业逻辑封装在不同的service,方便中大型项目的维护。

商业逻辑

商业逻辑中,常见的如:

  • 牵涉到外部行为:如发送Email,使用外部API…。
  • 使用PHP写的逻辑:如根据购买的件数,有不同的折扣。若将商业逻辑写在controller,会造成controller肥大,日后难以维护。

Service

牵涉到外部行为
如发送Email,初学者常会在controller直接调用Mail::queue():

public function store(Request $request)
{
    Mail::queue('email.index', $request->all(), function (Message $message) {
        $message->sender(env('MAIL_USERNAME'));
        $message->subject(env('MAIL_SUBJECT'));
        $message->to(env('MAIL_TO_ADDR'));
    });
}

__在中大型项目,会有几个问题__:

  1. 将牵涉到外部行为的商业逻辑写在controller,造成controller的肥大难以维护。
  2. 违反SOLID的单一职责原则:外部行为不应该写在controller。
  3. controller直接相依于外部行为,使得我们无法对controller做单元测试。

__比较好的方式是使用service__:

  1. 将外部行为注入到service。
  2. 在service使用外部行为。
  3. 将service注入到controller。
  • EmailService.php
app/Services/EmailService.php
namespace App\Services;
use Illuminate\Mail\Mailer;
use Illuminate\Mail\Message;
class EmailService
{
    /** @var Mailer */
    private $mail;

    /**
     * EmailService constructor.
     * @param Mailer $mail
     */
    public function __construct(Mailer $mail)
    {
//将相依的Mailer注入到EmailService。
        $this->mail = $mail;
    }

    /**
     * 發送Email
     * @param array $request
     */
    public function send(array $request)
    {
//将发送Emai的商业逻辑写在send()。不是使用Mail facade,而是使用注入的$this->mail
        $this->mail->queue('email.index', $request, function (Message $message) {
            $message->sender(env('MAIL_USERNAME'));
            $message->subject(env('MAIL_SUBJECT'));
            $message->to(env('MAIL_TO_ADDR'));
        });
    }
}
  • UserController.php
app/Http/Controllers/UserController.php
namespace App\Http\Controllers;
use App\Http\Requests;
use Illuminate\Http\Request;
use MyBlog\Services\EmailService;
class UserController extends Controller
{
    /** @var EmailService */
    protected $emailService;

    /**
     * UserController constructor.
     * @param EmailService $emailService
     */
    public function __construct(EmailService $emailService)
    {
#将相依的EmailService注入到UserController。
        $this->emailService = $emailService;
    }

    /**
     * Store a newly created resource in storage.
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
#从原本直接相依于Mail facade,改成相依于注入的EmailService
        $this->emailService->send($request->all());
    }
}

用这种写法,有几个优点

  • 将外部行为写在service,解决controller肥大问题。
  • 符合SOLID的单一职责原则:外部行为写在service,没写在controller。
  • 符合SOLID的依赖反转原则:controller并非直接相依于service,而是将service依赖注入进controller。

使用PHP写的逻辑

如根据购买的件数,有不同的折扣,初学者常会在controller直接写if…else逻辑。

public function store(Request $request)
{
 $qty = $request->input('qty');
 $price = 500;
 if ($qty == 1) {
     $discount = 1.0;
 }
 elseif ($qty == 2) {
     $discount = 0.9;
 }
 elseif ($qty == 3) {
     $discount = 0.8;
 }
 else {
     $discount = 0.7;
 }
 $total = $price * $qty * $discount;
 echo($total);
}

在中大型项目,会有几个问题:

  1. 将PHP写的商业逻辑直接写在controller,造成controller的肥大难以维护。
  2. 违反SOLID的单一职责原则:商业逻辑不应该写在controller。
  3. 违反SOLID的单一职责原则:若未来想要改变折扣与加总的算法,都需要改到此method,也就是说,此method同时包含了计算折扣与计算加总的职责,因此违反SOLID的单一职责原则。
  4. 直接写在controller的逻辑无法被其他controller使用。

比较好的方式是使用service。

  1. 将相依物件注入到service。
  2. 在service写PHP逻辑使用相依物件。
  3. 将service注入到controller。
  • OrderService.php
app/Services/OrderService.php
namespace App\Services;
class OrderService
{
    /**为了符合SOLID的单一职责原则,将计算折扣独立成getDiscount(),将PHP写的判断逻辑写在里面。
     * 計算折扣
     * @param int $qty
     * @return float
     */
    public function getDiscount($qty)
    {
        if ($qty == 1) {
            return 1.0;
        } elseif ($qty == 2) {
            return 0.9;
        } elseif ($qty == 3) {
            return 0.8;
        } else {
            return 0.7;
        }
    }

    /**
     * 計算最後價錢
     * @param integer $qty
     * @param float $discount
     * @return float
     */
    public function getTotal($qty, $discount)
    {
#为了符合SOLID的单一职责原则,将计算加总独立成getTotal(),将PHP写的计算逻辑写在里面。
        return 500 * $qty * $discount;
    }
}
  • OrderController.php
app/Http/Controllers/OrderController.php
namespace App\Http\Controllers;
use App\Http\Requests;
use App\MyBlog\Services\OrderService;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    /** @var OrderService */
    protected $orderService;

    /**
     * OrderController constructor.
     * @param OrderService $orderService
     */
    public function __construct(OrderService $orderService)
    {
#将相依的OrderService注入到UserController。
        $this->orderService = $orderService;
    }

    /**
     * Store a newly created resource in storage.
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
#将原本的if…else逻辑改成呼叫OrderService,controller变得非常干净,也达成原本controller接收HTTP request,调用其他class的责任。
        $qty = $request->input('qty');

        $discount = $this->orderService->getDiscount($qty);
        $total = $this->orderService->getTotal($qty, $discount);

        echo($total);
    }
}

这种写法,有几个优点:

  1. 将PHP写的商业逻辑写在service,解决controller肥大问题。
  2. 符合SOLID的单一职责原则:商业逻辑写在service,没写在controller。
  3. 符合SOLID的单一职责原则:计算折扣与计算加总分开在不同method,且归属于OrderService,而非OrderController。
  4. 符合SOLID的依赖反转原则:controller并非直接相依于service,而是将service依赖注入进controller。
  5. 其他controller也可以重复使用此段商业逻辑。

若使用了service辅助controller,再搭配依赖注入与service container,则controller就非常干净,能专心处理接收HTTP request,调用其他class的职责了。

Conclusion

实务上会有很多service,须自行依照SOLID原则去判断是否该建立service。
Service使得商业逻辑从controller中解放,不仅更容易维护、更容易扩展、更容易重复使用,且更容易测试。

Presenter

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

显示逻辑

显示逻辑中,常见的如:

将数据显示不同数据:如性别字段为M,就显示Mr.,若性别字段为F,就显示Mrs.。

是否显示某些数据:如根据字段值是否为Y,要不要显示该字段。

依需求显示不同格式:如依照不同的语系,显示不同的日期格式。

Presenter

如性别字段为M,就显示Mr.,若性别字段为F,就显示Mrs.,初学者常会直接用blade写在view。

<h2>@if($user->gender == 'M') {{ 'Mr.'}} @else {{ 'Mrs.' }} @endif {{ $user->name }}</h2>

在中大型项目,会有几个问题:

  1. 由于blade与HTML夹杂,不太适合写太复杂的程序,只适合做一些简单的binding,否则很容易流于传统PHP的义大利面程序。
  2. 无法对显示逻辑做重构与面向对象。

比较好的方式是使用presenter:

  1. 将相依物件注入到presenter。
  2. 在presenter内写格式转换。
  3. 将presenter注入到view。
  • UserPresenter.php
app/Presenters/UserPresenter.php
namespace App\Presenters;
class UserPresenter
{
    /**
     * 性别字段为M,就显示Mr.,若性别字段为F,就显示Mrs.
     * @param string $gender
     * @param string $name
     * @return string
     */
    public function getFullName($gender, $name)
    {
        if ($gender == 'M')
            $fullName = 'Mr. ' . $name;
        else
            $fullName = 'Mrs. ' . $name;

        return $fullName;
    }
}

将原本在blade用@if…@else…@endif写的逻辑,改写在presenter。

<html>
  <body>
    <div>
      @inject('UserPresenter', 'Project\Your\UserPresenter')
      @foreach($users as $user)
        <div>
          <h2>{{ $UserPresenter->getFullName($user->gender, $user->name) }}</h2>
        </div>
      @endforeach
    </div>
  </body>
</html>

使用@inject()注入UserPresenter,让view也可以如controller一样使用注入的物件。

将来无论显示逻辑怎么修改,都不用改到blade,直接在presenter内修改。

改用这种写法,有几个优点:

  1. 将数据显示不同格式的显示逻辑改写在presenter,解决写在blade不容易维护的问题。
  2. 可对显示逻辑做重构与面向对象。

是否显示某些数据

如根据字段值是否为Y,要不要显示该字段,初学者常会直接用blade写在view。

__在中大型项目,会有几个问题__:

  • 由于blade与HTML夹杂,不太适合写太复杂的程序,只适合做一些简单的binding,否则很容易流于传统PHP的义大利面程序。
  • 无法对显示逻辑做重构与面向对象。
  • 违反SOLID的开放封闭原则:若将来要支持新的语系,只能不断地在blade新增if…else。(开放封闭原则:软件中的类别、函式对于扩展是开放的,对于修改是封闭的。)

__比较好的方式是使用presenter__:

  1. 将相依物件注入到presenter。
  2. 在presenter内写不同的日期格式转换逻辑。
  3. 将presenter注入到view。
  • DateFormatPresenterInterface.php
app/Presenters/DateFormatPresenterInterface.php
namespace App\Presenters;
use Carbon\Carbon;
interface DateFormatPresenterInterface
{
    /**
     * 显示日期格式
     * @param Carbon $date
     * @return string
     */
    public function showDateFormat(Carbon $date) : string;
}

定义了showDateFormat(),各语言必须在showDateFormat()使用Carbonformat()去转换日期格式。

  • DateFormatPresenter_uk.php
app/Presenters/DateFormatPresenter_uk.php
namespace App\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,并将转换成英国日期格式的Carbonformat()写在showDateFormat()内。

  • DateFormatPresenter_tw.php
app/Presenters/DateFormatPresenter_tw.php
namespace App\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,并将转换成湾湾日期格式的Carbonformat()写在showDateFormat()内。

Presenter工厂

由于每个语言的日期格式都是一个presenter物件,那势必遇到一个最基本的问题:我们必须根据不同的语言去new不同的presenter物件,直觉我们可能会在controllernew presenter

public function index(Request $request)
{
    $users = $this->userRepository->getAgeLargerThan(10);

    $locale = $request['lang'];

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

    return view('users.index', compact('users'));
}

这种写法虽然可行,但有几个问题:

  1. 违反SOLID的开放封闭原则:若将来有新的语言需求,只能不断去修改index(),然后不断的新增elseif,就算改用switch也是一样。
  2. 违反SOLID的依赖反转原则:controller直接根据语言去new相对应的class,高层直接相依于低层,直接将实作写死在程序中。(依赖反转原则:高层不应该依赖于低层,两者都应该要依赖抽象;抽象不要依赖细节,细节要依赖抽象)
  3. 无法单元测试:由于presenter直接new在controller,因此要测试时,无法对presenter做mock。

比较好的方式是使用Factory Pattern

  • DataFormatPresenterFactory.php
app/Presenters/DateFormatPresenterFactory.php
namespace App\Presenters;
use Illuminate\Support\Facades\App;
class DateFormatPresenterFactory
{
 /**
  * @param string $locale
  */
 public static function bind(string $locale)
 {
     App::bind(DateFormatPresenterInterface::class,
         'MyBlog\Presenters\DateFormatPresenter_' . $locale);
 }
}

使用Presenter Factory的create()去取代new建立物件。
这里当然可以在create()去写if…elseif去建立presenter物件,不过这样会违反SOLID的开放封闭原则,比较好的方式是改用App::bind(),直接根据$localebinding相对应的class,这样无论在怎么新增语言与日期格式,controller与Presenter Factory都不用做任何修改,完全符合开放封闭原则。

Controller

  • UserController.php
app/Http/Controllers/UserController.php
namespace App\Http\Controllers;

use App\Http\Requests;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use MyBlog\Presenters\DateFormatPresenterFactory;
use MyBlog\Repositories\UserRepository;

class UserController extends Controller
{
    /** @var  UserRepository 注入的UserRepository */
    protected $userRepository;

    /**
     * UserController constructor.
     * @param UserRepository $userRepository
     */
    public function __construct(UserRepository $userRepository)
    {
      #将相依的UserRepository注入到UserController。
        $this->userRepository = $userRepository;
    }

    /**
     * Display a listing of the resource.
     * @param Request $request
     * @param DateFormatPresenterFactory $dateFormatPresenterFactory
     * @return \Illuminate\Http\Response
     */
    public function index(Request $request)
    {
      #使用$dateFormatPresenterFactory::bind()切换App::bind()的presenter物件,如此controller将开放封闭,将来有新的语言需求,也不用修改controller。
        $users = $this->userRepository->getAgeLargerThan(10);
        $locale = ($request['lang']) ? $request['lang'] : 'us';
        $dateFormatPresenterFactory::bind($locale);

        return view('users.index', compact('users'));
    }
}

使用$dateFormatPresenterFactory::bind()切换App::bind()的presenter物件,如此controller将开放封闭,将来有新的语言需求,也不用修改controller。

我们可以发现改用factory pattern之后,controller有了以下的优点:

  • 符合SOLID的开放封闭原则:若将来有新的语言需求,controller完全不用做任何修改。
  • 符合SOLID的依赖反转原则:controller不再直接相依于presenter,而是改由factory去建立presenter。
  • 可以做单元测试:可直接对各presenter做单元测试,不需要跑验收测试就可以测试显示逻辑。

Blade

<html>
  <body>
    <div>
      @inject('DateFormatPresenter', 'Project\Your\DateFormatPresenter')
      @foreach($users as $user)
        <div>
          <h2>{{ $dataFormatPresenter->showDateFormat($user->created_at) }}</h2>
        </div>
      @endforeach
    </div>
  </body>
</html>

使用@inject注入presenter,让view也可以如controller一样使用注入的物件。
使用presenter的showDateFormat()将日期转成想要的格式。

改用这种写法,有几个优点:

  • 将依需求显示不同格式的显示逻辑改写在presenter,解决写在blade不容易维护的问题。
  • 可对显示逻辑做重构与面向对象。
  • 符合SOLID的开放封闭原则:将来若有新的语言,对于扩展是开放的,只要新增class实践DateFormatPresenterInterface即可;对于修改是封闭的,controller、factory interface、factory与view都不用做任何修改。
  • 不单只有PHP可以使用service container,连blade也可以使用service container,甚至搭配service provider。
  • 可单独对presenter的显示逻辑做单元测试。

View

若使用了presenter辅助blade,再搭配@inject()注入到view,view就会非常干净,可专心处理将数据binding到HTML的职责。

将来只有layout改变才会动到blade,若是显示逻辑改变都是修改presenter。

Conclusion

Presenter使得显示逻辑从blade中解放,不仅更容易维护、更容易扩展、更容易重复使用,且更容易测试。

单元测试

由于现在model、view、controller的相依物件都已经拆开,也都使用依赖注入,因此每个部分都可以单独的做单元测试,如要测试service,就将repository加以mock,也可以将其他service加以mock。
Presenter也可以单独跑单元测试,将其他service加以mock,不一定要跑验收测试才能测显示逻辑。

Conclusion

本文谈到的构架只是开始,你可以依照实际需求增加更多的目录与class,当你发现你的MVC违反SOLID原则时,就大胆的将class从MVC拆开重构,然后依照以下手法:

  1. 建立新的class或interface。
  2. 将相依物件依赖注入到class。
  3. 在class内处理他的职责。
  4. 将class或interface注入到controller或view。

最后搭配单元测试,测试重构后的构架是否与原来的需求结果相同。

转载自原作者博文

标签: Laravel

添加新评论