Laravel 编码技巧 - API

作者: 温新

图书: 【Laravel 编码技巧】

阅读: 202

时间: 2024-02-29 11:08:23

API 资源: 有 "数据 "还是无 "数据"?

如果你使用 Eloquent API 资源来返回数据,它们将自动包装在 'data' 中。如果您想要移除这个包装,可以在 app/Providers/AppServiceProvider.php 文件中添加 JsonResource::withoutWrapping()

use Illuminate\Support\ServiceProvider;
use Illuminate\Http\Resources\Json\JsonResource;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        JsonResource::withoutWrapping();
    }
}

API 资源上的条件关系计数

你可以通过使用 whenCounted 方法来有条件地在资源响应中包含关系的计数。通过这样做,如果关系计数不可用,该属性将不被包含在响应中。

public function toArray($request)
{
    return [
        'id'          => $this->id,
        'name'        => $this->name,
        'email'       => $this->email,
        'posts_count' => $this->whenCounted('posts'),
        'created_at'  => $this->created_at,
        'updated_at'  => $this->updated_at,
    ];
}

在上述示例中,'posts_count' => $this->whenCounted('posts') 用于在资源数组中添加关系 posts 的计数,只有在计数可用时才包含该属性。这对于按需包含关系计数的场景非常有用。

API 返回“一切正常”

如果您有一个执行某些操作但没有响应的 API 端点,因此您只想返回 "一切都正常",您可以返回 204 状态码 "No Content"。在 Laravel 中,这很容易实现:return response()->noContent();

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
use App\Models\Country;

public function reorder(Request $request)
{
    foreach ($request->input('rows', []) as $row) {
        Country::find($row['id'])->update(['position' => $row['position']]);
    }

    return Response::noContent();
}

避免在 API 资源中进行 N+1 查询

使用 whenLoaded() 方法可以在 API 资源中避免 N+1 查询。

这只会在 Employee 模型中已加载部门时才追加部门信息。

没有使用 whenLoaded() 时,总是会对部门进行查询。

use Illuminate\Http\Resources\Json\JsonResource;

class EmployeeResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'         => $this->uuid,
            'fullName'   => $this->full_name,
            'email'      => $this->email,
            'jobTitle'   => $this->job_title,
            'department' => DepartmentResource::make($this->whenLoaded('department')),
        ];
    }
}

从 Authorization header 中获取 BearerToken

bearerToken() 函数在处理 API 并希望从授权头获取令牌时非常方便。

// 不要手动解析 API 标头,如下所示:
$tokenWithBearer = $request->header('Authorization');
$token = substr($tokenWithBearer, 7);

// 相反,使用以下方式:
$token = $request->bearerToken();

对 API 结果进行排序

这是一个用于处理 API 单列排序,并带有方向控制的 Laravel 路由的示例。

对于单列排序(例如,/dogs?sort=name/dogs?sort=-name):

use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Models\Dog;

Route::get('dogs', function (Request $request) {
    // 获取排序查询参数(如果没有提供,默认为 "name")
    $sortColumn = $request->input('sort', 'name');

    // 根据键是否以 - 开头设置排序方向
    // 使用 Laravel 的 Str::startsWith() 辅助函数
    $sortDirection = Str::startsWith($sortColumn, '-') ? 'desc' : 'asc';
    $sortColumn = ltrim($sortColumn, '-');

    return Dog::orderBy($sortColumn, $sortDirection)->paginate(20);
});

对于多列排序(例如,?sort=name,-weight):

use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Models\Dog;

Route::get('dogs', function (Request $request) {
    // 获取查询参数并通过逗号拆分为数组
    $sorts = explode(',', $request->input('sort', ''));

    // 创建查询构造器
    $query = Dog::query();

    // 逐个添加排序
    foreach ($sorts as $sortColumn) {
        $sortDirection = Str::startsWith($sortColumn, '-') ? 'desc' : 'asc';
        $sortColumn = ltrim($sortColumn, '-');

        $query->orderBy($sortColumn, $sortDirection);
    }

    // 返回结果
    return $query->paginate(20);
});

这些路由允许通过查询参数进行排序,支持单列或多列,以及指定升序(asc)或降序(desc)。

自定义 API 的异常排除

这是 Laravel 8 及以下版本和 Laravel 9 及以上版本中异常处理的不同之处。

Laravel 8 及以下版本

在 Laravel 8 及以下版本

异常处理通常在 App\Exceptions 类的 render 方法中定义:

