yiisoft/input-http
Yii HTTP input package for extracting request data from PSR-7 requests, including query parameters, body fields, headers, and uploaded files, with a consistent API for safe access to user input in Yii applications.
This is the Yii3 package that gives you something close to Laravel’s request injection / FormRequest data binding, but split into two pieces: hydrator attributes that pull values out of a PSR-7 request, and middleware-dispatcher integration so middleware constructors can also receive request-derived values. It is small (11 stars), BSD-3-Clause licensed, and the current release is 1.0.1, which specifically adds support for yiisoft/validator ^2.0 (previous releases targeted validator ^1.0). It is worth using if you are already on Yii3 with yiisoft/hydrator and yiisoft/middleware-dispatcher; otherwise the integration cost is higher than the package’s surface area suggests.
Minimal steps:
Install it:
composer require yiisoft/input-http
Confirm your app has the expected neighbours: a PSR-7 implementation, yiisoft/hydrator, and yiisoft/middleware-dispatcher. If you also validate hydrated DTOs with yiisoft/validator, use ^2.0 on 1.0.1; on older releases you must stay on validator ^1.0. In the standard Yii3 app template the package config is auto-discovered through yiisoft/config. If you are assembling config manually, include the package’s config/di.php in your config plugin and check config/params.php for defaults.
Look first at src/Attribute to see what is available (Body, Query, RequestAttribute, UploadedFiles), then config/di.php to see how body parsers and the middleware factory are wired. The tests are the best documentation for edge cases.
First use case: define an input DTO and type-hint it in a controller action.
use Yiisoft\Input\Http\Attribute\Body;
use Yiisoft\Input\Http\Attribute\Query;
final readonly class CreatePostInput
{
public function __construct(
#[Body] public string $title,
#[Body] public string $content,
#[Body] public array $tags = [],
#[Query] public bool $notify = false,
) {}
}
public function create(CreatePostInput $input): ResponseInterface
{
// $input is already populated from the current request.
}
The exact namespaces above are from the 1.x line; if an import fails, check src/Attribute in the installed version. The key idea is that the attribute tells the hydrator “this parameter comes from the request body / query string / request attribute / uploaded files”, not from the DI container.
One input DTO per endpoint or action. This is the closest analogue to a Laravel FormRequest without the validation layer. Keep the DTO focused on transport shape, and run yiisoft/validator (or your own rules) after hydration. The package hydrates; it does not validate. On 1.0.1 you can pair it with yiisoft/validator ^2.0 without a version conflict.
Prefer readonly promoted constructor properties. The hydrator reads parameter attributes, so promoted parameters are the least surprising form:
final readonly class SearchInput
{
public function __construct(
#[Query] public string $q = '',
#[Query] public int $page = 1,
#[Query] public int $perPage = 20,
) {}
}
Defaults make absent query parameters safe. Without defaults, a missing value can become a type error rather than an empty string.
Combine sources deliberately. A typical create/update action reads route identity from a request attribute and payload from the body:
use Yiisoft\Input\Http\Attribute\Body;
use Yiisoft\Input\Http\Attribute\RequestAttribute;
final readonly class UpdatePostInput
{
public function __construct(
#[RequestAttribute('id')] public string $id,
#[Body] public string $title,
#[Body] public ?string $content = null,
) {}
}
Route parameters are usually PSR-7 request attributes after routing, so RequestAttribute is the bridge. Confirm the attribute name your router actually sets; do not assume it matches the route placeholder name.
Use whole-body hydration for flexible JSON. If the payload shape is dynamic, hydrate an array:
final readonly class WebhookInput
{
public function __construct(
#[Body] public array $payload,
) {}
}
For stable APIs, prefer explicit typed fields. You get earlier failures and better IDE support.
Pair hydration with yiisoft/validator ^2.0. The 1.0.1 release explicitly adds validator ^2.0 support, which is the natural next step after hydration. A pattern that mirrors Laravel’s FormRequest without the package owning the rules:
$input = /* hydrated DTO */;
$result = (new Validator())->validate($input, [
'title' => [new HasLength(min: 1, max: 255)],
'content' => [new HasLength(max: 10000)],
]);
if ($result->isFailure()) {
// map to 422
}
Keep rules in a service, not on the DTO, so the input object stays a pure transport shape.
Middleware pattern: request-scoped, not singleton. The middleware-dispatcher integration lets middleware constructor parameters use the same attributes:
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Yiisoft\Input\Http\Attribute\RequestAttribute;
final class TenantMiddleware implements MiddlewareInterface
{
public function __construct(
#[RequestAttribute('tenant')] private readonly Tenant $tenant,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// ...
return $handler->handle($request);
}
}
This is useful for middleware that needs tenant, user, feature flag, or route data without re-parsing the request in every process() call. The trade-off is that the middleware is hydrated per request; do not register such middleware as a shared singleton and do not store per-request state on it.
Extension point: custom body parsers. The default body parsing covers JSON and URL-encoded forms. For XML, CSV, or vendor media types, implement the package’s BodyParserInterface and register it in the body-parser service/config. Keep the parser strict: reject malformed input with the package’s exception type so your error handler can map it to 400 rather than 500.
Testing pattern. Unit-test DTO hydration by constructing the DTO directly; integration-test the request-to-DTO path with a real PSR-7 request:
$request = (new ServerRequestFactory())
->createServerRequest('POST', '/posts')
->withHeader('Content-Type', 'application/json')
->withBody(Stream::create(json_encode(['title' => 'Hello', 'content' => 'Body'])));
Then run it through the same hydrator/action path your app uses. For middleware, assert that attribute-derived constructor values are correct and that the middleware still behaves when the attribute is absent or malformed.
Error-handling pattern. Wrap hydration failures in your HTTP error handler. Malformed JSON, unsupported content types, missing request attributes, and type mismatches are client errors, not server errors. A small middleware or error-handler mapping from the package exceptions to 400/422 responses keeps controllers clean.
Attributes on properties are not attributes on parameters. With promoted constructor properties, #[Body] sits on the parameter and works. If you declare a separate property and put the attribute there, the hydrator will not see it. Keep attributes on constructor parameters.
Content-Type drives body parsing. application/json and application/x-www-form-urlencoded are the happy paths. A missing or unknown Content-Type may produce an empty body or an exception. multipart/form-data is not a normal body-parser case; read uploaded files through UploadedFiles and the PSR-7 parsed body instead.
Charset parameters matter. application/json; charset=utf-8 should still be treated as JSON, but custom parsers must parse the media type robustly rather than comparing the raw header string.
Empty JSON bodies are a common 500. Content-Type: application/json with an empty body can hydrate to null/empty data. A non-nullable typed parameter with no default then fails. Use nullable types or defaults, and decide explicitly whether an empty body is valid for the endpoint.
Query values are strings. The hydrator can coerce simple scalars, but booleans are a trap: ?notify=false is not necessarily false, and ?notify=0 may depend on the caster. Validate and normalise boolean query flags yourself, or accept 0/1 as the documented contract.
Query arrays need bracket syntax. ?ids[]=1&ids[]=2 becomes an array; ?ids=1&ids=2 usually keeps only the last value. Document this for API consumers.
Reading the body consumes the stream. If earlier middleware reads the body before the hydrator does, rewind the stream. The same applies if you read the raw body after hydration for logging or signature verification.
Middleware ordering and lazy instantiation. A middleware that reads #[RequestAttribute('tenant')] only works if the middleware that sets that attribute has already run. Verify whether your dispatcher instantiates middleware lazily in order. If it eagerly instantiates all middleware, attributes set by earlier middleware will not be visible at constructor-hydration time; in that case read from $request inside process() instead.
Do not make request-hydrated middleware shared. Constructor-injected request data makes the instance request-specific. Registering it as a singleton will leak data across requests or fail on the second request.
Validation is still not included, but validator ^2.0 is now supported. Unlike Laravel’s FormRequest, there is no rules() method and no automatic 422. Hydrate first, then validate with yiisoft/validator (v2 on 1.0.1) or your own layer. Keep the two concerns separate so the DTO stays reusable. If you are upgrading from an older 1.0.x release and still on validator v1, either stay on the old input-http release or upgrade the validator alongside this package.
How can I help you explore Laravel packages today?