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 isVerfiedClient()
{
    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](https://laravel-china.org/users/5651) (User::all() as $user)
    {{ $user->profile->name }}
@endforeach

下面的做法,對於 100 個用戶來說,僅僅只執行 2 個 DB 查詢:

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

...

[[@foreach](https://laravel-china.org/users/5651)](https://laravel-china.org/users/5651) ($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_000000_create_articles_table 2017_01_01_000000_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 的使用

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章