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
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 aSchemaInterfaceclass 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 aFileFactoryservice 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
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-18