指南

模型

了解如何通过 API 暴露模型。

设置控制器

要通过 API 暴露一个模型,首先需要为它创建一个控制器。正如你在快速上手 - 简单 CRUD一节中所看到的,定义模型控制器非常简单。

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * 模型的完全限定类名
     */
    protected $model = Post::class; // 或 "App\Models\Post"
}

禁用分页

默认情况下,index 端点返回的资源会进行分页。要禁用分页并返回全部资源,可以使用 DisablePagination trait。

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;
use Orion\Concerns\DisablePagination;

class PostsController extends Controller
{
    use DisablePagination;

    /**
     * 模型的完全限定类名
     */
    protected $model = Post::class; // 或 "App\Models\Post"
}
要点回顾
  • 模型控制器始终继承 Orion\Http\Controllers\Controller
  • $model 属性设置为完全限定的模型类名
  • 使用 DisablePagination trait 可以同时为模型资源和关联资源禁用分页

设置路由

控制器创建完成后,就可以注册路由了。

<?php

use Illuminate\Support\Facades\Route;
use Orion\Facades\Orion;
use App\Http\Controllers\PostsController;

Route::group(['as' => 'api.'], function() {
    ...
    Orion::resource('posts', PostsController::class);
    ...
});

本质上,Orion::resource 方法与 Laravel 默认的 Route::apiResource 相同——它会创建多条路由,用于处理针对资源的各种操作。

+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
| Domain | Method    | URI                                             | Name                                   | Action                                                                    | Middleware                                      |
+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
...
|        | GET|HEAD  | api/posts                                       | api.posts.index                        | App\Http\Controllers\Api\PostsController@index                            | api                                             |
|        | POST      | api/posts/search                                | api.posts.search                       | App\Http\Controllers\Api\PostsController@index                            | api                                             |
|        | POST      | api/posts                                       | api.posts.store                        | App\Http\Controllers\Api\PostsController@store                            | api                                             |
|        | GET|HEAD  | api/posts/{post}                                | api.posts.show                         | App\Http\Controllers\Api\PostsController@show                             | api                                             |  
|        | PUT|PATCH | api/posts/{post}                                | api.posts.update                       | App\Http\Controllers\Api\PostsController@update                           | api                                             |
|        | DELETE    | api/posts/{post}                                | api.posts.destroy                      | App\Http\Controllers\Api\PostsController@destroy                          | api                                             |
|        | POST      | api/posts/batch                                 | api.posts.batchStore                   | App\Http\Controllers\Api\PostsController@batchStore                       | api                                             |
|        | PATCH     | api/posts/batch                                 | api.posts.batchUpdate                  | App\Http\Controllers\Api\PostsController@batchUpdate                      | api                                             |
|        | DELETE    | api/posts/batch                                 | api.posts.batchDestroy                 | App\Http\Controllers\Api\PostsController@batchDestroy                     | api                                             |

软删除

如果你的模型使用了 SoftDeletes trait,并且希望通过 API 暴露相同的功能,请在注册资源时调用 withSoftDeletes 方法。

<?php

use Illuminate\Support\Facades\Route;
use Orion\Facades\Orion;

Route::group(['as' => 'api.'], function() {
    ...
    Orion::resource('posts', 'Api\PostsController')->withSoftDeletes();
    ...
});

这会引入 restorebatchRestore 端点。要了解如何通过 API 永久删除资源(强制删除),请参阅相关的查询参数一节。

+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
| Domain | Method    | URI                                             | Name                                   | Action                                                                    | Middleware                                      |
+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
...
|        | POST      | api/posts/{post}/restore                        | api.posts.restore                      | App\Http\Controllers\Api\PostsController@restore                          | api                                             |
|        | POST      | api/posts/batch/restore                         | api.posts.batchRestore                 | App\Http\Controllers\Api\PostsController@batchRestore                     | api                                             |

自定义键名

默认情况下,端点使用主键(通常是 id)从数据库中检索模型。但在某些情况下,你可能希望在保持主键不变的同时,使用其他字段来获取模型。为此,请在控制器上重写 keyName 方法:

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * 模型的完全限定类名
     */
    protected $model = Post::class; // 或 "App\Models\Post"

    /**
     * 用于从数据库中获取资源的字段名称。
     *
     * @return string
     */
    protected function keyName(): string
    {
        return 'slug';
    }
}

自定义查询

Orion 非常灵活,允许你重新定义每个端点的 Eloquent 查询是如何构建和运行的。

针对单个端点

构建查询

假设你希望 index 端点只返回已发布的博客文章。为此,请在控制器上重写 buildIndexFetchQuery 方法:

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * 模型的完全限定类名
     */
    protected $model = Post::class; // 或 "App\Models\Post"

    /**
     * 构建用于在 index 方法中获取实体的 Eloquent 查询。
     *
     * @param Request $request
     * @param array $requestedRelations
     * @return Builder
     */
    protected function buildIndexFetchQuery(Request $request, array $requestedRelations): Builder
    {
        $query = parent::buildIndexFetchQuery($request, $requestedRelations);

        $query->whereNotNull('published_at');

        return $query;
    }
}

运行查询