public function render($request, Exception $exception)
{
    if ($request->wantsJson() || $request->is('api/*')) {
        if ($exception instanceof ModelNotFoundException) {
            return response()->json(['message' => 'Item Not Found'], 404);
        }

        if ($exception instanceof AuthenticationException) {
            return response()->json(['message' => 'unAuthenticated'], 401);
        }

        if ($exception instanceof ValidationException) {
            return response()->json(['message' => 'UnprocessableEntity', 'errors' => []], 422);
        }

        if ($exception instanceof NotFoundHttpException) {
            return response()->json(['message' => 'The requested link does not exist'], 400);
        }
    }

    return parent::render($request, $exception);
}

Laravel 9 及以上版本

在 Laravel 9 及以上版本

异常处理使用 renderable 方法进行注册。这个方法可以在 App\Exceptions 类的 register 方法中调用:

public function register()
{
    $this->renderable(function (ModelNotFoundException $e, $request) {
        if ($request->wantsJson() || $request->is('api/*')) {
            return response()->json(['message' => 'Item Not Found'], 404);
        }
    });

    $this->renderable(function (AuthenticationException $e, $request) {
        if ($request->wantsJson() || $request->is('api/*')) {
            return response()->json(['message' => 'unAuthenticated'], 401);
        }
    });

    $this->renderable(function (ValidationException $e, $request) {
        if ($request->wantsJson() || $request->is('api/*')) {
            return response()->json(['message' => 'UnprocessableEntity', 'errors' => []], 422);
        }
    });

    $this->renderable(function (NotFoundHttpException $e, $request) {
        if ($request->wantsJson() || $request->is('api/*')) {
            return response()->json(['message' => 'The requested link does not exist'], 400);
        }
    });
}

这两个版本的异常处理逻辑保持相似,但 Laravel 9 引入了 renderable 方法,提供了更加灵活和清晰的方式来注册异常处理逻辑。

对 API 请求强制 JSON 响应

如果构建了一个 API,并且在请求中没有包含 "Accept: application/JSON " HTTP 头时遇到错误,那么错误将作为 HTML 或在 API 路由上返回重定向响应,为了避免这种情况,我们可以强制所有 API 响应返回 JSON。

第一步是通过运行以下命令创建中间件:

php artisan make:middleware ForceJsonResponse

App/Http/Middleware/ForceJsonResponse.php 文件的 handle 函数中编写以下代码:

public function handle($request, Closure $next)
{
    $request->headers->set('Accept', 'application/json');
    return $next($request);
}

第二步,在 app/Http/Kernel.php 文件中注册创建的中间件:

protected $middlewareGroups = [        
    'api' => [
        \App\Http\Middleware\ForceJsonResponse::class,
    ],
];

这将确保 API 路由的请求强制使用 JSON 格式进行响应。

API 版本控制

何时进行版本控制?

如果你正在处理一个未来可能有多个版本发布或者你的端点有一个破坏性的变化,比如响应数据格式的改变,并且你希望确保在对代码进行更改时 API 版本仍然可用。

更改默认路由文件

第一步是更改 App\Providers\RouteServiceProvider 文件中的路由映射,让我们开始:

Laravel 8 及更高版本:

添加一个 ApiNamespace 属性

/**
 * @var string
 */
protected string $ApiNamespace = 'App\Http\Controllers\Api';

boot 方法中添加以下代码:

$this->routes(function () {
    Route::prefix('api/v1')
        ->middleware('api')
        ->namespace($this->ApiNamespace.'\\V1')
        ->group(base_path('routes/API/v1.php'));

    Route::prefix('api/v2')
        ->middleware('api')
        ->namespace($this->ApiNamespace.'\\V2')
        ->group(base_path('routes/API/v2.php'));
});

Laravel 7 及以下版本:

添加一个 ApiNamespace 属性

/**
 * @var string
 */
protected string $ApiNamespace = 'App\Http\Controllers\Api';

map 方法中添加以下代码:

// 移除这行 $this->mapApiRoutes(); 
$this->mapApiV1Routes();
$this->mapApiV2Routes();

并添加以下方法:

protected function mapApiV1Routes()
{
    Route::prefix('api/v1')
        ->middleware('api')
        ->namespace($this->ApiNamespace.'\\V1')
        ->group(base_path('routes/Api/v1.php'));
}

protected function mapApiV2Routes()
{
    Route::prefix('api/v2')
        ->middleware('api')
        ->namespace($this->ApiNamespace.'\\V2')
        ->group(base_path('routes/Api/v2.php'));
}

控制器文件夹版本控制

Controllers
└── Api
    ├── V1
    │   └──AuthController.php
    └── V2
        └──AuthController.php

路由文件版本控制

routes
└── Api
   │    └── v1.php     
   │    └── v2.php 
   └── web.php
请登录后再评论