Laravel 的十八个最佳实践

file

本文翻译改编自 Laravel 的十八个最佳实践

这篇文章并不是什么由 Laravel 改编的 SOLID 原则、模式等。

只是为了让你注意你在现实生活的 Laravel 项目中最常忽略的内容。

单一责任原则

一个类和一个方法应该只有一个职责。
错误的做法:

public function getFullNameAttribute()
{
    if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
        return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' $this->last_name;
    } else {
        return $this->first_name[0] . '. ' . $this->last_name;
    }
}

推荐的做法:

public function getFullNameAttribute()
{
    return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}

public function isVerifiedClient()
{
    return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
}

public function getFullNameLong()
{
    return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}

public function getFullNameShort()
{
    return $this->first_name[0] . '. ' . $this->last_name;
}

强大的模型 & 简单控制器

如果你使用查询构造器或原始 SQL 来查询,请将所有与数据库相关的逻辑放入 Eloquent 模型或存储库类中。

坏:

public function index()
{
    $clients = Client::verified()
        ->with(['orders' => function ($q) {
            $q->where('created_at', '>', Carbon::today()->subWeek());
        }])
        ->get();

    return view('index', ['clients' => $clients]);
}

好:

public function index()
{
    return view('index', ['clients' => $this->client->getWithNewOrders()]);
}

Class Client extends Model
{
    public function getWithNewOrders()
    {
        return $this->verified()
            ->with(['orders' => function ($q) {
                $q->where('created_at', '>', Carbon::today()->subWeek());
            }])
            ->get();
    }
}

验证

将验证从控制器移动到请求类。

很常见但不推荐的做法:

public function store(Request $request)
{
    $request->validate([
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
        'publish_at' => 'nullable|date',
    ]);

    ....
}

最好是这样:

public function store(PostRequest $request)
{    
    ....
}

class PostRequest extends Request
{
    public function rules()
    {
        return [
            'title' => 'required|unique:posts|max:255',
            'body' => 'required',
            'publish_at' => 'nullable|date',
        ];
    }
}

业务逻辑应该在服务类中

一个控制器必须只有一个职责,因此应该将业务逻辑从控制器移到服务类。

坏:

public function store(Request $request)
{
    if ($request->hasFile('image')) {
        $request->file('image')->move(public_path('images') . 'temp');
    }

    ....
}

好:

public function store(Request $request)
{
    $this->articleService->handleUploadedImage($request->file('image'));

    ....
}

class ArticleService
{
    public function handleUploadedImage($image)
    {
        if (!is_null($image)) {
            $image->move(public_path('images') . 'temp');
        }
    }
}

不要重复你自己(DRY)

尽可能重用代码。 SRP(单一职责原则)正在帮助你避免重复。当然,这也包括了 Blade 模板、Eloquent 的范围等。

坏:

public function getActive()
{
    return $this->where('verified', 1)->whereNotNull('deleted_at')->get();
}

public function getArticles()
{
    return $this->whereHas('user', function ($q) {
            $q->where('verified', 1)->whereNotNull('deleted_at');
        })->get();
}

好:

public function scopeActive($q)
{
    return $q->where('verified', 1)->whereNotNull('deleted_at');
}

public function getActive()
{
    return $this->active()->get();
}

public function getArticles()
{
    return $this->whereHas('user', function ($q) {
            $q->active();
        })->get();
}

最好倾向于使用 Eloquent 而不是 Query Builder 和原生的 SQL 查询。要优先于数组的集合

Eloquent 可以编写可读和可维护的代码。此外,Eloquent 也拥有很棒的内置工具,比如软删除、事件、范围等。

比如你这样写:

SELECT *
FROM `articles`
WHERE EXISTS (SELECT *
              FROM `users`
              WHERE `articles`.`user_id` = `users`.`id`
              AND EXISTS (SELECT *
                          FROM `profiles`
                          WHERE `profiles`.`user_id` = `users`.`id`) 
              AND `users`.`deleted_at` IS NULL)
AND `verified` = '1'
AND `active` = '1'
ORDER BY `created_at` DESC

还不如这样写:

Article::has('user.profile')->verified()->latest()->get();

