gilsegura/psr-messages-bundle 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

gilsegura/psr-messages-bundle

Composer 安装命令:

composer require gilsegura/psr-messages-bundle

包简介

Symfony integration for gilsegura/psr-messages: registers the parsers, schema resolver, response factories and PSR-15 middlewares as services, wires the Symfony<->PSR bridge, and adds a #[Pipeline] attribute plus an exception subscriber for JSON:API error responses.

README 文档

README

tests codecov static analysis coding standards

Symfony bundle that turns a controller into a PSR-15 pipeline. Each controller declares, with a #[Pipeline] attribute, the middlewares that should run around it — body parsing, header/query parsing and request/response validation — using the real middlewares from the gilsegura/psr-messages, gilsegura/psr-validator and gilsegura/psr-server packages. The controller is the terminal PSR-15 handler; the pipeline only adds middlewares in front of it.

How it works

#[Pipeline]  →  configurations (plain data; each names its factory)
                     ↓  PipelineResolver  (memoized per controller)
              ($factory)($configuration)   ← factory is an invokable service
                     ↓
              real psr-* middleware  (stateless, reused per worker)
                     ↓
              MiddlewareRunner  [ terminal handler = your controller ]

A kernel.controller subscriber reads the #[Pipeline], builds the MiddlewareRunner once per controller class (memoized for the lifetime of the worker), converts the Symfony request to PSR-7, runs the pipeline and converts the PSR-7 response back. A controller with no #[Pipeline] simply runs with no middlewares — it is still the terminal handler.

Installation

composer require gilsegura/psr-messages-bundle

Register the bundle (or let Symfony Flex do it):

// config/bundles.php
return [
    Psr\Messages\Bundle\PsrMessagesBundle::class => ['all' => true],
];

Writing a controller

Extend AbstractController and declare the pipeline. Controllers are autoconfigured as Symfony controllers, so you only route to them as usual.

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Messages\Bundle\Attribute\Pipeline;
use Psr\Messages\Bundle\Controller\AbstractController;
use Psr\Messages\Bundle\Pipeline\Config\HeadersConfiguration;
use Psr\Messages\Bundle\Pipeline\Config\MediaTypeParser;
use Psr\Messages\Bundle\Pipeline\Config\ParsedBodyConfiguration;
use Psr\Messages\Bundle\Pipeline\Config\Validation\BodyValidationConfiguration;
use Psr\Messages\Bundle\Pipeline\Config\Validation\RequestValidationConfiguration;
use Psr\Messages\Bundle\Pipeline\Config\ValidationConfiguration;

#[Pipeline(
    new HeadersConfiguration(RequestHeaders::class),
    new ValidationConfiguration(
        new RequestValidationConfiguration(new BodyValidationConfiguration('create-user')),
    ),
    new ParsedBodyConfiguration(MediaTypeParser::JSON_API, CreateUserRequest::class),
)]
final readonly class PostUserController extends AbstractController
{
    #[\Override]
    public function __invoke(ServerRequestInterface $request): ResponseInterface
    {
        $body = $request->getParsedBody();   // a CreateUserRequest, already parsed
        // ...
    }
}

The order of the configurations in #[Pipeline] is the order the middlewares run in.

The two kinds of schema

The bundle deliberately resolves schemas in two different ways, because they are two different things:

  • Parsing (ParsedBodyConfiguration, HeadersConfiguration, QueryParamsConfiguration) references a SchemaInterface class by its class-string. That class deserializes the raw input into a typed object; it is resolved inline (no container needed).
  • Validation (BodyValidationConfiguration, etc.) references a JSON Schema file by an alias. The bundle scans the schema directory, registers a FileFactory service per .json, and the validation factory resolves it by that alias.

So a parser turns the body into a CreateUserRequest, while validation checks the raw body against create-user.json — independent pieces.

Schemas (validation)

JSON Schema files live in config/schemas/ by default. The file name (without .json) is the alias you pass to the validation configurations:

config/schemas/create-user.json   →  new BodyValidationConfiguration('create-user')

Override the directory:

// config/packages/psr_messages.php
return static function (Symfony\Config\PsrMessagesConfig $config): void {
    $config->schemaDir('%kernel.project_dir%/config/schemas');
};

The directory is registered as a resource, so adding or changing a .json invalidates the container in dev.

Mapping exceptions to HTTP status

The bundle is exception-agnostic: it ships no status mappers. Provide your own by implementing ExceptionStatusMapper — it is autoconfigured by tag and collected by the exception subscriber, which renders a JSON:API error response. The first mapper that returns a Status wins; unmapped exceptions fall back to 500.

use Psr\Messages\Bundle\Exception\ExceptionStatusMapper;
use Psr\Server\ResponseFactory\Status;

final readonly class ValidationStatusMapper implements ExceptionStatusMapper
{
    #[\Override]
    public function statusFor(\Throwable $throwable): ?Status
    {
        return $throwable instanceof ValidationExceptionInterface
            ? Status::UNPROCESSABLE_CONTENT
            : null;
    }
}

Adding your own middleware

Implement a MiddlewareConfiguration (plain data that names its factory) and a MiddlewareFactory (an invokable that builds the PSR-15 middleware). The factory is autoconfigured by tag, so it is reachable from any #[Pipeline] without touching the bundle.

use Psr\Http\Server\MiddlewareInterface;
use Psr\Messages\Bundle\Pipeline\MiddlewareConfiguration;
use Psr\Messages\Bundle\Pipeline\MiddlewareFactory;

final readonly class RateLimitConfiguration implements MiddlewareConfiguration
{
    public function __construct(public int $perMinute = 60) {}

    #[\Override]
    public static function factory(): string
    {
        return RateLimitMiddlewareFactory::class;
    }
}

/** @implements MiddlewareFactory<RateLimitConfiguration> */
final readonly class RateLimitMiddlewareFactory implements MiddlewareFactory
{
    #[\Override]
    public function __invoke(MiddlewareConfiguration $configuration): MiddlewareInterface
    {
        return new RateLimitMiddleware($configuration->perMinute);
    }
}

Then reference it in a pipeline: #[Pipeline(new RateLimitConfiguration(120), ...)].

Performance

The reflection of the #[Pipeline] and the MiddlewareRunner are built once per controller class and reused for the lifetime of the worker. The first request to a controller pays that one-off cost; every later request is a cached lookup plus the actual middleware work (validation/parsing). Run make benchmark to measure it in your environment.

License

MIT. See LICENSE.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-18

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固