๋ฒ์ญ:
ํ๊ตญ์ด (by ์์๋ก(cherrypick))
ะ ัััะบะธะน(by alexeymezenin)
Portuguรชs (by jonaselan)
Tiแบฟng Viแปt (by Chung Nguyแป n)
์ด ๋ฌธ์๊ฐ ๋์์ด ๋์
จ๋ค๋ฉด ํ์ฌ ๋ ํผ์งํ ๋ฆฌ๋ฟ๋ง ์๋๋ผ, ์๋ณธ ๋ ํผ์งํ ๋ฆฌ๋ ํ ๋ฒ์ฉ star๋ฅผ ๋๋ฌ์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค. :D
์๋ณธ ๋ ํผ์งํ ๋ฆฌ: https://github.com/alexeymezenin/laravel-best-practices
์ด ๋ฌธ์๋ ๋ผ๋ผ๋ฒจ ํ๋ ์์ํฌ์์ ๊ฐ์ฒด์งํฅ ๋์์ธ์ 5์์น(SOLID), ํจํด ๋ฑ์ ์ ์ฉํ ๋ด์ฉ์ด ์๋๋๋ค. ๋ผ๋ผ๋ฒจ ํ๋ ์์ํฌ๋ก ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ๋์น ์ ์๋ Best practice์ ๋ํด ์ ๋ฆฌํ ๊ธ์ ๋๋ค.
๋ชจ๋ธ์ ๋ฌด๊ฒ๊ฒ, ์ปจํธ๋กค๋ฌ๋ ๊ฐ๋ณ๊ฒ
๋น์ฆ๋์ค ๋ก์ง์ ์๋น์ค ํด๋์ค์ ์์ด์ผ ํฉ๋๋ค.
์ค๋ณต ๋ฐฐ์ (Don't repeat yourself)
Query Builder, raw SQL ์ฟผ๋ฆฌ๋ณด๋ค Eloquent๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๋ฌด๊ฑฐ์ด ๋ฐ์ดํฐ ์์ ์ ๋ฐ์ดํฐ๋ฅผ ๋๋๋๋ค.
๋ผ๋ผ๋ฒจ ์ปค๋ฎค๋ํฐ์์ ์์ฉํ๋ ํ์ค ๋ผ๋ผ๋ฒจ ๋๊ตฌ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
๋ผ๋ผ๋ฒจ ๋ค์ด๋ฐ ๊ท์น์ ๋ฐ๋ฆ ๋๋ค.
๋ ์ ์์ผ๋ฉด ์งง๊ณ ์ฝ๊ธฐ ์ฌ์ด ๋ฌธ๋ฒ์ ์ฌ์ฉํฉ๋๋ค.
new Class ๋์ IoC ์ปจํ ์ด๋ ๋๋ ํ์ฌ๋๋ฅผ ์ฌ์ฉํฉ๋๋ค.
.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;
}
DB์ ๊ด๋ จ๋ ๋ก์ง์ 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(): Collection
{
return $this->verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
}
}
์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง์ ์ปจํธ๋กค๋ฌ์์ 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(): array
{
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): void
{
if (!is_null($image)) {
$image->move(public_path('images') . 'temp');
}
}
}
์ฝ๋๋ฅผ ์ฌ์ฌ์ฉํฉ๋๋ค. ๋จ์ผ ์ฑ ์ ์์น๋ฟ๋ง ์๋๋ผ ๋ธ๋ ์ด๋ ํ ํ๋ฆฟ, 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(): Collection
{
return $this->active()->get();
}
public function getArticles(): Collection
{
return $this->whereHas('user', function ($q) {
$q->active();
})->get();
}
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());
๋ธ๋ ์ด๋ ํ ํ๋ฆฟ์์ ์ฟผ๋ฆฌ๋ฅผ ์คํํ์ง ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ฆ์ ๋ก๋ฉ์ ์ฌ์ฉํฉ๋๋ค.(N + 1 ๋ฌธ์ )
๋์์ (์ ์ ์ ์ฒด๋ฅผ ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ(1๋ฒ) + ํด๋น ์ ์ ์ ํ๋กํ์ ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ(100๋ฒ) = 101๋ฒ ์คํ):
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
์ข์ ์ (์ ์ ์ ์ฒด๋ฅผ ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ(1๋ฒ) + ํด๋น ์ ์ ์ ํ๋กํ์ ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ(1๋ฒ) = 2๋ฒ ์คํ):
$users = User::with('profile')->get();
...
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
๋์ ์:
$users = $this->get();
foreach ($users as $user) {
...
}
์ข์ ์:
$this->chunk(500, function ($users) {
foreach ($users as $user) {
...
}
});
์ฝ๋์ ์ฃผ์์ ์์ฑํฉ๋๋ค. ํ์ง๋ง ์ฃผ์๋ณด๋ค ์๋ฏธ์๋ ๋ฉ์๋ ์ด๋ฆ๊ณผ ๋ณ์ ์ด๋ฆ์ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ ์ข์ต๋๋ค.
๋์ ์:
if (count((array) $builder->getQuery()->joins) > 0)
์กฐ๊ธ ๋ ๋์ ์:
// Determine if there are any joins.
if (count((array) $builder->getQuery()->joins) > 0)
์ข์ ์:
if ($this->hasJoins())
๋ธ๋ ์ด๋ ํ ํ๋ฆฟ์ JS์ CSS๋ฅผ ์์ฑํ์ง ์๊ณ PHP ํด๋์ค์ HTML์ ์์ฑํ์ง ์์ต๋๋ค.
๋์ ์:
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>
์๋ฐ์คํฌ๋ฆฝํธ ํ์ผ:
let article = $('#article').val();
The best way is to use specialized PHP to JS package to transfer the data.
์ฝ๋์ ํ ์คํธ๋ก ์์ฑํ์ง ์๊ณ , ์ค์ ํ์ผ, ์ธ์ด ํ์ผ, ์์ ๋ฑ์ ์ฌ์ฉํฉ๋๋ค.
๋์ ์:
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'));
์จ๋ํํฐ ํจํค์ง ๋ฐ ๋๊ตฌ ๋์ ๋ด์ฅ๋์ด์๋ ๋ผ๋ผ๋ฒจ ๊ธฐ๋ฅ๊ณผ ์ปค๋ฎค๋ํฐ ํจํค์ง๋ฅผ ์ฌ์ฉํฉ๋๋ค. ํ๋ก์ ํธ์ ์ฐธ์ฌํ๊ฒ ๋๋ ๊ฐ๋ฐ์๋ ์๋ก์ด ๋๊ตฌ์ ๋ํด ํ์ต์ ํด์ผํฉ๋๋ค. ๋ํ ์จ๋ํํฐ ํจํค์ง๋ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ ๋ ๋ผ๋ผ๋ฒจ ์ปค๋ฎค๋ํฐ์ ๋์์ ๋ฐ์ ์ ์๋ ๊ธฐํ๊ฐ ์ค์ด๋ญ๋๋ค.
Task | Standard tools | 3rd party tools |
---|---|---|
Authorization | Policies | Entrust, Sentinel and other packages |
Compiling assets | Laravel Mix, Vite | Grunt, Gulp, 3rd party packages |
Development Environment | Laravel Sail, Homestead | Docker |
Deployment | Laravel Forge | Deployer and other solutions |
Unit testing | PHPUnit, Mockery | Phpspec, Pest |
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 ํ์ค์ ๋ฐ๋ฆ ๋๋ค.
๋ํ ๋ผ๋ผ๋ฒจ ์ปค๋ฎค๋ํฐ์์ ์์ฉํ๊ณ ์๋ ๋ค์ด๋ฐ ๊ท์น์ ๋ฐ๋ฆ ๋๋ค:
What | How | Good | Bad |
---|---|---|---|
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 | snake_case | show_filtered.blade.php | |
Config | snake_case | google_calendar.php | |
Contract (interface) | adjective or noun | Authenticatable | |
Trait | adjective | Notifiable | |
Trait (PSR) | adjective | NotifiableTrait | |
Enum | singular | UserType | |
FormRequest | singular | UpdateUserRequest | |
Seeder | singular | UserSeeder |
๋์ ์:
$request->session()->get('cart');
$request->input('name');
์ข์ ์:
session('cart');
$request->name;
๋ ๋ง์ ์์:
Common syntax | Shorter and more readable 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 (in PHP 8: $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') |
new Class ๋ฌธ๋ฒ์ ํด๋์ค ๊ฐ์ ๊ฒฐํฉ๋๋ฅผ ๋์ด๊ณ ํ ์คํธ๋ฅผ ๋ณต์กํ๊ฒ ๋ง๋ญ๋๋ค. new Class ๋ฌธ๋ฒ ๋์ ์ IoC ์ปจํ ์ด๋ ๋๋ ํ์ฌ๋๋ฅผ ์ฌ์ฉํฉ๋๋ค.
๋์ ์:
$user = new User;
$user->create($request->all());
์ข์ ์:
public function __construct(User $user)
{
$this->user = $user;
}
....
$this->user->create($request->validated());
๋ฐ์ดํฐ๋ฅผ ์ค์ ํ์ผ์ ์ ๋ฌํ ๋ค์ config()
helper ํจ์๋ฅผ ํตํด ์ ํ๋ฆฌ์ผ์ด์
์์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
๋์ ์:
$apiKey = env('API_KEY');
์ข์ ์:
// config/api.php
'key' => env('API_KEY'),
// Use the data
$apiKey = config('api.key');
๋ ์ง๋ฅผ ํ์ค ํ์์ผ๋ก ์ ์ฅํฉ๋๋ค. accessors(get), mutators(set)์ ์ฌ์ฉํด ๋ ์ง ํ์์ ์์ ํฉ๋๋ค.
๋์ ์:
{{ 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 }}
๋ผ์ฐํธ ํ์ผ์ ๋ก์ง์ ์์ฑํ์ง ์์ต๋๋ค.
๋ธ๋ ์ด๋ ํ ํ๋ฆฟ์ ๋ฐ๋๋ผ PHP์ ์ฌ์ฉ์ ์ต์ํํฉ๋๋ค.
ํ ์คํธ์ in-memory DB ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
ํ๋ ์์ํฌ ๋ฒ์ ์ ๋ฐ์ดํธ ํน์ ๋ค๋ฅธ ์ด์์ ๊ด๋ จ๋ ๋ฌธ์ ๋ฅผ ํผํ๊ธฐ ์ํด ํ๋ ์์ํฌ ํ์ค ์ฌ์๋ค์ ์ค๋ฒ๋ผ์ด๋ ํ์ง๋ง์ธ์.
๊ฐ๋ฅํ๋ฉด Modern PHP ๋ฌธ๋ฒ์ ์ฌ์ฉํ๊ณ ๊ฐ๋ ์ฑ์ ์ ๊ฒฝ์จ์ฃผ์ธ์.
์ ์๊ณ ์ฌ์ฉํ๋๊ฒ ์๋ ์ด์ View Composers ์ ์ด์ ๋น์ทํ ํด ์ฌ์ฉ์ ํผํ์ธ์. ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ์ด๋ณด๋ค ๋ ๋์ ํด๊ฒฐ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.