Laravel 编码技巧 - API
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