├── README.md ├── images ├── .gitkeep └── Service&Repository.png └── src ├── CodeConvention ├── CodeLayer.md ├── Controller.md ├── DatabaseManage.md ├── EDP.md ├── Exception.md ├── ExtendValidationRules.md ├── IOCandFacade.md ├── LargeProjectBootstrapper.md ├── Model.md ├── NamingConvention.md ├── PermissonOrPolicy.md ├── Route.md ├── Semantics.md ├── Service.md ├── SingleResponsibility.md ├── Throttle.md └── UseRuleObject.md ├── NginxConf └── OneDomainHostMultiSites.conf └── TestCase ├── FeatureTest.md └── TDD.md /README.md: -------------------------------------------------------------------------------- 1 | # laravel_best_practices_cn 2 | 3 | ## Laravel最佳实践 4 | 5 | ### 前言 6 | 这个Repository会持续更新关于Laravel开发的最佳实践,这些实践都已经应用在了我的日常项目开发和规划中,这些分享都是随着项目开发总结出来或者是从国外开发社区学习并应用到项目开发中的实践经验。现在文章还比较少并且文章之间没什么依赖关系,你可以直接去看你想要了解的模块的最佳实践,等文章汇集的多了再来重新规划整体的目录结构和层次。 当然这更多的是我在开发时总结的一些东西,如果你有补充或者不赞同的地方欢迎提PR和Issue。 7 | 8 | 随着时间的推移和对软件开发的理解我也很有可能会推翻之前的一些思路,对一些已分享的最佳实践进行重写。 9 | 10 | 另外如果你对Laravel内核实现原理感兴趣可以参考我写的[Laravel内核阅读指南](https://github.com/kevinyan815/Learning_Laravel_Kernel) 11 | 12 | ### 目录 13 | 14 | - 代码规范 15 | - [命名约定](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/NamingConvention.md) 16 | - [语义话命名](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/Semantics.md) 17 | - [单一责任原则](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/SingleResponsibility.md) 18 | - [使用依赖注入和Facade](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/IOCandFacade.md) 19 | - [应用代码层次](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/CodeLayer.md) 20 | - 最佳实践 21 | - [路由](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/Route.md) 22 | - [控制器](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/Controller.md) 23 | - [数据模型](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/Model.md) 24 | - [数据库管理](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/DatabaseManage.md) 25 | - [Service服务类](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/Service.md) 26 | - [事件驱动编程](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/EDP.md) 27 | - [扩展Validation规则](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/ExtendValidationRules.md) 28 | - [Rule对象应对复杂的验证规则](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/UseRuleObject.md) 29 | - [权限控制](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/PermissonOrPolicy.md) 30 | - [API请求频率限制](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/Throttle.md) 31 | - [异常处理](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/Exception.md) 32 | - 测试用例 33 | - [测试驱动开发](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/TestCase/TDD.md) 34 | - [功能测试](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/TestCase/FeatureTest.md) 35 | - 构建大型项目 36 | - [Laravel大型项目脚手架](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/CodeConvention/LargeProjectBootstrapper.md) 37 | - Nginx配置 38 | - [前后端分离项目的Nginx配置推荐](https://github.com/kevinyan815/laravel_best_practices_cn/blob/master/src/NginxConf/OneDomainHostMultiSites.conf) 39 | 40 | 41 | ### 其他推荐 42 | 43 | - [Go Web编程入门指南](https://github.com/kevinyan815/go_web_programming) 44 | - [Laravel内核学习指南](https://github.com/kevinyan815/Learning_Laravel_Kernel) 45 | 46 | ### 捐赠 47 | 48 | 49 | ### 联系我 50 | 51 | ![](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/images/WX20200119-143845%402x.png) 52 | -------------------------------------------------------------------------------- /images/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/Service&Repository.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyan815/laravel_best_practices_cn/ff283d555edef88409c6e035044d5fe57e56fb80/images/Service&Repository.png -------------------------------------------------------------------------------- /src/CodeConvention/CodeLayer.md: -------------------------------------------------------------------------------- 1 | ### 应用代码分层 2 | 3 | 我们在写应用里的代码时根据代码负责的不同任务讲其分为五大块`Controller`, `Repository`, `Service`, `Model`, `View`。 4 | 5 | - `Model` 数据模型, 数据模型面向的是数据层,在这里我们只关心数据表的问题,在Model中应该只定义数据与对象映射相关的属性和方法如:表名、主键名、是否让laravel管理时间字段等属性,以及模型关联、查询作用域等方法。其他与数据表无关的业务逻辑都不应出现在这里。 6 | - `Repository` 数据逻辑访问层,由它来对接Model层,理论上有一个Model就会有一个相应的`Repository`,它的目的是将上层程序与数据层进行隔离。`Repository`是具体`interface`的实现,比如做订单相关的业务,应该有`OrderRepositoryInterface`定义`Order`数据交互流程中必须要实现的方法然后由`OrderRepository`去具体实现,之后将`OrderRepositoryInterface`和`OrderRepository`注册到服务容器中,解析时直接使用`OrderRepositoryInterface`解析出具体实现,这样上层应用程序既不需要关心数据来自哪里是`Mysql`还是`MongoDB`,同时也给项目提供了足够的灵活性。当数据来源从`Mysql`更改为`MongoDB`后,我们只需要重新写一个实现类`OrderMongoRepository`然后将服务容器里`OrderRepositoryInterface`的实现切换成`OrderMongoRepository`就好,上层完全不需要改动。 7 | (Repository是我之前一直觉得在程序设计中特别多余而现在觉得特别重要的一个Layer,之前在Service中揉进去了Repository的职能,后续会把相关的Example也做一下修改) 8 | - `Service` 项目的逻辑应用层,它在业务逻辑实现时会根据业务规则调用数据访问层`Repository`进行数据更新或者访问,除了数据的CRUD还会有图片上传、请求外部API获取数据、发送邮件等等其他这些功能,这些功能应该定义在`Service`层,就是所有与应用的业务相关的代码要封装在Service里,并且我们应该增强自己的领域建模能力根据业务领域抽象出具体的`Service`,以及`Service`中具有的典型方法。 9 | - `Controller` 控制器,面向的对象是一个完整的页面或者是接口,其主要职责是作为应用与外界通信的接口,接收请求和发送响应,通过调度项目中的Service来完成请求、组合响应数据,并通过页面响应或者接口数据的形式将响应返回给客户端。 10 | - `View` 视图, 负责渲染HTML响应,使用Laravel自带的blade模版引擎,并且应该尽量减少PHP代码。 11 | 12 | 总结:所以如果一个请求对应的业务逻辑相当复杂,我们可以通过Controller方法调用一个或者多个Service方法(单一逻辑)来完成这个复杂的逻辑,在Service方法中我们通过Repository操作来实现更新、获取数据。通过这种原则来对复杂逻辑进行解耦。 13 | 14 | 15 | 16 | 17 | 我们通过看两个例子来更好的理解代码如何分层: 18 | 19 | #### 代码示例 20 | 21 | ##### Example 1: 22 | 23 | 现在假设在Controller中直接用Model查询,查出所有的管理员用户: 24 | 25 | ```php 26 | $admins = User::where('type', 'admin')->get(); 27 | ``` 28 | 29 | 后来随着项目的开发你需要在不止一个Controller方法中用到这个查询, 你可以在`UserRepository`类里包装对`User`模型的访问: 30 | 31 | ```php 32 | interface UserRepositoryInterface 33 | { 34 | public function getAllAdmins() 35 | } 36 | 37 | class UserRepository implements UserRepositoryInterface 38 | { 39 | public function getAlladmins() 40 | { 41 | return User::where('type', 'admin')->get(); 42 | } 43 | } 44 | ``` 45 | 46 | 将UserRepositoryInterface的实现绑定到Laravel的服务容器中 47 | ``` 48 | 49 | app->singleton(UserRepositoryInterface::class, UserRepository::class); 64 | } 65 | } 66 | ``` 67 | 68 | 现在你可以在用到`UserRepository`的`Controller`中通过依赖注入`UserRepositoryInterface`, 然后通过这个UserRepository方法获取所有管理员用户: 69 | 70 | ```php 71 | //Controller 72 | public function __construct(UserRepositoryInterface $UserRepository) 73 | { 74 | $this->UserRepository = $UserRepository; 75 | } 76 | //Controller action 77 | public function index() 78 | { 79 | $admins = $this->UserRepository->getAllAdmins(); 80 | } 81 | ``` 82 | 83 | 现在我们的控制器就完全和数据层面无关了。在这里我们的数据可能来自MySQL,MongoDB或者Redis。我们的控制器不知道也不需要知道他们的区别。这样我们就可以独立于数据层来测试Web层了,将来切换存储实现也会很容易。 84 | 85 | 上面的例子简单说明了我们把代码分层后每个层里应该写什么类型的程序,以及代码分层后在可读性、耦合性、维护成本等方面带来的收益。接下来会有专门章节来说明各个部分的规范和常用的最佳实践。 86 | -------------------------------------------------------------------------------- /src/CodeConvention/Controller.md: -------------------------------------------------------------------------------- 1 | ### Controller 2 | 3 | #### controller 的职责 4 | 5 | Controller的职责应该是接收请求、验证请求、调用单一或者多个Service方法完成业务逻辑、返回响应。 6 | 7 | ``` 8 | class ArticleController extends Controller 9 | { 10 | public function setArticleOff(Request $request, ArticleService $artService) 11 | { 12 | ...//表单验证 13 | 14 | $article = Article::find($request->get('article_id')); 15 | $this->articleService->setArticleOffline($article); 16 | 17 | ...//返回响应给客户端 18 | } 19 | } 20 | ``` 21 | 22 | #### 把复杂的请求验证移到请求类中去 23 | 24 | 很常见但不推荐的做法: 25 | 26 | ``` 27 | class ArticleController extends Controller 28 | { 29 | public function store(Request $request) 30 | { 31 | $request->validate([ 32 | 'title' => 'required|unique:posts|max:255', 33 | 'body' => 'required', 34 | 'publish_at' => 'nullable|date', 35 | ]); 36 | 37 | .... 38 | } 39 | } 40 | ``` 41 | 42 | 最好是这样: 43 | 44 | ``` 45 | class ArticleController extends Controller 46 | { 47 | public function store(StoreArticleRequest $request) 48 | { 49 | .... 50 | } 51 | } 52 | 53 | 54 | class StoreArticleRequest extends Request 55 | { 56 | public function rules() 57 | { 58 | return [ 59 | 'title' => 'required|unique:posts|max:255', 60 | 'body' => 'required', 61 | 'publish_at' => 'nullable|date', 62 | ]; 63 | } 64 | } 65 | ``` 66 | 67 | ### 使用依赖注入 68 | 69 | Laravel使用服务容器来解析所有的控制器。因此,你可以在控制器的构造函数中使用类型提示需要的依赖项,而声明的依赖项会自动解析并注入控制器实例中 70 | 71 | 将依赖项注入控制器能提供更好的可测试性。 72 | ``` 73 | namespace App\Http\Controllers; 74 | 75 | use App\Repositories\UserRepository; 76 | 77 | class UserController extends Controller 78 | { 79 | protected $users; 80 | 81 | public function __construct(UserRepository $users) 82 | { 83 | $this->users = $users; 84 | } 85 | 86 | public function store(Request $request) 87 | { 88 | $name = $request->name; 89 | } 90 | } 91 | ``` 92 | 如果控制器方法需要从路由参数中获取输入内容,只需要在其他依赖项后列出路由参数即可。比如: 93 | 94 | ``` 95 | Route::put('user/{id}', 'UserController@update'); 96 | ``` 97 | ``` 98 | namespace App\Http\Controllers; 99 | use Illuminate\Http\Request; 100 | class UserController extends Controller 101 | { 102 | public function update(Request $request, User $user) 103 | { 104 | // 105 | } 106 | } 107 | ``` 108 | 109 | 110 | ### 使用隐式模型绑定 111 | 112 | Laravel 会自动解析定义在路由或控制器行为中与类型提示的变量名匹配的路由段名称的 Eloquent 模型, 例如: 113 | ``` 114 | Route::Put('api/users/{user}', 'UserController@update'); 115 | 116 | class UserController extends Controller 117 | { 118 | public function update(Request $request, User $user) 119 | { 120 | // 121 | } 122 | } 123 | ``` 124 | ***注意: 隐式模型绑定是由`\Illuminate\Routing\Middleware\SubstituteBindings`中间件来完成的,想要使用模型绑定就必须应用上这个中间件*** 125 | 126 | 在这个例子中,由于`$user`变量被类型提示为 Eloquent 模型 User,变量名称又与 URI 中的 `{user}` 匹配,因此Laravel 会自动注入与请求 URI 中传入的 ID 匹配的用户模型实例。如果在数据库中找不到对应的模型实例,将会抛出`ModelNotFound`异常 127 | 128 | ### 再谈依赖注入 129 | 上面说了控制器的方法支持依赖注入,控制器方法中我们常用的请求`Request`对象就是通过Laravel的服务容器注入进去的,在控制器方法中定义了路由参数、以及路由参数模型绑定后如果你还需要注入其他依赖,很简单在路由参数前添加你需要注入的依赖参数就好了。比如下面的例子 130 | ``` 131 | class UserController extends Controller 132 | { 133 | public function update(Request $request, UserRepository $userRepo, User $user) 134 | { 135 | //$request request对象 136 | //$userRepo UserRepository对象 137 | } 138 | } 139 | ``` 140 | 141 | 142 | ### 保持精炼 143 | 144 | 绝不遗留「死方法」,就是没有用到的方法,控制器里的所有方法,都应该被使用到,否则应该删除; 145 | 146 | 绝不在控制器里批量注释掉代码,无用的逻辑代码就必须清除掉。 147 | -------------------------------------------------------------------------------- /src/CodeConvention/DatabaseManage.md: -------------------------------------------------------------------------------- 1 | ## 数据库管理 2 | 3 | ### 使用迁移管理数据表 4 | 使用迁移来创建和修改数据库中的表,数据库迁移就像是数据库的版本控制,可以让团队轻松修改并共享应用程序的数据库结构。 5 | 6 | ``` 7 | #新建数据表 8 | php artisan make:migration migration_name --create=table_name 9 | #修改数据表 10 | php artisan make:migration migration_name --table=table_name 11 | ``` 12 | 创建后在迁移类里有两个方法, up方法是执行migrate时创建或者更改数据表的操作,down是执行artisan migrate:rollback时要执行的方法, 所以down方法要对up方法里的操作做reverse。 13 | 14 | 例: 15 | 16 | 如果使用名为`create_user_table`的迁移创建了`users`表,现在需要给`users`表增加`email`字段,这种情况下我们应该新建一个名为`add_email_on_user_table`的迁移来完成增加`email`字段的任务 17 | 18 | ``` 19 | php artisan make:migration add_email_on_user_table —table=users 20 | ``` 21 | 22 | ``` 23 | class AddEmailOnUserTable extends Migration 24 | { 25 | /** 26 | * Run the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function up() 31 | { 32 | Schema::table('users', function (Blueprint $table) { 33 | $table->string('email', 40); 34 | }); 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | * down一定是up的逆向操作, 否则通过artisan命令无法回滚和重建数据库 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | Schema::table('users', function (Blueprint $table) { 46 | $table->dropColumn('email'); 47 | }); 48 | } 49 | } 50 | ``` 51 | 52 | ### 使用Seeder填充测试数据 53 | 54 | 数据库中的初始测试数据一定要通过Seeder来填充, 这样便于测试数据的管理同时在数据库重置后也能快速填充回测试数据。 55 | 56 | 此外对于主外键依赖的表的填充, 一定要控制先填充主表的测试数据, 再根据主表里的数据来填充副表的数据。 57 | 控制方法是在`DatabaseSeeder`的`run`方法中按照主外键的先后顺序调用对应的填充器 58 | 59 | 下面的例子中`QueueTableSeeder`填充数据时需要填充上面三个表的外键, 所以先填充依赖的三个表的数据: 60 | 61 | ``` 62 | use Illuminate\Database\Seeder; 63 | 64 | class DatabaseSeeder extends Seeder 65 | { 66 | /** 67 | * Run the database seeds. 68 | * 69 | * @return void 70 | */ 71 | public function run() 72 | { 73 | $this->call(AccessTableSeeder::class); 74 | $this->call(ChannelTableSeeder::class); 75 | $this->call(BusinessTableSeeder::class); 76 | $this->call(QueueTableSeeder::class); 77 | } 78 | } 79 | ``` 80 | 81 | ### 常用的迁移命令: 82 | 83 | - 通过 --force选项来更改生产数据库(用在CD部署生产环境的Job中) 84 | 85 | php artisan migrate --force 86 | 87 | - 重建数据库,并用Seeder执行数据填充(用在CI测试Job中) 88 | 89 | php artisan migrate:fresh --seed 90 | -------------------------------------------------------------------------------- /src/CodeConvention/EDP.md: -------------------------------------------------------------------------------- 1 | 在这篇文章中我们将了解到什么是“事件驱动编程”以及在Laravel中如何开始构建一个事件驱动应用,同时我们还将看到如何通过事件驱动编程来对应用程序的逻辑进行解耦。 2 | 3 | 在开始之前,先说明一下这篇文章主要是阐述事件驱动这种编程思维和理念的,所以不会涉及到Laravel Events的方方面面。如果你需要更全面地了解Laravel Events和它的各种用法可以访问[Laravel Events](https://laravel-china.org/docs/laravel/5.5/events)文档来了解详细信息。 4 | 5 | ### 何为事件驱动编程 6 | 7 | 在我们深入事件驱动应用之前,我们先看一下在维基百科里对事件驱动编程的定义: 8 | 9 | >事件驱动编程是一种编程模式,其中的程序流由诸如用户动作(鼠标点击,按键)、传感器输出或来自其他程序/线程的消息等事件来决定确定。事件驱动编程是图形用户界面和其他应用程序(例如JavaScript Web应用程序)中使用的主要范例,用于执行某些操作来响应用户输入。 10 | 11 | 事件驱动应用程序会响应用户的动作,然后执行对应的代码来响应用户的动作。 12 | 13 | ### Laravel Events 14 | 15 | 通过上面的定义,事件是发生在应用程序中的动作。`Javascript`的事件是像鼠标点击、鼠标悬浮、按下键盘这样的用户动作。在Laravel中事件是发生在应用程序中的动作,像邮件通知、记录日志、用户注册、CRUD操作等。`Laravel Events`系统提供了简易的观察者模式实现,让开发者能够订阅和监听发生在应用中的动作。 16 | 17 | 应用中有些事件是由Laravel框架自动发起。比如说当使用`Eloquent Model`执行create、save、update或者delete操作时Laravel将分别发起`created`、`saved`、`updated`、和`deleted`事件。如果需要的话我们可以监听这些事件从而执行相应的代码来完成自己的需求。除了Laravel框架自动发起的事件,我们还可以根据自己应用的需要让Laravel发起我们自己定义的事件。比如说你可以发起一个`userRegistered`事件,在事件处理程序中发送用户验证邮件好让新注册的用户能够验证自己的邮箱。 18 | 19 | 发起一个事件并不会让应用程序执行任何相应的操作,我们必须在事件处理程序中对被发起的事件进行相应地回应。`Laravel Events`由两部分组成`Event Handler`和`Event Listener`。`Event Handler`中包含了发起事件相关的信息。`Event Listener`监听事件对象并对事件进行回应,`Event Listener`是我们实现事件逻辑的地方。在Laravel中Event类文件被存放在`app/Events`目录,Listener类文件被存放在`app/Listeners`目录。 20 | 21 | ### 为何使用事件驱动编程 22 | 我们已经了解事件驱动应用和`Laravel Events`的概念了,你可能会好奇为什么要采用事件驱动这种方法来构建你的应用程序。我们来看一下事件驱动编程带来的收益。 23 | 24 | 25 | 首先,事件是一种解耦应用程序各个方面的好方法,因为单个事件可以有多个不依赖于彼此的监听器。通过解耦,不会因为你使用了不适合域逻辑的代码而污染了代码库。其次,由于应用程序是松散耦合的,你可以轻松扩展应用程序的功能,而不必打乱/重写应用程序或应用程序的某些其他功能。 26 | 27 | ### 应用示例 28 | 29 | 现在假设新用户注册了我们的应用程序后,应用程序会给用户发送一封欢迎邮件,同时会自动给用户订阅应用上的每周新闻简报。在不应用事件驱动方式的情况下代码往往是如下这样: 30 | 31 | // without event-driven approach 32 | 33 | public function register(Request $request) 34 | { 35 | // validate input 36 | $this->validate($request->all(), [ 37 | 'name' => 'required', 38 | 'email' => 'required|unique:users', 39 | 'password' => 'required|min:6|confirmed', 40 | ]); 41 | 42 | // create user and persist in database 43 | $user = $this->create($request->all()); 44 | 45 | // send welcome email 46 | Mail::to($user)->send(new WelcomeToSiteName($user)); 47 | 48 | // Sign user up for weekly newsletter 49 | Newsletter::subscribe($user->email, [ 50 | 'FNAME': $user->fname, 51 | 'LNAME': $user->lname 52 | ], 'SiteName Weekly'); 53 | 54 | // login newly registered user 55 | $this->guard()->login($user); 56 | 57 | return redirect('/home'); 58 | } 59 | 60 | 61 | 62 | 你可以看到发送欢迎邮件和订阅新闻简报的逻辑紧密耦合到了`register`方法里, 根据[关注点分离原则](https://baike.baidu.com/item/%E5%85%B3%E6%B3%A8%E7%82%B9%E5%88%86%E7%A6%BB),`register`方法不应该关心发送欢迎邮件和订阅新闻简报的具体实现。你可能会觉得发送欢迎邮件和订阅新闻放到`register`方法里也没什么,但是如果在注册时除了发送邮件还要给用户发送短信呢?继续写在`register`方法里: 63 | 64 | 65 | public function register(Request $request) 66 | { 67 | // validate input 68 | 69 | // create user and persist in database 70 | 71 | // send welcome email 72 | Mail::to($user)->send(new WelcomeToSiteName($user)); 73 | 74 | // send SMS 75 | Nexmo::message()->send([ 76 | 'to' => $user->phone_number, 77 | 'from' => 'SiteName', 78 | 'text' => 'Welcome and thanks for signup on SiteName.' 79 | ]); 80 | 81 | // Sign user up for weekly newsletter 82 | Newsletter::subscribe($user->email, [ 83 | 'FNAME': $user->fname, 84 | 'LNAME': $user->lname 85 | ], 'SiteName Weekly'); 86 | 87 | // login newly registered user 88 | 89 | return redirect('/home'); 90 | } 91 | 92 | 可以看到代码库开始变得臃肿。现在让我们看看采用事件驱动编程方法如何实现上述相同的功能。 93 | 94 | // with event-driven approach 95 | 96 | public function register(Request $request) 97 | { 98 | // validate input 99 | $this->validate($request->all(), [ 100 | 'name' => 'required', 101 | 'email' => 'required|unique:users', 102 | 'password' => 'required|min:6|confirmed', 103 | ]); 104 | 105 | // create user and persist in database 106 | $user = $this->create($request->all()); 107 | 108 | // fire event once user has been created 109 | event(new UserRegistered($user)); 110 | 111 | // login newly registered user 112 | $this->guard()->login($user); 113 | 114 | return redirect('/home'); 115 | } 116 | 117 | 118 | 一旦创建了用户,`UserRegistered`事件就会被触发。回想一下,我们之前提到,发起一个事件后应用并不会自己做任何事情,我们需要监听`UserRegistered`事件并执行必要的操作。让我们创建`UserRegistered`事件类和`SendWelcomeMail`以及`SignupForWeeklyNewsletter`监听器类: 119 | 120 | php artisan make:event UserRegistered 121 | php artisan make:listener SendWelcomeMail --event=UserRegistered 122 | php artisan make:listener SignupForWeeklyNewsletter --event=UserRegistered 123 | 124 | 事件和监听器之间的对应关系需要注册到EventServiceProvider的`$listen`属性里: 125 | 126 | protected $listen = [ 127 | UserRegistered::class => [ 128 | SendWelcomeMail::class, 129 | SignupForWeeklyNewsletter::class, 130 | ], 131 | ]; 132 | 133 | 打开`app/Events/UserRegistered.php`文件更新它的构造方法: 134 | 135 | public $user; 136 | 137 | public function __construct(User $user) 138 | { 139 | $this->user = $user; 140 | } 141 | 142 | 143 | 声明$user为public,它将被传递给监听器,而监听器可以用它来执行必要的逻辑。接下来,事件监听器将在其handle方法中接收到事件实例。在handle方法中,我们可以执行响应事件的操作。 144 | 145 | // app/Listeners/SendWelcomeMail.php 146 | public function handle(UserRegistered $event) 147 | { 148 | // send welcome email 149 | Mail::to($event->user)->send(new WelcomeToSiteName($event->user)); 150 | } 151 | 152 | 153 | // app/Listeners/SignupForWeeklyNewsletter.php 154 | public function handle(UserRegistered $event) 155 | { 156 | // Sign user up for weekly newsletter 157 | Newsletter::subscribe($event->user->email, [ 158 | 'FNAME': $event->user->fname, 159 | 'LNAME': $event->user->lname 160 | ], 'SiteName Weekly'); 161 | } 162 | 163 | 可以看到通过事件驱动的方式我们让register方法的代码尽可能的少并且专注于用户注册这件事上,其它的逻辑由`UserRegistered`事件的监听器来负责,现在如果说我们想在用户注册后发送短信给新注册的用户,我们所要做的就是创建一个新的事件监听器来监听UserRegistered事件何时被触发 164 | 165 | php artisan make:listener SendWelcomeSMS --event=UserRegistered 166 | 167 | // app/Listeners/SendWelcomeSMS.php 168 | public function handle(UserRegistered $event) 169 | { 170 | // send SMS 171 | Nexmo::message()->send([ 172 | 'to' => $event->user->phone_number, 173 | 'from' => 'SiteName', 174 | 'text' => 'Welcome and thanks for signup on SiteName.' 175 | ]); 176 | } 177 | 178 | ***注:记得要更新EventServiceProvider里的$listen属性*** 179 | 180 | ### Conclusion 181 | 182 | 183 | 在这篇文章中,我们已经能够理解事件驱动的编程是什么,事件驱动的应用程序是什么以及Laravel事件是什么。我们还研究了事件驱动应用程序的优势。但是,像跟所有有积极影响的编程概念一样,它也有缺点。事件驱动型应用程序的主要缺点是让程序流变得复杂了,尤其一些刚接触开发的人可能很难真正理解应用程序的流程。以上面的实现为例,通过`register`方法我们并不能直观地看到程序在创建用户后会向新用户发送一封欢迎邮件,并将其注册到新闻通讯中。 184 | 185 | 所以在开发中应该根据场景创造性地使用它,利用它的优势为你的应用程序解耦,而不是过度使用它。 186 | -------------------------------------------------------------------------------- /src/CodeConvention/Exception.md: -------------------------------------------------------------------------------- 1 | 异常处理是编程中十分重要但也最容易被人忽视的语言特性,它为开发者提供了处理程序运行时错误的机制,对于程序设计来说正确的异常处理能够防止泄露程序自身细节给用户,给开发者提供完整的错误回溯堆栈,同时也能提高程序的健壮性。 2 | 3 | ### 常用的Laravel异常实例 4 | 5 | `Laravel`中针对常见的程序异常情况抛出了相应的异常实例,这让开发者能够捕获这些运行时异常并根据自己的需要来做后续处理(比如:在catch中调用另外一个补救方法、记录异常到日志文件、发送报警邮件、短信) 6 | 7 | 在这里我列一些开发中常遇到异常,并说明他们是在什么情况下被抛出的,平时编码中一定要注意在程序里捕获这些异常做好异常处理才能让程序更健壮。 8 | 9 | - `Illuminate\Database\QueryException` Laravel中执行SQL语句发生错误时会抛出此异常,它也是使用率最高的异常,用来捕获SQL执行错误,比方执行Update语句时很多人喜欢判断SQL执行后判断被修改的行数来判断UPDATE是否成功,但有的情景里执行的UPDATE语句并没有修改记录值,这种情况就没法通过被修改函数来判断UPDATE是否成功了,另外在事务执行中如果捕获到QueryException 可以在catch代码块中回滚事务。 10 | - `Illuminate\Database\Eloquent\ModelNotFoundException` 通过模型的`findOrFail`和`firstOrFail`方法获取单条记录时如果没有找到会抛出这个异常(`find`和`first`找不到数据时会返回NULL)。 11 | - `Illuminate\Validation\ValidationException` 请求未通过Laravel的FormValidator验证时会抛出此异常。 12 | - `Illuminate\Auth\Access\AuthorizationException` 用户请求未通过Laravel的策略(Policy)验证时抛出此异常 13 | - `Symfony\Component\Routing\Exception\MethodNotAllowedException` 请求路由时HTTP Method不正确 14 | - `Illuminate\Http\Exceptions\HttpResponseException` Laravel的处理HTTP请求不成功时抛出此异常 15 | 16 | 17 | 18 | ### 扩展Laravel的异常处理器 19 | 20 | 上面说了Laravel把`\App\Exceptions\Handler` 注册成功了全局的异常处理器,代码中没有被`catch`到的异常,最后都会被`\App\Exceptions\Handler`捕获到,处理器先上报异常记录到日志文件里然后渲染异常响应再发送响应给客户端。但是自带的异常处理器的方法并不好用,很多时候我们想把异常上报到邮件或者是错误日志系统中,下面的例子是将异常上报到Sentry系统中,Sentry是一个错误收集服务非常好用: 21 | 22 | ``` 23 | public function report(Exception $exception) 24 | { 25 | if (app()->bound('sentry') && $this->shouldReport($exception)) { 26 | app('sentry')->captureException($exception); 27 | } 28 | 29 | parent::report($exception); 30 | } 31 | ``` 32 | 33 | 34 | 35 | 还有默认的渲染方法在表单验证时生成响应的JSON格式往往跟我们项目里统一的`JOSN`格式不一样这就需要我们自定义渲染方法的行为。 36 | 37 | ``` 38 | public function render($request, Exception $exception) 39 | { 40 | //如果客户端预期的是JSON响应, 在API请求未通过Validator验证抛出ValidationException后 41 | //这里来定制返回给客户端的响应. 42 | if ($exception instanceof ValidationException && $request->expectsJson()) { 43 | return $this->error(422, $exception->errors()); 44 | } 45 | 46 | if ($exception instanceof ModelNotFoundException && $request->expectsJson()) { 47 | //捕获路由模型绑定在数据库中找不到模型后抛出的NotFoundHttpException 48 | return $this->error(424, 'resource not found.'); 49 | } 50 | 51 | 52 | if ($exception instanceof AuthorizationException) { 53 | //捕获不符合权限时抛出的 AuthorizationException 54 | return $this->error(403, "Permission does not exist."); 55 | } 56 | 57 | return parent::render($request, $exception); 58 | } 59 | ``` 60 | 61 | 62 | 63 | 自定义后,在请求未通过`FormValidator`验证时会抛出`ValidationException`, 之后异常处理器捕获到异常后会把错误提示格式化为项目统一的JSON响应格式并输出给客户端。这样在我们的控制器中就完全省略了判断表单验证是否通过如果不通过再输出错误响应给客户端的逻辑了,将这部分逻辑交给了统一的异常处理器来执行能让控制器方法瘦身不少。 64 | 65 | 66 | 67 | ### 使用自定义异常 68 | 69 | 这部分内容其实不是针对`Laravel`框架自定义异常,在任何项目中都可以应用我这里说的自定义异常。 70 | 71 | 我见过很多人在`Repository`或者`Service`类的方法中会根据不同错误返回不同的数组,里面包含着响应的错误码和错误信息,这么做当然是可以满足开发需求的,但是并不能记录发生异常时的应用的运行时上下文,发生错误时没办法记录到上下文信息就非常不利于开发者进行问题定位。 72 | 73 | 下面的是一个自定义的异常类 74 | 75 | ``` 76 | namespace App\Exceptions\; 77 | 78 | use RuntimeException; 79 | use Throwable; 80 | 81 | class UserManageException extends RuntimeException 82 | { 83 | /** 84 | * The primitive arguments that triggered this exception 85 | * 86 | * @var array 87 | */ 88 | public $primitives; 89 | /** 90 | * QueueManageException constructor. 91 | * @param array $primitives 92 | * @param string $message 93 | * @param int $code 94 | * @param Throwable|null $previous 95 | */ 96 | public function __construct(array $primitives, $message = "", $code = 0, Throwable $previous = null) 97 | { 98 | parent::__construct($message, $code, $previous); 99 | $this->primitives = $primitives; 100 | } 101 | 102 | /** 103 | * get the primitive arguments that triggered this exception 104 | */ 105 | public function getPrimitives() 106 | { 107 | return $this->primitives; 108 | } 109 | } 110 | ``` 111 | 112 | 定义完异常类我们就能在代码逻辑中抛出异常实例了 113 | 114 | ``` 115 | class UserRepository 116 | { 117 | 118 | public function updateUserFavorites(User $user, $favoriteData) 119 | { 120 | ...... 121 | if (!$executionOne) { 122 | throw new UserManageException(func_get_args(), 'Update user favorites error', '501'); 123 | } 124 | 125 | ...... 126 | if (!$executionTwo) { 127 | throw new UserManageException(func_get_args(), 'Another Error', '502'); 128 | } 129 | 130 | return true; 131 | } 132 | } 133 | 134 | class UserController extends ... 135 | { 136 | public function updateFavorites(User $user, Request $request) 137 | { 138 | ....... 139 | $favoriteData = $request->input('favorites'); 140 | try { 141 | $this->userRepo->updateUserFavorites($user, $favoritesData); 142 | } catch (UserManageException $ex) { 143 | ....... 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | 除了上面`Repository`列出的情况更多的时候我们是在捕获到上面列举的通用异常后在`catch`代码块中抛出与业务相关的更细化的异常实例方便开发者定位问题,我们将上面的`updateUserFavorites` 按照这种策略修改一下 150 | 151 | ``` 152 | public function updateUserFavorites(User $user, $favoriteData) 153 | { 154 | try { 155 | // database execution 156 | 157 | // database execution 158 | } catch (QueryException $queryException) { 159 | throw new UserManageException(func_get_args(), 'Error Message', '501' , $queryException); 160 | } 161 | 162 | return true; 163 | } 164 | ``` 165 | -------------------------------------------------------------------------------- /src/CodeConvention/ExtendValidationRules.md: -------------------------------------------------------------------------------- 1 | # 自定义Validator规则 2 | 3 | 项目中扩展了一些验证规则, 可以作为Laravel自带验证规则的补充应用到项目中 4 | 5 | ### 扩展自定义Validator规则的步骤 6 | 7 | #### 创建ValidatorServiceProvider 8 | 我们先新建一个服务提供器用来存放新建的Validator规则 9 | ``` 10 | php artisan make:provider ValidatorServiceProvider 11 | ``` 12 | 13 | #### 让Laravel加载ValidatorServiceProvider 14 | 将ValidatorServiceProvider加到config/app.php配置文件的`providers`配置中 15 | ``` 16 | 'providers' => [ 17 | ...... 18 | /** 19 | * Application Service Providers... 20 | */ 21 | App\Providers\ValidatorServiceProvider::class, 22 | ] 23 | ``` 24 | 25 | #### 扩展Validator规则 26 | 在ValidatorServiceProvider中通过`Validator::extend()`扩展规则 27 | ``` 28 | 'not_exists:表名,字段名[字段名,字段值]' 46 | Validator::extend('not_exists', function($attribute, $value, $parameters) { 47 | $query = DB::table($parameters[0])->where($parameters[1], '=', $value); 48 | for ($i = 2; $i < count($parameters); $i += 2) { 49 | $index = $i; 50 | $column = $parameters[$index]; 51 | $value = $parameters[$index + 1]; 52 | if (strtolower($value) == 'null') { 53 | $value = NULL;//解决Validator的parameters自动将NULL转为空字符串的问题 54 | } 55 | $query->where($column, '=', $value); 56 | } 57 | return $query->count() < 1; 58 | }, 'the filed value is duplicated'); 59 | } 60 | 61 | /** 62 | * Register the application services. 63 | * 64 | * @return void 65 | */ 66 | public function register() 67 | { 68 | // 69 | } 70 | } 71 | ``` 72 | 73 | ### 项目中扩展的Validator规则 74 | 75 | #### not_exists:table,column 76 | 77 | 与exists规则相反, 验证的字段必须不存在于给定的数据库表中. 78 | 79 | 使用方法: 80 | ``` 81 | 1. 'name' => 'not_exists:accesses,name'//验证request的name值不存在于accesses表的name字段中 82 | 83 | 2. 'name' => 'not_exists:accesses,name,deleted_at,NULL'......;//额外条件可以直接在后面附加字段和字段值 84 | ``` 85 | -------------------------------------------------------------------------------- /src/CodeConvention/IOCandFacade.md: -------------------------------------------------------------------------------- 1 | ### 使用服务容器注入依赖和Facade 2 | 3 | 创建类的对象时,不仅使得类与类之间紧密耦合,还加重了测试的复杂度。推荐改用服务容器注入依赖类对象或 facades。 4 | 5 | 坏: 6 | 7 | ``` 8 | $user = new User; 9 | $user->create($request->all()); 10 | 11 | $redis = new \Predis\Predis(...); 12 | $redis->get(...); 13 | ``` 14 | 15 | 好: 16 | 17 | ``` 18 | public function __construct(User $user) 19 | { 20 | $this->user = $user; 21 | } 22 | 23 | .... 24 | 25 | $this->user->create($request->all()); 26 | 27 | \Redis::get(...); 28 | ``` 29 | -------------------------------------------------------------------------------- /src/CodeConvention/LargeProjectBootstrapper.md: -------------------------------------------------------------------------------- 1 | # 大型项目脚手架 2 | 3 | 关于用 Laravel 构建大型应用时如何对原生框架做自定义以及安排项目目录,我做了一个初步的脚手架放在了GitHub 仓库[LaravelBootstrapper](https://github.com/kevinyan815/laravel-bootstrapper)里。大家可以下载下来参考。 4 | -------------------------------------------------------------------------------- /src/CodeConvention/Model.md: -------------------------------------------------------------------------------- 1 | ### Model 2 | 3 | Model只关注对数据表的抽象,模型中不能写业务代码,应该只出现与数据表属性、模型关联、查询作用域相关的代码。 4 | 5 | #### 定义数据表的属性 6 | 7 | 任何数据表相关的属性都需要在Model中进行相应设置 8 | 9 | ``` 10 | /** 11 | * Model对应的数据库连接名,不指定则使用默认连接 12 | * 13 | * @var string 14 | */ 15 | protected $connection = 'lenovoweixin'; 16 | 17 | /** 18 | * Model对应的table name, 在table name不遵从Laravel默认的规范时才需指定 19 | * 20 | * @var string 21 | */ 22 | protected $table = 'wx_machineinfo'; 23 | 24 | /** 25 | * 指定id以外的其他字段名为主键 26 | * 27 | * @var string 28 | */ 29 | protected $primaryKey = 'ProductSn'; 30 | 31 | /** 32 | * 指定是否需要Model自动维护时间字段 create_at 和 updated_at 33 | * 34 | * @var bool 35 | */ 36 | public $timestamps = false; 37 | 38 | /** 39 | * 指定数据表的主键是否自增 40 | * 41 | * @var bool 42 | */ 43 | public $incrementing = false; 44 | 45 | /** 46 | * 可被Model::create()方法批量赋值的字段 47 | * 48 | * @var array 49 | */ 50 | protected $fillable = [ 51 | 'name', 'email', 'password', 'email_token' 52 | ]; 53 | 54 | /** 55 | * 序列化(toArray, toJson)Model对象时会隐藏的属性值 56 | * 57 | * @var array 58 | */ 59 | protected $hidden = []; 60 | 61 | 62 | ``` 63 | 64 | ### 模型关联 65 | 66 | 需要连表查询时不要使用`DB::join()`方法改为使用模型关联方法 67 | 68 | #### 定义关联 69 | 70 | ``` 71 | class MachineInfo extends Model 72 | { 73 | /** 74 | * 定义到 UserMachine的一对多模型关联 75 | */ 76 | public function userMachines() 77 | { 78 | return $this->hasMany(\App\Models\UserMachine::class, 'machine_no', 'ProductSn'); 79 | } 80 | } 81 | ``` 82 | 83 | #### 定义反向关联 84 | 85 | ``` 86 | class UserMachine extends Model 87 | { 88 | /** 89 | * 定义UserMachine到MachineInfo的多对一关联 90 | */ 91 | public function machineInfo() 92 | { 93 | return $this->belongsTo(\App\Models\MachineInfo::class, 'machine_no', 'ProductSn'); 94 | } 95 | } 96 | ``` 97 | 98 | #### 使用with预加载关联 99 | 100 | 使用with预加载关联的数据,避免每次使用关联时都去查询数据库, 而且with预加载关联数据在底层是Laravel把两次查询的结果 101 | 匹配到一起的效率要高过SQL关联查询。 102 | 103 | ``` 104 | $userMachines = UserMachine::with('machineInfo')->get(); 105 | foreach ($userMachines as $machine) { 106 | $machine->machineInfo->productSn 107 | } 108 | ``` 109 | 整个操作只执行了两条查询, 之后Laravel会把machineInfo数据关联到匹配的userMachine数据上去 110 | ``` 111 | SELECT * FROM user_machine; 112 | 113 | SELECT * FROM machine_info where machine_no in (1,2,3......); 114 | ``` 115 | 116 | ### 查询作用域 117 | 118 | 查询作用域是针对数据表单个where查询条件的抽象,我们应该把模型常用的where条件查询封装在查询作用域中,查询作用域中的一般只包含一个where条件查询,或者有强相关性的多个where条件查询,例如 119 | 120 | #### 定义查询作用域 121 | 122 | ``` 123 | class UserMachine extends Model 124 | { 125 | 126 | /** 127 | * 限制查询只查询有效的设备 128 | * @param \Illuminate\Database\Eloquent\Builder $query 129 | * @return \Illuminate\Database\Eloquent\Builder 130 | */ 131 | public function scopeValid($query) 132 | { 133 | return $query->where('is_delete', '=', 0); 134 | } 135 | 136 | /** 137 | * 限制查询只查询在指定生产日期内的设备 138 | */ 139 | public function scopeCreateTimeBetween($query, $startTime, $endTime) 140 | { 141 | return $query->where('created_at', $startTime)->where('created_at', $endTime); 142 | } 143 | } 144 | ``` 145 | 146 | #### 使用查询作用域 147 | 148 | ``` 149 | //查询在指定日期内创建的有效用户设备 150 | $userMachines = UserMachine::with('machineInfo')->valid()->createTimeBwtween($timeStart, $timeEnd)->get(); 151 | ``` 152 | 153 | #### 作用域的好处 154 | 155 | - 使查询能够更语义话,更接近英文句子,容易让人看懂。 156 | - 解耦查询语句,让应用不再充斥着复杂的查询语句。 157 | 158 | #### 作用域使用注意事项 159 | 160 | - 作用域只是对一个where条件查询,或者有强相关性的多个where条件查询的封装,封装的查询条件应该尽量少而不是多。 161 | - 查询作用域方法中不能使用get() 162 | - 查询作用域方法的命名应该使用形容词性质的单词或者短语,不能动宾形式(getXXX), 因为查询作用域只是对查询条件的封装,具体的数据获取(get)不是它关心的。 163 | 164 | #### 减少使用QueryBuilder(DB) ,禁止使用原生的SQL查询 165 | 166 | 与数据表的数据交互应该尽量使用数据表对应的模型而不是DB查询构建器方法,在项目中应该明令禁止使用DB直接执行SQL语句进行查询 167 | 168 | 错误的写法: 169 | 170 | ``` 171 | DB::select('SELECT * FROM users')->get() 172 | 173 | DB::select('select * from users where id = :id', ['id' => 1]); 174 | 175 | DB::select(SELECT * 176 | FROM `articles` 177 | WHERE EXISTS (SELECT * 178 | FROM `users` 179 | WHERE `articles`.`user_id` = `users`.`id` 180 | AND EXISTS (SELECT * 181 | FROM `profiles` 182 | WHERE `profiles`.`user_id` = `users`.`id`) 183 | AND `users`.`deleted_at` IS NULL) 184 | AND `verified` = '1' 185 | AND `active` = '1' 186 | ORDER BY `created_at` DESC); 187 | ``` 188 | 189 | 正确的写法: 190 | 191 | ``` 192 | User::all(); 193 | 194 | User::find(1); 195 | 196 | //获取所有profile有效的用户发布的文章 197 | Article::has('user.profile')->verified()->latest()->get(); 198 | ``` 199 | 200 | ### 用异常捕获判断Model更新是否成功用 201 | 202 | Laravel中在如果执行SQL查询不成功会抛出`Illuminate\Database\QueryException`异常,所以我们判断数据添加、删除、更新是否成功最准确的办法是捕获这个异常,而不是去判断SQL执行后受影响的行数。 203 | 与此同时如果service方法通过catch异常的方式来回滚事务,那么在回滚后一定要再将QueryException重新抛给外部,由外部调用着再来catch。 204 | 这样做的原因是: 205 | - 外部调用这可以通过异常来定义响应行为 206 | - Service方法保持抛出异常写测试用例时才能对方法进行反向单元测试(查看以后的单元测试章节)。 207 | 208 | 下面是一个例子: 209 | 210 | ``` 211 | class MemberService 212 | { 213 | /** 214 | * 恢复会员卡状态和库存 215 | * 216 | * @param \App\Models\Card $card 217 | * @return boolean 218 | */ 219 | public function restoreCard(Card $card) 220 | { 221 | if($card->status != Card::STATUS_PREUSED) {//只有发放中状态的卡才能恢复 222 | return false; 223 | } 224 | DB::beginTransaction(); 225 | try { 226 | $card->status = Card::STATUS_UNUSED;//将这张卡号设置为未使用 227 | $card->save(); 228 | RemainingNum::firstData()->increment('remaining_num');//将剩余会员卡数加1 229 | DB::commit(); 230 | } catch (QueryException $exc) { 231 | \Log::error('Restore Card Error: ' . $exc->getMessage()); 232 | DB::rollback(); 233 | throw $exc; 234 | } 235 | 236 | return true; 237 | } 238 | } 239 | 240 | class MemberController extends Controller 241 | { 242 | public function restore(Request $request, MemberService $memService) 243 | { 244 | ...... 245 | try { 246 | ...... 247 | $memService->restoreCard($card); 248 | } catch (QueryException) { 249 | //发生错误后的响应行为 250 | } 251 | // 正确的响应行为 252 | } 253 | } 254 | ``` 255 | -------------------------------------------------------------------------------- /src/CodeConvention/NamingConvention.md: -------------------------------------------------------------------------------- 1 | 2 | ## 命名约定 3 | 4 | 遵循 [PSR 标准](http://www.php-fig.org/psr/psr-2/)。 另外,请遵循 Laravel 社区接受的命名约定: 5 | 6 | | 类型 | 规则 | 正确示例 | 错误示例 | 7 | | -------------------------------- | ------------------------------------------------------------ | --------------------------------------- | --------------------------------------------------- | 8 | | 控制器 | 单数 | ArticleController | ~~ArticlesController~~ | 9 | | 路由 | 复数 | articles/1 | ~~article/1~~ | 10 | | 命名路由 | 带点符号的蛇形命名 | users.show_active | ~~users.show-active, show-active-users~~ | 11 | | 模型 | 单数 | User | ~~Users~~ | 12 | | hasOne 和 belongsTo 模型关联方法 | 单数 | comment | ~~comments, article_comment~~ | 13 | | | | | | 14 | | 其他模型关联方法 | 复数 | comments | ~~articleComment, article_comments~~ | 15 | | 数据表 | 复数 | article_comments | ~~article_comment, articleComments~~ | 16 | | 多对多关联的中间表(Pivot table) | 按字母顺序排列的单数模型名称 | article_user | ~~user_article, articles_users~~ | 17 | | 数据表字段 | 小写字母、单数、蛇形命名 | meta_title | ~~MetaTitle; meta_titles~~ | 18 | | 外键 | 带_id后缀的单数型名称 | article_id | ~~ArticleId, id_article, articles_id~~ | 19 | | 主键 | - | id | ~~custom_id~~ | 20 | | Migration | - | 2017_01_create_articles_table, 2017_01_add_name_filed_on_articles_table | ~~2017_01_01_000000_articles~~ | 21 | | Method Name | 小驼峰命名 | getAll | ~~get_all~~ | 22 | | 资源控制器中的方法 | [具体看表格](https://laravel.com/docs/master/controllers#resource-controllers) | store、index、detail… | ~~saveArticle~~ | 23 | | 单元测试类中的方法 | 小驼峰命名 | testGuestCannotSeeArticle | ~~test_guest_cannot_see_article~~ | 24 | | 变量 | 小驼峰命名 | $articlesWithAuthor | ~~$articles_with_author~~ | 25 | | 集合(Collection)类型数据的变量名 | 具描述性的复数形式 | $activeUsers = User::active()->get() | ~~$active, $data~~ | 26 | | 对象 | 具描述性的单数形式 | $activeUser = User::active()->first() | ~~$users, $obj~~ | 27 | | Config and language files index | 蛇形命名 | articles_enabled | ~~ArticlesEnabled; articles-enabled~~ | 28 | | View文件 | 蛇形命名 | show_filtered.blade.php | ~~showFiltered.blade.php, show-filtered.blade.php~~ | 29 | | Config文件 | 蛇形命名 | google_calendar.php | ~~googleCalendar.php, google-calendar.php~~ | 30 | | 契约 (interface) | 形容词或名词 | Authenticatable | ~~AuthenticationInterface, IAuthentication~~ | 31 | | Trait | 形容词 | Notifiable | ~~NotificationTrait~~ | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/CodeConvention/PermissonOrPolicy.md: -------------------------------------------------------------------------------- 1 | # 用户权限控制 2 | 3 | ### 采用RBAC模型 4 | 我们使用基于角色的权限访问控制(Role-Based Access Control)来管理用户权限和角色 5 | 6 | 网上有很多开源的package让Laravel框架集成RBAC的能力,我们使用的是`spatie/laravel-permission` 7 | 8 | - Installation 9 | 10 | ``` 11 | composer require spatie/laravel-permission 12 | ``` 13 | Laravel 5.5之前的版本需要手工注册服务提供器 14 | 15 | ``` 16 | 'providers' => [ 17 | // ... 18 | Spatie\Permission\PermissionServiceProvider::class, 19 | ]; 20 | 21 | ``` 22 | 23 | - Usage 24 | 25 | 创建角色和权限: 26 | 27 | ``` 28 | use Spatie\Permission\Models\Role; 29 | use Spatie\Permission\Models\Permission; 30 | 31 | $role = Role::create(['name' => 'writer']); 32 | $permission = Permission::create(['name' => 'edit articles']); 33 | 34 | ``` 35 | 将权限授权给用户: 36 | 37 | ``` 38 | $role->givePermissionTo($permission); 39 | $permission->assignRole($role); 40 | ``` 41 | 给用户分配角色: 42 | 43 | ``` 44 | $user->assignRole('writer'); 45 | 46 | // 一次分配多个角色 47 | $user->assignRole('writer', 'admin'); 48 | // 通过数据分配多个角色 49 | $user->assignRole(['writer', 'admin']); 50 | ``` 51 | 52 | 除了通过角色给用户分配权限外还可以将权限分配到用户身上: 53 | 54 | ``` 55 | $user->givePermissionTo('edit articles'); 56 | 57 | // 一次赋予多个权限 58 | $user->givePermissionTo('edit articles', 'delete articles'); 59 | 60 | $user->givePermissionTo(['edit articles', 'delete articles']); 61 | ``` 62 | 63 | 获取用户的权限列表: 64 | ``` 65 | // 用户直接拥有的权限 66 | $user->getDirectPermissions() // 也可以用模型关联的动态属性 $user->permissions; 67 | 68 | // 用户通过角色获取的权限 69 | $user->getPermissionsViaRoles(); 70 | 71 | // 用户全部的权限 72 | $user->getAllPermissions(); 73 | ``` 74 | 校验用户是否拥有给定权限和角色: 75 | ``` 76 | user->hasRole('writer'); 77 | 78 | $user->hasPermissionTo('edit articles'); 79 | ``` 80 | 81 | 更详细的使用方法请参考GitHub项目的README文件: https://github.com/spatie/laravel-permission 82 | 83 | 此外还有外国网友专门基于`laravel-permission`这个包做了一套后台界面, 也是很不错的资源 https://github.com/LaravelDaily/laravel-roles-permissions-manager 84 | 85 | ### 使用Policy处理用户授权动作 86 | 87 | 项目中统一使用[授权策略](http://d.laravel-china.org/docs/5.5/authorization#policies)类来做用户授权。 88 | 89 | 所有 Policy 授权策略类 必须 继承 app/Policies/Policy.php 基类。基类类似文件如下: 90 | ``` 91 | hasRole('Admin')) { 116 | return true; 117 | } 118 | } 119 | } 120 | 121 | ``` 122 | 123 | #### 授权策略命名 124 | Policy授权策略类须遵循资源路由的方式进行命名,posts 对应 /app/Policies/Post.php 。 125 | 126 | #### 生成策略 127 | 128 | 策略是在特定模型或者资源中组织授权逻辑的类。例如,如果你的应用是一个博客,会有一个 Post 模型和一个相应的 PostPolicy 来授权用户动作,比如创建或者更新博客。 129 | 130 | 可以使用如下命令来生成策略。 131 | ``` 132 | php artisan make:policy PostPolicy 133 | ``` 134 | `make:policy`会生成空的策略类。如果希望生成的类包含基本的「CRUD」策略方法, 可以在使用命令时指定 --model 选项: 135 | ``` 136 | php artisan make:policy PostPolicy --model=Post 137 | ``` 138 | 我们在项目中统一使用第二条生成包含基本策略方法的策略类 139 | 140 | ### 编写策略方法 141 | ``` 142 | id === $post->user_id; 163 | } 164 | } 165 | ``` 166 | 167 | #### 使用授权策略 168 | - 在Controller中使用 169 | 在控制器中使用控制器基类提供的`authorize`方法,它会自动查找与调用它的控制器方法同名的策略方法,这样控制器和授权类的方法名就统一起来了。 170 | 171 | ``` 172 | public function update(Request $request, Post $post) 173 | { 174 | $this->authorize('update', $post); 175 | 176 | // 当前用户可以更新博客... 177 | } 178 | ``` 179 | 180 | 一些动作,比如 create,并不需要指定模型实例。在这种情况下,可传递一个类名给 authorize 方法。当授权动作时,这个类名将被用来判断使用哪个策略: 181 | 182 | ``` 183 | public function create(Request $request) 184 | { 185 | $this->authorize('create', Post::class); 186 | 187 | // 当前用户可以新建博客... 188 | } 189 | ``` 190 | 191 | - 其他应用策略的方法 192 | 193 | ``` 194 | $user->can('update', $post)// 通过用户模型对象使用 195 | @can('update', $post)// 在blade模版中使用 196 | @can('create', App\Post::class)// 在blade模版中使用,不需要指定模型的动作 197 | ``` 198 | 199 | -------------------------------------------------------------------------------- /src/CodeConvention/Route.md: -------------------------------------------------------------------------------- 1 | ### 路由 2 | 3 | #### 不要使用路由闭包 4 | 5 | 不要在路由配置文件里书写『闭包路由』或者其他业务逻辑代码,因为一旦使用将无法使用路由缓存 。 6 | 7 | 路由器要保持干净整洁,绝不 放置除路由配置以外的其他程序逻辑。 8 | 9 | #### 使用RESTful风格的路由 10 | 11 | |动作| URI |行为| 路由名称 | 12 | |------|------|-----|------| 13 | |GET | /photos | index |photos.index| 14 | |GET | /photos/create| create| photos.create| 15 | |POST| /photos| store| photos.store| 16 | |GET | /photos/{photo} | show| photos.show| 17 | |GET | /photos/{photo}/edit| edit| photos.edit| 18 | |PUT/PATCH| /photos/{photo} |update |photos.update | 19 | |DELETE | /photos/{photo} |destroy | photos.destroy| 20 | 21 | [RESTful API 设计指南](http://www.ruanyifeng.com/blog/2014/05/restful_api.html) 22 | 23 | 下面是RESTful URI的一些例子: 24 | 25 | - GET /zoos:列出所有动物园 26 | - POST /zoos:新建一个动物园 27 | - GET /zoos/ID:获取某个指定动物园的信息 28 | - PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息) 29 | - PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息) 30 | - DELETE /zoos/ID:删除某个动物园 31 | - GET /zoos/ID/animals:列出某个指定动物园的所有动物 32 | - DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物 33 | 34 | 创建资源路由 35 | 36 | ``` 37 | php artisan make:controller PhotoController --resource 38 | ``` 39 | 40 | 给资源控制器注册一个资源路由 41 | ``` 42 | Route::resource('photos', 'PhotoController'); 43 | ``` 44 | 45 | 声明用于 APIs 的资源路由 (排除显示 HTML 模板的路由(如 create 和 edit )) 46 | ``` 47 | Route::apiResource('photo', 'PhotoController'); 48 | ``` 49 | 50 | 声明资源路由时,你可以指定控制器处理的部分行为,而不是所有默认的行为: 51 | 52 | ``` 53 | Route::resource('photo', 'PhotoController', ['only' => [ 54 | 'index', 'show' 55 | ]]); 56 | 57 | 58 | Route::resource('photo', 'PhotoController', ['except' => [ 59 | 'create', 'store', 'update', 'destroy' 60 | ]]); 61 | 62 | ``` 63 | 64 | #### 路由缓存 65 | 66 | >{note} 基于闭包的路由不能被缓存。如果要使用路由缓存,你必须将所有的闭包路由转换成控制器类路由。 67 | 68 | 如果你的应用只使用了基于控制器的路由,那么你应该充分利用 Laravel 的路由缓存。使用路由缓存将极大地减少注册所有应用路由所需的时间。某些情况下,路由注册的速度甚至可以快一百倍。要生成路由缓存,只需执行 Artisan 命令 route:cache: 69 | ``` 70 | php artisan route:cache 71 | ``` 72 | 运行这个命令之后,每一次请求的时候都将会加载缓存的路由文件。如果你添加了新的路由,你需要生成一个新的路由缓存。因此,你应该只在生产环境运行`route:cache`命令: 73 | 74 | 你可以使用`route:clear`命令清除路由缓存: 75 | ``` 76 | php artisan route:clear 77 | ``` 78 | -------------------------------------------------------------------------------- /src/CodeConvention/Semantics.md: -------------------------------------------------------------------------------- 1 | ### 命名语义话 2 | 3 | 整个规范中我们多次强调了命名规范的事情,目的就是希望能够通过够通过合理的命名让程序能够跟语义话, 4 | 让开发人员看到代码就能大概知道功能和场景是什么。 5 | 6 | 7 | #### 常见的错误: 8 | 9 | - Model中属性和方法的命名一定都是针对数据层面的,代表的是数据的属性和行为,所以命名时不能出现`getXXX`类似的动宾形式词汇, 10 | 具体请看数据模型章节。 11 | 12 | 13 | - 所有变量和方法的命名都要与当前逻辑和场景相关,比如当前变量里存放的是用户数据,那么你不能把变量名命名成不关的`$postData`, 14 | 当前逻辑里处理的是文章下面,那么调用的Service方法就应该是`setArticleOff`而不是`setPostOff`。这一条列的情况特别常见于拷贝别的项目代码到当前项目用的情况,即使拷贝过来的代码能用也请把命名更正到当前业务相关的名称。 15 | 16 | - Controller命名也一味以`getXXX`开头,`getXXX`这个句型在开发人员中实在是太受欢迎,控制器方法的命名也是尽量以[资源路由](https://laravel-china.org/docs/laravel/5.5/controllers/1296#resource-controllers)里的风格来给方法命名。 17 | 18 | 19 | #### 封装方法带来收益不仅仅是复用性 20 | 21 | 我们都知道封装方法可以提高代码块的复用性,其实除此之外还会提高程序的可读性。 22 | 23 | 比如项目中生成用户unique id时我们使用了如下一行代码 24 | ```angular2html 25 | $uniqueId = uniqid(); 26 | ``` 27 | 但我们还是建议将其封装成一个方法, 并按照功能命名方法名称 28 | ``` 29 | function genUserIdentifier()// short for generate user identifier 30 | { 31 | return uniqid(); 32 | } 33 | ``` 34 | 35 | 这样我们在用到生成用户标示的地方调用这个方法就知道,此时程序生成的是一个用户标示 36 | ```angular2html 37 | $identifier = genUserIdentifier(); 38 | ``` 39 | 40 | 同时根据关注点分离原则,调用他的程序不需要关注生成唯一标示的细节,即使我们需要调整生成规则时只需要修改`genUserIdentifier`里生成unique id的规则即可。 41 | -------------------------------------------------------------------------------- /src/CodeConvention/Service.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### 所有的业务要封装在Service类里 4 | 5 | 上面说了模型中应该只写与数据表抽象相关的代码,但是业务逻辑往往都是伴随着数据更新的,所以业务代码应该单独抽离到Service层级,由Service再去调用Model里定义的方法来实现数据更新。 6 | 7 | ``` 8 | class AritcleService 9 | { 10 | public function setArticleOffline(Article $article) 11 | { 12 | if ($article->is_top == 1) {//如果是置顶文章需要将文章取消置顶 13 | $this->cancelTopArticle($article); 14 | } 15 | $article->status = Article::STATUS_OFFLINE; 16 | $article->offline_time = date('Y-m-d H:i:s'); 17 | $article->save(); 18 | 19 | return true; 20 | } 21 | 22 | /** 23 | * 取消文章的置顶 24 | * @param \App\Models\Article $article 25 | */ 26 | public function cancelTopArticle($article) 27 | { 28 | if (TopArticle::specificArticle($article->id)->count()) { 29 | //删除置顶表里的记录(待上线的置顶文章上线后置顶表中才能有相应的记录) 30 | TopArticle::specificArticle($article->id)->delete(); 31 | } 32 | //将文章的is_top字段置为0 33 | $article->is_top = 0; 34 | $article->save(); 35 | 36 | return true; 37 | } 38 | } 39 | ``` 40 | 41 | #### 单一业务中涉及多个Model的读写 42 | 43 | ``` 44 | class MemberService 45 | { 46 | /** 47 | * 恢复会员卡状态和库存 48 | * 49 | * @param \App\Models\Card $card 50 | * @return boolean 51 | */ 52 | public function restoreCard(Card $card) 53 | { 54 | if($card->status != Card::STATUS_PREUSED) {//只有发放中状态的卡才能恢复 55 | return false; 56 | } 57 | DB::beginTransaction(); 58 | try { 59 | $card->status = Card::STATUS_UNUSED;//将这张卡号设置为未使用 60 | $card->save(); 61 | RemainingNum::firstData()->increment('remaining_num');//将剩余会员卡数加1 62 | DB::commit(); 63 | } catch (QueryException $exc) { 64 | \Log::error('Restore Card Error: ' . $exc->getMessage()); 65 | DB::rollback(); 66 | return false; 67 | } 68 | 69 | return true; 70 | } 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /src/CodeConvention/SingleResponsibility.md: -------------------------------------------------------------------------------- 1 | ### 单一责任原则 2 | 3 | 一个方法应该只有一个职责,一个强大的功能由多个单一职责的方法组合而成,而不是把所有功能都写到一个方法里,如果那样的话方法复用的可能性几乎为零,并且难以维护。 4 | 5 | 一个类同样应该是针对单一事物的抽象,里边包含的方法和属性应该只与类代表的事物有关。 6 | 7 | 错误: 8 | 9 | ``` 10 | public function getFullNameAttribute() 11 | { 12 | if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) { 13 | return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name; 14 | } else { 15 | return $this->first_name[0] . '. ' . $this->last_name; 16 | } 17 | } 18 | ``` 19 | 20 | 正确: 21 | 22 | ``` 23 | public function getFullNameAttribute() 24 | { 25 | return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort(); 26 | } 27 | 28 | public function isVerifiedClient() 29 | { 30 | return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified(); 31 | } 32 | 33 | public function getFullNameLong() 34 | { 35 | return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name; 36 | } 37 | 38 | public function getFullNameShort() 39 | { 40 | return $this->first_name[0] . '.' . $this->last_name; 41 | } 42 | ``` 43 | 44 | 同时单一责任原则也符合关注点分离原则的设计思路,先将复杂问题做合理的分解,再分别仔细研究问题的不同侧面(关注点),最后综合各方面的结果,合成整体的解决方案。 45 | -------------------------------------------------------------------------------- /src/CodeConvention/Throttle.md: -------------------------------------------------------------------------------- 1 | # API请求频率限制 2 | 3 | 在向公网提供API供外部访问数据时,为了避免被恶意攻击除了token认证最好还要给API加上请求频次限制,而在Laravel中从5.2开始框架自带的组件Throttle就支持访问频次限制了,并提供了一个Throttle中间件供我们使用,不过Throttle中间件在访问API频次达到限制后会返回一个HTML响应告诉你请求超频,在应用中我们往往更希望返回一个API响应而不是一个HTML响应,所以在文章中会提供一个自定义的中间件替换默认的Throttle中间件来实现自定义响应内容。 4 | 5 | ### 访问频次限制概述 6 | 7 | 频次限制经常用在API中,用于限制独立请求者对特定API的请求频率。例如,如果设置频次限制为每分钟1000次,如果一分钟内超过这个限制,那么服务器就会返回 `429: Too Many Attempts.`响应。 8 | 9 | 通常,一个编码良好的、实现了频率限制的应用还会回传三个响应头: `X-RateLimit-Limit`, `X-RateLimit-Remaining`和 `Retry-After`(`Retry-After`头只有在达到限制次数后才会返回)。 `X-RateLimit-Limit`告诉我们在指定时间内允许的最大请求次数, `X-RateLimit-Remaining`指的是在指定时间段内剩下的请求次数, `Retry-After`指的是距离下次重试请求需要等待的时间(s) 10 | 11 | >注意:每个应用都会选择一个自己的频率限制时间跨度,Laravel应用访问频率限制的时间跨度是一分钟,所以频率限制限制的是一分钟内的访问次数。 12 | 13 | ### 使用Throttle中间件 14 | 15 | 让我们先来看看这个中间件的用法,首先我们定义一个路由,将中间件throttle添加到其中,throttle默认限制每分钟尝试60次,并且在一分钟内访问次数达到60次后禁止访问: 16 | 17 | ``` 18 | Route::group(['prefix'=>'api','middleware'=>'throttle'], function(){ 19 | Route::get('users', function(){ 20 | return \App\User::all(); 21 | }); 22 | }); 23 | ``` 24 | 访问路由/api/users时你会看见响应头里有如下的信息: 25 | 26 | >X-RateLimit-Limit: 60 27 | X-RateLimit-Remaining: 58 28 | 29 | 如果请求超频,响应头里会返回`Retry-After`: 30 | 31 | >Retry-After: 58 32 | X-RateLimit-Limit: 60 33 | X-RateLimit-Remaining: 0 34 | 35 | 上面的信息表示58秒后页面或者API的访问才能恢复正常。 36 | 37 | #### 定义频率和重试等待时间 38 | 频率默认是60次可以通过throttle中间件的第一个参数来指定你想要的频率,重试等待时间默认是一分钟可以通过throttle中间件的第二个参数来指定你想要的分钟数。 39 | 40 | ``` 41 | Route::group(['prefix'=>'api','middleware'=>'throttle:5'],function(){ 42 | Route::get('users',function(){ 43 | return \App\User::all(); 44 | }); 45 | });//频次上限5 46 | 47 | Route::group(['prefix'=>'api','middleware'=>'throttle:5,10'],function(){ 48 | Route::get('users',function(){ 49 | return \App\User::all(); 50 | }); 51 | });//频次上限5,重试等待时间10分钟 52 | ``` 53 | ***注: Laravel5.5的api route文件里的路由会默认应用api中间件组,在这个中间件组中包含throttle中间件并且设定了默认每分钟60次的限制*** 54 | 55 | ### 自定义Throttle中间件,返回API响应 56 | 在请求频次达到上限后Throttle除了返回那些响应头,返回的响应内容是一个HTML页面,页面上告诉我们Too Many Attempts。在调用API的时候我们显然更希望得到一个json响应,下面提供一个自定义的中间件替代默认的Throttle中间件来自定义响应信息。 57 | 58 | 首先创建一个ThrottleRequests中间件: `php artisan make:middleware ThrottleRequests`. 59 | 60 | 将下面的代码拷贝到`app/Http/Middlewares/ThrottleReuqests`文件中: 61 | ``` 62 | limiter = $limiter; 87 | } 88 | 89 | /** 90 | * Handle an incoming request. 91 | * 92 | * @param \Illuminate\Http\Request $request 93 | * @param \Closure $next 94 | * @param int $maxAttempts 95 | * @param int $decayMinutes 96 | * @return mixed 97 | */ 98 | public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1) 99 | { 100 | $key = $this->resolveRequestSignature($request); 101 | 102 | if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) { 103 | return $this->buildResponse($key, $maxAttempts); 104 | } 105 | 106 | $this->limiter->hit($key, $decayMinutes); 107 | 108 | $response = $next($request); 109 | 110 | return $this->addHeaders( 111 | $response, $maxAttempts, 112 | $this->calculateRemainingAttempts($key, $maxAttempts) 113 | ); 114 | } 115 | 116 | /** 117 | * Resolve request signature. 118 | * 119 | * @param \Illuminate\Http\Request $request 120 | * @return string 121 | */ 122 | protected function resolveRequestSignature($request) 123 | { 124 | return $request->fingerprint(); 125 | } 126 | 127 | /** 128 | * Create a 'too many attempts' response. 129 | * 130 | * @param string $key 131 | * @param int $maxAttempts 132 | * @return \Illuminate\Http\Response 133 | */ 134 | protected function buildResponse($key, $maxAttempts) 135 | { 136 | $message = json_encode([ 137 | 'error' => [ 138 | 'message' => 'Too many attempts, please slow down the request.' //may comes from lang file 139 | ], 140 | 'status_code' => 4029 //your custom code 141 | ]); 142 | 143 | $response = new Response($message, 429); 144 | 145 | $retryAfter = $this->limiter->availableIn($key); 146 | 147 | return $this->addHeaders( 148 | $response, $maxAttempts, 149 | $this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter), 150 | $retryAfter 151 | ); 152 | } 153 | 154 | /** 155 | * Add the limit header information to the given response. 156 | * 157 | * @param \Symfony\Component\HttpFoundation\Response $response 158 | * @param int $maxAttempts 159 | * @param int $remainingAttempts 160 | * @param int|null $retryAfter 161 | * @return \Illuminate\Http\Response 162 | */ 163 | protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null) 164 | { 165 | $headers = [ 166 | 'X-RateLimit-Limit' => $maxAttempts, 167 | 'X-RateLimit-Remaining' => $remainingAttempts, 168 | ]; 169 | 170 | if (!is_null($retryAfter)) { 171 | $headers['Retry-After'] = $retryAfter; 172 | $headers['Content-Type'] = 'application/json'; 173 | } 174 | 175 | $response->headers->add($headers); 176 | 177 | return $response; 178 | } 179 | 180 | /** 181 | * Calculate the number of remaining attempts. 182 | * 183 | * @param string $key 184 | * @param int $maxAttempts 185 | * @param int|null $retryAfter 186 | * @return int 187 | */ 188 | protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null) 189 | { 190 | if (!is_null($retryAfter)) { 191 | return 0; 192 | } 193 | 194 | return $this->limiter->retriesLeft($key, $maxAttempts); 195 | } 196 | } 197 | ``` 198 | 然后将`app/Http/Kernel.php`文件里的: 199 | 200 | ``` 201 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 202 | ``` 203 | 替换成: 204 | 205 | ``` 206 | 'throttle' => \App\Http\Middleware\ThrottleRequests::class, 207 | ``` 208 | 就大功告成了。 209 | 210 | ### Throttle信息存储 211 | 最后再来说下,`Throttle`这些频次数据都是存储在`cache`里的,`Laravel`默认的`cache driver`是`file`也就是`throttle`信息会默认存储在框架的`cache`文件里, 如果你的`cache driver`换成redis那么这些信息就会存储在redis里,记录的信息其实很简单,`Throttle`会将请求对象的signature(以HTTP请求方法、域名、URI和客户端IP做哈希)作为缓存key记录客户端的请求次数。 212 | -------------------------------------------------------------------------------- /src/CodeConvention/UseRuleObject.md: -------------------------------------------------------------------------------- 1 | ## 使用规则对象实现超级复杂的验证规则 2 | 3 | 有的时候由于业务需求的需要,有的字段验证规则比较复杂,比如不简简单单依赖于当前字段值来校验参数是否合法可能还需要依赖请求中的其他字段值去数据库查询后才能够判定当前字段值是否合法, 4 | 针对这种对单字段复杂的验证规则我们应该使用规则对象,而不是把这些复杂的查询验证写在控制器或者其他地方。 5 | ``` 6 | request = $request; 23 | } 24 | 25 | 26 | /** 27 | * 判断验证规则是否通过。 28 | * 29 | * @param string $attribute 30 | * @param mixed $value 31 | * @return bool 32 | */ 33 | public function passes($attribute, $value) 34 | { 35 | //some complicated judgement 36 | if ($value == $something && $this->request->input('key') { 37 | // if element is not valid then return false 38 | return false; 39 | } 40 | 41 | return true 42 | } 43 | 44 | /** 45 | * 获取验证错误信息。 46 | * 47 | * @return string 48 | */ 49 | public function message() 50 | { 51 | return 'The :attribute must be ......'; 52 | } 53 | } 54 | ``` 55 | 56 | 一旦规则对象被定义好后,你可以通过将规则对象的实例传递给其他验证规则来将其附加到验证器: 57 | 58 | ``` 59 | 60 | ['required', new ValidElementRule($this)] 87 | ]; 88 | } 89 | } 90 | 91 | ``` 92 | -------------------------------------------------------------------------------- /src/NginxConf/OneDomainHostMultiSites.conf: -------------------------------------------------------------------------------- 1 | server 2 | { 3 | listen 80; 4 | listen 443 ssl; 5 | server_name laravel-best-prictice.kevinyan.com; 6 | index index.html index.php; 7 | error_log /data/log/nginx/{your-project}/{your-project}.error.log; 8 | access_log /data/log/nginx/{your-project}/{your-project}.access.log; 9 | set $fe_root_path '/data/{your-front-end-root-path}/dist'; 10 | set $rd_root_path '/data/{your-rd-root-path}/public'; 11 | root $fe_root_path; 12 | 13 | ssl_certificate /usr/local/nginx/conf/ssl/server.pem; 14 | ssl_certificate_key /usr/local/nginx/conf/ssl/server.key; 15 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 16 | 17 | location / { 18 | try_files $uri $uri/ /index.php?$query_string; 19 | } 20 | 21 | location ~ \.php { 22 | root $rd_root_path; 23 | try_files $uri =404; 24 | fastcgi_index /index.php; 25 | fastcgi_pass 127.0.0.1:9000; 26 | include fastcgi_params; 27 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 28 | fastcgi_param PATH_INFO $fastcgi_path_info; 29 | fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; 30 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 31 | } 32 | 33 | location ~* ^/(css|img|js|flv|swf|download)/(.+)$ { 34 | root $fe_root_path; 35 | } 36 | 37 | location ~ /\.ht { 38 | deny all; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/TestCase/FeatureTest.md: -------------------------------------------------------------------------------- 1 | # 功能测试 2 | 3 | 功能测试主要是针对项目中对外提供的各种Feature的验证,由于后端项目里只提供API所以这里讲的功能测试更多是针对API返回结果的测试。 4 | 5 | ### 生成功能测试用例 6 | ``` 7 | php artisan make:test SampleTest 8 | ``` 9 | 10 | ### 发送JSON请求 11 | 12 | 因为我们的API都接受JOSN input所以在测试用例里使用`json`方法发送JSON请求。 13 | `json`方法的参数: 14 | - method: 定义请求方法 GET、POST、PUT、DELETE等 15 | - uri: 接口的路由 16 | - data: 请求体里的数据,类型为associate array如`['key' => 'name', ...]` 17 | - headers: 请求头,类型为associate array如`['Authorization' => 'Bearer ejoYg......', ...]` 18 | 19 | ### 在测试用例里打印响应结果 20 | 写用例时经常会需要打印响应的返回值来帮助调试,使用`dump`方法可以直接输出响应,假设有如下用例: 21 | 22 | ```$xslt 23 | class EngineerTest extends TestCase 24 | { 25 | /** 26 | * @test 27 | */ 28 | public function itShouldReceiveSuccessResponeWhenFetchEngineerInfo() 29 | { 30 | $response = $this->json('GET', '/api/wb/engineer/info'); 31 | $response->assertStatus(200); 32 | } 33 | } 34 | ``` 35 | 如果用例执行失败,想要看看响应返回的是什么结果,那么只需要在发送json请求时链式调用dump()方法即可: 36 | ``` 37 | $response = $this->json('GET', '/api/wb/engineer/info')->dump(); 38 | ``` 39 | 然后在项目根目录下执行如下命令就能在终端里看到格式化好的响应返回值: 40 | ``` 41 | phpunit ./tests/Feature/EngineerTest.php 42 | ``` 43 | 44 | ### 断言方法 45 | 列几个常用的断言方法 46 | 47 | - assertStatus 断言HTTP响应的状态码 48 | - assertJson 断言返回值是给定JSON的超集(包含参数中给定的JSON) 49 | - assertJsonStructure 断言返回值的JSON符合制定的JOSN结构(支持嵌套结构的校验) 50 | 51 | 下面是使用的例子: 52 | ``` 53 | class EngineerTest extends TestCase 54 | { 55 | /** 56 | * @test 57 | */ 58 | public function itShouldReceiveSuccessResponeWhenFetchEngineerInfo() 59 | { 60 | $response = $this->json('GET', '/api/wb/engineer/info'); 61 | $response->assertStatus(200); 62 | $response->assertJson(['statusCode' => 200,]); 63 | $response->assertJsonStructure([ 64 | 'data' => [ 65 | 'code', 66 | 'name', 67 | 'avatar', 68 | 'business_id', 69 | ] 70 | ]); 71 | } 72 | } 73 | ``` 74 | 因为我们所有API响应的基础格式固定,接口的返回数据都放在data键的值里面,所以上面验证了返回数据data里必须包含`code`, `name`, `avatar`和`business_id`这几个字段。 75 | 更多断言响应的方法可以参考`Illuminate\Foundation\Testing\TestResponse`里的源码,代码注释里标明了每个方法的作用。 76 | 77 | 有时候接口返回的JSON数据里会返回列表数据,针对这种数据数据我们该如何断言每个列表子项目的结构呢?比如我有一个接口返回如下的JSON数据: 78 | ``` 79 | HTTP/1.1 200 Success 80 | { 81 | "statusCode": 200, 82 | "message": { 83 | "info": "Success" 84 | }, 85 | "data": { 86 | "user_avatar": "http://gavatar.com/xhfnl", 87 | "engineer_avatar": "http://gavatar.com/xhfnl", 88 | "message_list": [ 89 | { 90 | "session_id": "4nksy34L-CJ", 91 | "chat_type": "Chat", 92 | "message_type": "Text", 93 | "media_url": "", 94 | "text": "1", 95 | "send_type": 100, 96 | "lenovo_id": "10087115893", 97 | "open_id": "ohP4Z6PuEzGB6Sr61VqWOtKZtXpE", 98 | "engineer_code": "A03976", 99 | "send_time": "2018-11-02 20:16:18", 100 | "emotion": 2.5, 101 | "client_type": "wechat", 102 | "access_name": "联想服务公众号" 103 | }, 104 | ] 105 | } 106 | } 107 | ``` 108 | 那么针对`message_list`这个列表里的子元素我们应该怎么断言它们的结构呢,我们可以使用`*`来假设所有的列表内容都包含至少数组里面的内容: 109 | 110 | ``` 111 | first(); 126 | $response = $this->json('get', '/session/' . $session->id . '/messages'); 127 | $response->assertStatus(200); 128 | $response->assertJson(['statusCode' => 200]); 129 | $response->assertJsonStructure([ 130 | 'data' => [ 131 | 'user_avatar', 132 | 'engineer_avatar', 133 | 'message_list' => [ 134 | '*' => [ 135 | 'session_id', 136 | 'chat_type', 137 | 'message_type', 138 | 'media_url', 139 | ] 140 | ], 141 | ] 142 | ]); 143 | } 144 | } 145 | ``` 146 | -------------------------------------------------------------------------------- /src/TestCase/TDD.md: -------------------------------------------------------------------------------- 1 | # 测试驱动开发 2 | 3 | 4 | **测试驱动开发**(英语:Test-driven development,缩写为TDD)是一种[软件开发过程](https://zh.wikipedia.org/wiki/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E8%BF%87%E7%A8%8B)中的应用方法,由[极限编程](https://zh.wikipedia.org/wiki/%E6%9E%81%E9%99%90%E7%BC%96%E7%A8%8B)中倡导,以其倡导先写测试程序,然后编码实现其功能得名。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。 5 | 6 | 下面几篇文章是我翻译的国外开发者在Medium上分享的TDD教程: 7 | 8 | - [简单的十一步在Laravel中实现测试驱动开发](https://segmentfault.com/a/1190000015653724) 9 | - [Laravel测试驱动开发之正向单元测试](https://segmentfault.com/a/1190000015738971) 10 | - [Laravel测试驱动开发之反向单元测试](https://segmentfault.com/a/1190000015777098) 11 | - [Laravel测试驱动开发之功能测试](https://segmentfault.com/a/1190000015922734) 12 | --------------------------------------------------------------------------------