├── README.md
├── images
├── logo-english.png
└── logo-russian.png
└── russian.md
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | 切换语言:
4 |
5 | + [English](https://github.com/alexeymezenin/laravel-best-practices)
6 | + [Русский](russian.md)
7 |
8 | 我们这里要讨论的并不是 Laravel 版的 SOLID 原则(想要了解更多 SOLID 原则细节查看这篇文章 )亦或是设计模式,而是 Laravel 实际开发中容易被忽略的最佳实践。
9 |
内容概览
10 |
30 | 单一职责原则
31 | 一个类和方法只负责一项职责。
32 |
33 | 坏代码:
34 | public function getFullNameAttribute()
35 | {
36 | if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
37 | return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' $this->last_name;
38 | } else {
39 | return $this->first_name[0] . '. ' . $this->last_name;
40 | }
41 | }
42 |
43 | 好代码:
44 | public function getFullNameAttribute()
45 | {
46 | return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
47 | }
48 |
49 | public function isVerfiedClient()
50 | {
51 | return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
52 | }
53 |
54 | public function getFullNameLong()
55 | {
56 | return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
57 | }
58 |
59 | public function getFullNameShort()
60 | {
61 | return $this->first_name[0] . '. ' . $this->last_name;
62 | }
63 |
64 | 胖模型、瘦控制器
65 | 如果你使用的是查询构建器或原生 SQL 查询的话将所有 DB 相关逻辑都放到 Eloquent 模型或 Repository 类。
66 |
67 | 坏代码:
68 | public function index()
69 | {
70 | $clients = Client::verified()
71 | ->with(['orders' => function ($q) {
72 | $q->where('created_at', '>', Carbon::today()->subWeek());
73 | }])
74 | ->get();
75 |
76 | return view('index', ['clients' => $clients]);
77 | }
78 |
79 | 好代码:
80 | public function index()
81 | {
82 | return view('index', ['clients' => $this->client->getWithNewOrders()]);
83 | }
84 |
85 | Class Client extends Model
86 | {
87 | public function getWithNewOrders()
88 | {
89 | return $this->verified()
90 | ->with(['orders' => function ($q) {
91 | $q->where('created_at', '>', Carbon::today()->subWeek());
92 | }])
93 | ->get();
94 | }
95 | }
96 |
97 | 验证
98 | 将验证逻辑从控制器转移到请求类。
99 |
100 | 坏代码:
101 | public function store(Request $request)
102 | {
103 | $request->validate([
104 | 'title' => 'required|unique:posts|max:255',
105 | 'body' => 'required',
106 | 'publish_at' => 'nullable|date',
107 | ]);
108 |
109 | ....
110 | }
111 |
112 | 好代码:
113 | public function store(PostRequest $request)
114 | {
115 | ....
116 | }
117 |
118 | class PostRequest extends Request
119 | {
120 | public function rules()
121 | {
122 | return [
123 | 'title' => 'required|unique:posts|max:255',
124 | 'body' => 'required',
125 | 'publish_at' => 'nullable|date',
126 | ];
127 | }
128 | }
129 |
130 | 业务逻辑需要放到服务类
131 | 一个控制器只负责一项职责,所以需要把业务逻辑都转移到服务类中。
132 |
133 | 坏代码:
134 | public function store(Request $request)
135 | {
136 | if ($request->hasFile('image')) {
137 | $request->file('image')->move(public_path('images') . 'temp');
138 | }
139 |
140 | ....
141 | }
142 |
143 | 好代码:
144 | public function store(Request $request)
145 | {
146 | $this->articleService->handleUploadedImage($request->file('image'));
147 |
148 | ....
149 | }
150 |
151 | class ArticleService
152 | {
153 | public function handleUploadedImage($image)
154 | {
155 | if (!is_null($image)) {
156 | $image->move(public_path('images') . 'temp');
157 | }
158 | }
159 | }
160 |
161 | DRY
162 | 尽可能复用代码,单一职责原则可以帮助你避免重复,此外,尽可能复用 Blade 模板,使用 Eloquent 作用域。
163 |
164 | 坏代码:
165 | public function getActive()
166 | {
167 | return $this->where('verified', 1)->whereNotNull('deleted_at')->get();
168 | }
169 |
170 | public function getArticles()
171 | {
172 | return $this->whereHas('user', function ($q) {
173 | $q->where('verified', 1)->whereNotNull('deleted_at');
174 | })->get();
175 | }
176 |
177 | 好代码:
178 | public function scopeActive($q)
179 | {
180 | return $q->where('verified', 1)->whereNotNull('deleted_at');
181 | }
182 |
183 | public function getActive()
184 | {
185 | return $this->active()->get();
186 | }
187 |
188 | public function getArticles()
189 | {
190 | return $this->whereHas('user', function ($q) {
191 | $q->active();
192 | })->get();
193 | }
194 |
195 | 优先使用 Eloquent 和 集合
196 | 通过 Eloquent 可以编写出可读性和可维护性更好的代码,此外,Eloquent 还提供了强大的内置工具如软删除、事件、作用域等。
197 |
198 | 坏代码:
199 | SELECT *
200 | FROM `articles`
201 | WHERE EXISTS (SELECT *
202 | FROM `users`
203 | WHERE `articles`.`user_id` = `users`.`id`
204 | AND EXISTS (SELECT *
205 | FROM `profiles`
206 | WHERE `profiles`.`user_id` = `users`.`id`)
207 | AND `users`.`deleted_at` IS NULL)
208 | AND `verified` = '1'
209 | AND `active` = '1'
210 | ORDER BY `created_at` DESC
211 |
212 | 好代码:
213 | Article::has('user.profile')->verified()->latest()->get();
214 |
215 | 批量赋值
216 | 关于批量赋值细节可查看对应文档 。
217 |
218 | 坏代码:
219 | $article = new Article;
220 | $article->title = $request->title;
221 | $article->content = $request->content;
222 | $article->verified = $request->verified;
223 | // Add category to article
224 | $article->category_id = $category->id;
225 | $article->save();
226 |
227 | 好代码:
228 | $category->article()->create($request->all());
229 |
230 | 不要在 Blade 执行查询 & 使用渴求式加载
231 | 坏代码:
232 | @foreach (User::all() as $user)
233 | {{ $user->profile->name }}
234 | @endforeach
235 |
236 | 好代码:
237 | $users = User::with('profile')->get();
238 |
239 | ...
240 |
241 | @foreach ($users as $user)
242 | {{ $user->profile->name }}
243 | @endforeach
244 |
245 | 注释你的代码
246 | 坏代码:
247 | if (count((array) $builder->getQuery()->joins) > 0)
248 |
249 | 好代码:
250 | // Determine if there are any joins.
251 | if (count((array) $builder->getQuery()->joins) > 0)
252 |
253 | 最佳:
254 | if ($this->hasJoins())
255 |
256 | 将前端代码和 PHP 代码分离:
257 | 不要把 JS 和 CSS 代码写到 Blade 模板里,也不要在 PHP 类中编写 HTML 代码。
258 |
259 | 坏代码:
260 | let article = `{{ json_encode($article) }}`;
261 |
262 | 好代码:
263 | <input id="article" type="hidden" value="{{ json_encode($article) }}">
264 |
265 | 或者
266 |
267 | <button class="js-fav-article" data-article="{{ json_encode($article) }}">{{ $article->name }}<button>
268 |
269 | 在 JavaScript 文件里:
270 | let article = $('#article').val();
271 |
272 | 使用配置、语言文件和常量取代硬编码
273 | 坏代码:
274 | public function isNormal()
275 | {
276 | return $article->type === 'normal';
277 | }
278 |
279 | return back()->with('message', 'Your article has been added!');
280 |
281 | 好代码:
282 | public function isNormal()
283 | {
284 | return $article->type === Article::TYPE_NORMAL;
285 | }
286 |
287 | return back()->with('message', __('app.article_added'));
288 |
289 | 使用被社区接受的标准 Laravel 工具
290 | 优先使用 Laravel 内置功能和社区版扩展包,其次才是第三方扩展包和工具。这样做的好处是降低以后的学习和维护成本。
291 |
292 |
293 |
294 | 任务
295 | 标准工具
296 | 第三方工具
297 |
298 |
299 |
300 |
301 | 授权
302 | 策略类
303 | Entrust、Sentinel等
304 |
305 |
306 | 编译资源
307 | Laravel Mix
308 | Grunt、Gulp等
309 |
310 |
311 | 开发环境
312 | Homestead
313 | Docker
314 |
315 |
316 | 部署
317 | Laravel Forge
318 | Deployer等
319 |
320 |
321 | 单元测试
322 | PHPUnit、Mockery
323 | Phpspec
324 |
325 |
326 | 浏览器测试
327 | Laravel Dusk
328 | Codeception
329 |
330 |
331 | DB
332 | Eloquent
333 | SQL、Doctrine
334 |
335 |
336 | 模板
337 | Blade
338 | Twig
339 |
340 |
341 | 处理数据
342 | Laravel集合
343 | 数组
344 |
345 |
346 | 表单验证
347 | 请求类
348 | 第三方扩展包、控制器中验证
349 |
350 |
351 | 认证
352 | 内置功能
353 | 第三方扩展包、你自己的解决方案
354 |
355 |
356 | API认证
357 | Laravel Passport
358 | 第三方 JWT 和 OAuth 扩展包
359 |
360 |
361 | 创建API
362 | 内置功能
363 | Dingo API和类似扩展包
364 |
365 |
366 | 处理DB结构
367 | 迁移
368 | 直接操作DB
369 |
370 |
371 | 本地化
372 | 内置功能
373 | 第三方工具
374 |
375 |
376 | 实时用户接口
377 | Laravel Echo、Pusher
378 | 第三方直接处理 WebSocket的扩展包
379 |
380 |
381 | 生成测试数据
382 | 填充类、模型工厂、Faker
383 | 手动创建测试数据
384 |
385 |
386 | 任务调度
387 | Laravel Task Scheduler
388 | 脚本或第三方扩展包
389 |
390 |
391 | DB
392 | MySQL、PostgreSQL、SQLite、SQL Server
393 | MongoDB
394 |
395 |
396 |
397 | 遵循 Laravel 命名约定
398 | 遵循 PSR 标准 。此外,还要遵循 Laravel 社区版的命名约定:
399 |
400 |
401 |
402 | What
403 | How
404 | Good
405 | Bad
406 |
407 |
408 |
409 |
410 | 控制器
411 | 单数
412 | ArticleController
413 | ArticlesController
414 |
415 |
416 | 路由
417 | 复数
418 | articles/1
419 | article/1
420 |
421 |
422 | 命名路由
423 | 下划线+'.'号分隔
424 | users.show_active
425 | users.show-active,show-active-users
426 |
427 |
428 | 模型
429 | 单数
430 | User
431 | Users
432 |
433 |
434 | 一对一关联
435 | 单数
436 | articleComment
437 | articleComments,article_comment
438 |
439 |
440 | 其他关联关系
441 | 复数
442 | articleComments
443 | articleComment,article_comments
444 |
445 |
446 | 数据表
447 | 复数
448 | article_comments
449 | article_comment,articleComments
450 |
451 |
452 | 中间表
453 | 按字母表排序的单数格式
454 | article_user
455 | user_article,article_users
456 |
457 |
458 | 表字段
459 | 下划线,不带模型名
460 | meta_title
461 | MetaTitle; article_meta_title
462 |
463 |
464 | 外键
465 | 单数、带_id后缀
466 | article_id
467 | ArticleId, id_article, articles_id
468 |
469 |
470 | 主键
471 | -
472 | id
473 | custom_id
474 |
475 |
476 | 迁移
477 | -
478 | 2017_01_01_000000_create_articles_table
479 | 2017_01_01_000000_articles
480 |
481 |
482 | 方法
483 | 驼峰
484 | getAll
485 | get_all
486 |
487 |
488 | 资源类方法
489 | 文档
490 | store
491 | saveArticle
492 |
493 |
494 | 测试类方法
495 | 驼峰
496 | testGuestCannotSeeArticle
497 | test_guest_cannot_see_article
498 |
499 |
500 | 变量
501 | 驼峰
502 | $articlesWithAuthor
503 | $articles_with_author
504 |
505 |
506 | 集合
507 | 复数
508 | $activeUsers = User::active()->get()
509 | $active, $data
510 |
511 |
512 | 对象
513 | 单数
514 | $activeUser = User::active()->first()
515 | $users, $obj
516 |
517 |
518 | 配置和语言文件索引
519 | 下划线
520 | articles_enabled
521 | ArticlesEnabled; articles-enabled
522 |
523 |
524 | 视图
525 | 下划线
526 | show_filtered.blade.php
527 | showFiltered.blade.php, show-filtered.blade.php
528 |
529 |
530 | 配置
531 | 下划线
532 | google_calendar.php
533 | googleCalendar.php, google-calendar.php
534 |
535 |
536 | 契约(接口)
537 | 形容词或名词
538 | Authenticatable
539 | AuthenticationInterface, IAuthentication
540 |
541 |
542 | Trait
543 | 形容词
544 | Notifiable
545 | NotificationTrait
546 |
547 |
548 |
549 | 使用缩写或可读性更好的语法
550 | 坏代码:
551 | $request->session()->get('cart');
552 | $request->input('name');
553 |
554 | 好代码:
555 | session('cart');
556 | $request->name;
557 |
558 | 更多示例:
559 |
560 |
561 |
562 | 通用语法
563 | 可读性更好的
564 |
565 |
566 |
567 |
568 | Session::get('cart')
569 | session('cart')
570 |
571 |
572 | $request->session()->get('cart')
573 | session('cart')
574 |
575 |
576 | Session::put('cart', $data)
577 | session(['cart' => $data])
578 |
579 |
580 | $request->input('name'), Request::get('name')
581 | $request->name, request('name')
582 |
583 |
584 | return Redirect::back()
585 | return back()
586 |
587 |
588 | is_null($object->relation) ? $object->relation->id : null }
589 | optional($object->relation)->id
590 |
591 |
592 | return view('index')->with('title', $title)->with('client', $client)
593 | return view('index', compact('title', 'client'))
594 |
595 |
596 | $request->has('value') ? $request->value : 'default';
597 | $request->get('value', 'default')
598 |
599 |
600 | Carbon::now(), Carbon::today()
601 | now(), today()
602 |
603 |
604 | App::make('Class')
605 | app('Class')
606 |
607 |
608 | ->where('column', '=', 1)
609 | ->where('column', 1)
610 |
611 |
612 | ->orderBy('created_at', 'desc')
613 | ->latest()
614 |
615 |
616 | ->orderBy('age', 'desc')
617 | ->latest('age')
618 |
619 |
620 | ->orderBy('created_at', 'asc')
621 | ->oldest()
622 |
623 |
624 | ->select('id', 'name')->get()
625 | ->get(['id', 'name'])
626 |
627 |
628 | ->first()->name
629 | ->value('name')
630 |
631 |
632 |
633 | 使用 IoC 容器或门面
634 | 自己创建新的类会导致代码耦合度高,且难于测试,取而代之地,我们可以使用 IoC 容器或门面。
635 |
636 | 坏代码:
637 | $user = new User;
638 | $user->create($request->all());
639 |
640 | 好代码:
641 | public function __construct(User $user)
642 | {
643 | $this->user = $user;
644 | }
645 |
646 | ....
647 |
648 | $this->user->create($request->all());
649 |
650 | 不要从直接从 .env 获取数据
651 | 传递数据到配置文件然后使用 config
辅助函数获取数据。
652 |
653 | 坏代码:
654 | $apiKey = env('API_KEY');
655 |
656 | 好代码:
657 | // config/api.php
658 | 'key' => env('API_KEY'),
659 |
660 | // Use the data
661 | $apiKey = config('api.key');
662 |
663 | 以标准格式存储日期
664 | 使用访问器和修改器来编辑日期格式。
665 |
666 | 坏代码:
667 | {{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
668 | {{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
669 |
670 | 好代码:
671 | // Model
672 | protected $dates = ['ordered_at', 'created_at', 'updated_at']
673 | public function getMonthDayAttribute($date)
674 | {
675 | return $date->format('m-d');
676 | }
677 |
678 | // View
679 | {{ $object->ordered_at->toDateString() }}
680 | {{ $object->ordered_at->monthDay }}
681 |
682 | 其他好的实践
683 | 不要把任何业务逻辑写到路由文件中。
684 |
685 | 在 Blade 模板中尽量不要编写原生 PHP。
686 |
--------------------------------------------------------------------------------
/images/logo-english.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonfu/laravel-best-practices/65afcf5d8dd565c133b840db7f941ccbbe51382c/images/logo-english.png
--------------------------------------------------------------------------------
/images/logo-russian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonfu/laravel-best-practices/65afcf5d8dd565c133b840db7f941ccbbe51382c/images/logo-russian.png
--------------------------------------------------------------------------------
/russian.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Это не пересказ лучших практик вроде SOLID, паттернов и пр. с адаптацией под Laravel. Здесь собраны именно практики, которые игнорируются в реальных Laravel проектах. Также, рекомендую ознакомитсья с [хорошими практиками в контексте PHP](https://github.com/jupeter/clean-code-php). Смотрите также [обсуждение хороших практик Laravel](https://laravel.ru/forum/viewforum.php?id=17).
4 |
5 | ## Содержание
6 |
7 | [Принцип единственной ответственности (Single responsibility principle)](#Принцип-единственной-ответственности-single-responsibility-principle)
8 |
9 | [Тонкие контроллеры, толстые модели](#Тонкие-контроллеры-толстые-модели)
10 |
11 | [Валидация](#Валидация)
12 |
13 | [Бизнес логика в сервис-классах](#Бизнес-логика-в-сервис-классах)
14 |
15 | [Не повторяйся (DRY)](#Не-повторяйся-dry)
16 |
17 | [Предпочитайте Eloquent конструктору запросов (query builder) и сырым запросам в БД. Предпочитайте работу с коллекциями работе с массивами](#Предпочитайте-eloquent-конструктору-запросов-query-builder-и-сырым-запросам-в-БД-Предпочитайте-работу-с-коллекциями-работе-с-массивами)
18 |
19 | [Используйте массовое заполнение (mass assignment)](#Используйте-массовое-заполнение-mass-assignment)
20 |
21 | [Не выполняйте запросы в представлениях и используйте нетерпеливую загрузку (проблема N + 1)](#Не-выполняйте-запросы-в-представлениях-и-используйте-нетерпеливую-загрузку-проблема-n--1)
22 |
23 | [Комментируйте код, предпочитайте читаемые имена методов комментариям](#Комментируйте-код-предпочитайте-читаемые-имена-методов-комментариям)
24 |
25 | [Выносите JS и CSS из шаблонов Blade и HTML из PHP кода](#Выносите-js-и-css-из-шаблонов-blade-и-html-из-php-кода)
26 |
27 | [Используйте инструменты и практики принятые сообществом](#Используйте-инструменты-и-практики-принятые-сообществом)
28 |
29 | [Соблюдайте соглашения сообщества об именовании](#Соблюдайте-соглашения-сообщества-об-именовании)
30 |
31 | [Конфиги, языковые файлы и константы вместо текста в коде](#Конфиги-языковые-файлы-и-константы-вместо-текста-в-коде)
32 |
33 | [Короткий и читаемый синтаксис там, где это возможно](#Короткий-и-читаемый-синтаксис-там-где-это-возможно)
34 |
35 | [Используйте IoC или фасады вместо new Class](#Используйте-ioc-или-фасады-вместо-new-class)
36 |
37 | [Не работайте с данными из файла `.env` напрямую](#Не-работайте-с-данными-из-файла-env-напрямую)
38 |
39 | [Храните даты в стандартном формате. Используйте читатели и преобразователи для преобразования формата](#Храните-даты-в-стандартном-формате-Используйте-читатели-и-преобразователи-для-преобразования-формата)
40 |
41 | [Другие советы и практики](#Другие-советы-и-практики)
42 |
43 | ### **Принцип единственной ответственности (Single responsibility principle)**
44 |
45 | Каждый класс и метод должны выполнять лишь одну функцию.
46 |
47 | Плохо:
48 |
49 | ```
50 | public function getFullNameAttribute()
51 | {
52 | if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
53 | return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' $this->last_name;
54 | } else {
55 | return $this->first_name[0] . '. ' . $this->last_name;
56 | }
57 | }
58 | ```
59 |
60 | Хорошо:
61 |
62 | ```
63 | public function getFullNameAttribute()
64 | {
65 | return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
66 | }
67 |
68 | public function isVerfiedClient()
69 | {
70 | return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
71 | }
72 |
73 | public function getFullNameLong()
74 | {
75 | return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
76 | }
77 |
78 | public function getFullNameShort()
79 | {
80 | return $this->first_name[0] . '. ' . $this->last_name;
81 | }
82 | ```
83 |
84 | [🔝 Наверх](#Содержание)
85 |
86 | ### **Тонкие контроллеры, толстые модели**
87 |
88 | По своей сути, это лишь один из частных случаев принципа единой ответственности. Выносите работу с данными в модели при работе с Eloquent или в репозитории при работе с Query Builder или "сырыми" SQL запросами.
89 |
90 | Плохо:
91 |
92 | ```
93 | public function index()
94 | {
95 | $clients = Client::verified()
96 | ->with(['orders' => function ($q) {
97 | $q->where('created_at', '>', Carbon::today()->subWeek());
98 | }])
99 | ->get();
100 |
101 | return view('index', ['clients' => $clients]);
102 | }
103 | ```
104 |
105 | Хорошо:
106 |
107 | ```
108 | public function index()
109 | {
110 | return view('index', ['clients' => $this->client->getWithNewOrders()]);
111 | }
112 |
113 | Class Client extends Model
114 | {
115 | public function getWithNewOrders()
116 | {
117 | return $this->verified()
118 | ->with(['orders' => function ($q) {
119 | $q->where('created_at', '>', Carbon::today()->subWeek());
120 | }])
121 | ->get();
122 | }
123 | }
124 | ```
125 |
126 | [🔝 Наверх](#Содержание)
127 |
128 | ### **Валидация**
129 |
130 | Следуя принципам тонкого контроллера и SRP, выносите валидацию из контроллера в Request классы.
131 |
132 | Плохо:
133 |
134 | ```
135 | public function store(Request $request)
136 | {
137 | $request->validate([
138 | 'title' => 'required|unique:posts|max:255',
139 | 'body' => 'required',
140 | 'publish_at' => 'nullable|date',
141 | ]);
142 |
143 | ....
144 | }
145 | ```
146 |
147 | Хорошо:
148 |
149 | ```
150 | public function store(PostRequest $request)
151 | {
152 | ....
153 | }
154 |
155 | class PostRequest extends Request
156 | {
157 | public function rules()
158 | {
159 | return [
160 | 'title' => 'required|unique:posts|max:255',
161 | 'body' => 'required',
162 | 'publish_at' => 'nullable|date',
163 | ];
164 | }
165 | }
166 | ```
167 |
168 | [🔝 Наверх](#Содержание)
169 |
170 | ### **Бизнес логика в сервис-классах**
171 |
172 | Контроллер должен выполнять только свои прямые обязанности, поэтому выносите всю бизнес логику в отдельные классы и сервис классы.
173 |
174 | Плохо:
175 |
176 | ```
177 | public function store(Request $request)
178 | {
179 | if ($request->hasFile('image')) {
180 | $request->file('image')->move(public_path('images') . 'temp');
181 | }
182 |
183 | ....
184 | }
185 | ```
186 |
187 | Хорошо:
188 |
189 | ```
190 | public function store(Request $request)
191 | {
192 | $this->articleService->handleUploadedImage($request->file('image'));
193 |
194 | ....
195 | }
196 |
197 | class ArticleService
198 | {
199 | public function handleUploadedImage($image)
200 | {
201 | if (!is_null($image)) {
202 | $image->move(public_path('images') . 'temp');
203 | }
204 | }
205 | }
206 | ```
207 |
208 | [🔝 Наверх](#Содержание)
209 |
210 | ### **Не повторяйся (DRY)**
211 |
212 | Этот принцип призывает вас переиспользовать код везде, где это возможно. Если вы следуете принципу SRP, вы уже избегаете повторений, но Laravel позволяет вам также переиспользовать представления, части Eloquent запросов и т.д.
213 |
214 | Плохо:
215 |
216 | ```
217 | public function getActive()
218 | {
219 | return $this->where('verified', 1)->whereNotNull('deleted_at')->get();
220 | }
221 |
222 | public function getArticles()
223 | {
224 | return $this->whereHas('user', function ($q) {
225 | $q->where('verified', 1)->whereNotNull('deleted_at');
226 | })->get();
227 | }
228 | ```
229 |
230 | Хорошо:
231 |
232 | ```
233 | public function scopeActive($q)
234 | {
235 | return $q->where('verified', 1)->whereNotNull('deleted_at');
236 | }
237 |
238 | public function getActive()
239 | {
240 | return $this->active()->get();
241 | }
242 |
243 | public function getArticles()
244 | {
245 | return $this->whereHas('user', function ($q) {
246 | $q->active();
247 | })->get();
248 | }
249 | ```
250 |
251 | [🔝 Наверх](#Содержание)
252 |
253 | ### **Предпочитайте Eloquent конструктору запросов (query builder) и сырым запросам в БД. Предпочитайте работу с коллекциями работе с массивами**
254 |
255 | Eloquent позволяет писать максимально читаемый код, а изменять функционал приложения несоизмеримо легче. У Eloquent также есть ряд удобных и мощных инструментов.
256 |
257 | Плохо:
258 |
259 | ```
260 | SELECT *
261 | FROM `articles`
262 | WHERE EXISTS (SELECT *
263 | FROM `users`
264 | WHERE `articles`.`user_id` = `users`.`id`
265 | AND EXISTS (SELECT *
266 | FROM `profiles`
267 | WHERE `profiles`.`user_id` = `users`.`id`)
268 | AND `users`.`deleted_at` IS NULL)
269 | AND `verified` = '1'
270 | AND `active` = '1'
271 | ORDER BY `created_at` DESC
272 | ```
273 |
274 | Хорошо:
275 |
276 | ```
277 | Article::has('user.profile')->verified()->latest()->get();
278 | ```
279 |
280 | [🔝 Наверх](#Содержание)
281 |
282 | ### **Используйте массовое заполнение (mass assignment)**
283 |
284 | Плохо:
285 |
286 | ```
287 | $article = new Article;
288 | $article->title = $request->title;
289 | $article->content = $request->content;
290 | $article->verified = $request->verified;
291 | // Привязать статью к категории.
292 | $article->category_id = $category->id;
293 | $article->save();
294 | ```
295 |
296 | Хорошо:
297 |
298 | ```
299 | $category->article()->create($request->all());
300 | ```
301 |
302 | [🔝 Наверх](#Содержание)
303 |
304 | ### **Не выполняйте запросы в представлениях и используйте нетерпеливую загрузку (проблема N + 1)**
305 |
306 | Плохо (будет выполнен 101 запрос в БД для 100 пользователей):
307 |
308 | ```
309 | @foreach (User::all() as $user)
310 | {{ $user->profile->name }}
311 | @endforeach
312 | ```
313 |
314 | Хорошо (будет выполнено 2 запроса в БД для 100 пользователей):
315 |
316 | ```
317 | $users = User::with('profile')->get();
318 |
319 | ...
320 |
321 | @foreach ($users as $user)
322 | {{ $user->profile->name }}
323 | @endforeach
324 | ```
325 |
326 | [🔝 Наверх](#Содержание)
327 |
328 | ### **Комментируйте код, предпочитайте читаемые имена методов комментариям**
329 |
330 | Плохо:
331 |
332 | ```
333 | if (count((array) $builder->getQuery()->joins) > 0)
334 | ```
335 |
336 | Лучше:
337 |
338 | ```
339 | // Determine if there are any joins.
340 | if (count((array) $builder->getQuery()->joins) > 0)
341 | ```
342 |
343 | Хорошо:
344 |
345 | ```
346 | if ($this->hasJoins())
347 | ```
348 |
349 | [🔝 Наверх](#Содержание)
350 |
351 | ### **Выносите JS и CSS из шаблонов Blade и HTML из PHP кода**
352 |
353 | Плохо:
354 |
355 | ```
356 | let article = `{{ json_encode($article) }}`;
357 | ```
358 |
359 | Лучше:
360 |
361 | ```
362 |
363 |
364 | Или
365 |
366 | {{ $article->name }}
367 | ```
368 |
369 | В Javascript файле:
370 |
371 | ```
372 | let article = $('#article').val();
373 | ```
374 |
375 | Еще лучше использовать специализированный пакет для передачи данных из бэкенда во фронтенд.
376 |
377 | [🔝 Наверх](#Содержание)
378 |
379 | ### **Конфиги, языковые файлы и константы вместо текста в коде**
380 |
381 | Непосредственно в коде не должно быть никакого текста.
382 |
383 | Плохо:
384 |
385 | ```
386 | public function isNormal()
387 | {
388 | return $article->type === 'normal';
389 | }
390 |
391 | return back()->with('message', 'Ваша статья была успешно добавлена');
392 | ```
393 |
394 | Хорошо:
395 |
396 | ```
397 | public function isNormal()
398 | {
399 | return $article->type === Article::TYPE_NORMAL;
400 | }
401 |
402 | return back()->with('message', __('app.article_added'));
403 | ```
404 |
405 | [🔝 Наверх](#Содержание)
406 |
407 | ### **Используйте инструменты и практики принятые сообществом**
408 |
409 | Laravel имеет встроенные инструменты для решения часто встречаемых задач. Предпочитайте пользоваться ими использованию сторонних пакетов и инструментов. Laravel разработчику, пришедшему в проект после вас, придется изучать и работать с новым для него инструментом, со всеми вытекающими последствиями. Получить помощь от сообщества будет также гораздо труднее. Не заставляйте клиента или работодателя платить за ваши велосипеды.
410 |
411 | Задача | Стандартные инструмент | Нестандартные инструмент
412 | ------------ | ------------- | -------------
413 | Авторизация | Политики | Entrust, Sentinel и др. пакеты, собственное решение
414 | Работа с JS, CSS и пр. | Laravel Mix | Grunt, Gulp, сторонние пакеты
415 | Среда разработки | Homestead | Docker
416 | Разворачивание приложений | Laravel Forge | Deployer и многие другие
417 | Тестирование | Phpunit, Mockery | Phpspec
418 | e2e тестирование | Laravel Dusk | Codeception
419 | Работа с БД | Eloquent | SQL, построитель запросов, Doctrine
420 | Шаблоны | Blade | Twig
421 | Работа с данными | Коллекции Laravel | Массивы
422 | Валидация форм | Request классы | Сторонние пакеты, валидация в контроллере
423 | Аутентификация | Встроенный функционал | Сторонние пакеты, собственное решение
424 | Аутентификация API | Laravel Passport | Сторонние пакеты, использующие JWT, OAuth
425 | Создание API | Встроенный функционал | Dingo API и другие пакеты
426 | Работа со структурой БД | Миграции | Работа с БД напрямую
427 | Локализация | Встроенный функционал | Сторонние пакеты
428 | Обмен данными в реальном времени | Laravel Echo, Pusher | Пакеты и работа с веб сокетами напрямую
429 | Генерация тестовых данных | Seeder классы, фабрики моделей, Faker | Ручное заполнение и пакеты
430 | Планирование задач | Планировщик задач Laravel | Скрипты и сторонние пакеты
431 | БД | MySQL, PostgreSQL, SQLite, SQL Server | MongoDb
432 |
433 | [🔝 Наверх](#Содержание)
434 |
435 | ### **Соблюдайте соглашения сообщества об именовании**
436 |
437 | Следуйте [стандартам PSR](http://www.php-fig.org/psr/psr-2/) при написании кода.
438 |
439 | Также, соблюдайте другие cоглашения об именовании:
440 |
441 | Что | Правило | Принято | Не принято
442 | ------------ | ------------- | ------------- | -------------
443 | Контроллер | ед. ч. | ArticleController | ~~ArticlesController~~
444 | Маршруты | мн. ч. | articles/1 | ~~article/1~~
445 | Имена маршрутов | snake_case | users.show_active | ~~users.show-active, show-active-users~~
446 | Модель | ед. ч. | User | ~~Users~~
447 | Отношения hasOne и belongsTo | ед. ч. | articleComment | ~~articleComments, article_comment~~
448 | Все остальные отношения | мн. ч. | articleComments | ~~articleComment, article_comments~~
449 | Таблица | мн. ч. | article_comments | ~~article_comment, articleComments~~
450 | Pivot таблица | имена моделей в алфавитном порядке в ед. ч. | article_user | ~~user_article, articles_users~~
451 | Столбец в таблице | snake_case без имени модели | meta_title | ~~MetaTitle; article_meta_title~~
452 | Внешний ключ | имя модели ед. ч. и _id | article_id | ~~ArticleId, id_article, articles_id~~
453 | Первичный ключ | - | id | ~~custom_id~~
454 | Миграция | - | 2017_01_01_000000_create_articles_table | ~~2017_01_01_000000_articles~~
455 | Метод | camelCase | getAll | ~~get_all~~
456 | Метод в контроллере ресурсов | [таблица](https://laravel.com/docs/master/controllers#resource-controllers) | store | ~~saveArticle~~
457 | Метод в тесте | camelCase | testGuestCannotSeeArticle | ~~test_guest_cannot_see_article~~
458 | Переменные | camelCase | $articlesWithAuthor | ~~$articles_with_author~~
459 | Коллекция | описательное, мн. ч. | $activeUsers = User::active()->get() | ~~$active, $data~~
460 | Объект | описательное, ед. ч. | $activeUser = User::active()->first() | ~~$users, $obj~~
461 | Индексы в конфиге и языковых файлах | snake_case | articles_enabled | ~~ArticlesEnabled; articles-enabled~~
462 | Представление | snake_case | show_filtered.blade.php | ~~showFiltered.blade.php, show-filtered.blade.php~~
463 | Конфигурационный файл | snake_case | google_calendar.php | ~~googleCalendar.php, google-calendar.php~~
464 | Контракт (интерфейс) | прилагательное или существительное | Authenticatable | ~~AuthenticationInterface, IAuthentication~~
465 | Трейт | прилагательное | Notifiable | ~~NotificationTrait~~
466 |
467 | [🔝 Наверх](#Содержание)
468 |
469 | ### **Короткий и читаемый синтаксис там, где это возможно**
470 |
471 | Плохо:
472 |
473 | ```
474 | $request->session()->get('cart');
475 | $request->input('name');
476 | ```
477 |
478 | Хорошо:
479 |
480 | ```
481 | session('cart');
482 | $request->name;
483 | ```
484 |
485 | Еще примеры:
486 |
487 | Часто используемый синтаксис | Более короткий и читаемый синтаксис
488 | ------------ | -------------
489 | `Session::get('cart')` | `session('cart')`
490 | `$request->session()->get('cart')` | `session('cart')`
491 | `Session::put('cart', $data)` | `session(['cart' => $data])`
492 | `$request->input('name'), Request::get('name')` | `$request->name, request('name')`
493 | `return Redirect::back()` | `return back()`
494 | `is_null($object->relation) ? $object->relation->id : null }` | `optional($object->relation)->id`
495 | `return view('index')->with('title', $title)->with('client', $client)` | `return view('index', compact('title', 'client'))`
496 | `$request->has('value') ? $request->value : 'default';` | `$request->get('value', 'default')`
497 | `Carbon::now(), Carbon::today()` | `now(), today()`
498 | `App::make('Class')` | `app('Class')`
499 | `->where('column', '=', 1)` | `->where('column', 1)`
500 | `->orderBy('created_at', 'desc')` | `->latest()`
501 | `->orderBy('age', 'desc')` | `->latest('age')`
502 | `->orderBy('created_at', 'asc')` | `->oldest()`
503 | `->select('id', 'name')->get()` | `->get(['id', 'name'])`
504 | `->first()->name` | `->value('name')`
505 |
506 | [🔝 Наверх](#Содержание)
507 |
508 | ### **Используйте IoC или фасады вместо new Class**
509 |
510 | Внедрение классов через синтаксис new Class создает сильное сопряжение между частями приложения и усложняет тестирование. Используйте контейнер или фасады.
511 |
512 | Плохо:
513 |
514 | ```
515 | $user = new User;
516 | $user->create($request->all());
517 | ```
518 |
519 | Хорошо:
520 |
521 | ```
522 | public function __construct(User $user)
523 | {
524 | $this->user = $user;
525 | }
526 |
527 | ....
528 |
529 | $this->user->create($request->all());
530 | ```
531 |
532 | [🔝 Наверх](#Содержание)
533 |
534 | ### **Не работайте с данными из файла `.env` напрямую**
535 |
536 | Передайте данные из `.env` файла в кофигурационный файл и используйте `config()` в приложении, чтобы использовать эти данными.
537 |
538 | Плохо:
539 |
540 | ```
541 | $apiKey = env('API_KEY');
542 | ```
543 |
544 | Хорошо:
545 |
546 | ```
547 | // config/api.php
548 | 'key' => env('API_KEY'),
549 |
550 | // Используйте данные в приложении
551 | $apiKey = config('api.key');
552 | ```
553 |
554 | [🔝 Наверх](#Содержание)
555 |
556 | ### **Храните даты в стандартном формате. Используйте читатели и преобразователи для преобразования формата**
557 |
558 | Плохо:
559 |
560 | ```
561 | {{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
562 | {{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
563 | ```
564 |
565 | Хорошо:
566 |
567 | ```
568 | // Модель
569 | protected $dates = ['ordered_at', 'created_at', 'updated_at']
570 | // Читатель (accessor)
571 | public function getMonthDayAttribute($date)
572 | {
573 | return $date->format('m-d');
574 | }
575 |
576 | // Шаблон
577 | {{ $object->ordered_at->toDateString() }}
578 | {{ $object->ordered_at->monthDay }}
579 | ```
580 |
581 | [🔝 Наверх](#Содержание)
582 |
583 | ### **Другие советы и практики**
584 |
585 | Не размещайте логику в маршрутах.
586 |
587 | Старайтесь не использовать "сырой" PHP в шаблонах Blade.
588 |
589 | [🔝 Наверх](#Содержание)
590 |
--------------------------------------------------------------------------------