├── README.md
├── images
├── logo-english.png
└── logo-russian.png
└── russian.md
/README.md:
--------------------------------------------------------------------------------
1 | 
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 |