การแปลภาษา:
Nederlands (by Protoqol)
한국어 (by cherrypick)
Українська (by Tenevyk)
فارسی (by amirhossein baghaie)
Tiếng Việt (by Chung Nguyễn)
Español (by César Escudero)
Français (by Mikayil S.)
Polski (by Maciej Jeziorski)
ภาษาไทย (by Kongvut Sangkla)
Deutsch (by Sujal Patel)
Italiana (by Sujal Patel)
العربية (by ahmedsaoud31)
เอกสารนี้ไม่ใช่การดัดแปลงหลักการ SOLID หรือรูปแบบและอื่น ๆ ของ Laravel โดยบทความนี้คุณจะพบแนวทางปฏิบัติในการ Coding ที่ดีที่สุด ซึ่งหลายคนมักจะละเลยในงานโปรเจค Laravel จริงของคุณ
1. แนวทางรูปแบบการตอบกลับเพียงที่เดียว
2. ความอ้วนของ Models และ Controllers ขนาดเล็ก
4. Business logic ควรจะอยู่ใน Service คลาส
7. ความอ้วนเบอะบะของการกำหนดค่า
8. ไม่ควรที่จะเรียกรัน SQL Queries ในเทมเพลต Blade และใช้เทคนิค Eager loading แทน (เพราะปัญหา N + 1)
9. หมั่นคอมเมนต์โค้ดของคุณ อีกทั้งควรจะอธิบายการทำงานของเมธอด และชื่อตัวแปร มากกว่าการคอมเมนต์เฉย ๆ
10. อย่าใส่ JS และ CSS ในเทมเพลต Blade และอย่าใส่ HTML ใด ๆ ในคลาส PHP
11. ใช้ค่าคงที่ Config และค่าคงที่ของข้อความไฟล์ภาษา แทนการใส่ข้อความตรง ๆ ลงในโค้ด
12. ใช้เครื่องมือมาตรฐานของ Laravel ที่ชุมชนยอมรับ
13. ปฏิบัติตามแนวทางการตั้งชื่อต่าง ๆ ตามกรอบกติกา Laravel
14. ใช้ไวยากรณ์ที่สั้นกว่าและอ่านง่ายกว่าถ้าเป็นไปได้
15. ใช้ชุดรูปแบบ IoC หรือ Facades แทนเรียกคลาสใหม่
16. อย่าเรียกข้อมูลจากไฟล์ .env
โดยตรง
17. เก็บวันที่ในรูปแบบมาตรฐาน อีกทั้งใช้ Accessors และ Mutators เพื่อแก้ไขรูปแบบวันที่
ภายในคลาส ซึ่งในเมธอดควรมีการ Return ค่าเพียงที่เดียว
ที่แย่:
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;
}
เขียนความสัมพันธ์ฐานข้อมูลทั้งหมด (รวมทั้งแบบ Query Builder หรือ raw SQL queries) ลงใน Model Eloquent หรือในคลาส Repository สร้างเป็น Method สำหรับเรียกใช้งาน เพื่อลดความซ้ำซ้อนของ Logic และขนาด Controllers เพื่อให้มีขนาดเล็กลง
ที่แย่:
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();
}
}
ย้ายการตรวจสอบ Validation จาก Controllers ไปที่ Request classes แทน
ที่แย่:
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',
];
}
}
เพื่อให้ Method ภายใน Controller มีขนาดที่เล็กลง ดังนั้นควรย้าย Business logic จาก Controllers ไปที่คลาส Service แทน
ที่แย่:
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');
}
}
}
ทำการ Reuse โค้ดเพื่อช่วยหลีกเลี่ยงโค้ดที่ซ้ำซ้อน เช่นเดียวกันกับการ Reuse เทมเพลต Blade โดยสำหรับ Model ให้ใช้ 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();
}
6. ควรที่จะใช้ Eloquent มากกว่า Query Builder หรือ Raw SQL queries และชอบที่จะใช้ Collections มากกว่า Arrays แบบปกติ
Eloquent ช่วยให้คุณสามารถอ่านโค้ดเข้าใจง่าย และบำรุงรักษาได้ง่าย นอกจากนี้ Eloquent ยังมีเครื่องมือในตัวที่ยอดเยี่ยม เช่น soft deletes, events, scopes เป็นต้น
ที่แย่:
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());
8. ไม่ควรที่จะเรียกรัน SQL Queries ในเทมเพลต Blade และใช้เทคนิค Eager loading แทน (เพราะปัญหา N + 1)
ที่แย่: (สำหรับข้อมูลตารางผู้ใช้ 100 users โดยจะมีการรันคำสั่ง Queries 101 ครั้ง):
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
ที่ดี: (สำหรับข้อมูลตารางผู้ใช้ 100 users โดยจะมีการรันคำสั่ง Queries 2 ครั้ง):
$users = User::with('profile')->get();
...
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
ที่แย่:
if (count((array) $builder->getQuery()->joins) > 0)
ที่ควร:
// Determine if there are any joins.
if (count((array) $builder->getQuery()->joins) > 0)
ที่ดี:
if ($this->hasJoins())
ที่แย่:
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();
ที่แย่:
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'));
ควรที่จะใช้ฟังก์ชันมาตรฐานที่ Built-in มาใน Laravel และแพ็คเกจคอมมิวนิตี้ยอดนิยม แทนการใช้แพ็คเกจและเครื่องมือของ 3rd party ปัญหาก็คือนักพัฒนาใหม่ ๆ ที่จะมาพัฒนาร่วมกับแอพของคุณในอนาคต จะต้องเรียนรู้เครื่องมือใหม่ ๆ (3rd party packages) นอกจากนี้โอกาสที่จะได้รับความช่วยเหลือจากชุมชน Laravel จะน้อยอย่างมากเมื่อคุณใช้แพ็คเกจหรือเครื่องมือของ 3rd party อีกทั้งอย่าทำให้ลูกค้าของคุณจ่ายเงินเพิ่มเติมสำหรับสิ่งพวกนั้น (Licenses)
*เพิ่มเติม: ปัญหาอีกอย่างของการใช้ 3rd party packages คืออาจจะถูกละเลยการอัพเดทแพ็คเกจสำหรับคุณสมบัติใหม่ ๆ หรือฟังก์ชันความปลอดภัย
ฟังก์ชัน | เครื่องมือมาตรฐาน | เครื่องมือ 3rd party |
---|---|---|
Authorization | Policies | Entrust, Sentinel and other packages |
Compiling assets | Laravel Mix | Grunt, Gulp, 3rd party packages |
Development Environment | Homestead | Docker |
Deployment | Laravel Forge | Deployer and other solutions |
Unit testing | PHPUnit, Mockery | Phpspec |
Browser testing | Laravel Dusk | Codeception |
DB | Eloquent | SQL, Doctrine |
Templates | Blade | Twig |
Working with data | Laravel collections | Arrays |
Form validation | Request classes | 3rd party packages, validation in controller |
Authentication | Built-in | 3rd party packages, your own solution |
API authentication | Laravel Passport, Laravel Sanctum | 3rd party JWT and OAuth packages |
Creating API | Built-in | Dingo API and similar packages |
Working with DB structure | Migrations | Working with DB structure directly |
Localization | Built-in | 3rd party packages |
Realtime user interfaces | Laravel Echo, Pusher | 3rd party packages and working with WebSockets directly |
Generating testing data | Seeder classes, Model Factories, Faker | Creating testing data manually |
Task scheduling | Laravel Task Scheduler | Scripts and 3rd party packages |
DB | MySQL, PostgreSQL, SQLite, SQL Server | MongoDB |
ปฏิบัติตามแนวทาง มาตรฐาน PSR.
นอกจากนี้ให้ปฏิบัติตามแบบแผนการตั้งชื่อที่ชุมชน Laravel ยอมรับ:
เกี่ยวกับ | แนวทาง | ที่ดี | ที่แย่ |
---|---|---|---|
Controller | singular | ArticleController | |
Route | plural | articles/1 | |
Named route | snake_case with dot notation | users.show_active | |
Model | singular | User | |
hasOne or belongsTo relationship | singular | articleComment | |
All other relationships | plural | articleComments | |
Table | plural | article_comments | |
Pivot table | singular model names in alphabetical order | article_user | |
Table column | snake_case without model name | meta_title | |
Model property | snake_case | $model->created_at | |
Foreign key | singular model name with _id suffix | article_id | |
Primary key | - | id | |
Migration | - | 2017_01_01_000000_create_articles_table | |
Method | camelCase | getAll | |
Method in resource controller | table | store | |
Method in test class | camelCase | testGuestCannotSeeArticle | |
Variable | camelCase | $articlesWithAuthor | |
Collection | descriptive, plural | $activeUsers = User::active()->get() | |
Object | descriptive, singular | $activeUser = User::active()->first() | |
Config and language files index | snake_case | articles_enabled | |
View | kebab-case | show-filtered.blade.php | |
Config | snake_case | google_calendar.php | |
Contract (interface) | adjective or noun | AuthenticationInterface | |
Trait | adjective | Notifiable |
ที่แย่:
$request->session()->get('cart');
$request->input('name');
ที่ดี:
session('cart');
$request->name;
ตัวอย่างอื่น ๆ เพิ่มเติม:
Syntax ทั่วไป | Syntax ที่สั้นและอ่านง่ายกว่า |
---|---|
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 หรือ Facades แทน
*เพิ่มเติม: ในกรณี Controller ของ Model เดียวกันแนะนำให้ทำดังตัวอย่าง
ที่แย่:
$user = new User;
$user->create($request->validated());
ที่ดี:
public function __construct(User $user)
{
$this->user = $user;
}
....
$this->user->create($request->validated());
แนะนำให้ส่งผ่านข้อมูลเพื่อกำหนดค่าจากไฟล์ Config แทน จากนั้นเรียกใช้ฟังก์ชันตัวช่วย 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 }}
- อย่าใส่ Logic ใด ๆ ในไฟล์ routes
- ลดการใช้ Vanilla PHP ให้น้อยที่สุดในเทมเพลต Blade