批量赋值

比如你这样写:

$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
// Add category to article
$article->category_id = $category->id;
$article->save();

是不是还不如这样写:

$category->article()->create($request->all());

不要在 Blade 模板中执行查询并使用关联加载(N + 1 问题)

不好的地方在于,这对于100 个用户来说,等于执行 101 个 DB 查询:

@foreach (User::all() as $user)
    {{ $user->profile->name }}
@endforeach

下面的做法,对于 100 个用户来说,仅仅只执行 2 个 DB 查询:

$users = User::with('profile')->get();

...

@foreach ($users as $user)
    {{ $user->profile->name }}
@endforeach

与其花尽心思给你的代码写注释,还不如对方法或变量写一个描述性的名称

坏:

if (count((array) $builder->getQuery()->joins) > 0)

好:

// 确定是否有任何连接。
if (count((array) $builder->getQuery()->joins) > 0)

最好:

if ($this->hasJoins())

不要把 JS 和 CSS 放在 Blade 模板中,也不要将任何 HTML 放在 PHP 类中

坏:

let article = `{{ json_encode($article) }}`;

好:

<input id="article" type="hidden" value="{{ json_encode($article) }}">

Or

<button class="js-fav-article" data-article="{{ json_encode($article) }}">{{ $article->name }}<button>

最好的方法是使用在 Javascript 中这样来传输数据:

let article = $('#article').val();

在代码中使用配置和语言文件、常量,而不是写死它

坏:

public function isNormal()
{
    return $article->type === 'normal';
}

return back()->with('message', 'Your article has been added!');

好:

public function isNormal()
{
    return $article->type === Article::TYPE_NORMAL;
}

return back()->with('message', __('app.article_added'));

使用社区接受的标准的 Laravel 工具

最好使用内置的 Laravel 功能和社区软件包,而不是其他第三方软件包和工具。因为将来与你的应用程序一起工作的开发人员都需要学习新的工具。另外,使用第三方软件包或工具的话,如果遇到困难,从 Laravel 社区获得帮助的机会会大大降低。不要让你的客户为此付出代价!

任务 标准工具 第三方工具
授权 Policies Entrust, Sentinel and other packages
前端编译 Laravel Mix Grunt, Gulp, 3rd party packages
开发环境 Homestead Docker
部署 Laravel Forge Deployer and other solutions
单元测试 PHPUnit, Mockery Phpspec
浏览器测试 Laravel Dusk Codeception
数据库操作 Eloquent SQL, Doctrine
模板 Blade Twig
数据操作 Laravel collections Arrays
表单验证 Request classes 3rd party packages, validation in controller
认证 Built-in 3rd party packages, your own solution
API 认证 Laravel Passport 3rd party JWT and OAuth packages
创建 API Built-in Dingo API and similar packages
数据库结构操作 Migrations Working with DB structure directly
局部化 Built-in 3rd party packages
实时用户接口 Laravel Echo, Pusher 3rd party packages and working with WebSockets directly
Generating testing data Seeder classes, Model Factories, Faker Creating testing data manually
生成测试数据 Laravel Task Scheduler Scripts and 3rd party packages
数据库 MySQL, PostgreSQL, SQLite, SQL Server MongoDB

遵循Laravel命名约定

遵循 PSR 标准。 另外,请遵循 Laravel 社区接受的命名约定:

