Laravel 最佳实践

老牛浏览 486评论 0发表于

本文章并不是 Laravel 对应的 SOLID 原则和相关模式的说明。而是我们在实际的 Laravel 项目中经常忽略的最佳实践。

1. 单一职责原则

一个类和一个方法应该只有一个职责。

错误:

php
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;
    }
}

正确:

php
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;
}

2. 丰富模型,精简控制器

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

错误:

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

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

正确:

php
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();
    }
}

3. 验证

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

错误:

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

    ....
}

正确:

php
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',
        ];
    }
}

4. 业务逻辑放在业务类中

由于一个控制器必须只有一个职责,因此将业务逻辑从控制器移动到业务类中。

错误:

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

    ....
}

正确:

php
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');
        }
    }
}

5. 不要重复自己(DRY)

在可以复用代码的地方复用代码。SRP(单一职责原则)帮助你避免重复代码。当然,也可以复用 Blade 模板,使用 Eloquent 作用域等。

错误:

php
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();
}

正确:

php
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();
}

6. 优先使用 Eloquent 和集合

优先使用 Eloquent 而不是查询构造器或原生 SQL。优先使用集合而不是数组。

Eloquent 可以让您编写易于阅读和易于维护的代码。同时,Eloquent 也内置了很好的工具,例如软删除、事件、作用域等。

错误:

sql
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

正确:

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

7. 批量赋值

错误:

php
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
// 为文章添加分类
$article->category_id = $category->id;
$article->save();

正确:

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

8. 不要在 Blade 模板中执行查询,使用预加载(避免 N+1)

错误(如果有 100 个用户,会执行 101 条数据库查询):

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

正确(如果有 100 个用户,会执行 2 条数据库查询):

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

...

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

9. 为代码写注释,但优先使用描述性方法名或变量名

错误:

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

更好:

php
// 判断是否有任何连接
if (count((array) $builder->getQuery()->joins) > 0)

正确:

php
if ($this->hasJoins())

10. 不要在 Blade 模板中放 JS 和 CSS,不要在 PHP 类中放 HTML

错误:

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

更好:

php
<input id="article" type="hidden" value="@json($article)">

或

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

在 Javascript 文件中:

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

正确的方式是使用专门的 PHP 转 JS 扩展包来传递数据。

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

错误:

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

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

正确:

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

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

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

优先使用自带的 Laravel 功能和社区扩展包,而不是使用第三方扩展包和工具。因为任何要加入应用的开发者都需要学习新的工具。同时,使用第三方扩展包或工具时,从 Laravel 社区获取帮助的机会会大大降低。不要让客户为此付出代价。

任务

标准工具

第三方工具

授权

策略类

Entrust,Sentinel 和其它扩展包

编译资源文件

Laravel Mix

Grunt,Gulp,第三方扩展包

开发环境

Homestead

Docker

部署

Laravel Forge

Deployer 或其它解决方案

单元测试

PHPUnit,Mockery

Phpspec

浏览器测试

Laravel Dusk

Codeception

操作数据库

Eloquent

SQL,Doctrine

模板

Blade

Twig

处理数据

Laravel 集合

数组

表单验证

Request 类

第三方扩展包,控制器中的验证

身份认证

自带

第三方扩展包,你自己的解决方案

API 认证

Laravel Passport

第三方 JWT 和 OAuth 扩展包

创建 API

自带

Dingo API 和类似的扩展包

操作数据库结构

数据库迁移

直接操作数据库结构

本地化

自带

第三方扩展包

实时用户接口

Laravel Echo,Pusher

第三方扩展包,直接使用 WebSockets 处理

生成测试数据

Seeder 类,模型工厂,Faker

手动创建测试数据

任务调度

Laravel 任务调度程序

脚本或第三方扩展包

数据库

MySQL,PostgreSQL,SQLite,SQL Server

MongoDB

13. 遵循 Laravel 命名约定

遵循 PSR-2 规范

同时,遵循 Laravel 社区接受的命名约定:

类型

约定

正确

错误

控制器

单数

ArticleController

ArticlesController

路由

复数

articles/1

article/1

命名路由

带点的蛇形命名法

users.show_active

users.show-active,show-active-users

模型

单数

User

Users

hasOne 或 belongsTo 关联

单数

articleComment

articleComments,article_comment

所有其它关联

复数

articleComments

articleComment,article_comments

数据表

复数

article_comments

article_comment,articleComments

中间表

按字母排序的模型名单数

article_user

user_article,articles_users

数据表字段

不带模型名的蛇形命名法

meta_title

MetaTitle,article_meta_title

模型属性

蛇形命名法

$model->created_at

$model->createdAt

外键

模型名单数加 _id 后缀

article_id

ArticleId,id_article,articles_id

主键

-

id

custom_id

迁移文件

-

2017_01_01_000000_create_articles_table

2017_01_01_000000_articles

方法

驼峰命名法

getAll

get_all

资源控制器方法

参见表格

store

saveArticle

测试类方法

驼峰命名法

testGuestCannotSeeArticle

test_guest_cannot_see_article

变量

驼峰命名法

$articlesWithAuthor

$articles_with_author

集合

描述性,复数

$activeUsers = User::active()->get()

$active,$data

对象

描述性,单数

$activeUser = User::active()->first()

$users,$obj

配置和语言文件索引项

蛇形命名法

articles_enabled

ArticlesEnabled,articles-enabled

视图

蛇形命名法

show_filtered.blade.php

showFiltered.blade.php,show-filtered.blade.php

配置

蛇形命名法

google_calendar.php

googleCalendar.php,google-calendar.php

Contract(接口)

形容词或名词

Authenticatable

AuthenticationInterface,IAuthentication

Trait

形容词

Notifiable

NotificationTrait

14. 尽量使用更短、更易阅读的语法

错误:

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

正确:

php
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) ? null : $object->relation->id

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')

15. 使用容器的依赖注入而不是用 new 实例化一个类

使用 new 实例化类会在类和复杂的测试紧密耦合。因此应该使用容器的依赖注入或 Facade。

错误:

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

正确:

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

....

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

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

应该将环境变量传递给配置文件,然后使用 config() 辅助函数在应用中使用数据。

错误:

php
$apiKey = env('API_KEY');

正确:

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

// 获取数据
$apiKey = config('api.key');

17. 以标准格式存储日期。使用访问器和修改器修改日期格式

错误:

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

正确:

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

// 视图
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->some_date }}

18. 其它好的实践

绝不在路由文件中放置任何逻辑。

在 Blade 模板中少使用原生 PHP。


本文翻译自:Laravel best practices

点赞
收藏
暂无评论,快来发表评论吧~