├── README.md ├── images ├── logo-english.png └── logo-russian.png └── russian.md /README.md: -------------------------------------------------------------------------------- 1 | ![Laravel best practices](/images/logo-english.png?raw=true) 2 | 3 | Translations: 4 | 5 | [Русский](russian.md) 6 | 7 | 8 | 9 | It's not a Laravel adaptation of SOLID principles, patterns etc. Here you'll find the best practices which are usually ignored in real life Laravel projects. 10 | 11 | ## Contents 12 | 13 | [Single responsibility principle](#single-responsibility-principle) 14 | 15 | [Fat models, skinny controllers](#fat-models-skinny-controllers) 16 | 17 | [Validation](#validation) 18 | 19 | [Business logic should be in service class](#business-logic-should-be-in-service-class) 20 | 21 | [Don't repeat yourself (DRY)](#dont-repeat-yourself-dry) 22 | 23 | [Prefer to use Eloquent over using Query Builder and raw SQL queries. Prefer collections over arrays](#prefer-to-use-eloquent-over-using-query-builder-and-raw-sql-queries-prefer-collections-over-arrays) 24 | 25 | [Mass assignment](#mass-assignment) 26 | 27 | [Do not execute queries in Blade templates and use eager loading (N + 1 problem)](#do-not-execute-queries-in-blade-templates-and-use-eager-loading-n--1-problem) 28 | 29 | [Comment your code, but prefer descriptive method and variable names over comments](#comment-your-code-but-prefer-descriptive-method-and-variable-names-over-comments) 30 | 31 | [Do not put JS and CSS in Blade templates and do not put any HTML in PHP classes](#do-not-put-js-and-css-in-blade-templates-and-do-not-put-any-html-in-php-classes) 32 | 33 | [Use config and language files, constants instead of text in the code](#use-config-and-language-files-constants-instead-of-text-in-the-code) 34 | 35 | [Use standard Laravel tools accepted by community](#use-standard-laravel-tools-accepted-by-community) 36 | 37 | [Follow Laravel naming conventions](#follow-laravel-naming-conventions) 38 | 39 | [Use shorter and more readable syntax where possible](#use-shorter-and-more-readable-syntax-where-possible) 40 | 41 | [Use IoC container or facades instead of new Class](#use-ioc-container-or-facades-instead-of-new-class) 42 | 43 | [Do not get data from the `.env` file directly](#do-not-get-data-from-the-env-file-directly) 44 | 45 | [Store dates in the standard format. Use accessors and mutators to modify date format](#store-dates-in-the-standard-format-use-accessors-and-mutators-to-modify-date-format) 46 | 47 | [Other good practices](#other-good-practices) 48 | 49 | ### **Single responsibility principle** 50 | 51 | A class and a method should have only one responsibility. 52 | 53 | Bad: 54 | 55 | ```php 56 | public function getFullNameAttribute() 57 | { 58 | if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) { 59 | return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' $this->last_name; 60 | } else { 61 | return $this->first_name[0] . '. ' . $this->last_name; 62 | } 63 | } 64 | ``` 65 | 66 | Good: 67 | 68 | ```php 69 | public function getFullNameAttribute() 70 | { 71 | return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort(); 72 | } 73 | 74 | public function isVerfiedClient() 75 | { 76 | return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified(); 77 | } 78 | 79 | public function getFullNameLong() 80 | { 81 | return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name; 82 | } 83 | 84 | public function getFullNameShort() 85 | { 86 | return $this->first_name[0] . '. ' . $this->last_name; 87 | } 88 | ``` 89 | 90 | [🔝 Back to contents](#contents) 91 | 92 | ### **Fat models, skinny controllers** 93 | 94 | Put all DB related logic into Eloquent models or into Repository classes if you're using Query Builder or raw SQL queries. 95 | 96 | Bad: 97 | 98 | ```php 99 | public function index() 100 | { 101 | $clients = Client::verified() 102 | ->with(['orders' => function ($q) { 103 | $q->where('created_at', '>', Carbon::today()->subWeek()); 104 | }]) 105 | ->get(); 106 | 107 | return view('index', ['clients' => $clients]); 108 | } 109 | ``` 110 | 111 | Good: 112 | 113 | ```php 114 | public function index() 115 | { 116 | return view('index', ['clients' => $this->client->getWithNewOrders()]); 117 | } 118 | 119 | Class Client extends Model 120 | { 121 | public function getWithNewOrders() 122 | { 123 | return $this->verified() 124 | ->with(['orders' => function ($q) { 125 | $q->where('created_at', '>', Carbon::today()->subWeek()); 126 | }]) 127 | ->get(); 128 | } 129 | } 130 | ``` 131 | 132 | [🔝 Back to contents](#contents) 133 | 134 | ### **Validation** 135 | 136 | Move validation from controllers to Request classes. 137 | 138 | Bad: 139 | 140 | ```php 141 | public function store(Request $request) 142 | { 143 | $request->validate([ 144 | 'title' => 'required|unique:posts|max:255', 145 | 'body' => 'required', 146 | 'publish_at' => 'nullable|date', 147 | ]); 148 | 149 | .... 150 | } 151 | ``` 152 | 153 | Good: 154 | 155 | ```php 156 | public function store(PostRequest $request) 157 | { 158 | .... 159 | } 160 | 161 | class PostRequest extends Request 162 | { 163 | public function rules() 164 | { 165 | return [ 166 | 'title' => 'required|unique:posts|max:255', 167 | 'body' => 'required', 168 | 'publish_at' => 'nullable|date', 169 | ]; 170 | } 171 | } 172 | ``` 173 | 174 | [🔝 Back to contents](#contents) 175 | 176 | ### **Business logic should be in service class** 177 | 178 | A controller must have only one responsibility, so move business logic from controllers to service classes. 179 | 180 | Bad: 181 | 182 | ```php 183 | public function store(Request $request) 184 | { 185 | if ($request->hasFile('image')) { 186 | $request->file('image')->move(public_path('images') . 'temp'); 187 | } 188 | 189 | .... 190 | } 191 | ``` 192 | 193 | Good: 194 | 195 | ```php 196 | public function store(Request $request) 197 | { 198 | $this->articleService->handleUploadedImage($request->file('image')); 199 | 200 | .... 201 | } 202 | 203 | class ArticleService 204 | { 205 | public function handleUploadedImage($image) 206 | { 207 | if (!is_null($image)) { 208 | $image->move(public_path('images') . 'temp'); 209 | } 210 | } 211 | } 212 | ``` 213 | 214 | [🔝 Back to contents](#contents) 215 | 216 | ### **Don't repeat yourself (DRY)** 217 | 218 | Reuse code when you can. SRP is helping you to avoid duplication. Also, reuse Blade templates, use Eloquent scopes etc. 219 | 220 | Bad: 221 | 222 | ```php 223 | public function getActive() 224 | { 225 | return $this->where('verified', 1)->whereNotNull('deleted_at')->get(); 226 | } 227 | 228 | public function getArticles() 229 | { 230 | return $this->whereHas('user', function ($q) { 231 | $q->where('verified', 1)->whereNotNull('deleted_at'); 232 | })->get(); 233 | } 234 | ``` 235 | 236 | Good: 237 | 238 | ```php 239 | public function scopeActive($q) 240 | { 241 | return $q->where('verified', 1)->whereNotNull('deleted_at'); 242 | } 243 | 244 | public function getActive() 245 | { 246 | return $this->active()->get(); 247 | } 248 | 249 | public function getArticles() 250 | { 251 | return $this->whereHas('user', function ($q) { 252 | $q->active(); 253 | })->get(); 254 | } 255 | ``` 256 | 257 | [🔝 Back to contents](#contents) 258 | 259 | ### **Prefer to use Eloquent over using Query Builder and raw SQL queries. Prefer collections over arrays** 260 | 261 | Eloquent allows you to write readable and maintainable code. Also, Eloquent has great built-in tools like soft deletes, events, scopes etc. 262 | 263 | Bad: 264 | 265 | ```sql 266 | SELECT * 267 | FROM `articles` 268 | WHERE EXISTS (SELECT * 269 | FROM `users` 270 | WHERE `articles`.`user_id` = `users`.`id` 271 | AND EXISTS (SELECT * 272 | FROM `profiles` 273 | WHERE `profiles`.`user_id` = `users`.`id`) 274 | AND `users`.`deleted_at` IS NULL) 275 | AND `verified` = '1' 276 | AND `active` = '1' 277 | ORDER BY `created_at` DESC 278 | ``` 279 | 280 | Good: 281 | 282 | ```php 283 | Article::has('user.profile')->verified()->latest()->get(); 284 | ``` 285 | 286 | [🔝 Back to contents](#contents) 287 | 288 | ### **Mass assignment** 289 | 290 | Bad: 291 | 292 | ```php 293 | $article = new Article; 294 | $article->title = $request->title; 295 | $article->content = $request->content; 296 | $article->verified = $request->verified; 297 | // Add category to article 298 | $article->category_id = $category->id; 299 | $article->save(); 300 | ``` 301 | 302 | Good: 303 | 304 | ```php 305 | $category->article()->create($request->all()); 306 | ``` 307 | 308 | [🔝 Back to contents](#contents) 309 | 310 | ### **Do not execute queries in Blade templates and use eager loading (N + 1 problem)** 311 | 312 | Bad (for 100 users, 101 DB queries will be executed): 313 | 314 | ```php 315 | @foreach (User::all() as $user) 316 | {{ $user->profile->name }} 317 | @endforeach 318 | ``` 319 | 320 | Good (for 100 users, 2 DB queries will be executed): 321 | 322 | ```php 323 | $users = User::with('profile')->get(); 324 | 325 | ... 326 | 327 | @foreach ($users as $user) 328 | {{ $user->profile->name }} 329 | @endforeach 330 | ``` 331 | 332 | [🔝 Back to contents](#contents) 333 | 334 | ### **Comment your code, but prefer descriptive method and variable names over comments** 335 | 336 | Bad: 337 | 338 | ```php 339 | if (count((array) $builder->getQuery()->joins) > 0) 340 | ``` 341 | 342 | Better: 343 | 344 | ```php 345 | // Determine if there are any joins. 346 | if (count((array) $builder->getQuery()->joins) > 0) 347 | ``` 348 | 349 | Good: 350 | 351 | ```php 352 | if ($this->hasJoins()) 353 | ``` 354 | 355 | [🔝 Back to contents](#contents) 356 | 357 | ### **Do not put JS and CSS in Blade templates and do not put any HTML in PHP classes** 358 | 359 | Bad: 360 | 361 | ```php 362 | let article = `{{ json_encode($article) }}`; 363 | ``` 364 | 365 | Better: 366 | 367 | ```php 368 | 369 | 370 | Or 371 | 372 |