类型 规则 正确示例 错误示例
Controller 单数 ArticleController ArticlesController
Route 复数 articles/1 article/1
Named route 带点符号的蛇形命名 users.show_active users.show-active, show-active-users
Model 单数 User Users
hasOne or belongsTo relationship 单数 articleComment articleComments, article_comment
All other relationships 复数 articleComments articleComment, article_comments
Table 复数 article_comments article_comment, articleComments
Pivot table 按字母顺序排列的单数模型名称 article_user user_article, articles_users
Table column 带着模型名称的蛇形命名 meta_title MetaTitle; article_meta_title
Foreign key 带_id后缀的单数型号名称 article_id ArticleId, id_article, articles_id
Primary key - id custom_id
Migration - 2017_01_01_000_create_xx_table 2017_01_01_0000_articles
Method 小驼峰命名 getAll get_all
Method in resource controller 具体看表格 store saveArticle
Method in test class 小驼峰命名 testGuestCannotSeeArticle test_guest_cannot_see_article
Variable 小驼峰命名 $articlesWithAuthor $articles_with_author
Collection 具描述性的复数形式 $activeUsers = User::active()->get() $active, $data
Object 具描述性的单数形式 $activeUser = User::active()->first() $users, $obj
Config and language files index 蛇形命名 articles_enabled ArticlesEnabled; articles-enabled
View 蛇形命名 show_filtered.blade.php showFiltered.blade.php, show-filtered.blade.php
Config 蛇形命名 google_calendar.php googleCalendar.php, google-calendar.php
Contract (interface) 形容词或名词 Authenticatable AuthenticationInterface, IAuthentication
Trait 形容词 Notifiable NotificationTrait

尽可能使用更短、更易读的语法

坏:

$request->session()->get('cart');
$request->input('name');

好:

session('cart');
$request->name;

更多示例:

通用语法 更短、更可读的语法
Session::get('cart') session('cart')
$request->session()->get('cart') session('cart')
Session::put('cart', $data) session(['cart' => $data])
$request->input('name'), Request::get('name') $request->name, request('name')
return Redirect::back() return back()
is_null($object->relation) ? $object->relation->id : null } optional($object->relation)->id
return view('index')->with('title', $title)->with('client', $client) return view('index', compact('title', 'client'))
$request->has('value') ? $request->value : 'default'; $request->get('value', 'default')
Carbon::now(), Carbon::today() now(), today()
App::make('Class') app('Class')
->where('column', '=', 1) ->where('column', 1)
->orderBy('created_at', 'desc') ->latest()
->orderBy('age', 'desc') ->latest('age')
->orderBy('created_at', 'asc') ->oldest()
->select('id', 'name')->get() ->get(['id', 'name'])
->first()->name ->value('name')

使用 IoC 容器或 facades 代替新的 Class

新的 Class 语法创建类时,不仅使得类与类之间紧密耦合,还加重了测试的复杂度。推荐改用 IoC 容器或 facades。

坏:

$user = new User;
$user->create($request->all());

好:

public function __construct(User $user)
{
    $this->user = $user;
}

....

$this->user->create($request->all());

不要直接从 .env 文件获取数据

将数据传递给配置文件,然后使用辅助函数 config() 在应用程序中使用数据。

坏:

$apiKey = env('API_KEY');

好:

// config/api.php
'key' => env('API_KEY'),

// Use the data
$apiKey = config('api.key');

以标准格式存储日期,必要时就使用访问器和修改器来修改日期格式

坏:

{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}

好:

// Model
protected $dates = ['ordered_at', 'created_at', 'updated_at']
public function getMonthDayAttribute($date)
{
    return $date->format('m-d');
}

// View
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->monthDay }}

其他良好做法

  • 千万不要在路由文件中放置任何逻辑。
  • 在 Blade 模板中最小化 vanilla PHP 的使用。
本作品采用《CC 协议》,转载必须注明作者和本文链接
Stay Hungry, Stay Foolish.
本帖由系统于 5年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 44
幽弥狂

我刚翻译完,算了算了

file

5年前 评论

话说,既然有这么多标准.为什么平时很少有遵守,包括你们出的教程也没有遵守额.我不是来找茬,只是想了解下.

5年前 评论
幽弥狂

我刚翻译完,算了算了

file

5年前 评论

话说,既然有这么多标准.为什么平时很少有遵守,包括你们出的教程也没有遵守额.我不是来找茬,只是想了解下.

5年前 评论

@Olivia-outshine 这个部分是在讲注释。不好的是没有注释,好的是有注释,下面更好的方案是名字起得好连注释都免了。这样能理解了么?

5年前 评论

@FreeMason 关于

$this->articleService->handleUploadedImage($request->file('image')); 

是在构造函数注入过,只是作者省略了这一步

public function __construct(ArticleService $articleService) {
    $this->articleService = $articleService;
}

IOC的思想了解一下

