Laravel 13 API资源进阶深度解析

API资源是 Laravel 提供的一种优雅的方式来转换模型和模型集合为 JSON 格式。本文将深入探讨 Laravel 13 中 API资源的高级用法。

API资源基础

创建资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String(),
];
}
}

使用资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Http\Controllers;

use App\Http\Resources\UserResource;
use App\Models\User;

class UserController extends Controller
{
public function show(int $id): UserResource
{
$user = User::findOrFail($id);
return new UserResource($user);
}

public function index(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection
{
$users = User::paginate(15);
return UserResource::collection($users);
}
}

条件属性

when() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'is_admin' => $this->when(
$this->isAdmin(),
$this->role === 'admin'
),
'secret' => $this->when(
$request->user()->isAdmin(),
'secret-value'
),
'subscription' => $this->when(
$this->hasSubscription(),
fn() => new SubscriptionResource($this->subscription)
),
];
}
}

whenLoaded() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'posts' => PostResource::collection(
$this->whenLoaded('posts')
),
'comments' => CommentResource::collection(
$this->whenLoaded('comments')
),
'profile' => new ProfileResource(
$this->whenLoaded('profile')
),
];
}
}

whenCounted() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'posts_count' => $this->whenCounted('posts'),
'comments_count' => $this->whenCounted('comments'),
'followers_count' => $this->whenCounted('followers'),
];
}
}

whenPivotLoaded() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class BookResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'author' => new AuthorResource($this->author),
'pivot' => $this->whenPivotLoaded(
'book_user',
fn() => [
'rating' => $this->pivot->rating,
'read_at' => $this->pivot->read_at?->toIso8601String(),
'notes' => $this->pivot->notes,
]
),
];
}
}

资源嵌套

嵌套资源示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'order_number' => $this->order_number,
'status' => $this->status,
'total' => $this->total,
'user' => new UserResource($this->whenLoaded('user')),
'items' => OrderItemResource::collection(
$this->whenLoaded('items')
),
'shipping_address' => new AddressResource(
$this->whenLoaded('shippingAddress')
),
'billing_address' => new AddressResource(
$this->whenLoaded('billingAddress')
),
'payments' => PaymentResource::collection(
$this->whenLoaded('payments')
),
];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class OrderItemResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'quantity' => $this->quantity,
'unit_price' => $this->unit_price,
'total' => $this->total,
'product' => new ProductResource(
$this->whenLoaded('product')
),
];
}
}

资源集合

自定义集合资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
public function toArray($request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => route('users.index'),
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
'meta' => [
'current_page' => $this->currentPage(),
'from' => $this->firstItem(),
'last_page' => $this->lastPage(),
'per_page' => $this->perPage(),
'to' => $this->lastItem(),
'total' => $this->total(),
],
];
}
}

带统计的集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class OrderCollection extends ResourceCollection
{
protected array $statistics;

public function __construct($resource, array $statistics = [])
{
parent::__construct($resource);
$this->statistics = $statistics;
}

public function toArray($request): array
{
return [
'data' => $this->collection,
'statistics' => $this->statistics,
'meta' => [
'total_orders' => $this->collection->count(),
'total_amount' => $this->collection->sum('total'),
'average_amount' => $this->collection->avg('total'),
],
];
}
}

数据包装

自定义包装键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
public static $wrap = 'user';

public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
];
}
}

禁用包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
public static $wrap = null;

public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
];
}
}

全局禁用包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

namespace App\Providers;

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

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

附加响应数据

with() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}

public function with($request): array
{
return [
'meta' => [
'api_version' => '1.0',
'generated_at' => now()->toIso8601String(),
],
];
}
}

withResponse() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\JsonResponse;

class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
];
}

public function withResponse($request, JsonResponse $response): void
{
$response->setStatusCode(200);
$response->header('X-Resource-Type', 'User');
$response->header('X-Resource-Id', $this->id);
}
}

资源条件合并

merge() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
$this->mergeWhen(
$request->user()->isAdmin(),
[
'email' => $this->email,
'role' => $this->role,
'last_login' => $this->last_login?->toIso8601String(),
]
),
];
}
}

mergeWhen() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class ProductResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
$this->mergeWhen(
$this->hasDiscount(),
fn() => [
'discount_price' => $this->discount_price,
'discount_percentage' => $this->discount_percentage,
'discount_ends_at' => $this->discount_ends_at?->toIso8601String(),
]
),
];
}
}

资源继承

基础资源类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

abstract class BaseResource extends JsonResource
{
protected function formatTimestamp($timestamp): ?string
{
return $timestamp?->toIso8601String();
}

protected function formatPrice(float $price): string
{
return number_format($price, 2, '.', '');
}

protected function getCommonFields(): array
{
return [
'id' => $this->id,
'created_at' => $this->formatTimestamp($this->created_at),
'updated_at' => $this->formatTimestamp($this->updated_at),
];
}
}

继承资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace App\Http\Resources;

class ProductResource extends BaseResource
{
public function toArray($request): array
{
return array_merge($this->getCommonFields(), [
'name' => $this->name,
'description' => $this->description,
'price' => $this->formatPrice($this->price),
'stock' => $this->stock,
'category' => new CategoryResource($this->whenLoaded('category')),
]);
}
}

分页资源

自定义分页响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class PaginatedCollection extends ResourceCollection
{
public function toArray($request): array
{
return [
'data' => $this->collection,
'pagination' => [
'total' => $this->total(),
'count' => $this->count(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'total_pages' => $this->lastPage(),
],
'links' => [
'self' => $this->url($this->currentPage()),
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
}

测试资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace Tests\Unit\Resources;

use Tests\TestCase;
use App\Http\Resources\UserResource;
use App\Models\User;

class UserResourceTest extends TestCase
{
public function test_user_resource_structure(): void
{
$user = User::factory()->create([
'name' => 'John Doe',
'email' => 'john@example.com',
]);

$resource = new UserResource($user);
$array = $resource->toArray(request());

$this->assertEquals($user->id, $array['id']);
$this->assertEquals('John Doe', $array['name']);
$this->assertEquals('john@example.com', $array['email']);
}

public function test_conditional_attributes(): void
{
$user = User::factory()->create(['role' => 'admin']);

$adminRequest = request()->setUserResolver(fn() => $user);

$resource = new UserResource($user);
$array = $resource->toArray($adminRequest);

$this->assertArrayHasKey('is_admin', $array);
}
}

最佳实践

1. 保持资源简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

class CleanUserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}

2. 使用关系加载

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

class OrderResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'user' => UserResource::make($this->whenLoaded('user')),
'items' => OrderItemResource::collection($this->whenLoaded('items')),
];
}
}

3. 格式化数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

class FormattedResource extends JsonResource
{
protected function formatDate($date): string
{
return $date->format('Y-m-d H:i:s');
}

protected function formatMoney(float $amount): string
{
return '$' . number_format($amount, 2);
}
}

总结

Laravel 13 的 API资源提供了一种强大的方式来转换数据为 JSON 格式。通过合理使用资源类,可以创建一致、可维护的 API 响应格式,同时保持代码的清晰和可测试性。