Laravel 開發最佳實踐

arvin-hermit發表於2020-03-29


這並非laravel官方強制要求的規範,而是我們在日常開發過程中遇到的一些容易忽視的優秀實現方式。

內容

單一職責原則

保持控制器的簡潔

使用自定義Request類來進行驗證

業務程式碼要放到服務層中

DRY原則 不要重複自己

使用ORM而不是純sql語句,使用集合而不是陣列

集中處理資料

不要在模板中查詢,儘量使用惰性載入

註釋你的程式碼,但是更優雅的做法是使用描述性的語言來編寫你的程式碼

不要把 JS 和 CSS 放到 Blade 模板中,也不要把任何 HTML 程式碼放到 PHP 程式碼裡

在程式碼中使用配置、語言包和常量,而不是使用硬編碼

使用社群認可的標準Laravel工具

遵循laravel命名約定

儘可能使用簡短且可讀性更好的語法

使用IOC容器來建立例項 而不是直接new一個例項

避免直接從 .env 檔案裡獲取資料

使用標準格式來儲存日期,用訪問器和修改器來修改日期格式

其他的好建議

單一職責原則

一個類和一個方法應該只有一個責任。

例如:

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

使用自定義Request類來進行驗證

把驗證規則放到 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社群獲得什麼幫助。 不要讓你的客戶為額外的問題付錢。

想要實現的功能 標準工具 第三方工具
許可權 Policies Entrust, Sentinel 或者其他擴充套件包
資源編譯工具 Laravel Mix Grunt, Gulp, 或者其他第三方包
開發環境 Homestead Docker
部署 Laravel Forge Deployer 或者其他解決方案
自動化測試 PHPUnit, Mockery Phpspec
頁面預覽測試 Laravel Dusk Codeception
DB操縱 Eloquent SQL, Doctrine
模板 Blade Twig
資料操縱 Laravel集合 陣列
表單驗證 Request classes 他第三方包,甚至在控制器中做驗證
許可權 Built-in 他第三方包或者你自己解決
API身份驗證 Laravel Passport 第三方的JWT或者 OAuth 擴充套件包
建立 API Built-in Dingo API 或者類似的擴充套件包
建立資料庫結構 Migrations 直接用 DB 語句建立
本土化 Built-in 第三方包
實時訊息佇列 Laravel Echo, Pusher 使用第三方包或者直接使用WebSockets
建立測試資料 Seeder classes, Model Factories, Faker 手動建立測試資料
任務排程 Laravel Task Scheduler 指令碼和第三方包
資料庫 MySQL, PostgreSQL, SQLite, SQL Server MongoDB

遵循laravel命名約定

來源 PSR standards.

另外,遵循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
資源控制器 table 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
內容 (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) ? 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')

使用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 程式碼。

原文地址 https://github.com/Arvin-Lee/laravel-best-...

arvin.hermit@gmail.com

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章