个性化阅读
专注于IT技术分析

维护具有分层结构的Slim PHP MVC框架

点击下载

本文概述

胖控制器和模型:对于大多数基于MVC框架(例如Yii和Laravel)的大型项目而言, 这都是不可避免的问题。增强控制器和模型的主要功能是Active Record, 它是此类框架的强大且必不可少的组成部分。

问题:活动记录及其对SRP的违反

活动记录是一种体系结构模式, 是一种访问数据库中数据的方法。它由Martin Fowler在其2003年的《企业应用程序体系结构的模式》中命名, 并在PHP框架中广泛使用。

尽管这是一个非常必要的方法, 但活动记录(AR)模式违反了单一职责原则(SRP), 因为AR模型:

  • 处理查询和数据保存。
  • 对系统中的其他模型了解太多(通过关系)。
  • 通常直接与应用程序的业务逻辑有关(因为数据存储的实现与所述业务逻辑紧密相关)。

当你需要尽快创建应用程序原型时, 这种违反SRP的要求是快速开发的一个不错的权衡, 但是当应用程序成长为中型或大型项目时, 这是非常有害的。 “上帝”模型和胖控制器难以测试和维护, 并且当你不可避免地不得不更改数据库结构时, 在控制器中随处使用模型会带来巨大的困难。

解决方案很简单:将Active Record的责任分为几层, 并注入跨层依赖关系。这种方法还将简化测试, 因为它允许你模拟当前未测试的那些层。

解决方案:PHP MVC框架的分层结构

一个”胖” PHP MVC应用程序到处都有依赖项, 这些依赖项互锁且容易出错, 而分层结构则使用依赖项注入来保持事物的整洁和清晰。

我们将涵盖五个主要层:

  • 控制器层
  • 服务层
  • DTO, 服务层的子集
  • 查看装饰器, 服务层的子集
  • 资料库层
分层的PHP结构

要实现分层结构, 我们需要一个依赖项注入容器, 这是一个知道如何实例化和配置对象的对象。你无需创建类, 因为该框架可以处理所有问题。考虑以下:

class SiteController extends \Illuminate\Routing\Controller
{
   protected $userService;

   public function __construct(UserService $userService)
   {
       $this->userService = $userService;
   }

   public function showUserProfile(Request $request)
   {
       $user = $this->userService->getUser($request->id);
       return view('user.profile', compact('user'));
   }
}

class UserService
{
   protected $userRepository;

   public function __construct(UserRepository $userRepository)
   {
       $this->userRepository = $userRepository;
   }

   public function getUser($id)
   {
       $user = $this->userRepository->getUserById($id);
       $this->userRepository->logSession($user);
       return $user;
   }
}

class UserRepository
{
   protected $userModel, $logModel;

   public function __construct(User $user, Log $log)
   {
       $this->userModel = $user;
       $this->logModel = $log;
   }

   public function getUserById($id)
   {
       return $this->userModel->findOrFail($id);
   }

   public function logSession($user)
   {
       $this->logModel->user = $user->id;
       $this->logModel->save();
   }
}

在上面的示例中, 将UserService注入SiteController, 将UserRepository注入UserService, 将AR模型User和Logs注入UserRepository类。该容器代码非常简单, 因此让我们来谈谈这些层。

控制器层

Laravel和Yii等现代MVC框架为你带来了许多传统的控制器挑战:输入验证和预过滤器移至应用程序的另一部分(在Laravel中, 它称为中间件, 而在Yii中, 称为行为)而路由和HTTP动词规则由框架处理。这为程序员留下了非常狭窄的功能, 可以将它们编码到控制器中。

控制器的本质是获取请求并交付结果。控制器不应包含任何应用程序业务逻辑;否则, 很难重用代码或更改应用程序的通信方式。例如, 如果你需要创建一个API而不是呈现视图, 并且你的控制器不包含任何逻辑, 则只需更改返回数据的方式即可。

这种薄的控制器层通常会使程序员感到困惑, 并且由于控制器是默认层和最顶层的切入点, 因此许多开发人员只是不断向其控制器中添加新代码, 而无需考虑架构。结果, 增加了过多的责任, 这些责任包括:

  • 业务逻辑(不可能重用业务逻辑代码)。
  • 模型状态的直接更改(在这种情况下, 数据库中的任何更改都将导致代码中各处发生巨大更改)。
  • 模型关系逻辑(例如复杂的查询, 多个模型的连接;同样, 如果数据库或关系逻辑中发生了某些更改, 我们将必须在所有控制器中进行更改)。

让我们考虑一个过度设计的控制器示例:

//A bad example of a controller
public function user(Request $request)
{
   $user = User::where('id', '=', $request->id)
   ->leftjoin('posts', function ($join) {
       $join->on('posts.user_id', '=', 'user.id')
           ->where('posts.status', '=', Post::STATUS_APPROVED);
   })
   ->first();
   if (!empty($user)) {
       $user->last_login = date('Y-m-d H:i:s');
   } else {
       $user = new User();
       $user->is_new = true;
       $user->save();
   }
   return view('user.index', compact('user'));
}

为什么这个例子不好?由于多种原因:

  • 它包含过多的业务逻辑。
  • 它直接与Active Record一起使用, 因此, 如果你更改数据库中的某些内容(如重命名last_login字段), 则必须在所有控制器中进行更改。
  • 它了解数据库关系, 因此, 如果数据库中发生某些更改, 我们必须在任何地方进行更改。
  • 它不可重用, 导致代码重复。

控制器应该很薄;实际上, 它所要做的就是接受请求并返回结果。这是一个很好的例子:

//A good example of a controller
public function user (Request $request)
{
 $user = $this->userService->getUserById($request->id);
 return view('user.index', compact('user'));
}

但是其他所有东西都去了哪里?它属于服务层。

服务层

服务层是业务逻辑层。在这里, 也只有在这里, 应该放置有关业务流程和业务模型之间交互的信息。这是一个抽象层, 对于每个应用程序来说都是不同的, 但是总的原则是独立于数据源(控制器的职责)和数据存储(较低层的职责)。

这是最有可能出现增长问题的阶段。通常, 将Active Record模型返回给控制器, 结果, 视图(或在API响应的情况下, 控制器)必须与模型一起使用, 并了解其属性和依赖性。这使事情变得混乱。如果决定更改Active Record模型的关系或属性, 则必须在所有视图和控制器中的所有位置进行更改。

这是一个常见的示例, 你可能会遇到视图中使用的Active Record模型:

<h1>{{$user->first_name}} {{$user->last_name}}</h1>
<ul>
@foreach($user->posts as $post)
<li>{{$post->title}}</li> 
@endforeach
</ul>

它看起来很简单, 但是如果我重命名first_name字段, 突然我必须更改所有使用此模型字段的视图, 这是一个容易出错的过程。避免此难题的最简单方法是使用数据传输对象或DTO。

数据传输对象

服务层中的数据需要包装到一个简单的不可变对象中, 这意味着它在创建后就无法更改, 因此DTO不需要任何设置方法。此外, DTO类应该是独立的, 不能扩展任何Active Record模型。但是要小心-业务模型并不总是与AR模型相同。

考虑杂货配送应用程序。从逻辑上讲, 杂货店的订单需要包含交货信息, 但是在数据库中, 我们存储订单并将其链接到用户, 并且该用户已链接到交货地址。在这种情况下, 有多个AR模型, 但是上层不应该知道它们。我们的DTO类不仅包含订单, 还包含交货信息以及与业务模型一致的任何其他部分。如果我们更改与此业务模型相关的AR模型(​​例如, 将交货信息移到订单表中), 我们将仅更改DTO对象中的字段映射, 而不更改代码中所有地方的AR模型字段的用法。

通过采用DTO方法, 我们消除了在控制器或视图中更改Active Record模型的诱惑。其次, DTO方法解决了物理数据存储与抽象业务模型的逻辑表示之间的连通性问题。如果需要在数据库级别上进行某些更改, 则更改将影响DTO对象而不是控制器和视图。看到图案了吗?

让我们看一个简单的DTO:

//Example of simple DTO class. You can add any logic of conversion from an Active Record object to business model here 
class DTO
{
   private $entity;

   public static function make($model)
   {
       return new self($model);
   }

   public function __construct($model)
   {
       $this->entity = (object) $model->toArray();
   }

   public function __get($name)
   {
       return $this->entity->{$name};
   }

}

使用我们的新DTO同样简单:

//usage example
public function user (Request $request)
{
 $user = $this->userService->getUserById($request->id);
 $user = DTO::make($user);
 return view('user.index', compact('user'));
}

查看装饰器

为了分离视图逻辑(例如根据某些状态选择按钮的颜色), 可以使用附加的装饰层。装饰器是一种设计模式, 允许通过使用自定义方法将其包装来修饰核心对象。它通常在视图中以某种特殊的逻辑发生。

尽管DTO对象可以执行装饰器的工作, 但它实际上仅适用于日期格式化等常见操作。 DTO应该表示一种商业模型, 而装饰器则用HTML修饰特定页面的数据。

