编程

Laravel 最佳实践

1209 2023-01-10 18:16:35

内容

单一职责原则

保持控制器的简洁

使用自定义 Request 类来进行验证

业务代码要放到服务层中

DRY 原则 不要重复自己

使用 ORM 而不是纯 sql 语句,使用集合而不是数组

集中处理数据

不要在模板中查询,尽量使用惰性加载

注释你的代码,但是更优雅的做法是使用描述性的语言来编写你的代码

不要把 JS 和 CSS 放到 Blade 模板中,也不要把任何 HTML 代码放到 PHP 代码里

在代码中使用配置、语言包和常量,而不是使用硬编码

使用社区认可的标准 Laravel 工具

遵循 Laravel 命名约定

尽可能使用简短且可读性更好的语法

使用 IOC 容器来创建实例 而不是直接 new 一个实例

避免直接从 .env 文件里获取数据

使用标准格式来存储日期,用访问器和修改器来修改日期格式

其他的好建议

单一职责原则

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

例如:

public function getFullNameAttribute(): string
{
    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(): string
{
    return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}

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

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

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

保持控制器的简洁

如果使用的是查询生成器或原始 SQL 查询,请将所有与数据库相关的逻辑放入 Eloquent 模型或 Repository 类中。

例如:

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

使用自定义 Reques类来进行验证

把验证规则放到 Request 类中.

例子:

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 的 scopes 方法来实现代码。

例子:

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

使用 ORM 而不是纯 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->validated());

不要在模板中查询,尽量使用惰性加载

例子 (对于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($article)'>

Or

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

在 Javascript 文件中加上:

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

当然最好的办法还是使用专业的 PHP 的 JS 包传输数据。

在代码中使用配置、语言包和常量,而不是使用硬编码

例子:

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 社区获得什么帮助。不要让你的客户为额外的问题付钱。

要实现的功能标准工具第三方工具
权限PoliciesEntrust, Sentinel 或者其他扩展包
资源编译工具Laravel Mix, ViteGrunt, Gulp, 或者其他第三方包
开发环境Laravel Sail, HomesteadDocker
部署Laravel ForgeDeployer 或者其他解决方案
自动化测试PHPUnit, MockeryPhpspec, Pest
页面预览测试Laravel DuskCodeception
DB操纵EloquentSQL, Doctrine
模板BladeTwig
数据操纵Laravel集合数组
表单验证Request classes他第三方包,甚至在控制器中做验证
权限Built-in他第三方包或者你自己解决
API身份验证Laravel Passport, Laravel Sanctum第三方的JWT或者 OAuth 扩展包
创建 APIBuilt-inDingo API 或者类似的扩展包
创建数据库结构Migrations直接用 DB 语句创建
本土化Built-in第三方包
实时消息队列Laravel Echo, Pusher使用第三方包或者直接使用WebSockets
创建测试数据Seeder classes, Model Factories, Faker手动创建测试数据
任务调度Laravel Task Scheduler脚本和第三方包
数据库MySQL, PostgreSQL, SQLite, SQL ServerMongoDB

 

遵循  Laravel 命名约定

来源 PSR standards.

另外,遵循 Laravel 社区认可的命名约定:

对象规则更优的写法应避免的写法
控制器单数ArticleControllerArticlesController
路由复数articles/1article/1
路由命名带点符号的蛇形命名users.show_activeusers.show-active, show-active-users
模型单数UserUsers
hasOne或belongsTo关系单数articleCommentarticleComments, article_comment
所有其他关系复数articleCommentsarticleComment, article_comments
表单复数article_commentsarticle_comment, articleComments
透视表按字母顺序排列模型article_useruser_article, articles_users
数据表字段使用蛇形并且不要带表名meta_titleMetaTitle; article_meta_title
模型参数蛇形命名$model->created_at$model->createdAt
外键带有_id后缀的单数模型名称article_idArticleId, id_article, articles_id
主键-idcustom_id
迁移-2017_01_01_000000_create_articles_table2017_01_01_000000_articles
方法驼峰命名getAllget_all
资源控制器tablestoresaveArticle
测试类驼峰命名testGuestCannotSeeArticletest_guest_cannot_see_article
变量驼峰命名$articlesWithAuthor$articles_with_author
集合描述性的, 复数的$activeUsers = User::active()->get()$active, $data
对象描述性的, 单数的$activeUser = User::active()->first()$users, $obj
配置和语言文件索引蛇形命名articles_enabledArticlesEnabled; articles-enabled
视图短横线命名show-filtered.blade.phpshowFiltered.blade.php, show_filtered.blade.php
配置蛇形命名google_calendar.phpgoogleCalendar.php, google-calendar.php
内容 (interface)形容词或名词AuthenticationInterfaceAuthenticatable, IAuthentication
Trait使用形容词NotifiableNotificationTrait
Trait (PSR)adjectiveNotifiableTraitNotification
EnumsingularUserTypeUserTypes, UserTypeEnum
FormRequestsingularUpdateUserRequestUpdateUserFormRequest, UserFormRequest, UserRequest
SeedersingularUserSeederUsersSeeder

尽可能使用简短且可读性更好的语法

例子:

$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->idoptional($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 容器来创建实例 而不是直接 new 一个实例

创建新的类会让类之间的更加耦合,使得测试越发复杂。请改用 IoC 容器或注入来实现。

例子:

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

更优的写法:

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

...

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

避免直接从 .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 getSomeDateAttribute($date)
{
    return $date->format('m-d');
}

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

其他的一些好建议

永远不要在路由文件中放任何的逻辑代码。

尽量不要在 Blade 模板中写原始 PHP 代码。