API 资源

构建 API 时,通常需要在将数据模型发送给客户端前,将其转换为统一格式。通过 Transformer 实现的 API 资源提供了一种将实体、数组或对象转换为结构化 API 响应的简洁方式。这有助于将内部数据结构与 API 暴露的内容分离,使 API 的维护和演进更加容易。

快速示例

以下示例展示了应用中 Transformer 的常见使用模式。

<?php

namespace App\Transformers;

use CodeIgniter\API\BaseTransformer;

class UserTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'id'         => $resource['id'],
            'name'       => $resource['name'],
            'email'      => $resource['email'],
            'created_at' => $resource['created_at'],
        ];
    }
}

// In your controller
$user        = model('UserModel')->find(1);
$transformer = new UserTransformer();

return $this->respond($transformer->transform($user));

在此示例中,UserTransformer 定义了 API 响应中应包含的 User 实体字段。transform() 方法转换单个资源,而 transformMany() 则处理资源集合。

创建 Transformer

若要创建 Transformer,请继承 BaseTransformer 类并实现 toArray() 方法来定义 API 资源结构。toArray() 方法接收被转换的资源作为参数,以便访问并转换其数据。

基础 Transformer

<?php

namespace App\Transformers;

use CodeIgniter\API\BaseTransformer;

class UserTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'id'           => $resource['id'],
            'username'     => $resource['name'],  // Renaming the field
            'email'        => $resource['email'],
            'member_since' => date('Y-m-d', strtotime($resource['created_at'])), // Formatting
        ];
    }
}

toArray() 方法接收资源(实体、数组或对象)作为参数,并定义 API 响应的结构。可以根据需要包含资源中的任何字段,也可以重命名或转换其值。

生成 Transformer 文件

CodeIgniter 提供了一个 CLI 命令,可快速生成 Transformer 骨架文件:

php spark make:transformer User

这将在 app/Transformers/User.php 创建一个新的 Transformer 文件,并已生成基础结构。

命令选项

make:transformer 命令支持以下选项:

--suffix

在类名后添加 “Transformer”:

php spark make:transformer User --suffix

将创建 app/Transformers/UserTransformer.php

--namespace

指定自定义根命名空间:

php spark make:transformer User --namespace="MyCompany\\API"
--force

强制覆盖现有文件:

php spark make:transformer User --force

子目录

通过在名称中包含路径,可以将 Transformer 组织到子目录中:

php spark make:transformer api/v1/User

这将创建 app/Transformers/Api/V1/User.php,并带有相应的命名空间 App\Transformers\Api\V1

在控制器中使用 Transformer

创建 Transformer 后,即可在控制器中转换数据,然后将其返回给客户端。

<?php

namespace App\Controllers;

use App\Transformers\UserTransformer;
use CodeIgniter\API\ResponseTrait;

class Users extends BaseController
{
    use ResponseTrait;

    public function show($id)
    {
        $user = model('UserModel')->find($id);

        if (! $user) {
            return $this->failNotFound('User not found');
        }

        $transformer = new UserTransformer();

        return $this->respond($transformer->transform($user));
    }

    public function index()
    {
        $users = model('UserModel')->findAll();

        $transformer = new UserTransformer();

        return $this->respond($transformer->transformMany($users));
    }
}

字段筛选

Transformer 通过当前 URL 的 fields 查询参数自动支持字段筛选。这允许 API 客户端仅请求所需的特定字段,从而节省带宽并提高性能。

<?php

namespace App\Transformers;

use CodeIgniter\API\BaseTransformer;

class UserTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'id'         => $resource['id'],
            'name'       => $resource['name'],
            'email'      => $resource['email'],
            'created_at' => $resource['created_at'],
            'updated_at' => $resource['updated_at'],
        ];
    }
}

// Request: GET /users/1?fields=id,name
// Response: {"id": 1, "name": "John Doe"}

若请求 /users/1?fields=id,name,将仅返回:

