跳至正文

映射 HTTP 请求

该库提供了一种将 HTTP 请求映射到控制器操作参数或对象属性的方法。参数可以从路由、查询和正文值映射。

三个特性可用于将参数显式绑定到单个源,确保永远不会从错误的源解析该值:

  • #[FromRoute] — 用于路由器从 URL 路径中提取的参数
  • #[FromQuery] — 用于查询字符串参数
  • #[FromBody] — 用于请求正文值

如果参数未绑定到特定源,则可以完全省略这些特性,在这种情况下,如果在多个源中找到相同的键,则会引发冲突错误。

这为控制器提供了干净、类型安全的签名,无需耦合到框架的请求对象,同时受益于库的验证和错误处理。

正常映射规则适用于此处:参数是必需的,除非它们具有默认值。

来自 HTTP 请求的路由和查询参数值通常是字符串。映射器自动处理这些参数的标量值转换:字符串"42"将正确映射到int参数。

提示

Valinor Symfony Bundle提供与 Symfony 的 HTTP Foundation 组件的本机集成。


Mezzio 集成包会自动配置映射器,从而允许 PSR-15 请求处理程序轻松地将 PSR-7 请求映射到域对象中。

使用特性映射请求

考虑一个列出给定作者文章的 API。作者标识符来自 URL 路径,而过滤和分页来自查询字符串。

use CuyZ\Valinor\Mapper\Http\FromQuery;
use CuyZ\Valinor\Mapper\Http\FromRoute;
use CuyZ\Valinor\Mapper\Http\HttpRequest;
use CuyZ\Valinor\MapperBuilder;

final class ListArticles
{
    /**
     * GET /api/authors/{authorId}/articles?status=X&page=X&limit=X
     * 
     * @param non-empty-string $status
     * @param positive-int $page
     * @param int<10, 100> $limit
     */
    public function __invoke(
        // Comes from the route
        #[FromRoute] string $authorId,

        // All come from query parameters
        #[FromQuery] string $status,
        #[FromQuery] int $page = 1,
        #[FromQuery] int $limit = 10,
    ): ResponseInterface { … }
}

// GET /api/authors/42/articles?status=published&page=2
$request = new HttpRequest(
    routeParameters: ['authorId' => 42],
    queryParameters: [
        'status' => 'published',
        'page' => 2,
    ],
);

$controller = new ListArticles();

$arguments = (new MapperBuilder())
    ->argumentsMapper()
    ->mapArguments($controller, $request);

$response = $controller(...$arguments);

不使用特性映射请求

当不需要区分参数来自哪个源时,可以完全省略该特性 - 映射器将从包含匹配键的源解析每个参数。

use CuyZ\Valinor\Mapper\Http\HttpRequest;
use CuyZ\Valinor\MapperBuilder;

final class PostComment
{
    /**
     * POST /api/posts/{postId}/comments
     *
     * @param positive-int $postId
     * @param non-empty-string $author
     * @param non-empty-string $content
     */
    public function __invoke(
        int $postId,
        string $author,
        string $content,
    ): ResponseInterface { … }
}

// POST /api/posts/1337/comments
$request = new HttpRequest(
    routeParameters: ['postId' => 1337],
    bodyValues: [
        'author' => 'jane.doe@example.com',
        'content' => 'Great article, thanks for sharing!',
    ],
);

$controller = new PostComment();

$arguments = (new MapperBuilder())
    ->argumentsMapper()
    ->mapArguments($controller, $request);

$response = $controller(...$arguments);

注意

如果在多个源中找到没有特性的参数的相同键,则会引发冲突错误。

一次映射所有参数

可以使用asRoot选项将所有查询参数或正文值一次映射到单个参数,而不是将各个查询参数或正文值映射到单独的参数。当处理复杂的数据结构或参数数量很大时,这非常有用。

use CuyZ\Valinor\Mapper\Http\FromQuery;
use CuyZ\Valinor\Mapper\Http\FromRoute;

final readonly class ArticleFilters
{
    public function __construct(
        /** @var non-empty-string */
        public string $status,
        /** @var positive-int */
        public int $page = 1,
        /** @var int<10, 100> */
        public int $limit = 10,
    ) {}
}

final class ListArticles
{
    /**
     * GET /api/authors/{authorId}/articles?status=X&&page=X&limit=X
     */
    public function __invoke(
        #[FromRoute] string $authorId,
        #[FromQuery(asRoot: true)] ArticleFilters $filters,
    ): ResponseInterface { … }
}

对于正文值,同样的方法适用于#[FromBody(asRoot: true)] 。

提示

成形数组可以与asRoot一起使用,将所有值映射到单个参数:

use CuyZ\Valinor\Mapper\Http\FromQuery;
use CuyZ\Valinor\Mapper\Http\FromRoute;

final class ListArticles
{
    /**
     * GET /api/authors/{authorId}/articles?status=X&&page=X&limit=X
     *
     * @param array{
     *     status: non-empty-string,
     *     page?: positive-int, 
     *     limit?: int<10, 100>,
     * } $filters
     */
    public function __invoke(
        #[FromRoute] string $authorId,
        #[FromQuery(asRoot: true)] array $filters,
    ): ResponseInterface { … }
}

