如何为 REST 关系组织控制器? [Laravel 条路线]
How to organise controllers for REST relationships? [Laravel routes]
假设实体 Genre
和 Book
。
每个都有 API 个资源端点 /genre
和 /book
。在 Laravel 条路线中可能是:
$app->resource('/genre', GenreController::class);
我想要关系的终点。 GET /genre/1/book
, 获取书本Genre
#1.
此处的最佳做法是什么?将处理程序放在 GenreController
、BookController
或一个全新的控制器中?
旁注,我正在使用 dingo-api
包,但我认为这没有任何区别。
虽然这里没有 100% 的具体答案 - 为每个资源配备一个控制器几乎总是更容易,然后如果您想直接点击它,则为关系配备一个控制器,这听起来就是您想要的想要做。
如果您(通常)能够坚持执行主要操作(索引、创建、存储、显示、编辑、更新、删除),就会变得更容易。它将使事情井井有条,未来从事您项目的开发人员将能够轻松地遵循该结构。
很棒的阅读:
大本营的 DHH 方法:http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/
怀特豪斯API指南:https://github.com/WhiteHouse/api-standards#white-house-web-api-standards
一种选择是只使用您当前的 BooksController
。将以下内容添加到您的 BooksController
:
public function __construct(Request $request)
{
if ($genreId = $request->route('genre')) {
$request->route()->forgetParameter('genre');
Book::addGlobalScope('genreScope', function ($query) use ($genreId) {
$query->whereGenreId($genreId);
});
}
}
这将使您的图书成为 Genre
的范围,并将其作为路由参数删除。
那么你的路线就是:
$api->resource('genre.book');
请注意,使用此方法,您仍会以相同的方式使用 store
和 update
方法,即在请求中传递 genre_id
。
希望对您有所帮助!
所以@Chris 建议为关系使用一个专用控制器,而@RossWilson 有一个天才的方法来为关系重新使用控制器(至少对于加载 Book
的操作)。
不幸的是,Lumen 的 RouteProvider
returns 是一个简单的数组,因此没有 $request->route($param)
的便利性,更重要的是 $request->route()->forgetParameter($param)
.
=== 新解===
我最终所做的基本上与@RossWilson 建议的完全相同,只是采用了 Lumen 支持的方式。我没有在控制器的 __construct
中获取 路由参数 ,而是制作了一个中间件,将 路由参数 移动到请求的输入和查询数组。
中间件看起来像这样:
public function handle($request, $next)
{
if ($genre_id = Arr::get($request->route()[2], 'genre')) {
// Add 'genre_id' to the input array (not replacing it if it already exists).
$request->merge(['genre_id' => $request->input('genre_id', $genre_id)]);
// Add 'genre_id' to the query array.
$request->query->add('genre_id', $genre_id);
// Forget the route parameter
// Has to be done manually, because Lumen...
$route = $request->route();
$request->setRouteProvider(function() use ($route) {
Arr::forget($route[2], 'genre');
return $route;
});
}
// Pass the updated $request to $next.
return $next($request);
}
在我的实现中,我只为 GET
和 DELETE
请求设置了查询参数,并为 POST
和 PUT
.
设置了输入参数
然后您可以将 BookController
重新用于 genre.book
资源,过滤来自 $request->query('genre_id')
并关联来自 $request->input('genre_id')
的关系。
===原解===
相反,我最终得到了一个具有专用关系控制器 GenreBookController
的,它继承自非关系控制器 BookController
。由于需要匹配的方法声明(请参阅下面的 $book_id = null
如何解决此问题),它并不像它应该的那样优雅,但它非常苗条和干燥。
GenreBookController extends BookController
:
protected function addGlobalScope($genre_id)
{
// Thanks Ross Wilson for the global scope suggestion.
Book::addGlobalScope('genreScope', function ($query) use ($genre_id) {
$query->where('genre_id', $genre_id);
});
}
public function show(Request $request, $genre_id, $book_id = null)
{
$this->addGlobalScope($genre_id);
return parent::show($request, $book_id);
}
对于 BookController,show 方法照常进行(它不需要知道 show
上的额外参数)。
我还想出了一个简单的方法让GenreBookController
将genre参数传递给store
方法:
public function store(Request $request, $genre_id)
{
// This way lets an input genre_id override the Route parameter.
$request->merge(['genre_id' => $request->input('genre_id', $genre_id)]);
// This way forces the Route parameter to be used over input parameters.
$request->merge(['genre_id' => $genre_id]);
return parent::store($request);
}
再说一次,对于 BookController
来说一切如常,它当然可以为通过 $request->input('genre_id')
的流派创造任何 validation/authorization。这样就没有重复的验证和授权逻辑。
关于 FormRequests 的注释
如果您使用 FormRequests 来验证 genre_id
,验证发生在 之前 GenreBookController
可以设置 genre_id
输入变量来自路由参数。
据我所知,您有两个选择:
- 使用中间件将路由参数移动到请求输入上。
- 把它放在你的
FormRequest
的 authorize
方法上(我没有测试过这个,因为我不喜欢把这样的逻辑放在这里)。
Laravel: 如果你没有使用 Lumen,我建议看看@RossWilson 的回答,我认为它更干净一些。
假设实体 Genre
和 Book
。
每个都有 API 个资源端点 /genre
和 /book
。在 Laravel 条路线中可能是:
$app->resource('/genre', GenreController::class);
我想要关系的终点。 GET /genre/1/book
, 获取书本Genre
#1.
此处的最佳做法是什么?将处理程序放在 GenreController
、BookController
或一个全新的控制器中?
旁注,我正在使用 dingo-api
包,但我认为这没有任何区别。
虽然这里没有 100% 的具体答案 - 为每个资源配备一个控制器几乎总是更容易,然后如果您想直接点击它,则为关系配备一个控制器,这听起来就是您想要的想要做。
如果您(通常)能够坚持执行主要操作(索引、创建、存储、显示、编辑、更新、删除),就会变得更容易。它将使事情井井有条,未来从事您项目的开发人员将能够轻松地遵循该结构。
很棒的阅读: 大本营的 DHH 方法:http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/
怀特豪斯API指南:https://github.com/WhiteHouse/api-standards#white-house-web-api-standards
一种选择是只使用您当前的 BooksController
。将以下内容添加到您的 BooksController
:
public function __construct(Request $request)
{
if ($genreId = $request->route('genre')) {
$request->route()->forgetParameter('genre');
Book::addGlobalScope('genreScope', function ($query) use ($genreId) {
$query->whereGenreId($genreId);
});
}
}
这将使您的图书成为 Genre
的范围,并将其作为路由参数删除。
那么你的路线就是:
$api->resource('genre.book');
请注意,使用此方法,您仍会以相同的方式使用 store
和 update
方法,即在请求中传递 genre_id
。
希望对您有所帮助!
所以@Chris 建议为关系使用一个专用控制器,而@RossWilson 有一个天才的方法来为关系重新使用控制器(至少对于加载 Book
的操作)。
不幸的是,Lumen 的 RouteProvider
returns 是一个简单的数组,因此没有 $request->route($param)
的便利性,更重要的是 $request->route()->forgetParameter($param)
.
=== 新解===
我最终所做的基本上与@RossWilson 建议的完全相同,只是采用了 Lumen 支持的方式。我没有在控制器的 __construct
中获取 路由参数 ,而是制作了一个中间件,将 路由参数 移动到请求的输入和查询数组。
中间件看起来像这样:
public function handle($request, $next)
{
if ($genre_id = Arr::get($request->route()[2], 'genre')) {
// Add 'genre_id' to the input array (not replacing it if it already exists).
$request->merge(['genre_id' => $request->input('genre_id', $genre_id)]);
// Add 'genre_id' to the query array.
$request->query->add('genre_id', $genre_id);
// Forget the route parameter
// Has to be done manually, because Lumen...
$route = $request->route();
$request->setRouteProvider(function() use ($route) {
Arr::forget($route[2], 'genre');
return $route;
});
}
// Pass the updated $request to $next.
return $next($request);
}
在我的实现中,我只为 GET
和 DELETE
请求设置了查询参数,并为 POST
和 PUT
.
然后您可以将 BookController
重新用于 genre.book
资源,过滤来自 $request->query('genre_id')
并关联来自 $request->input('genre_id')
的关系。
===原解===
相反,我最终得到了一个具有专用关系控制器 GenreBookController
的,它继承自非关系控制器 BookController
。由于需要匹配的方法声明(请参阅下面的 $book_id = null
如何解决此问题),它并不像它应该的那样优雅,但它非常苗条和干燥。
GenreBookController extends BookController
:
protected function addGlobalScope($genre_id)
{
// Thanks Ross Wilson for the global scope suggestion.
Book::addGlobalScope('genreScope', function ($query) use ($genre_id) {
$query->where('genre_id', $genre_id);
});
}
public function show(Request $request, $genre_id, $book_id = null)
{
$this->addGlobalScope($genre_id);
return parent::show($request, $book_id);
}
对于 BookController,show 方法照常进行(它不需要知道 show
上的额外参数)。
我还想出了一个简单的方法让GenreBookController
将genre参数传递给store
方法:
public function store(Request $request, $genre_id)
{
// This way lets an input genre_id override the Route parameter.
$request->merge(['genre_id' => $request->input('genre_id', $genre_id)]);
// This way forces the Route parameter to be used over input parameters.
$request->merge(['genre_id' => $genre_id]);
return parent::store($request);
}
再说一次,对于 BookController
来说一切如常,它当然可以为通过 $request->input('genre_id')
的流派创造任何 validation/authorization。这样就没有重复的验证和授权逻辑。
关于 FormRequests 的注释
如果您使用 FormRequests 来验证 genre_id
,验证发生在 之前 GenreBookController
可以设置 genre_id
输入变量来自路由参数。
据我所知,您有两个选择:
- 使用中间件将路由参数移动到请求输入上。
- 把它放在你的
FormRequest
的authorize
方法上(我没有测试过这个,因为我不喜欢把这样的逻辑放在这里)。
Laravel: 如果你没有使用 Lumen,我建议看看@RossWilson 的回答,我认为它更干净一些。