{
    "id": 1,
    "name": "John Doe"
}

限制可用字段

默认情况下,客户端可以请求 toArray() 方法中定义的任何字段。可通过重写 getAllowedFields() 方法来限制允许的字段:

<?php

namespace App\Transformers;

use CodeIgniter\API\BaseTransformer;

class UserTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'id'         => $resource['id'],
            'name'       => $resource['name'],
            'email'      => $resource['email'],
            'created_at' => $resource['created_at'],
            'updated_at' => $resource['updated_at'],
        ];
    }

    protected function getAllowedFields(): ?array
    {
        // Only these fields can be requested
        return ['id', 'name', 'created_at'];
    }
}

此时,即使客户端请求 /users/1?fields=email,也会抛出 ApiException,因为 email 不在允许的字段列表中。

包含关联资源

Transformer 支持通过 include 查询参数加载关联资源。这遵循了常见的 API 模式,即客户端可以指定需要包含哪些关联关系。虽然关联关系是最常见的用例,但也可以通过定义自定义 include 方法来包含任何额外数据。

定义 include 方法

若要支持包含关联资源,请创建以 include 为前缀、后跟资源名称的方法。在这些方法中,可以通过 $this->resource 访问当前正在转换的资源:

<?php

namespace App\Transformers;

use CodeIgniter\API\BaseTransformer;

class UserTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'id'    => $resource['id'],
            'name'  => $resource['name'],
            'email' => $resource['email'],
        ];
    }

    protected function includePosts(): array
    {
        // Use $this->resource to access the current resource being transformed
        $posts = model('PostModel')->where('user_id', $this->resource['id'])->findAll();

        return (new PostTransformer())->transformMany($posts);
    }

    protected function includeComments(): array
    {
        $comments = model('CommentModel')->where('user_id', $this->resource['id'])->findAll();

        return (new CommentTransformer())->transformMany($comments);
    }
}

请注意 include 方法如何使用 $this->resource['id'] 访问被转换用户的 ID。调用 transform() 时,Transformer 会自动设置 $this->resource 属性。

客户端现在可以请求:/users/1?include=posts,comments

响应将包含:

{
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com",
    "posts": [
        {
            "id": 1,
            "title": "First Post"
        }
    ],
    "comments": [
        {
            "id": 1,
            "content": "Great article!"
        }
    ]
}

限制可用 include

与字段筛选类似,可以通过重写 getAllowedIncludes() 方法来限制可包含的关联关系:

<?php

namespace App\Transformers;

use App\Transformers\CommentTransformer;
use App\Transformers\PostTransformer;
use CodeIgniter\API\BaseTransformer;

class UserTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'id'    => $resource['id'],
            'name'  => $resource['name'],
            'email' => $resource['email'],
        ];
    }

    protected function getAllowedIncludes(): ?array
    {
        // Only these relationships can be included
        return ['posts', 'comments'];
    }

    protected function includePosts(): array
    {
        $posts = model('PostModel')->where('user_id', $this->resource['id'])->findAll();

        return (new PostTransformer())->transformMany($posts);
    }

    protected function includeComments(): array
    {
        $comments = model('CommentModel')->where('user_id', $this->resource['id'])->findAll();

        return (new CommentTransformer())->transformMany($comments);
    }

    protected function includeOrders(): array
    {
        // This method exists but won't be callable from the API
        // because 'orders' is not in getAllowedIncludes()
        $orders = model('OrderModel')->where('user_id', $this->resource['id'])->findAll();

        return (new OrderTransformer())->transformMany($orders);
    }
}

如果想禁用所有 include,请返回一个空数组:

<?php

namespace App\Transformers;

use CodeIgniter\API\BaseTransformer;

class UserTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'id'    => $resource['id'],
            'name'  => $resource['name'],
            'email' => $resource['email'],
        ];
    }

    protected function getAllowedIncludes(): ?array
    {
        // Return empty array to disable all includes
        return [];
    }
}