映射到对象

HttpRequest可以直接映射到对象,而不是映射到可调用的参数。这些属性在构造函数参数或提升属性上的工作方式相同。

use CuyZ\Valinor\Mapper\Http\FromBody;
use CuyZ\Valinor\Mapper\Http\FromRoute;
use CuyZ\Valinor\Mapper\Http\HttpRequest;
use CuyZ\Valinor\MapperBuilder;

final readonly class PostComment
{
    public function __construct(
        /** @var positive-int */
        #[FromRoute] public int $postId,
        /** @var non-empty-string */
        #[FromBody] public string $author,
        /** @var non-empty-string */
        #[FromBody] public string $content,
    ) {}
}

$request = new HttpRequest(
    routeParameters: ['postId' => 1337],
    bodyValues: [
        'author' => 'jane.doe@example.com',
        'content' => 'Great article, thanks for sharing!',
    ],
);

$comment = (new MapperBuilder())
    ->mapper()
    ->map(PostComment::class, $request);

// $comment->postId  === 1337
// $comment->author  === 'jane.doe@example.com'
// $comment->content === 'Great article, thanks for sharing!'

使用 PSR-7 请求

HttpRequest实例可以直接从PSR-7 ServerRequestInterface构建。这是与使用 PSR-7 的框架集成时推荐的方法。

use CuyZ\Valinor\Mapper\Http\HttpRequest;
use CuyZ\Valinor\MapperBuilder;

// `$psrRequest` is a PSR-7 `ServerRequestInterface` instance
// `$routeParameters` are the parameters extracted by the router
$request = HttpRequest::fromPsr($psrRequest, $routeParameters);

$arguments = (new MapperBuilder())
    ->argumentsMapper()
    ->mapArguments($controller, $request);

工厂方法从getQueryParams()中提取查询参数,从getParsedBody()中提取正文值。它还传递原始 PSR-7 请求对象,因此可以根据需要将其注入控制器参数中(见下文)。

访问原始请求对象

构建HttpRequest时,可以提供原始请求对象。如果控制器参数的类型与该对象匹配,则会自动注入;不需要任何特性。

use CuyZ\Valinor\Mapper\Http\FromRoute;
use CuyZ\Valinor\Mapper\Http\HttpRequest;
use CuyZ\Valinor\MapperBuilder;
use Psr\Http\Message\ServerRequestInterface;

final class ListArticles
{
    /**
     * GET /api/authors/{authorId}/articles
     */
    public function __invoke(
        // Request object injected automatically
        ServerRequestInterface $request,

        #[FromRoute] string $authorId,
    ): ResponseInterface {
        $acceptHeader = $request->getHeaderLine('Accept');

        // …
    }
}

$request = HttpRequest::fromPsr($psrRequest, $routeParameters);

$arguments = (new MapperBuilder())
    ->argumentsMapper()
    ->mapArguments(new ListArticles(), $request);

// $arguments['request'] is the original PSR-7 request instance

约束与转换键名的大小写格式

API 通常使用与 PHP 代码库不同的命名约定 - 例如,带有映射到camelCase法属性的snake_case键的 JSON 负载。两个系列的配置器可以帮助解决这个问题:

  • RestrictKeysTo*Case配置器拒绝与预期格式不匹配的键(例如snake_case 、 camelCase ),从而引发映射错误。
  • MapKeysTo*Case配置器在映射之前将输入键转换为目标格式(例如first_name → firstName )。

可以将它们组合起来,以便限制验证原始键并转换重新映射它们。两者都适用于 HTTP 请求映射。

use CuyZ\Valinor\Mapper\Configurator\MapKeysToCamelCase;
use CuyZ\Valinor\Mapper\Configurator\RestrictKeysToSnakeCase;
use CuyZ\Valinor\MapperBuilder;

$controllerArguments = (new MapperBuilder())
    ->configureWith(
        new RestrictKeysToSnakeCase(),
        new MapKeysToCamelCase(),
    )
    ->argumentsMapper()
    ->mapArguments($controller, $httpRequest);

请阅读通用映射器配置器章节,了解可用配置器的完整列表和详细用法。

提示

可以使用#[MapFromKey]特性结合#[FromRoute] 、 #[FromQuery]或#[FromBody]从特定键读取单个参数:

use CuyZ\Valinor\Mapper\Configurator\MapFromKey;
use CuyZ\Valinor\Mapper\Http\FromQuery;

$controller = function (
    #[FromQuery] #[MapFromKey('zipCode')]
    string $postalCode,
): ResponseInterface { … };

键按原样读取,不受上述键名的大小写格式配置器的影响。有关完整行为,请参阅转换源键。

错误处理

当映射失败时(例如,由于缺少必需的查询参数或主体值的类型错误),会抛出MappingError ,就像常规映射一样。

请阅读验证和错误处理章节以获取更多信息。