Laravel 第七章 回复数据

一、添加回复

1. 增加路由

只有登录用户才可以进行回复

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,为某个话题添加回复,这样会让资源与资源的关系更加直观。

2. 增加 Request

创建 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 文件 。

3. 增加 Transformer

$ 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(),
        ];
    }
}

4. 增加 Controller

$ 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);
    }
}

5. PostMan 调试

调试成功,状态码为 201, 响应 body 为回复数据。保存接口,新建话题回复目录。

代码版本控制

$ git add -A
$ git commit -m 'replies store'

二、删除回复

本章节我们将开发帖子回复的删除功能。每一次开发『删除』这种危险性较高的功能时,我们需要特别注意权限的控制。根据现有的 Larabbs 回复功能,拥有删除回复权限的身份只有以下三种:

  • 『回复的作者』
  • 『回复话题的作者』
  • 『管理员』

接下来我们开始开发此功能,同时做好权限控制。

1. 增加路由

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');
.
.
.

2. 修改 Controller

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);
    }
}

设定了只有 话题的作者和评论的作者,才有权限删除评论。

3. PostMan 调试

用非管理员账户,找一个不是自己发布的话题,尝试删除他人的回复,报错 403 没有权限。

尝试删除自己发布的回复,删除成功,返回 204。

注意这里截图中的 id 可能与你自己环境中的 id 不同,根据真实情况进行测试。

代码版本控制

$ git add -A
$ git commit -m 'replies destroy'

三、回复列表

某个话题的回复列表

1. 添加路由

第一步我们先添加路由,请注意该接口游客是可以访问的:

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');
.
.
.

2. 修改 Controller

app/Http/Controllers/Api/RepliesController.php

//查询话题所有评论
public function index(Topic $topic)
{
    $replies = $topic->replies()->paginate(20);

    return $this->response->paginator($replies, new ReplyTransformer());
}

代码很简单,分页查询话题的所有评论,使用 ReplyTransformer 转换评论数据并返回。

3. PostMan 调试

响应数据中包括中该话题的评论数据,及分页数据。

4. 调整 Include 参数

我们需要的不仅仅是回复数据,还需要显示回复人姓名,头像等用户数据。

再次阅读并回忆一下 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 参数,数据中多了用户数据。

某个用户回复列表

除了某个话题的回复,我们还可能查看某个用户发布的所有回复

1. 添加路由

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');
.
.
.

2. 修改 Controller

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 转换评论数据并返回。

3. 修改 Transformer

注意回复列表中,需要显示回复话题的标题,也就是我们需要 回复资源 关联的 话题资源

app/Transformers/ReplyTransformer.php

.
.
.
protected $availableIncludes = ['user', 'topic'];
.
.
.
public function includeTopic(Reply $reply)
{
    return $this->item($reply->topic, new TopicTransformer());
}
.
.
.

availableIncludes 中增加了 topic,增加了对应的 includeTopic 方法,查询出回复关联的话题模型,使用 TopicTransformer 转换并返回。

4. 使用 PostMan 调试

注意设置变量

返回 回复数据 以及 回复的话题数据

发布话题的用户数据

假设现在的客户端界面进行了调整,某个用户的回复列表页面,不仅需要显示话题的标题,还需要显示 发布话题 的用户的头像及姓名,也就是除了回复关联的话题资源,还需要话题关联的用户资源。

数据该如何嵌套,客户端界面变化了,我们需要调整接口吗

其实代码我们已经完成了,客户端只需要调整请求参数即可

注意我们传入的 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'

四、消息通知列表

接下来我们开发 消息通知 接口,开发过消息通知的功能,就是当话题有新回复时,我们将通知话题作者。

1. 增加路由

登录用户可以查看自己收到的通知

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 就是 我的通知

2. 增加 Transformer

$ 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

3. 增加 Controller  

第一次一直报错: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消息系统为我们提供的方法,按通知创建时间倒叙排序。

4. PostMan 调试

新增 消息通知 目录保存接口

5. 关于返回的数据

大家注意到了我们返回的数据里,reply_content 是 HTML 形式返回的,客户端可使用系统内置的 WebView UI 组件来渲染。iOS 有 UIWebView ,Android 有 WebView 。

代码版本控制

$ git add -A
$ git commit -m 'notifications index'

五、未读消息统计

未读消息数量

在网页端,用户有未读消息了,会在 header 中有红色提示。对于 APP 来说,需要一个接口查询当前用户 未读消息数量

1. 增加路由

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 的缩写,意思是统计,这个接口可以直观的表述为 —— 我的通知数据统计。

2. 修改 Controller

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; 就是用户未读消息数。

3. PostMan 调试

可以先登录 larabbs.test 回复某个用户的话题,为该用户新增几个未读通知。

新增 消息通知 目录,保存接口。

代码版本控制

$ git add -A
$ git commit -m 'notifications stats'

六、标记通知为已读

我们还没有标记通知数据为已读,有些同学可能会在 消息通知列表 接口中将所有未读消息标记为已读,只要调用了列表接口,就意味着消息已读。
这么做看似没有什么问题,但是违背了一些原则,也带来了一些问题。回忆一下 Github 的 Restful HTTP API 设计分解 这一节我们提到了 GET 是安全的请求。

另外需要注意的是,GET 请求是安全的,不允许通过 GET 请求改变(更新或创建)资源。

消息通知列表 接口是 GET 请求,不应该在这时候改变资源数据,而且客户端可能会有其他的方式标记已读,例如有个按钮 标记所有通知为已读。我们需要让接口符合规范,而且更加通用,所以一般需要客户端主动调用接口,来标记消息已读。

1. 增加路由

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

2. 修改 Controller

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();
}

3. 使用 PostMan 调试

再次访问消息通知统计接口

未读消息已经清零。

代码版本控制

$ git add -A
$ git commit -m 'notifications read'

猜你喜欢

转载自blog.csdn.net/jiangyangll/article/details/89675066