从“胖模型,瘦控制器"工作的做法是什么?对 Laravel Eloquent ORM 的看法?
阅读后,我一直在为胖模型,瘦控制器"的概念挑选其他开发人员的大脑:
I've been picking other developers' brains on the concept of "fat models, skinny controllers" after reading:
- http://culttt.com/2013/07/01/setting-up-your-first-laravel-4-controller/
- http://culttt.com/2013/05/13/setting-up-your-first-laravel-4-model/
大多数受访者正在使用我认为的胖控制器.
Most respondents are using what I'd consider fat controllers.
虽然这个话题出现在 Stack Overflow 上,但我还没有在实践中找到对该方法的详尽描述.
While the topic has come up on Stack Overflow I haven't found thorough description of the method in practice.
我刚刚在这里找到了一个旧的相关问题.
I just found an old related question here.
推荐答案
Skinny Controllers
您将在 PHP(vanilla 或 Laravel 或 Symfony)中看到的越来越多的是有史以来最瘦的控制器.这是您在 Rails 中已经看到的东西,人们也开始将其称为(通过其他一些实践)六边形.你的控制器中只需要一行代码,实际上他们说这应该是你所有方法的目标.这是一个例子,是的,比这多一点,但仍然很瘦:
Skinny Controllers
What you are going to see in PHP (vanilla or Laravel or Symfony) more and more are the skinniest controllers ever. This is something you already see in Rails, and people are also starting to call it (with some other practices) hexagonal. One line of code is all you need in your controller, actually they say this should be a goal for all your methods. This is an example with, yes, a little bit more than that, but still skinny:
<?php
class PostController extends Controller {
private $repository;
public function __construct(PostRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function store()
{
try
{
$this->repository->create(Input::all());
}
catch (ValidationException $e)
{
return Redirect::back()->withInput()->withErrors($e->all());
}
return Redirect::route('posts');
}
}
控制器是 HTTP 请求、业务逻辑和表示层之间的桥梁.所以它应该接收一个请求,将它发送到一个注入的对象,该对象将处理它并重定向到负责向客户端(或用户)提供反馈的路由(或呈现视图).其他一切,包括验证,都应该在您的存储库、服务、模型(MVC,是的!)等中进行.
A controller is a bridge between the HTTP requests, your business logic and your presentation layer. So it should receive one request, send it to an injected object which will process it and redirect to the route (or render a view) responsible for giving feedback to a client (or user). Everything else, including validation, should happen in your repositories, services, models (MVC, yay!), etc.
但是我们可以以六边形的方式重构这个控制器,以达到每个方法一行的目标:
But we could refactor this controller, in the hexagonal way, to reach the one-line-per-method goal:
<?php
class PostController extends Controller {
private $repository;
public function __construct(PostRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function store()
{
return $this->repository->create(Input::all(), $this);
}
public function createSucceeded()
{
return Redirect::route('posts');
}
public function createFailed()
{
return Redirect::back()->withInput()->withErrors($e->all());
}
}
基本上,您的存储库类将使用自己的调用者 ($this
) 来触发 succeeded
和 failed
方法.
Basically your repository classes will use the own caller ($this
) to fire the succeeded
and failed
methods.
模型与您的数据过于相关,有时它们是您的 ORM 并直接与您的数据库服务器通信,因此,现在您会看到人们使用存储库和服务作为它们之间的层.
Models are too related to your data, sometimes they are your ORM and talk directly to your database server, so, these days you'll see people use repositories and services as layers between them.
存储库是一个类,它通过直接与您的模型对话来处理和收集您的应用程序所需的信息.您的应用程序不应该知道在您的数据库中选择某些信息需要什么,选择,位置,排序,分组依据,这些有时只有您的模型应该知道,所以这是一个存储库:
A repository is a class that, by talking directly to your models, processes and gather the information your application needs. Your application should not be aware of what is necessary to select some information in your database, select, where, order, group by, those are things sometimes only your models should be aware of, so this is a repository:
class PostRepository implements PostRepositoryInterface {
private $model;
public function __construct(PostInterface $model)
{
$this->model = $model;
}
public function create($input)
{
return $this->model->create($input);
}
public findBySlug($slug)
{
return $this->model->where('slug', $slug)->first();
}
}
服务
所有不直接属于您的业务逻辑的东西,主要是外部服务,离您的应用程序代码最远的地方,您构建的越解耦越好.为这些服务创建外部包(Composer 包)是将它们与其他一切分离的好方法,如果你让它们与框架无关,你就有权获得 10 点鲟鱼点.在 Laravel 中,您可以通过集成三种类来创建服务:
Services
Everything that doesn't belongs directly to your business logic, mostly external services, the farthest from your application code, the more decoupled you build them, the better. Creating external packages (Composer packages) for those services are a good way of decoupling them from everything else, and you if you make them framework agnostic you're entitled to receive 10 Sturgeon points. In Laravel you can create services by integrating three kind of classes:
1) Service Class(es):负责做你的服务必须做的事情,你所有的服务逻辑都在这里.
1) Service Class(es): responsible for doing what your service must do, all your service logic goes here.
2) 服务提供者:负责启动你的服务并将其添加到 Laravel 的 IoC 容器中,以便随时可以使用,但请注意,Laravel 只会在你的应用程序真正使用它们时实例化你的服务类.
2) Service Provider: responsible for booting up your service and adding it to Laravel's IoC container so it can be ready to use at any time, but note that Laravel will only instantiate your service classes when your application really use them.
3) Facade:允许您使用静态 (::) 语法从应用程序的任何位置访问您的服务:
3) Facade: lets you access your service from anywhere in your application using the static (::) syntax:
Mailer::send($user->id, 'Thanks for registering', 'emails.registered');
这是邮件程序服务:
<?php namespace ACRServicesMailer;
use IlluminateMailMailer as IlluminateMailer;
use Sentry;
class Service {
public function __construct(IlluminateMailer $mailer)
{
$this->mailer = $mailer;
}
public function send($userId, $subject, $view, $data = [])
{
return $this->mailer->queue($view, $data, function($message) use ($userId, $subject)
{
$user = Sentry::findUserById($userId);
$message->to($user->email, $user->name);
$message->subject($subject);
});
}
}
服务提供商
<?php namespace ACRServicesMailer;
use IlluminateSupportServiceProvider as IlluminateServiceProvider;
use ACRServicesMailerService as Mailer;
class ServiceProvider extends IlluminateServiceProvider {
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bind('acr.mailer', function($app) {
return new Mailer($app->make('mailer'));
});
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return array('acr.mailer');
}
}
立面
<?php namespace ACRServicesMailer;
use IlluminateSupportFacadesFacade as IlluminateFacade;
class Facade extends IlluminateFacade {
protected static function getFacadeAccessor() { return 'acr.mailer'; }
}
模型/ORM
那些人应该是高度可交换的,今天您可能使用 Eloquent 作为 ORM,将数据存储在数据库中,但您可能需要将其更改为其他内容,有些人将 100% 的数据存储在 Redis 中,因此,您最好通过在 ORM 和域 loginc 之间使用接口(合同)层来为这样的更改做好准备,并真正为您的接口而不是您的具体类进行开发.Taylor Otwell 在他的书中甚至说你应该完全删除你的模型文件夹.
Models / ORM
Those guys should be highly swappable, today you may be using a Eloquent as your ORM, storing data in a database, but you might need to change it to something else, some foks are storing 100% of their data in Redis, so you better be prepared for a change like this by using an Interface (contract) layer between your ORM and your domain loginc and really develop for your interfaces, not your concrete classes. Taylor Otwell in his book even say that you should completely delete your models folder.
interface PostInterface {
public function all();
public function find($id);
}
class DbPost extends Eloquent implements PostInterface {
}
class RedisPost extends Eloquent implements PostInterface {
}
这背后的想法是很容易交换实现,所以在 Laravel 中你可以使用 IoC 容器来告诉 Laravel 你正在使用哪个实现:
The idea behind this is to swap implementations easily, so in Laravel you can use the IoC container to tell Laravel which implementation you are using:
App::bind('PostInterface', 'DbPost');
因此,如果您有一个存储库正在使用您的 PostInterface:
So, if you have a Repository is using your PostInterface:
class PostRepository implements PostRepositoryInterface {
private $model;
public function __construct(PostInterface $model)
{
$this->model = $model;
}
}
Laravel IoC 容器将使用 DbPost 实例自动实例化此存储库.如果您需要将其更改为 Redis,则只需更改一行:
Laravel IoC container will automatically instantiate this repository with an instance of DbPost. And if you ever need to change it to Redis, you just need to change one line:
App::bind('PostInterface', 'RedisPost');
观看次数/演示者
最笨的就是最棒的.
Views / Presenters
The dumbest the awesomer.
视图应该只负责显示信息.视图不应该知道您的模型、服务、存储库或系统中的任何其他内容.视图应该可以由 websigners 编辑,因此,您拥有的代码越多,您的非 php-programmer-designer 向它们添加的错误就越多.您的控制器应该从您的存储库中收集信息并将它们传递给您的视图:
Views should be responsible only for displaying information. Views should not be aware of your models, services, repositories, or anything else in your system. Views should be editable by webesigners so, the more code you have on them, the more bugs your non-php-programmer-designer will add to them. Your controller should gather the information from your repositories and pass them to your views:
<?php
class PostController extends Controller {
private $repository;
public function __construct(PostRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function index()
{
return View::make('posts.index')->with('posts', $this->repository->getPaginated());
}
}
您的视图的唯一责任应该是显示数据:
And the only responsibility of your view should be show that data:
@extends('layout')
@section('contents')
<ul>
@foreach($posts as $post)
<li>
{{ $post->title }} - {{ $post->author }} - {{ $post->published_at }}
</li>
@endforeach
</ul>
{{ $users->links() }}
@stop
演示者
您如何格式化数据?您在视图中编写原始属性,但您应该在幕后使用演示者来展示您的数据.展示者通常使用装饰器设计模式来格式化要在页面中展示的数据.这是一个使用 Shawn McCool 的 LaravelAutoPresenter 的例子:
Presenters
How do you format your data? You write raw properties in your views, but you should, behind the scenes, be using presenters to, yeah, present your data. Presenters usually use the Decorator Design Pattern to format your data to be presented in your pages. This is an example using Shawn McCool's LaravelAutoPresenter:
<?php namespace AppPresenters;
use McCoolLaravelAutoPresenterBasePresenter;
class Post extends BasePresenter {
public function __construct(UserModel $user)
{
$this->resource = $user;
}
public function author()
{
return $this->resource->author->name;
}
public function published_at()
{
return $this->date($this->resource->created_at);
}
public function dateTime($date)
{
return CarbonCarbon::createFromFormat('d-m-Y', $date, 'Sao_Paulo/Brazil')
->toFormattedDateString();
}
}
相关书籍
Taylor Otwell 的 Laravel:从学徒到工匠
Chris Fidao 的 Laravel 实现
相关文章