让我们看一下未使用装饰器的用户个人资料状态图标的摘要:

<div class="status">
   @if($user->status == \App\Models\User::STATUS_ONLINE)
       <label class="text-primary">Online</label>
   @else
       <label class="text-danger">Offline</label>
   @endif   
</div>
<div class="info"> {{date('F j, Y', strtotime($user->lastOnline))}} </div>        

尽管这个例子很简单, 但开发人员很容易迷失在更复杂的逻辑中。这是装饰器的用处, 以清理HTML的可读性。让我们将状态图标片段扩展为完整的装饰器类:

class UserProfileDecorator
{
   private $entity;

   public static function decorate($model)
   {
       return new self($model);
   }

   public function __construct($model)
   {
       $this->entity = $model;
   }

   public function __get($name)
   {
       $methodName = 'get' . $name;
       if (method_exists(self::class, $methodName)) {
           return $this->$methodName();
       } else {
           return $this->entity->{$name};
       }
   }

   public function __call($name, $arguments)
   {
       return $this->entity->$name($arguments);
   }

   public function getStatus()
   {
       if($this->entity->status == \App\Models\User::STATUS_ONLINE) {
           return '<label class="text-primary">Online</label>';
       } else {
           return '<label class="text-danger">Offline</label>';
       }
   }

   public function getLastOnline()
   {
       return  date('F j, Y', strtotime($this->entity->lastOnline));
   }
}

使用装饰器很容易:

public function user (Request $request)
{
 $user = $this->userService->getUserById($request->id);
 $user = DTO::make($user);
 $user = UserProfileDecorator::decorate($user);
 return view('user.index', compact('user'));
}

现在, 我们可以在视图中使用模型属性而无需任何条件和逻辑, 并且可读性更高:

<div class="status"> {{$user->status}} </div>    
<div class="info"> {{$user->lastOnline}} </div>

装饰器也可以组合使用:

public function user (Request $request)
{
 $user = $this->userService->getUserById($request->id);
 $user = DTO::make($user);
 $user = UserDecorator::decorate($user);
 $user = UserProfileDecorator::decorate($user);
 return view('user.index', compact('user'));
}

每个装饰者将完成其工作, 并仅装饰自己的部分。多个装饰器的这种递归嵌入可动态组合其功能, 而无需引入其他类。

资料库层

存储库层与数据存储的具体实现一起工作。最好通过界面注入存储库, 以实现灵活性和易于替换。如果更改数据存储, 则必须创建一个新的存储库以实现你的存储库界面, 但至少不必更改其他层。

存储库扮演查询对象的角色:它从数据库中获取数据并进行几种Active Record模型的工作。在这种情况下, Active Record模型扮演着单个数据模型实体的角色, 即你要在其中建模和存储信息的任何对象。虽然每个实体都包含信息, 但它不知道它的外观(如果是从数据库中创建或获得的), 或者如何保存和更改其自身的状态。存储库的责任是保存和/或更新实体;通过保持存储库中实体的管理并使实体更简单, 这可以更好地分离关注点。

这是一个简单的示例存储库方法示例, 它使用有关数据库和Active Record关系的知识来构建查询:

public function getUsers()
{
return User::leftjoin('posts', function ($join) {
    $join->on('posts.user_id', '=', 'user.id')
        ->where('posts.status', '=', Post::STATUS_APPROVED);
   })
   ->leftjoin('orders', 'orders.user_id', '=', 'user.id')
   ->where('user.status', '=', User::STATUS_ACTIVE)
   ->where('orders.price', '>', 100)
   ->orderBy('orders.date')
   ->with('info')
   ->get();
}

保持单一职责层的Slim

在新创建的应用程序中, 你将仅找到用于控制器, 模型和视图的文件夹。 Yii和Laravel都没有在示例应用程序的结构中添加其他层。 MVC结构既简单又直观, 即使对于新手而言, 也简化了使用该框架的工作。它不是标准或样式, 也没有施加有关应用程序体系结构的任何规则。通过将任务划分为单独的单一职责层, 我们获得了易于维护的灵活可扩展的体系结构。记得:

  • 实体是单一数据模型。
  • 存储库获取并准备数据。
  • 服务层仅具有业务逻辑。
  • 控制器与所有外部资源进行通信, 例如用户输入或第三方服务。

因此, 如果你开始一个复杂的项目或将来有机会发展的项目, 请考虑将职责明确划分为控制器, 服务和存储库层。

赞(0)
未经允许不得转载:srcmini » 维护具有分层结构的Slim PHP MVC框架

评论 抢沙发

评论前必须登录!