5年前 评论
licxisky (作者) 2年前
haodudecao 2年前
haodudecao 2年前
licxisky (作者) 2年前
尼古丁

刚刚在github看这个文章的原版

5年前 评论

@JokerLinly

MVC 思想每个人理解都会有点差异,个人简单理解:M 数据逻辑层,C 调度中心, V 显示层,看来之前 MVC 的理解还不够全不够深入,百度上的 MVC 大同小异,像你上面的模式。能推荐篇好的 MVC 文章吗?

5年前 评论
Mumujin

file

这到底是坏还是好!看了半年没能区分出来 :grin:

5年前 评论
Y_zhou 4年前
jianyong

@Olivia-outshine hasJoins

5年前 评论

真没注意使用IOC这条

5年前 评论

@Olivia-outshine 加了个注释,这样就变成好的了

5年前 评论

@jyliumin scope在手册中有讲,局部范围限制查询的时候用到,使用中不带scope

5年前 评论

@JokerLinly Service和Repository设计感觉好像?最近在了解repository仓库,意思貌似是讲控制器单一原则,将复杂的控制器逻辑交给repository在处理,这个service好像,他们可以理解为同一类意思吗?

5年前 评论

@Summer AT的识别机制缺陷啊,代码块里blade模板的@foreach 被强行替换为md格式的链接了。详情看文中的示例代码

5年前 评论

@nbutluo 单数居多吧,这种算约定,而且共识程度也不算高,没必要“必须”遵守。

3年前 评论
Summer

@mokeyjay fixed

5年前 评论
liyu001989

行动迅速啊,厉害了:+1:

5年前 评论

控制器命名社区文档中的《Laravel 开发规范》中不是建议使用复数命名的吗

Laravel

3年前 评论
萧风

file
这里单词错误

5年前 评论

verified() 这个啥意思

3年前 评论
JokerLinly (楼主) 3年前
幽弥狂

@select_and_action 那是必须的

4年前 评论

PSR-2 标准

学了 《L06 Laravel 教程 - 电商进阶 ( Laravel 5.8 ) 》
里面控制器命名为 ProductsControler,这里是复数

这里说控制器命名标准应该写为单数,那么应该是 ProductControler 不太懂区分了~

4年前 评论

这文章不错,之前写代码都没注意

4年前 评论

使用ioc容器在来构造方法里面注入,如果我注入的实例不是在每个方法都用到,注入进来不就浪费了吗?

4年前 评论

@FreeMason 不是,建议你去学习一下 MVC

5年前 评论

file

这里这里

5年前 评论

@bitqiu 这个应该是站长大人的坑 @Summer

5年前 评论
monkey

翻译的好快啊

5年前 评论
幽弥狂

@JokerLinly 他这里只显示标题 内容也翻译了

5年前 评论

好厉害,赞一个,哈哈

5年前 评论
Master-Xue

是否TP框架也是通用呢!! 感觉很规范的样子呢~

5年前 评论
ThinkQ

非常好!

5年前 评论

求指点,请问这里的服务是指服务提供者吗?

这句没看懂,在 Controller 里面直接调用 articleService 方法?

$this->articleService->handleUploadedImage($request->file('image')); 

file

5年前 评论

这篇文章真是每次看都会有收获,赞一个。

5年前 评论

控制器层这样写基本跟配置文件差不多了,控制器层应该负责调度。。。

5年前 评论

话说 既然都翻译完了 为什么不扔到github上呢 其实我之前一直想翻译这篇来着 手头的活太多了 然后刚想开始干活就被翻译了哈哈哈

5年前 评论
public function scopeActive($q)
{
    return $q->where('verified', 1)->whereNotNull('deleted_at');
}

public function getActive()
{
    return $this->active()->get();
}

public function getArticles()
{
    return $this->whereHas('user', function ($q) {
            $q->active();
        })->get();
}

这里不应该是$q->scopeActive()吗?怎么是 $q->active();

5年前 评论

@jyliumin 建议你回去翻文档,或者,直接看框架底层代码

5年前 评论

最近在重构以前写的代码。看这个深有体会。

5年前 评论

请问不要在路由文件中使用逻辑,是为什么?

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!