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

1. 单一职责原则

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

错误:

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

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

如果使用查询构造器或原生的 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();
    }
}

3. 验证

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

错误:

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

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

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

错误:

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

5. 不要重复自己(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();
}

6. 优先使用 Eloquent 和集合

优先使用 Eloquent 而不是查询构造器或原生 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();

7. 批量赋值

错误:

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

正确:

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

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

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

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

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

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

...

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

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

错误:

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

更好:

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

正确:

if ($this->hasJoins())

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

错误:

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

更好:

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

或

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

在 Javascript 文件中:

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

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

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

错误:

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

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. 尽量使用更短、更易阅读的语法

错误:

$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) ? 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。

错误:

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

正确:

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

....

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

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

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

错误:

$apiKey = env('API_KEY');

正确:

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

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

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

错误:

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

正确:

// 模型
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