【问题标题】:How to organise controllers for REST relationships? [Laravel routes]如何为 REST 关系组织控制器? [Laravel 路线]
【发布时间】:2017-07-24 13:09:29
【问题描述】:

想象实体GenreBook

每个都有 API 资源端点 /genre/book。在 Laravel 路线中可能是:

 $app->resource('/genre', GenreController::class);

我想要关系的端点。 GET /genre/1/book,获取Genre#1下的书籍。

这里的最佳做法是什么?将处理程序放在GenreControllerBookController 或一个全新的控制器中?

顺便说一句,我使用的是dingo-api 包,但我认为这没有什么区别。

【问题讨论】:

  • genre.books 的输出是否与books 相同,但只是将其范围限定为genre,或者您是否会包含其他信息。另外,您是想从genre.books 创建一个完整的资源,还是只是indexshow 方法?
  • 另外,你使用的是什么版本的 Laravel?
  • @RossWilson 我正在使用 Lumen/Laravel 5.4。我希望我想返回相同的结果,并且我喜欢使用 storeupdate 方法来保持一致性,但我不想以太多不必要的复杂性结束。
  • 酷,酷。是否只是您想要的路线使用 indexshow 方法?
  • @RossWilson 我喜欢使用storeupdate 方法来保持一致性的想法,但我不想以太多不必要的复杂性结束。

标签: php laravel rest laravel-5 dingo-api


【解决方案1】:

所以@Chris 建议为关系使用专用控制器,而@RossWilson 有一种天才的方法可以为关系重用控制器(至少对于加载 Book 的操作)。

不幸的是 Lumen 的RouteProvider 返回一个简单的数组,因此没有$request->route($param) 更重要的是$request->route()->forgetParameter($param) 的便利性。

=== 新解决方案 ===

我最终做了与@RossWilson 建议的基本完全相同的事情,只是以 Lumen 支持的方式。我没有在 Controller 的 __construct 中获取 Route 参数,而是制作了一个将 Route 参数 移动到 Request 的输入和查询数组的中间件。

中间件看起来像这样:

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

在我的实现中,我只为GETDELETE 请求设置查询参数,并为POSTPUT 设置输入参数。

然后您可以将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 将流派参数传递给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') 的流派进行任何验证/授权。这样就没有重复的验证和授权逻辑。

关于 FormRequests 的说明

如果您使用 FormRequests 来验证genre_id,则验证发生在之前GenreBookController 可以从路由参数中设置genre_id 输入变量。

在我看来,你有两个选择:

  1. 使用中间件将路由参数移动到请求输入。
  2. 把它放在你的FormRequestauthorize 方法上(我没有测试过这个,因为我不喜欢把这样的逻辑放在这里)。

Laravel:如果你不使用 Lumen,我建议看一下@RossWilson 的回答,我认为它更简洁一些。

【讨论】:

    【解决方案2】:

    一种选择是只使用您当前的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');
    

    请注意,使用此方法时,您仍会以相同的方式使用 storeupdate 方法,即在请求中传递 genre_id

    希望这会有所帮助!

    【讨论】:

    • 谢谢,我喜欢可重用性! $request->route($param = null) 对我来说是新的,你能指出我正确的方向吗,至于 route('genre') 如何/为什么是流派 ID?
    • @Zoon 当你做Route::resource()(或$app在你的情况下)它基本上会为你生成资源的所有路线。以show 为例,资源将创建基本上是get('genre/{genre}/book/{book}') 的路由。 {genre} 基本上是说 uri 段是可变的,可以使用索引“genre”访问,因此 $request->route('genre')
    • @Zoon 这解决了您的问题吗?如果是这样,你会把它标记为正确吗?
    • 我仍在对此进行测试并研究授权路由参数的方法。 Fx(在此示例中)genre 可能仅限于某些用户,但我不确定 $this->user 是否可用于构造函数。此外,您示例中的路由功能将在 Laravel 的 Lumen 版本中可用。
    • @Zoon 我不会尝试将身份验证逻辑放在上面。我建议为laravel.com/docs/5.4/authorization 使用类似策略的东西。如果您确实需要进一步的帮助,请打开一个新问题,因为它超出了这个问题的范围:)
    【解决方案3】:

    虽然这里没有 100% 的具体答案 - 每个资源都有一个控制器几乎总是更容易,如果你想直接点击它,然后一个用于关系的控制器,从它的声音来看,这就是你想要的做。

    如果您可以(通常)坚持主要操作(索引、创建、存储、显示、编辑、更新、删除),那将会更容易。它将使事情井井有条,未来从事您项目的开发人员将能够轻松地遵循该结构。

    很好的阅读: 大本营的DHH方法:http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/

    白宫 API 指南:https://github.com/WhiteHouse/api-standards#white-house-web-api-standards

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-10-29
      • 1970-01-01
      • 2013-05-06
      • 2015-01-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多