绝不 在路由配置文件里书写『闭包路由』或者其他业务逻辑代码,因为一旦使用将无法使用 路由缓存 。
路由器要保持干净整洁,绝不 放置除路由配置以外的其他程序逻辑。
路由中的控制器方法定义,必须 使用 Controller::class
这种方式加载。
✅ 正确
phpRoute::get('/photos', [PhotosController::class, 'index'])->name('photos.index');
❌ 错误的例子:
phpRoute::get('/photos', 'PhotosController@index')->name('photos.index');
这样做 IDE 可以加代码索引。有两个好处:
必须 优先使用 Restful 路由,配合资源控制器使用,见 文档。
超出 Restful 路由的,应该 模仿上图的方式来定义路由。
一般资源路由定义:
phpRoute::resource('photos', PhotosController::class);
等于以下路由定义:
phpRoute::get('/photos', [PhotosController::class, 'index'])->name('photos.index');
Route::get('/photos/create', [PhotosController::class, 'create'])->name('photos.create');
Route::post('/photos', [PhotosController::class, 'store'])->name('photos.store');
Route::get('/photos/{photo}', [PhotosController::class, 'show'])->name('photos.show');
Route::get('/photos/{photo}/edit', [PhotosController::class, 'edit'])->name('photos.edit');
Route::put('/photos/{photo}', [PhotosController::class, 'update'])->name('photos.update');
Route::delete('/photos/{photo}', [PhotosController::class, 'destroy'])->name('photos.destroy');
使用 resource
方法时,如果仅使用到部分路由,必须 使用 only
列出所有可用路由:
phpRoute::resource('photos', PhotosController::class, ['only' => ['index', 'show']]);
绝不 使用 except
,因为 only
相当于白名单,相对于 except
更加直观。路由使用白名单有利于养成『安全习惯』。
资源路由路由 URI 必须 使用复数形式,如:
/photos/create
/photos/{photo}
错误的例子如:
/photo/create
/photo/{photo}
在允许使用路由 模型绑定 的地方 必须 使用。
模型绑定代码 必须 放置于 app/Providers/RouteServiceProvider.php
文件的 boot
方法中:
php public function boot()
{
Route::bind('user_name', function ($value) {
return User::where('name', $value)->first();
});
Route::bind('photo', function ($value) {
return Photo::find($value);
});
parent::boot();
}
注:如果使用了
{id}
作为参数,Laravel 已经默认做了绑定。
出于安全考虑,应该 使用全局路由器参数限制,详见 文档。
必须 在 RouteServiceProvider
文件的 boot
方法里定义模式:
php/**
* 定义你的路由模型绑定, pattern 过滤器等。
*
* @return void
*/
public function boot()
{
Route::pattern('id', '[0-9]+');
parent::boot();
}
模式一旦被定义,便会自动应用到所有使用该参数名称的路由上:
phpRoute::get('users/{id}', [UsersController::class, 'show']);
Route::get('photos/{id}', [PhotosController::class, 'show']);
只有在 id
为数字时,才会路由到控制器方法中,否则 404 错误。
除了 resource
资源路由以外,其他所有路由都 必须 使用 name
方法进行命名。
必须 使用『资源前缀』作为命名规范,如下的 users.follow
,资源前缀的值是 users.
:
phpRoute::post('users/{id}/follow', [UsersController::class, 'follow'])->name('users.follow');
获取 URL 必须 遵循以下优先级:
$model->link()
route
方法url
方法在 Model 中创建 link()
方法:
phppublic function link($params = [])
{
$params = array_merge([$this->id], $params);
return route('models.show', $params);
}
所有单个模型数据链接使用:
php$model->link();
// 或者添加参数
$model->link($params = ['source' => 'list'])
『单个模型 URI』经常会发生变化,这样做将会让程序更加灵活。
除了『单个模型 URI』,其他路由 必须 使用 route
来获取 URL:
php$url = route('profile', ['id' => 1]);
无法使用 route
的情况下,可以 使用 url
方法来获取 URL:
phpurl('profile', [1]);
项目中的大部分业务逻辑,都应该封装到 Service 层。这不仅能更好地组织代码,还方便单元测试。
Model 的操作,涉及到业务逻辑的,绝不放置于控制器方法或模型文件中。
控制器方法只处理请求逻辑。模型只处理模型定义,以及数据关联逻辑。
业务逻辑必须封装到对应的 ModelService 类中。
例如 LearnKu.com 的 Reply 模型,用户发布 Reply 时需要的逻辑,如发送通知给话题的作者,或者增加话题的评论数等操作,放置于 ReplyService 类的 create 方法。
必须参照 Laravel Model 的方法来命名,如:
php$reply_service->create();
$reply_service->all();
$reply_service->update();
$reply_service->delete();
其他类型的类,都应该使用 Service 来封装,例如说:
请求第三方接口的类(SendCloudService)
图片处理的工具类(ImageService)
包含业务逻辑的类(对 Elasticsearch 封装的 SearchService )
所有的 Service 类都必须存放于 app/Services
目录中(注意是复数)。
应该避免直接将 Service 类放置于 app/Services
目录下,应该考虑通过业务逻辑,将其归类于子目录中。如:
Auth —— 存放登录、授权相关的 Service; Payment —— 存放支付相关的 Service; Book —— 存放课程相关的 Service.
必须 做到 Service 类无状态。
无状态意味着是无论在控制器方法、命令行、测试代码中,皆可调用。
❌ 错误的例子:
php// CommentService
public function create($content)
{
return Comment::create([
'content' => $content,
'user_id' => Auth::user()->id
]);
}
// PostService
public function update(Request $request)
{
return $this->comments()->create([
'content' => $request->get('content'),
'category_id' => $request->category_id
'user_id' => Auth::user()->id
]);
}
✅正确的例子
php// CommentService
public function create($content, $user)
{
return Comment::create([
'content' => $content,
'user_id' => $user->id
]);
}
// PostService
public function create($content, $category_id, $user)
{
return Post::create([
'content' => $content,
'category_id' => $category_id,
'user_id' => $user->id
]);
}
所有的数据模型文件,都 必须 存放在:app/Models/
文件夹中。
命名空间:
phpnamespace App\Models;
所有的 Eloquent 数据模型 都 必须 继承统一的基类 App\Models\Model
,此基类存放位置为 /app/Models/Model.php
,内容参考以下:
php<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model as EloquentModel;
class Model extends EloquentModel
{
public function scopeRecent($query)
{
return $query->orderBy('id', 'desc');
}
public function scopeOlder($query)
{
return $query->orderBy('id', 'asc');
}
public function scopeByUser($query, User $user)
{
return $query->where('user_id', $user->id);
}
}
以 Photo 数据模型作为例子继承 Model 基类:
php<?php
namespace App\Models;
class Photo extends Model
{
protected $fillable = ['id', 'user_id'];
public function user()
{
return $this->belongsTo(User::class);
}
}
数据模型相关的命名规范:
必须
为「单数」, 如:App\Models\Photo
必须
为「单数」,如:app/Models/Photo.php
必须
为「复数」,多个单词情况下使用「Snake Case」 如:photos
, my_photos
必须
为「复数」,如:2014_08_08_234417_create_photos_table.php
必须
为「复数」,如:PhotosTableSeeder.php
必须
为「Snake Case」,如:view_count
, is_vip
必须
为「id」必须
为「resource_id」,如:user_id
, post_id
必须
为「resource_id」,如:$user_id
, $post_id
数据关联内部 必须
使用「resource_id」,假如 User 模型有 id 和 UUID 两个唯一字段,其他模型关联 User 必须
使用 id 字段。也就是在其他模型的数据表里,使用 user_id
字段。
模型间,相同的模型逻辑,例如 User 和 Topic 都有一个 settings JSON 字段,用来实现单个模型的设置功能,应该 利用 Trait 来实现逻辑代码。
所有模型 Traits 必须存放于app/Models/Traits
目录下。
注意: 业务逻辑请使用 ModelService 模式来组织代码。
绝不 使用 Repository,因为我们不是在写 JAVA 代码,太多封装就成了「过度设计(Over Designed)」,极大降低了编码愉悦感,使用 MVC 够傻够简单。
代码的可读性,维护和开发的便捷性,直接关系到程序员开发时的愉悦感,直接影响到项目推进效率和程序 Debug 速度。
Laravel 的 Model 全局作用域 允许我们为给定模型的所有查询添加默认的条件约束。
所有的全局作用域都 必须 统一使用 闭包定义全局作用域
,如下:
php/**
* 数据模型的启动方法
*
* @return void
*/
protected static function boot()
{
parent::boot();
static::addGlobalScope('age', function(Builder $builder) {
$builder->where('age', '>', 200);
});
}
先看一段代码,以下是 Post 模型里创建文章评论的方法:
php public function createComment($content)
{
return $this->comments()->create([
'content' => $content,
'user_id' => Auth::user()->id
]);
}
注意 Auth::user()->id
,在数据层里使用当前登录用户状态,是默认假设这段代码永远是在 Web 用户请求下执行的。
然而事实并非如此,有时候你可能会在命令行下触发调用这个 createComment()
方法,有时候是管理员在后台触发,有时候是队列里触发。
一个最佳实践的做法是, 绝不 在数据层里使用用户登录状态信息。如果需要用户信息,必须 将其作为依赖进行传参,如以上代码可修改为:
php public function createComment($content, $user)
{
return $this->comments()->create([
'content' => $content,
'user_id' => $user->id
]);
}
在有需要的地方调用时,以参数传入:
phpPost::createComment($content, Auth::user())
命令行书写某些特殊逻辑时,例如使用 1 号用户的身份创建评论:
phpPost::createComment($content, User::find(1))
数据层,也就是模型里,不能跟用户的登录状态挂钩。
如果是一个长期维护的项目,必须 为模型文件按业务逻辑做分层。
一个长期维护的项目,很容易就会出现几十上百的表,每个表对应一个 Model 文件。笔者曾维护过一个项目,两百多个 Model 文件,app/models 目录完全没法看。
如果你能预期到 Model 文件会很多,那就 必须 做好目录划分,按照业务逻辑来分,以 LearnKu.com 为例,app/models 的目录结构如下:
├── Book │ ├── Article.php │ └── Book.php ├── Community │ ├── Reply.php │ └── Topic.php └── Project ├── ProjectAuthor.php └── Project.php
应该 尽量避免使用 Laravel 的 模型事件。
使用模型事件的问题在于,其职能很难界定,所有的业务逻辑都能写到模型事件中。
而我们在项目中,业务逻辑代码规都封装到 Service 层,开发者在书写业务逻辑代码时,就会面临两种选择。
例如说 ReplyService 类的 create 方法,将创建评论时需要的逻辑,如发送通知给话题的作者,或者增加话题的评论数等操作,放置于此方法中,效果跟放在 ReplyObserver 中是一样的。
不一样的是, ReplyService 是显示地书写业务逻辑,代码可读性比模型事件更高。
模型事件另一个缺点就是,模型操作,附带太多逻辑,有太多的 Side Effect,并且是隐性的。模型操作是一个使用频率很高的功能,在有些场景中,你就想创建一个 Reply,但是不想通知到用户,例如说 Seed 时。虽然 Laravel 有提供模型方法让你暂时关闭模型事件,但这在实践中,我见过太多开发者经常会忘记此操作。
必须 优先使用 Restful 资源控制器 。
必须 使用资源的复数形式,如:
- 类名:PhotosController
- 文件名:PhotosController.php
错误的例子:
- 类名:PhotoController
- 文件名:PhotoController.php
必须 保持控制器文件代码行数最小化,还有可读性。一般来讲,一个方法不应该超过 20 行代码,业务逻辑比较多,请封装到一个 Service 类里。
不应该 为「方法」书写很明显的注释,这要求方法取名要足够合理,不需要过多注释。
应该 为一些复杂的逻辑代码块书写注释,主要介绍产品逻辑 - 为什么要这么做。
,最重要的,写好上下文。
不应该 在控制器中书写「私有方法」,控制器里 应该
只存放「路由动作方法」。
多余的业务逻辑,请封装到 Service 类中。
绝不 遗留「死方法」,就是没有用到的方法,控制器里的所有方法,都应该被使用到,否则应该删除。
绝不 在控制器里批量注释掉代码,无用的逻辑代码就必须清除掉。
项目中会使用 Git 来做版本控制,删了后面也可以从记录中找到,无需将这些无用的代码留在项目中。
首先请熟悉以下的两个文档:
API 设计上有无法抉择的地方,应该参考 GitHub 的 API 文档:
GitHub 的 RESTful API 设计是业内比较知名的。
所有的 API,早期设计时都 必须 考虑版本控制。
随着业务的发展,需求的不断变化,API 的迭代是必然的,很可能当前版本正在使用,而我们就得开发甚至上线一个不兼容的新版本,为了让旧用户可以正常使用,为了保证开发的顺利进行,我们需要控制好 API 的版本。
将版本号直接加入 URL 中:
https://api.example.com/v1 https://api.example.com/v2 https://api.example.com/v3
开发 API 时,必须使用 RESTful 规范来架构 API。
具体规则下面罗列出来。
必须使用 URL 定位资源的规则。
在 RESTful 的架构中,所有的一切都表示资源,每一个 URL 都代表着一种资源,资源应当是一个名词,而且大部分情况下是名词的复数,尽量不要在 URL 中出现动词。
先来看看 GitHub 的 例子:
GET /issues 列出所有的 issue GET /orgs/:org/issues 列出某个项目的 issue GET /repos/:owner/:repo/issues/:number 获取某个项目的某个 issue POST /repos/:owner/:repo/issues 为某个项目创建 issue PATCH /repos/:owner/:repo/issues/:number 修改某个 issue PUT /repos/:owner/:repo/issues/:number/lock 锁住某个 issue DELETE /repos/:owner/:repo/issues/:number/lock 解锁某个 issue
例子中冒号开始的代表变量,例如 /repos/summerblue/larabbs/issues
在 GitHub 的实现中,我们可以总结出:
资源的设计可以嵌套,表明资源与资源之间的关系。
大部分情况下我们访问的是某个资源集合
,想得到单个资源
可以通过资源的 id 或 number 等唯一标识获取。
某些情况下,资源会是单数形式,例如某个项目某个 issue 的锁
,每个 issue 只会有一把锁,所以它是单数。
❌ 错误的例子:
POST https://api.example.com/createTopic GET https://api.example.com/topic/show/1 POST https://api.example.com/topics/1/comments/create POST https://api.example.com/topics/1/comments/100/delete
✅ 正确的例子:
POST https://api.example.com/topics GET https://api.example.com/topics/1 POST https://api.example.com/topics/1/comments DELETE https://api.example.com/topics/1/comments/100
Laravel 应该使用以下来定义资源路由:
phpRoute::apiResource('users', UserController::class);
以上等同于:
Verb Path Action Route Name GET /users index users.index POST /users store users.store GET /users/{user} show users.show PUT|PATCH /users/{user} update users.update DELETE /users/{user} destroy users.destroy
如果你不使用 apiResource()
方法,控制器方法 必须 按照以上的指纹来定义路由。
apiResource()
还可以使用以下方法来定制具体使用的路由:
phpRoute::apiResource('photos', PhotoController::class)->only([
'index', 'show'
]);
Route:: apiResource('photos', PhotoController::class)->except([
'create', 'store', 'destroy'
]);
必须使用 HTTP 动词来描述操作,绝不单一的使用 POST 来处理所有逻辑。
HTTP 设计了很多动词,来表示不同的操作,RESTful 很好的利用的这一点,我们需要正确的使用 HTTP 动词,来表明我们要如何操作资源。
先来解释一个概念,幂等性
,指一次和多次请求某一个资源应该具有同样的副作用,也就是一次访问与多次访问,对这个资源带来的变化是相同的。
常用的动词及幂等性
动词 | 描述 | 是否幂等 |
---|---|---|
GET | 获取资源,单个或多个 | 是 |
POST | 创建资源 | 否 |
PUT | 更新资源,客户端提供完整的资源数据 | 是 |
PATCH | 更新资源,客户端提供部分的资源数据 | 否 |
DELETE | 删除资源 | 是 |
为什么 PUT 是幂等的而 PATCH 是非幂等的,因为 PUT 是根据客户端提供了完整的资源数据,客户端提交什么就替换什么,而 PATCH 有可能是根据客户端提供的参数,动态的计算出某个值,例如每次请求后资源的某个参数减 1,所以多次调用,资源会有不同的变化。
另外需要注意的是,GET 请求对于资源来说是不安全的,绝不 通过 GET 请求改变(更新或创建)资源。
真实使用中,为了方便统计类的数据,会有一些例外情况,例如帖子详情,记录访问次数,每调用一次,访问次数 +1。这种情况下可以考虑页面展示成功后,再次调用一个 POST 请求去更新阅读数。
必须利用 HTTP 状态码和客户端进行通讯。
有一些 API 的设计,不论接口的状态成功与否,都会返回 200 ,然后使用自定的状态码,例如说 :
json{
// 数据不存在
error_code: 30404
}
这种方法是不可取的。
HTTP 状态码是行业标准,意味着成千上万开发者都在认同和使用这套规则,意味着他们写出来的 HTTP 通讯程序(类库)也在使用这套规则。所以没有必要,也不该重新发明自己的一套规则。
HTTP 提供了丰富的状态码供我们使用,正确的使用状态码可以让响应数据更具可读性。、
强制客户端在请求时,必须发送 User-Agent 信息。
User-Agent 信息包含两部分,客户端信息 + 版本,使用斜杆分隔:
User-Agent: Mixin Bot iOS/2.1.37 User-Agent: Mixin Bot Android/2.1.22 User-Agent: MixPay PHP SDK/2.1.22 User-Agent: MixPay GO SDK/2.1.22
API 后端接收到 User-Agent 数据后可以暂时不做处理,但是后续有特殊的业务需求时,可以针对某个客户端具体到版本,进行特殊的数据处理。
常见的使用场景,是废弃客户端:例如一个银行 APP,升级了交易时的加密算法,低于 5.0 版本的客户端因为安全原因,必须废弃。针对此情况,可通过后端 API 判断 User-Agent 标头,对低于 5.0 的版本的客户端请求,返回专属的数据,如 APP 首页的第一个 Banner 显示请升级客户端,安全升级无法使用的提示。
现实生产中,有些客户端用户会关闭系统的应用自动更新功能,多版本客户端是无法避免的问题。有了 User-Agent ,我们可以更加灵活的做针对性处理。
资源路由路由 URI 必须 使用复数形式,如:
/photos/create
/photos/{photo}
错误的例子如:
/photo/create
/photo/{photo}
本文作者:TyrantGenesis
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!