include 验证

Transformer 会自动验证所有请求的 include 是否在 Transformer 类中定义了对应的 include*() 方法。如果客户端请求了不存在的 include,将抛出 ApiException

例如,如果客户端请求:

GET /api/users?include=invalid

而 Transformer 中没有 includeInvalid() 方法,则会抛出异常,提示:“Missing include method for: invalid”。

这有助于捕获拼写错误并防止非预期行为。

转换集合

使用 transformMany() 方法可轻松转换资源数组:

<?php

namespace App\Controllers;

use App\Transformers\UserTransformer;
use CodeIgniter\API\ResponseTrait;

class Users extends BaseController
{
    use ResponseTrait;

    public function index()
    {
        $users = model('UserModel')->findAll();

        $transformer = new UserTransformer();
        $data        = $transformer->transformMany($users);

        return $this->respond($data);
    }
}

transformMany() 方法会对集合中的每一项应用相同的转换逻辑,包括请求中指定的任何字段筛选或 include。

处理不同的数据类型

Transformer 不仅能处理实体,还能处理各种数据类型。

转换实体

Entity 实例传递给 transform() 时,它会自动调用实体的 toArray() 方法获取数据:

<?php

use App\Entities\User;
use App\Transformers\UserTransformer;

$user = new User([
    'id'    => 1,
    'name'  => 'John Doe',
    'email' => 'john@example.com',
]);

$transformer = new UserTransformer();
$result      = $transformer->transform($user);

转换数组

也可以转换普通数组:

<?php

use App\Transformers\UserTransformer;

$userData = [
    'id'    => 1,
    'name'  => 'John Doe',
    'email' => 'john@example.com',
];

$transformer = new UserTransformer();
$result      = $transformer->transform($userData);

转换对象

任何对象都可以转换为数组并进行转换:

<?php

use App\Transformers\UserTransformer;

$user        = new \stdClass();
$user->id    = 1;
$user->name  = 'John Doe';
$user->email = 'john@example.com';

$transformer = new UserTransformer();
$result      = $transformer->transform($user);

仅使用 toArray()

如果未向 transform() 传递资源,它将使用来自 toArray() 方法的数据:

<?php

namespace App\Transformers;

use CodeIgniter\API\BaseTransformer;

class StaticDataTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'version' => '1.0',
            'status'  => 'active',
            'message' => 'API is running',
        ];
    }
}

// Usage
$transformer = new StaticDataTransformer();
$result      = $transformer->transform(null); // No resource passed

类参考

class CodeIgniter\API\BaseTransformer
__construct(?IncomingRequest $request = null)
参数:
  • $request (IncomingRequest|null) -- 可选的请求实例。若未提供,将使用全局请求。

初始化 Transformer,并从请求中提取 fieldsinclude 查询参数。

toArray(mixed $resource)
参数:
  • $resource (mixed) -- 正在转换的资源(实体、数组、对象或 null)

返回:

资源的数组表示

返回类型:

array

此抽象方法必须由子类实现,用以定义 API 资源的结构。resource 参数包含正在转换的数据。返回一个包含要在 API 响应中显示的字段的数组,并从 $resource 参数中获取数据。

<?php

namespace App\Transformers;

use CodeIgniter\API\BaseTransformer;

class ProductTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'id'          => $resource['id'],
            'name'        => $resource['name'],
            'price'       => $resource['price'],
            'in_stock'    => $resource['stock_quantity'] > 0,
            'description' => $resource['description'],
        ];
    }
}
transform($resource = null)
参数:
  • $resource (mixed) -- 要转换的资源(实体、数组、对象或 null)

返回:

转换后的数组

返回类型:

array

通过调用包含资源数据的 toArray(),将给定资源转换为数组。如果 $resourcenull,则向 toArray() 传递 null。如果是实体,则先提取其数组表示;否则将其强制转换为数组。

