记录网址:laravel controller_那扎的博客-CSDN博客
laravel controller介绍
记录语句:清空并重新自动加载
$ php artisan clear-compiled
$composer dump-autoload
一、分类列表
分类列表
在这个章节中,我们将开发帖子分类的获取接口,方便我们在下一章节『发布话题』中使用。
由于直接看当前第六章节,之前的内容中包含配置composer ,按照下方执行后一直报错 class ... does not exit
所以按照一下步骤进行配置
1)首先来搭建一下基础环境,创建一个基础 Controller,此类作为所有 API 请求控制器的【基类】。
$ php artisan make:controller Api/Controller
注意我们增加了一个命名空间 Api
,以后接口相关的控制器,统一会放在 Api
目录中,会让代码结构更清晰。前面提到过接口版本控制的重要性,我们还可以在 Api
目录中增加 V1,V2 等目录,进一步区分不同版本的接口,为了教学方便,我们暂时不做进一步区分。 将 Controller.php 文件替换为以下的内容。
app/Http/Controllers/Api/Controller.php
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use Dingo\Api\Routing\Helpers;
use App\Http\Controllers\Controller as BaseController;
class Controller extends BaseController
{
use Helpers;
}
注意上面的基础代码中,我们增加了 DingoApi 的 helper,这个 trait 可以帮助我们处理接口响应,后面我们在使用到的时候,我们会对其提供的功能做一一讲解。我们新建的 Api 控制器都会继承这个基础控制器。
下面我们开始写第一个接口 发送短信验证码
,先添加路由
routes/api.php
<?php
use Illuminate\Http\Request;
$api = app('Dingo\Api\Routing\Router');
$api->version('v1', [
'namespace' => 'App\Http\Controllers\Api'
], function($api) {
// 短信验证码
$api->post('verificationCodes', 'VerificationCodesController@store')
->name('api.verificationCodes.store');
});
上面的代码中,我们增加了一个参数 namespace
,使 v1 版本的路由都会指向 App\Http\Controllers\Api
,方便我们书写路由。
接下来创建 VerificationCodes
的控制器
修改文件如以下:
app/Http/Controllers/Api/VerificationCodesController.php
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
class VerificationCodesController extends Controller
{
public function store()
{
return $this->response->array(['test_message' => 'store verification code']);
}
}
通过 artisan 创建出来的控制器,默认会继承 App\Http\Controllers\Controller,我们只需要删除
use App\Http\Controllers\Controller
这一行即可,这样会继承相同命名空间下的 Controller,也就是我们上一步中添加的那个控制器。
增加了 store 方法,利用 DingoApi 的 Helpers trait,我们可以使用 $this->response->array
返回一个测试用的响应。接下来使用 PostMan 访问 发送短信验证码
接口,注意使用 POST
方式提交。
2)添加路由——routes/api.php
其中->get为调用get方法,get(param1, param2) param1:新建的Controller类 param2:调用的方法
->name name则是对你的路由别名,与dingo/api 没有关系,是laravel 自带的功能
可以参考网址:路由 | 基础功能 |《Laravel 5.5 中文文档 5.5》| Laravel China 社区
$api->version('v1', [
'namespace'=>'App\Http\Controllers\Api'
],function($api){
$api->get('categories', 'CategoriesController@index')->name('api.categories.index');
});
3)创建Transformer 用于显示哪些数据(类似于创建类,类中有变量用于显示)
$ touch app/Transformers/CategoryTransformer.php
修改如下
app/Transformers/CategoryTransformer.php
<?php
namespace App\Transformers;
use App\Models\Category;
use League\Fractal\TransformerAbstract;
class CategoryTransformer extends TransformerAbstract
{
public function transform(Category $category)
{
return [
'id' => $category->id,
'name' => $category->name,
'description' => $category->description,
];
}
}
4)、创建controller 由于需要大量的处理代码处理返回信息和操作,所以将有共同特征的路由处理函数放到一个共同的控制器中
php artisan make:controller Api/CategoriesController
修改如下
app/Http/Controllers/Api/CategoriesController.php
<?php
namespace App\Http\Controllers\Api;
use App\Models\Category;
use Illuminate\Http\Request;
use App\Transformers\CategoryTransformer;
class CategoriesController extends Controller
{
public function index()
{
return $this->response->collection(Category::all(), new CategoryTransformer());
}
}
分类数据是集合,所以我们使用 $this->response->collection
返回数据。
1. 添加路由
回忆一下 Larabbs 的模型关系,每一个话题一定会属于某个分类,所以发布话题的时候,我们会从列表中选择一个分类,APP 首页也会显示出来不同分类便签,便于切换。
新增 分类列表
接口,分类列表是游客可访问的接口,不需要 token 验证。
routes/api.php
$api->version('v1', [
'namespace' => 'App\Http\Controllers\Api',
'middleware' => 'serializer:array'
], function($api) {
$api->group([
'middleware' => 'api.throttle',
'limit' => config('api.rate_limits.sign.limit'),
'expires' => config('api.rate_limits.sign.expires'),
], function($api){
// 短信验证码
$api->post('verificationCodes', 'VerificationCodesController@store')
->name('api.verificationCodes.store');
//用户注册
$api->post('users', 'UsersController@store')
->name('api.users.store');
//图片验证码
$api->post('captchas', 'CaptchasController@store')
->name('api.captchas.store');
});
$api->group([
'middleware' => 'api.throttle',
'limit' => config('api.rate_limits.access.limit'),
'expires' => config('api.rate_limits.access.expires'),
], function($api){
//游客可以访问的接口
// 需要 token 验证的接口
$api->group(['middleware' => 'api.auth'], function($api) {
// 当前登录用户信息
$api->get('user', 'UsersController@me')
->name('api.user.show');
//图片资源
$api->post('images', 'ImagesController@store')
->name('api.images.store');
});
});
//游客可以访问的接口
$api->get('categories', 'CategoriesController@index')
->name('api.categories.index');
});
2. 创建 Transformer
$ touch app/Transformers/CategoryTransformer.php
修改如下
app/Transformers/CategoryTransformer.php
<?php
namespace App\Transformers;
use App\Models\Category;
use League\Fractal\TransformerAbstract;
class CategoryTransformer extends TransformerAbstract
{
public function transform(Category $category)
{
return [
'id' => $category->id,
'name' => $category->name,
'description' => $category->description,
];
}
}
3. 创建 controller
php artisan make:controller Api/CategoriesController
修改如下
app/Http/Controllers/Api/CategoriesController.php
<?php
namespace App\Http\Controllers\Api;
use App\Models\Category;
use Illuminate\Http\Request;
use App\Transformers\CategoryTransformer;
class CategoriesController extends Controller
{
public function index()
{
return $this->response->collection(Category::all(), new CategoryTransformer());
}
}
分类数据是集合,所以我们使用 $this->response->collection
返回数据。
4. 测试一下
使用 PostMan 调试一下接口并保存接口,实现数据读取的接口遵循以下流程:
- 增加路由
- 创建 transformer
- controller 处理数据,使用 transformer 转换后返回
提交代码
$ git add -A
$ git commit -m 'categories index'
二、发布话题
需要填写话题标题,话题内容,选择话题分类。如果这个话题内容,可以上传图片,插入 markdown 图片链接,则可以调用我们上一节写的 添加图片
接口,type 为 topic,将返回的图片 path 插入到话题内容中。
1. 增加路由
只有登录用户才可以发布话题,添加路由。
routes/api.php
$api->version('v1', [
'namespace' => 'App\Http\Controllers\Api',
'middleware' => 'serializer:array'
], function($api) {
$api->group([
'middleware' => 'api.throttle',
'limit' => config('api.rate_limits.sign.limit'),
'expires' => config('api.rate_limits.sign.expires'),
], function($api){
// 短信验证码
$api->post('verificationCodes', 'VerificationCodesController@store')
->name('api.verificationCodes.store');
//用户注册
$api->post('users', 'UsersController@store')
->name('api.users.store');
//图片验证码
$api->post('captchas', 'CaptchasController@store')
->name('api.captchas.store');
});
$api->group([
'middleware' => 'api.throttle',
'limit' => config('api.rate_limits.access.limit'),
'expires' => config('api.rate_limits.access.expires'),
], function($api){
//游客可以访问的接口
// 需要 token 验证的接口
$api->group(['middleware' => 'api.auth'], function($api) {
// 当前登录用户信息
$api->get('user', 'UsersController@me')
->name('api.user.show');
//图片资源下方加入
$api->post('images', 'ImagesController@store')
->name('api.images.store');
//发布话题
$api->post('topics', 'TopicsController@store')
->name('api.topics.store');
});
});
//游客可以访问的接口
$api->get('categories', 'CategoriesController@index')
->name('api.categories.index');
});
2. 增加 Request
创建 TopicRequest:
$ php artisan make:request Api/TopicRequest
如下修改:
app/Http/Requests/Api/TopicRequest.php
<?php
namespace App\Http\Requests\Api;
use Dingo\Api\Http\FormRequest;
class TopicRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => 'required|string',
'body' => 'required|string',
'category_id' => 'required|exists:categories,id',
];
}
public function attributes()
{
return [
'title' => '标题',
'body' => '话题内容',
'category_id' => '分类',
];
}
}
3. 增加 Transformer
$ touch app/Transformers/TopicTransformer.php
修改如下
app/Transformers/TopicTransformer.php
<?php
namespace App\Transformers;
use App\Models\Topic;
use League\Fractal\TransformerAbstract;
class TopicTransformer extends TransformerAbstract
{
public function transform(Topic $topic)
{
return [
'id' => $topic->id,
'title' => $topic->title,
'body' => $topic->body,
'user_id' => (int) $topic->user_id,
'category_id' => (int) $topic->category_id,
'reply_count' => (int) $topic->reply_count,
'view_count' => (int) $topic->view_count,
'last_reply_user_id' => (int) $topic->last_reply_user_id,
'excerpt' => $topic->excerpt,
'slug' => $topic->slug,
'created_at' => $topic->created_at->toDateTimeString(),
'updated_at' => $topic->updated_at->toDateTimeString(),
];
}
}
4. 增加 Controller
$ php artisan make:controller Api/TopicsController
修改文件
app/Http/Controllers/Api/TopicsController.php
<?php
namespace App\Http\Controllers\Api;
use App\Models\Topic;
use Illuminate\Http\Request;
use App\Transformers\TopicTransformer;
use App\Http\Requests\Api\TopicRequest;
class TopicsController extends Controller
{
public function store(TopicRequest $request, Topic $topic)
{
$topic->fill($request->all());
$topic->user_id = $this->user()->id;
$topic->save();
return $this->response->item($topic, new TopicTransformer())
->setStatusCode(201);
}
}
5. PostMan 调试
代码版本控制
$ git add -A
$ git commit -m 'topics store'
三、修改话题
本章节我们将一起开发修改话题信息的 API。
1. 添加路由
routes/api.php
.
.
.
// 发布话题下方加入
$api->post('topics', 'TopicsController@store')
->name('api.topics.store');
//修改话题
$api->patch('topics/{topic}', 'TopicsController@update')
->name('api.topics.update');
.
.
.
2. 修改 Request
app/Http/Requests/Api/TopicRequest.php
.
.
.
public function rules()
{
switch($this->method()) {
case 'POST':
return [
'title' => 'required|string',
'body' => 'required|string',
'category_id' => 'required|exists:categories,id',
];
break;
case 'PATCH':
return [
'title' => 'string',
'body' => 'string',
'category_id' => 'exists:categories,id',
];
break;
}
}
.
.
.
3. 修改 Controller
app/Http/Controllers/Api/TopicsController.php
.
.
.
public function update(TopicRequest $request, Topic $topic)
{
$this->authorize('update', $topic);
$topic->update($request->all());
return $this->response->item($topic, new TopicTransformer());
}
.
.
.
4. PostMan 调试
首先我们输入一个不存在的话题 id,想象中应该会返回 404
但是结果却是 TopicTransformer 报错了,好像是路由模型绑定出了问题。没错,因为路由交给 DingoApi 来处理了,所以模型绑定的中间件并没有注册上。手动增加 bindings
中间件。
routes/api.php
.
.
.
$api->version('v1', [
'namespace' => 'App\Http\Controllers\Api',
'middleware' => ['serializer:array', 'bindings']
], function ($api) {
.
.
.
5. API 的异常处理
状态码正确,但是 message 不是太好,暴露了一些代码细节,关于异常处理,DingoApi 提供了方法,让我们手动处理异常。
app/Providers/AppServiceProvider.php
.
.
.
public function register()
{
if (app()->isLocal()) {
$this->app->register(\VIACreative\SudoSu\ServiceProvider::class);
}
\API::error(function (\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $exception) {
throw new \Symfony\Component\HttpKernel\Exception\HttpException(404, '404 Not Found');
});
}
.
.
.
再次调用,查看结果
如果你使用的是 dingo 2.2.3
以下的版本,状态码是会是 500。
可以使用如下代码 :\API::error(function (\Illuminate\Database\Eloquent\ModelNotFoundException $exception) {
abort(404);
});
6. 用户权限控制
但是我们尝试修改别人的话题,这里我们使用 id 为 14 的用户,修改 id 为 1 的话题。
注意不要使用有
manage_contents
权限的用户,也就是 id 为 1 ,2 的用户,他们有管理内容的权限,所以可以修改任何人的话题。
同样的,状态码不太符合预期,403 可能更加合适,依然可以在 AppServiceProvider
中手动处理。
app/Providers/AppServiceProvider.php
.
.
.
public function register()
{
if (app()->isLocal()) {
$this->app->register(\VIACreative\SudoSu\ServiceProvider::class);
}
\API::error(function (\Illuminate\Database\Eloquent\ModelNotFoundException $exception) {
abort(404);
});
\API::error(function (\Illuminate\Auth\Access\AuthorizationException $exception) {
abort(403, $exception->getMessage());
});
}
.
.
.
再次调用,符合预期,最后尝试修改自己的话题
修改成功,注意 patch 需要使用 x-www-form-urlencoded
。
代码版本控制
$ git add -A
$ git commit -m 'topic update api'
四、删除话题
本章节我们将开发删除话题的 API 功能。
1. 添加路由
routes/api.php
.
.
.
// 发布话题
$api->post('topics', 'TopicsController@store')
->name('api.topics.store');
//修改话题
$api->patch('topics/{topic}', 'TopicsController@update')
->name('api.topics.update');
//删除话题
$api->delete('topics/{topic}', 'TopicsController@destroy')
->name('api.topics.destroy');
.
.
.
2. 修改 Controller
.
.
.
public function destroy(Topic $topic)
{
$this->authorize('destroy', $topic);
$topic->delete();
return $this->response->noContent();
}
.
.
.
注意这里我们使用的是 destroy
的权限控制。
3. PostMan 调试
删除成功
提交代码
$ git add -A
$ git commit -m 'topic delete api'
五、话题列表
可以通过分类搜索,根据创建时间及最新回复排序,列表显示了用户信息,话题信息,分类信息。
1. 添加路由
话题列表游客可以访问
routes/api.php
//游客可以访问的接口
//分类列表
$api->get('categories', 'CategoriesController@index')
->name('api.categories.index');
//话题列表
$api->get('topics', 'TopicsController@index')
->name('api.topics.index');
2. 修改 Controller
app/Http/Controllers/Api/TopicsController.php
.
.
.
public function index(Request $request, Topic $topic)
{
$query = $topic->query();
if ($categoryId = $request->category_id) {
$query->where('category_id', $categoryId);
}
// 为了说明 N+1问题,不使用 scopeWithOrder
switch ($request->order) {
case 'recent':
$query->recent();
break;
default:
$query->recentReplied();
break;
}
$topics = $query->paginate(20);
return $this->response->paginator($topics, new TopicTransformer());
}
.
.
.
PostMan 调用,返回了话题列表,并且有分页数据
Include 机制 获取额外的资源数据
话题数据我们有了,但是发布话题的 用户数据
,以及话题的 分类数据
还没有,那么
- 如何获取额外的资源数据?
- 资源数据该以什么样的结构返回?
其实 DingoApi 已经很好的解决了这两个问题。
首先修改 TopicTransformer
如下
app/Transformers/TopicTransformer.php
<?php
namespace App\Transformers;
use App\Models\Topic;
use League\Fractal\TransformerAbstract;
class TopicTransformer extends TransformerAbstract
{
protected $availableIncludes = ['user', 'category'];//为可以嵌套的额外资源有 user 和 category。
public function transform(Topic $topic)
{
return [
'id' => $topic->id,
'title' => $topic->title,
'body' => $topic->body,
'user_id' => $topic->user_id,
'category_id' => $topic->category_id,
'reply_count' => $topic->reply_count,
'view_count' => $topic->view_count,
'last_reply_user_id' => $topic->last_reply_user_id,
'excerpt' => $topic->excerpt,
'slug' => $topic->slug,
'created_at' => $topic->created_at->toDateTimeString(),
'updated_at' => $topic->updated_at->toDateTimeString(),
];
}
public function includeUser(Topic $topic)
{
return $this->item($topic->user, new UserTransformer());
}
public function includeCategory(Topic $topic)
{
return $this->item($topic->category, new CategoryTransformer());
}
}
上面的代码中,我们首先设置了 protected $availableIncludes = ['user', 'category']
,可以理解为可以嵌套的额外资源有 user
和 category
。那么额外的资源如何获取,如何转换,则通过 includeUser
和 includeCategory
确定,availableIncludes
中的每一个参数都对应一个具体的方法,方法命名规则为 include + user
、 include + category
驼峰命名。
那么什么时候才会引入额外的资源呢,由客户端提交的 include 参数指定,多个参数通过逗号分隔。
url 中增加 include=user
参数,调用 话题列表
接口,可以看到用户数据,并且是通过 UserTransformer 转换过后的数据,url 中增加 include=user,category
参数,调用 话题列表
接口,可以看到数据中增加了 user
和 category
。
UserTransformer
和 CategoryTransformer
在这里被复用,通常情况,每个资源只需要写好一个对应的 Transformer 即可。
整理一下完整的流程
- 访问 api/topics?include=user,category
- TopicsController 中查询出来话题,最终使用
return $this->response->paginator($topics, new TopicTransformer());
返回响应。 - TopicTransformer 处理每一个
话题模型
,使用 transform 方法转化模型数据 - 因为你请求中指定了 include=user,所以
TopicTransformer
自动调用 includeUser 方法。 - includeUser 方法中使用,查询到用户数据
$topic->user
,通过UserTransformer
格式化用户数据 - 重复上面两步,处理 category 分类相关的数据
- 最终 Fractal 帮我们整理好资源之间的嵌套关系,返回响应。
在 Transformer 中,我们可以使用:
- $this->item () 返回单个资源
- $this->collection () 返回集合资源
Include 让资源与资源之间以一种合理的嵌套返回,同时什么时候返回完全由请求参数决定,这让资源数据更加灵活。
N+1 问题以及查询日志
你可能会发现,我们并没有手动通过 with
或者 load
预加载模型关系,那么会不会带来 N+1
问题呢。首先我们需要输出 sql 查询日志,laravel-query-logger 是安正超写的一个查询日志组件,先来安装它
$ composer require overtrue/laravel-query-logger --dev
打开日志,再次调用接口 http://larabbs.test/api/topics?include=user,category
$ tail -f ./storage/logs/laravel.log
并没有产生 N+1 问题,因为当我们返回的是集合的时候,DingoApi 根据 include 参数以及 defaultInclude
帮助我们进行预加载,所以大部分情况我们不需要手动处理。如果遇到复杂的嵌套及关系加载,可以 app(\Dingo\Api\Transformer\Factory::class)->disableEagerLoading();
临时关闭 DingoApi 的预加载,手动处理。
某个用户发布的话题列表
除了首页不同分类的话题列表,我们还可能查看某个用户发布的所有话题
1. 添加路由
某个用户的发布的话题,同样是游客可访问的
routes/api.php
.
.
.
//话题列表下方添加
$api->get('topics', 'TopicsController@index')
->name('api.topics.index');
//某个用户发布的话题
$api->get('users/{user}/topics', 'TopicsController@userIndex')
->name('api.users.topics.index');
.
.
.
2. 修改 Controller
按创建时间倒叙返回该用户所有的话题。
app/Http/Controllers/Api/TopicsController.php
.
.
.
use App\Models\User;//需要添加 Models\User
.
.
.
//添加指定某个用户方法
public function userIndex(User $user, Request $request)
{
$topics = $user->topics()->recent()
->paginate(20);
return $this->response->paginator($topics, new TopicTransformer());
}
.
.
.
PostMan 调用接口
代码版本控制
$ git add -A
$ git commit -m 'topic index'
六、话题详情——根据话题ID获取话题详情及用户和种类详情
1. 添加路由
routes/api.php
.
.
.
$api->get('topics', 'TopicsController@index')
->name('api.topics.index');
//话题详情
$api->get('topics/{topic}', 'TopicsController@show')
->name('api.topics.show');
.
.
.
2. 修改 controller
app/Http/Controllers/Api/TopicsController.php
.
.
.
public function show(Topic $topic)
{
return $this->response->item($topic, new TopicTransformer());
}
.
.
.
PostMan 调用接口,可以看到,当 Transformer 准备好了之后,开发相关的接口是很方便的。详情接口依然可以使用 include 参数。
代码版本控制
$ git add -A
$ git commit -m 'topics show'