只有登录用户才可以进行回复
routes/api.php
.
.
.
//删除话题下方增加
$api->delete('topics/{topic}', 'TopicsController@destroy')
->name('api.topics.destroy');
//发布回复
$api->post('topics/{topic}/replies', 'RepliesController@store')
->name('api.topics.replies.store');
.
.
.
回复一定属于某个话题,所以我们设计为 topics/{topic}/replies
,为某个话题添加回复,这样会让资源与资源的关系更加直观。
创建 ReplyRequest:
$ php artisan make:request Api/ReplyRequest
如下修改:
app/Http/Requests/Api/ReplyRequest.php
<?php
namespace App\Http\Requests\Api;
use Dingo\Api\Http\FormRequest;
class ReplyRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'content' => 'required|min:2',
];
}
}
大家应该注意到了一个不合理的地方,我们在重复地编写 authorize()
这个方法。Laravel 的 make:request
命令为我们生成的每一份表单验证类里都有一个 authorize()
,为了不违背 DRY 原则(Don't Repeat Yourself 不重复你自己),我们需要做重构。
增加 FormRequest
$ php artisan make:request Api/FormRequest
app/Http/Requests/Api/FormRequest.php
<?php
namespace App\Http\Requests\Api;
use Dingo\Api\Http\FormRequest as BaseFormRequest;
class FormRequest extends BaseFormRequest
{
public function authorize()
{
return true;
}
}
再次修改 ReplyRequest.php
,删除 use Dingo\Api\Http\FormRequest
及 authorize
方法即可。
app/Http/Requests/Api/ReplyRequest.php
<?php
namespace App\Http\Requests\Api;
class ReplyRequest extends FormRequest
{
public function rules()
{
return [
'content' => 'required|min:2',
];
}
}
有了 FormRequest
基类,我们的代码更加简洁了。大家可以自行修改其他的 Request
文件 。
$ touch app/Transformers/ReplyTransformer.php
修改如下
app/Transformers/ReplyTransformer.php
<?php
namespace App\Transformers;
use App\Models\Reply;
use League\Fractal\TransformerAbstract;
class ReplyTransformer extends TransformerAbstract
{
public function transform(Reply $reply)
{
return [
'id' => $reply->id,
'user_id' => (int) $reply->user_id,
'topic_id' => (int) $reply->topic_id,
'content' => $reply->content,
'created_at' => $reply->created_at->toDateTimeString(),
'updated_at' => $reply->updated_at->toDateTimeString(),
];
}
}
$ php artisan make:controller Api/RepliesController
修改文件
app/Http/Controllers/Api/RepliesController.php
<?php
namespace App\Http\Controllers\Api;
use App\Models\Topic;//注意需要更改使用的命名空间
use App\Models\Reply;
use App\Http\Requests\Api\ReplyRequest;
use App\Transformers\ReplyTransformer;
class RepliesController extends Controller
{
public function store(ReplyRequest $request, Topic $topic, Reply $reply)
{
$reply->content = $request->content;
$reply->topic_id = $topic->id;
$reply->user_id = $this->user()->id;
$reply->save();
return $this->response->item($reply, new ReplyTransformer())
->setStatusCode(201);
}
}
调试成功,状态码为 201, 响应 body 为回复数据。保存接口,新建话题回复目录。
$ git add -A
$ git commit -m 'replies store'
本章节我们将开发帖子回复的删除功能。每一次开发『删除』这种危险性较高的功能时,我们需要特别注意权限的控制。根据现有的 Larabbs 回复功能,拥有删除回复权限的身份只有以下三种:
接下来我们开始开发此功能,同时做好权限控制。
routes/api.php
.
.
.
//发布回复下方添加删除回复
$api->post('topics/{topic}/replies', 'RepliesController@store')
->name('api.topics.replies.store');
//删除回复
$api->delete('topics/{topic}/replies/{reply}', 'RepliesController@destroy')
->name('api.topics.replies.destroy');
.
.
.
app/Http/Controllers/Api/RepliesController.php
.
.
.
public function destroy(Topic $topic, Reply $reply)
{
if ($reply->topic_id != $topic->id) {不是当前用户回复的话题,不允许删除
return $this->response->errorBadRequest();
}
$this->authorize('destroy', $reply);
$reply->delete();
return $this->response->noContent();
}
.
.
.
注意这里的 destroy
使用的是已存在的 授权策略 类:
app/Policies/ReplyPolicy.php
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Reply;
class ReplyPolicy extends Policy
{
public function destroy(User $user, Reply $reply)
{
return $user->isAuthorOf($reply) || $user->isAuthorOf($reply->topic);
}
}
设定了只有 话题的作者和评论的作者,才有权限删除评论。
用非管理员账户,找一个不是自己发布的话题,尝试删除他人的回复,报错 403 没有权限。
尝试删除自己发布的回复,删除成功,返回 204。
注意这里截图中的 id 可能与你自己环境中的 id 不同,根据真实情况进行测试。
$ git add -A
$ git commit -m 'replies destroy'
第一步我们先添加路由,请注意该接口游客是可以访问的:
routes/api.php
.
.
.
//某个用户发布的话题下方添加 话题回复列表
$api->get('users/{user}/topics', 'TopicsController@userIndex')
->name('api.users.topics.index');
//话题回复列表
$api->get('topics/{topic}/replies', 'RepliesController@index')
->name('api.topics.replies.index');
.
.
.
app/Http/Controllers/Api/RepliesController.php
//查询话题所有评论
public function index(Topic $topic)
{
$replies = $topic->replies()->paginate(20);
return $this->response->paginator($replies, new ReplyTransformer());
}
代码很简单,分页查询话题的所有评论,使用 ReplyTransformer
转换评论数据并返回。
响应数据中包括中该话题的评论数据,及分页数据。
我们需要的不仅仅是回复数据,还需要显示回复人姓名,头像等用户数据。
再次阅读并回忆一下 Include 机制,当我们需要在资源数据中,嵌套返回该资源 相关的其他资源
时,可以利用这个机制快速的实现。
设置 Transformer
中的 availableIncludes
参数
app/Transformers/ReplyTransformer.php
<?php
namespace App\Transformers;
use App\Models\Reply;
use League\Fractal\TransformerAbstract;
class ReplyTransformer extends TransformerAbstract
{
protected $availableIncludes = ['user'];
.
.
.
public function includeUser(Reply $reply)
{
return $this->item($reply->user, new UserTransformer());
}
}
增加 include=user
再次使用 PostMan 调试
因为多了 include 参数,数据中多了用户数据。
除了某个话题的回复,我们还可能查看某个用户发布的所有回复
routes/api.php
.
.
.
//话题回复列表下方添加 每个用户的回复列表
$api->get('topics/{topic}/replies', 'RepliesController@index')
->name('api.topics.replies.index');
//每个用户的回复列表
$api->get('users/{user}/replies', 'RepliesController@userIndex')
->name('api.users.replies.index');
.
.
.
app/Http/Controllers/Api/RepliesController.php
.
.
.
use App\Models\User;
.
.
.
public function userIndex(User $user)
{
$replies = $user->replies()->paginate(20);
return $this->response->paginator($replies, new ReplyTransformer());
}
.
.
.
分页查询用户的所有评论,使用 ReplyTransformer
转换评论数据并返回。
注意回复列表中,需要显示回复话题的标题,也就是我们需要 回复资源
关联的 话题资源
。
app/Transformers/ReplyTransformer.php
.
.
.
protected $availableIncludes = ['user', 'topic'];
.
.
.
public function includeTopic(Reply $reply)
{
return $this->item($reply->topic, new TopicTransformer());
}
.
.
.
availableIncludes
中增加了 topic
,增加了对应的 includeTopic
方法,查询出回复关联的话题模型,使用 TopicTransformer
转换并返回。
注意设置变量
返回 回复数据
以及 回复的话题数据
。
假设现在的客户端界面进行了调整,某个用户的回复列表页面,不仅需要显示话题的标题,还需要显示 发布话题
的用户的头像及姓名,也就是除了回复关联的话题资源,还需要话题关联的用户资源。
数据该如何嵌套,客户端界面变化了,我们需要调整接口吗?
其实代码我们已经完成了,客户端只需要调整请求参数即可
注意我们传入的 include
参数为 topic.user
,意思是包含话题资源
关联的用户资源
,用户数据嵌套在话题数据中。
相信你应该能发现 include 参数中 逗号
与点
的区别。
include=topic,user
;include=topic.user
;因为回复的话题是通过 TopicTransformer
格式化的:
public function includeTopic(Reply $reply)
{
return $this->item($reply->topic, new TopicTransformer());
}
所以 TopicTransformer 中 $availableIncludes
包含的资源,我们都可以使用 点
继续嵌套关联,例如 include=topic.user,topic.category
。
例如:http://{{host}}/api/users/:user_id/replies?include=topic.user, topic.category
我们是在面向资源处理数据,接口需要做的是,利用资源之间的关联,让客户端通过不同的参数组合,获取需要的资源,可以看到 Include 机制非常灵活和方便。
是否有 N+1 问题呢?
查看一下日志
$ tail -f ./storage/logs/laravel.log
DingoApi 已经帮我们处理好了
$ git add -A
$ git commit -m 'replies index'
接下来我们开发 消息通知
接口,开发过消息通知的功能,就是当话题有新回复时,我们将通知话题作者。
登录用户可以查看自己收到的通知
routes/api.php
.
.
.
//删除回复下方添加 通知列表
$api->delete('topics/{topic}/replies/{reply}', 'RepliesController@destroy')
->name('api.topics.replies.destroy');
//通知列表
$api->get('user/notifications', 'NotificationsController@index')
->name('api.user.notifications.index');
.
.
.
这里同 获取登录用户信息
的思路相同,user
表示当前登录的用户,user/notifications
就是 我的通知
。
$ touch app/Transformers/NotificationTransformer.php
修改文件
app/Transformers/NotificationTransformer.php
<?php
namespace App\Transformers;
use League\Fractal\TransformerAbstract;
use Illuminate\Notifications\DatabaseNotification;
class NotificationTransformer extends TransformerAbstract
{
public function transform(DatabaseNotification $notification)
{
return [
'id' => $notification->id,
'type' => $notification->type,
'data' => $notification->data,
'read_at' => $notification->read_at ? $notification->read_at->toDateTimeString() : null,
'created_at' => $notification->created_at->toDateTimeString(),
'updated_at' => $notification->updated_at->toDateTimeString(),
];
}
}
注意这里我们需要格式化的模型是 Illuminate\Notifications\DatabaseNotification
。
第一次一直报错:notificationsController is not exist原因为:
将这句话写成$ php artisan make:controller Api/NotificationsController.php,这是错误的,不能在最后加上php
$ php artisan make:controller Api/NotificationsController
修改文件
app/Http/Controllers/Api/NotificationsController.php
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Transformers\NotificationTransformer;
class NotificationsController extends Controller
{
public function index()
{
$notifications = $this->user->notifications()->paginate(20);
return $this->response->paginator($notifications, new NotificationTransformer());
}
}
用户模型的 notifications 方法是Laravel消息系统为我们提供的方法,按通知创建时间倒叙排序。
新增 消息通知
目录保存接口
大家注意到了我们返回的数据里,reply_content
是 HTML 形式返回的,客户端可使用系统内置的 WebView UI 组件来渲染。iOS 有 UIWebView ,Android 有 WebView 。
$ git add -A
$ git commit -m 'notifications index'
在网页端,用户有未读消息了,会在 header 中有红色提示。对于 APP 来说,需要一个接口查询当前用户 未读消息数量
。
routes/api.php
.
.
.
// 通知列表下方添加 通知统计
$api->get('user/notifications', 'NotificationsController@index')
->name('api.user.notifications.index');
//通知统计
$api->get('user/notifications/stats', 'NotificationsController@stats')
->name('api.user.notifications.stats');
.
.
.
这里我们设计为 user/notifications/stats
,stats 是 statistics 的缩写,意思是统计,这个接口可以直观的表述为 —— 我的通知数据统计。
app/Http/Controllers/Api/NotificationsController.php
.
.
.
public function stats()
{
return $this->response->array([
'unread_count' => $this->user()->notification_count,
]);
}
.
.
.
当有新的通知时,App\Observers\ReplyObserver.php
已经帮我们进行了统计。
// 如果评论的作者不是话题的作者,才需要通知
if ( ! $reply->user->isAuthorOf($topic)) {
$topic->user->notify(new TopicReplied($reply));
}
notify 方法会将 notification_count 进行 +1。所以 $this->user()->notification_count;
就是用户未读消息数。
可以先登录 larabbs.test
回复某个用户的话题,为该用户新增几个未读通知。
新增 消息通知
目录,保存接口。
$ git add -A
$ git commit -m 'notifications stats'
我们还没有标记通知数据为已读,有些同学可能会在 消息通知列表
接口中将所有未读消息标记为已读,只要调用了列表接口,就意味着消息已读。
这么做看似没有什么问题,但是违背了一些原则,也带来了一些问题。回忆一下 Github 的 Restful HTTP API 设计分解 这一节我们提到了 GET 是安全的请求。
另外需要注意的是,GET 请求是安全的,不允许通过 GET 请求改变(更新或创建)资源。
消息通知列表
接口是 GET 请求,不应该在这时候改变资源数据,而且客户端可能会有其他的方式标记已读,例如有个按钮 标记所有通知为已读
。我们需要让接口符合规范,而且更加通用,所以一般需要客户端主动调用接口,来标记消息已读。
routes/api.php
.
.
.
//通知统计下方添加 标记消息通知为已读
$api->get('user/notifications/stats', 'NotificationsController@stats')
->name('api.user.notifications.stats');
//标记消息通知为已读
$api->patch('user/read/notifications', 'NotificationsController@read')
->name('api.user.notifications.read');
.
.
.
这里我们参考了 Github Api Starring 的部分,PUT /user/starred/:owner/:repo
为 star 某个仓库,同样标记单个通知为已读我们可以设计为 PUT /user/read/notifications/{notification_id}
,但是这里我们会批量将所有未读消息标记为已读,考虑到幂等性原则,使用 PATCH 更为合适,最终设计为 PATCH /user/read/notifications
。
app/Http/Controllers/Api/NotificationsController.php
.
.
.
public function read()
{
$this->user()->markAsRead();
return $this->response->noContent();
}
.
.
.
markAsRead
是上一本教程中已经处理好的方法,会将用户 notification_count
设置为 0,将所有未读消息设置为已读。代码如下:
app\Models\User.php
public function markAsRead()
{
$this->notification_count = 0;
$this->save();
$this->unreadNotifications->markAsRead();
}
再次访问消息通知统计接口
未读消息已经清零。
$ git add -A
$ git commit -m 'notifications read'