资源还会存储在 $this->resource 中,以便 include 方法访问。

该方法会自动根据查询参数应用字段筛选和 include。

<?php

use App\Transformers\UserTransformer;

$user = model('UserModel')->find(1);

$transformer = new UserTransformer();

// Transform an entity
$result = $transformer->transform($user);

// Transform an array
$userData = ['id' => 1, 'name' => 'John Doe'];
$result   = $transformer->transform($userData);

// Use toArray() data
$result = $transformer->transform();
transformMany(array $resources)
参数:
  • $resources (array) -- 要转换的资源数组

返回:

转换后的资源数组

返回类型:

array

通过对每一项调用 transform() 来转换资源集合。字段筛选和 include 会一致地应用到所有项。

<?php

use App\Transformers\UserTransformer;

$users = model('UserModel')->findAll();

$transformer = new UserTransformer();
$results     = $transformer->transformMany($users);

// $results is an array of transformed user arrays
foreach ($results as $user) {
    // Each $user is the result of calling transform() on an individual user
}
getAllowedFields()
返回:

允许的字段名数组,或返回 null 以允许所有字段

返回类型:

array|null

重写此方法以限制可通过 fields 查询参数请求的字段。返回 null (默认值)以允许 toArray() 中的所有字段。返回字段名数组以创建允许字段的白名单。

<?php

namespace App\Transformers;

use CodeIgniter\API\BaseTransformer;

class UserTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'id'         => $resource['id'],
            'name'       => $resource['name'],
            'email'      => $resource['email'],
            'created_at' => $resource['created_at'],
        ];
    }

    protected function getAllowedFields(): ?array
    {
        // Clients can only request id, name, and created_at
        // Attempting to request 'email' will throw an ApiException
        return ['id', 'name', 'created_at'];
    }
}
getAllowedIncludes()
返回:

允许的 include 名称数组,或返回 null 以允许所有 include

返回类型:

array|null

重写此方法以限制可通过 include 查询参数包含的关联资源。返回 null (默认值)以允许所有具有对应方法的 include。返回 include 名称数组以创建白名单。返回空数组则禁用所有 include。

<?php

namespace App\Transformers;

use CodeIgniter\API\BaseTransformer;

class UserTransformer extends BaseTransformer
{
    public function toArray(mixed $resource): array
    {
        return [
            'id'    => $resource['id'],
            'name'  => $resource['name'],
            'email' => $resource['email'],
        ];
    }

    protected function getAllowedIncludes(): ?array
    {
        // Only 'posts' can be included via ?include=posts
        // Attempting to include 'orders' will throw an ApiException
        return ['posts'];
    }

    protected function includePosts(): array
    {
        $posts = model('PostModel')->where('user_id', $this->resource['id'])->findAll();

        return (new PostTransformer())->transformMany($posts);
    }

    protected function includeOrders(): array
    {
        // This method exists but cannot be called via the API
        $orders = model('OrderModel')->where('user_id', $this->resource['id'])->findAll();

        return (new OrderTransformer())->transformMany($orders);
    }
}

异常参考

class CodeIgniter\API\ApiException
static forInvalidFields(string $field)
参数:
  • $field (string) -- 无效的字段名

返回:

ApiException 实例

返回类型:

ApiException

当客户端通过 fields 查询参数请求了不在允许字段列表中的字段时抛出。

static forInvalidIncludes(string $include)
参数:
  • $include (string) -- 无效的 include 名称

返回:

ApiException 实例

返回类型:

ApiException

当客户端通过 include 查询参数请求了不在允许 include 列表中的内容时抛出。

static forMissingInclude(string $include)
参数:
  • $include (string) -- 缺失的 include 方法名

返回:

ApiException 实例

返回类型:

ApiException

当客户端通过 include 查询参数请求了某个 include,但 Transformer 类中不存在对应的 include*() 方法时抛出。此验证确保所有请求的 include 都有定义的处理方法。