Home

Awesome

Laravel best practices

번역:

한국어 (by 임영록(cherrypick))

Русский(by alexeymezenin)

Português (by jonaselan)

Tiếng Việt (by Chung Nguyễn)

이 문서가 도움이 되셨다면 현재 레퍼지토리뿐만 아니라, 원본 레퍼지토리도 한 번씩 star를 눌러주시면 감사하겠습니다. :D <br> 원본 레퍼지토리: https://github.com/alexeymezenin/laravel-best-practices

이 문서는 라라벨 프레임워크에서 객체지향 디자인의 5원칙(SOLID), 패턴 등을 적용한 내용이 아닙니다. 라라벨 프레임워크로 프로젝트를 진행하면서 놓칠 수 있는 Best practice에 대해 정리한 글입니다.

Contents

단일 책임 원칙

모델은 무겁게, 컨트롤러는 가볍게

Validation-유효성 검사

비즈니스 로직은 서비스 클래스에 있어야 합니다.

중복 배제(Don't repeat yourself)

Query Builder, raw SQL 쿼리보다 Eloquent를 사용하는 것이 좋습니다.

Mass assignment-대량 할당

블레이드 템플릿에서 쿼리를 실행하지 않습니다. 그리고 즉시 로딩을 사용합니다.(N + 1 문제)

무거운 데이터 작업은 데이터를 나눕니다.

코드에 주석을 작성합니다. 하지만 주석보다 의미있는 메서드 이름과 변수 이름을 사용하는 것이 더 좋습니다.

블레이드 템플릿에 JS와 CSS를 작성하지 않고 PHP 클래스에 HTML을 작성하지 않습니다.

코드에 텍스트로 작성하지 않고, 설정 파일, 언어 파일, 상수 등을 사용합니다.

라라벨 커뮤니티에서 수용하는 표준 라라벨 도구를 사용합니다.

라라벨 네이밍 규칙을 따릅니다.

될 수 있으면 짧고 읽기 쉬운 문법을 사용합니다.

new Class 대신 IoC 컨테이너 또는 파사드를 사용합니다.

.env 파일에서 직접 데이터를 가져오지 않습니다.

날짜를 표준 형식으로 저장합니다. accessors(get), mutators(set)을 사용해 날짜 형식을 수정합니다.

또 다른 좋은 사례

단일 책임 원칙

클래스와 메서드는 하나의 책임만 있어야 합니다.

나쁜 예:

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

🔝 목차로 돌아가기

Validation-유효성 검사

유효성 검사 로직을 컨트롤러에서 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');
        }
    }
}

🔝 목차로 돌아가기

중복 배제(Don't repeat yourself)

코드를 재사용합니다. 단일 책임 원칙뿐만 아니라 블레이드 템플릿, 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();
}

🔝 목차로 돌아가기

Query Builder, raw SQL 쿼리보다 Eloquent를 사용하는 것이 좋습니다.

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

🔝 목차로 돌아가기

Mass assignment-대량 할당

나쁜 예:

$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'));

🔝 목차로 돌아가기

라라벨 커뮤니티에서 수용하는 표준 라라벨 도구를 사용합니다.

써드파티 패키지 및 도구 대신 내장되어있는 라라벨 기능과 커뮤니티 패키지를 사용합니다. 프로젝트에 참여하게 되는 개발자는 새로운 도구에 대해 학습을 해야합니다. 또한 써드파티 패키지나 도구를 사용할 때 라라벨 커뮤니티의 도움을 받을 수 있는 기회가 줄어듭니다.

TaskStandard tools3rd party tools
AuthorizationPoliciesEntrust, Sentinel and other packages
Compiling assetsLaravel Mix, ViteGrunt, Gulp, 3rd party packages
Development EnvironmentLaravel Sail, HomesteadDocker
DeploymentLaravel ForgeDeployer and other solutions
Unit testingPHPUnit, MockeryPhpspec, Pest
Browser testingLaravel DuskCodeception
DBEloquentSQL, Doctrine
TemplatesBladeTwig
Working with dataLaravel collectionsArrays
Form validationRequest classes3rd party packages, validation in controller
AuthenticationBuilt-in3rd party packages, your own solution
API authenticationLaravel Passport, Laravel Sanctum3rd party JWT and OAuth packages
Creating APIBuilt-inDingo API and similar packages
Working with DB structureMigrationsWorking with DB structure directly
LocalizationBuilt-in3rd party packages
Realtime user interfacesLaravel Echo, Pusher3rd party packages and working with WebSockets directly
Generating testing dataSeeder classes, Model Factories, FakerCreating testing data manually
Task schedulingLaravel Task SchedulerScripts and 3rd party packages
DBMySQL, PostgreSQL, SQLite, SQL ServerMongoDB

🔝 목차로 돌아가기

라라벨 네이밍 규칙을 따릅니다.

PSR 표준을 따릅니다.

또한 라라벨 커뮤니티에서 수용하고 있는 네이밍 규칙을 따릅니다:

WhatHowGoodBad
ControllersingularArticleControllerArticlesController
Routepluralarticles/1article/1
Named routesnake_case with dot notationusers.show_activeusers.show-active, show-active-users
ModelsingularUserUsers
hasOne or belongsTo relationshipsingulararticleCommentarticleComments, article_comment
All other relationshipspluralarticleCommentsarticleComment, article_comments
Tablepluralarticle_commentsarticle_comment, articleComments
Pivot tablesingular model names in alphabetical orderarticle_useruser_article, articles_users
Table columnsnake_case without model namemeta_titleMetaTitle; article_meta_title
Model propertysnake_case$model->created_at$model->createdAt
Foreign keysingular model name with _id suffixarticle_idArticleId, id_article, articles_id
Primary key-idcustom_id
Migration-2017_01_01_000000_create_articles_table2017_01_01_000000_articles
MethodcamelCasegetAllget_all
Method in resource controllertablestoresaveArticle
Method in test classcamelCasetestGuestCannotSeeArticletest_guest_cannot_see_article
VariablecamelCase$articlesWithAuthor$articles_with_author
Collectiondescriptive, plural$activeUsers = User::active()->get()$active, $data
Objectdescriptive, singular$activeUser = User::active()->first()$users, $obj
Config and language files indexsnake_casearticles_enabledArticlesEnabled; articles-enabled
Viewsnake_caseshow_filtered.blade.phpshowFiltered.blade.php, show-filtered.blade.php
Configsnake_casegoogle_calendar.phpgoogleCalendar.php, google-calendar.php
Contract (interface)adjective or nounAuthenticatableAuthenticationInterface, IAuthentication
TraitadjectiveNotifiableNotificationTrait
Trait (PSR)adjectiveNotifiableTraitNotification
EnumsingularUserTypeUserTypes, UserTypeEnum
FormRequestsingularUpdateUserRequestUpdateUserFormRequest, UserFormRequest, UserRequest
SeedersingularUserSeederUsersSeeder

🔝 목차로 돌아가기

될 수 있으면 짧고 읽기 쉬운 문법을 사용합니다.

나쁜 예:

$request->session()->get('cart');
$request->input('name');

좋은 예:

session('cart');
$request->name;

더 많은 예시:

Common syntaxShorter 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->idoptional($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 대신 IoC 컨테이너 또는 파사드를 사용합니다.

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

🔝 목차로 돌아가기

.env 파일에서 직접 데이터를 가져오지 않습니다.

데이터를 설정 파일에 전달한 다음 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 와 이와 비슷한 툴 사용을 피하세요. 대부분의 경우 이보다 더 나은 해결방법이 있습니다.

🔝 목차로 돌아가기