一个常见的例子是在获取模型列表时只选取特定的列。为此,请重写 runIndexFetchQuery 方法:

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * 模型的完全限定类名
     */
    protected $model = Post::class; // 或 "App\Models\Post"

    ...

    /**
     * 运行给定的查询以在 index 方法中获取实体。
     *
     * @param Request $request
     * @param Builder $query
     * @param int $paginationLimit
     * @return LengthAwarePaginator
     */
    protected function runIndexFetchQuery(Request $request, Builder $query, int $paginationLimit): LengthAwarePaginator
    {
        return $query->paginate($paginationLimit, ['id', 'title', 'published_at']);
    }
}

执行操作

indexshow 等端点的主要目的是从数据库中检索数据,而不是修改数据。但 storeupdate 等端点会对数据库进行更改。你同样可以自定义某些操作(例如存储模型)的执行方式。

在下面的示例中,如果当前认证用户是管理员(这里的角色实现是虚构的),我们会对文章的属性进行强制填充:

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * 模型的完全限定类名
     */
    protected $model = Post::class; // 或 "App\Models\Post"

    ...

    /**
     * 填充给定实体的属性并将其存储到数据库中。
     *
     * @param Request $request
     * @param Model $entity
     * @param array $attributes
     */
    protected function performStore(Request $request, Model $entity, array $attributes): void
    {
        if ($this->resolveUser()->hasRole('admin')) {
            $entity->forceFill($attributes);
        } else {
            $entity->fill($attributes);
        }
        $entity->save();
    }
}
你还可以通过重写 performFill 方法来自定义属性在模型上的填充方式。

针对一组端点

很好,现在 index 端点只返回已发布的文章。但如果你希望对 show 端点也应用相同的约束呢?当然,我们可以复制这段逻辑并重写 buildShowFetchQuery 方法,但还有更好的办法。

构建查询

如果你查看 buildIndexFetchQuery 方法的实现,会注意到它使用了 buildFetchQuery 方法。实际上,indexshowupdatedestroyrestore 端点都使用这个方法来构建获取模型的查询,而你同样可以重写它!

如果你希望同时对 indexshow 端点(以及 updatedestroyrestore)应用相同的约束,代码如下:

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * 模型的完全限定类名
     */
    protected $model = Post::class; // 或 "App\Models\Post"

    /**
     * 构建用于获取实体的 Eloquent 查询。
     *
     * @param Request $request
     * @param array $requestedRelations
     * @return Builder
     */
    protected function buildFetchQuery(Request $request, array $requestedRelations): Builder
    {
        $query = parent::buildFetchQuery($request, $requestedRelations);

        $query->whereNotNull('published_at');

        return $query;
    }

    ...
}

运行查询

在运行查询方面,index 端点算是一个例外(因为它获取的是模型列表,而不是单个模型),而 showupdatedestroyrestore 端点共享相同的逻辑,底层都使用 runFetchQuery

稍加重构后,我们就得到了这样一个控制器:只获取已发布的博客文章,并且只从数据库中检索 idtitlepublished_at 这几个列(属性)。

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * 模型的完全限定类名
     */
    protected $model = Post::class; // 或 "App\Models\Post"

    /**
     * 要从数据库中选择的属性列表
     */
    protected $attributes = ['id', 'title', 'published_at'];

    /**
     * 构建用于获取实体的 Eloquent 查询。
     *
     * @param Request $request
     * @param array $requestedRelations
     * @return Builder
     */
    protected function buildFetchQuery(Request $request, array $requestedRelations): Builder
    {
        $query = parent::buildFetchQuery($request, $requestedRelations);

        $query->whereNotNull('published_at');

        return $query;
    }

    /**
     * 运行给定的查询以获取实体。
     *
     * @param Request $request
     * @param Builder $query
     * @param int|string $key
     * @return Model
     */
    protected function runFetchQuery(Request $request, Builder $query, $key): Model
    {
        return $query->select($this->attributes)->findOrFail($key);
    }

    /**
     * 运行给定的查询以在 index 方法中获取实体。
     *
     * @param Request $request
     * @param Builder $query
     * @param int $paginationLimit
     * @return LengthAwarePaginator
     */
    protected function runIndexFetchQuery(Request $request, Builder $query, int $paginationLimit): LengthAwarePaginator
    {
        return $query->paginate($paginationLimit, $this->attributes);
    }

    /**
     * 填充给定实体的属性并将其存储到数据库中。
     *
     * @param Request $request
     * @param Model $post
     * @param array $attributes
     */
    protected function performStore(Request $request, Model $post, array $attributes): void
    {
        if ($this->resolveUser()->hasRole('admin')) {
            $post->forceFill($attributes);
        } else {
            $post->fill($attributes);
        }
        $post->save();
    }
}

标准操作方法

方法构建运行执行
indexbuildIndexFetchQueryrunIndexFetchQuery-
storebuildStoreFetchQueryrunStoreFetchQueryperformStore
showbuildShowFetchQueryrunShowFetchQuery-
updatebuildUpdateFetchQueryrunUpdateFetchQueryperformUpdate
destroybuildDestroyFetchQueryrunDestroyFetchQueryperformDestroy
restorebuildRestoreFetchQueryrunRestoreFetchQueryperformRestore