laravel最佳实践

着意闻时不肯香,香在无心处。

Single responsibility principle

A class and a method should have only one responsibility.

Bad:

1
2
3
4
5
6
7
8
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;
}
}

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}

Fat models, skinny controllers

Put all DB related logic into Eloquent models or into Repository classes if you’re using Query Builder or raw SQL queries.

Bad:

1
2
3
4
5
6
7
8
9
10
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();

return view('index', ['clients' => $clients]);
}

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

Move validation from controllers to Request classes.

Bad:

1
2
3
4
5
6
7
8
9
10
public function store(Request $request)
{
$request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
]);

....
}

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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',
];
}
}

Business logic should be in service class

A controller must have only one responsibility, so move business logic from controllers to service classes.

Bad:

1
2
3
4
5
6
7
8
public function store(Request $request)
{
if ($request->hasFile('image')) {
$request->file('image')->move(public_path('images') . 'temp');
}

....
}

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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');
}
}
}

Don’t repeat yourself (DRY)

Reuse code when you can. SRP is helping you to avoid duplication. Also, reuse Blade templates, use Eloquent scopes etc.

Bad:

1
2
3
4
5
6
7
8
9
10
11
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();
}

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
}

Prefer to use Eloquent over using Query Builder and raw SQL queries. Prefer collections over arrays

Eloquent allows you to write readable and maintainable code. Also, Eloquent has great built-in tools like soft deletes, events, scopes etc.

Bad:

1
2
3
4
5
6
7
8
9
10
11
12
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

Good:

1
Article::has('user.profile')->verified()->latest()->get();

Mass assignment

Bad:

1
2
3
4
5
6
7
$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();

Good:

1
$category->article()->create($request->all());

Do not execute queries in Blade templates and use eager loading (N + 1 problem)

Bad (for 100 users, 101 DB queries will be executed):

1
2
3
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach

Good (for 100 users, 2 DB queries will be executed):

1
2
3
4
5
6
7
$users = User::with('profile')->get();

...

@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach

Comment your code, but prefer descriptive method and variable names over comments

Bad:

1
if (count((array) $builder->getQuery()->joins) > 0)

Better:

1
2
// Determine if there are any joins.
if (count((array) $builder->getQuery()->joins) > 0)

Good:

1
if ($this->hasJoins())

Do not put JS and CSS in Blade templates and do not put any HTML in PHP classes

Bad:

1
let article = `{{ json_encode($article) }}`;

Better:

1
<input id="article" type="hidden" value="{{ json_encode($article) }}">

Or

1
<button class="js-fav-article" data-article="{{ json_encode($article) }}">{{ $article->name }}<button>

In a Javascript file:

1
2
let article = $('#article').val();
The best way is to use specialized PHP to JS package to transfer the data.

Use config and language files, constants instead of text in the code

Bad:

1
2
3
4
5
6
public function isNormal()
{
return $article->type === 'normal';
}

return back()->with('message', 'Your article has been added!');

Good:

1
2
3
4
5
6
public function isNormal()
{
return $article->type === Article::TYPE_NORMAL;
}

return back()->with('message', __('app.article_added'));

Use shorter and more readable syntax where possible

Bad:

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

Good:

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

Use IoC container or facades instead of new Class

new Class syntax creates tight coupling between classes and complicates testing. Use IoC container or facades instead.

Bad:

1
2
$user = new User;
$user->create($request->all());

Good:

1
2
3
4
5
6
public function __construct(User $user)
{
$this->user = $user;
}

$this->user->create($request->all());

Do not get data from the .env file directly

Pass the data to config files instead and then use the config() helper function to use the data in an application.

Bad:

1
$apiKey = env('API_KEY');

Good:

1
2
3
4
5
// config/api.php
'key' => env('API_KEY'),

// Use the data
$apiKey = config('api.key');

Store dates in the standard format. Use accessors and mutators to modify date format

Bad:

1
2
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}

Good:

1
2
3
4
5
6
7
8
9
10
// 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 }}

Other good practices

Never put any logic in routes files.

Minimize usage of vanilla PHP in Blade templates.

https://github.com/alexeymezenin/laravel-best-practices
https://laravel-china.org/articles/12762/eighteen-best-practices-of-laravel