Structured input objects from HTTP with 100% test coverage.
Ray.InputQuery transforms flat HTTP data into structured PHP objects through explicit type declarations. Using the #[Input] attribute, you declare which parameters come from query data, while other parameters are resolved via dependency injection.
Core Mechanism:
- Attribute-Based Control -
#[Input]explicitly marks query-sourced parameters - Prefix-Based Nesting -
assigneeId,assigneeNamefields automatically composeUserInputobjects - Type-Safe Conversion - Leverages PHP's type system for automatic scalar conversion
- DI Integration - Parameters without
#[Input]are resolved from dependency injection
The Problem:
// Manual parameter extraction and object construction
$data = $request->getParsedBody(); // or $_POST
$title = $data['title'] ?? '';
$assigneeId = $data['assigneeId'] ?? '';
$assigneeName = $data['assigneeName'] ?? '';
$assigneeEmail = $data['assigneeEmail'] ?? '';
**Ray.InputQuery Solution:**
```php
// Declarative structure definition
final class TodoInput {
public function __construct(
#[Input] public readonly string $title,
#[Input] public readonly UserInput $assignee, // Auto-composed from assigneeId, assigneeName, assigneeEmail
private LoggerInterface $logger // From DI container
) {}
}
public function createTodo(TodoInput $input) {
// $input automatically structured from request data
}composer require ray/input-queryTo see file upload integration in action:
php -S localhost:8080 -t demo/Then visit http://localhost:8080 in your browser.
Comprehensive documentation including design philosophy, AI prompts for development assistance, and sample data examples can be found in the docs/ directory.
For framework-specific integration examples, see the Framework Integration Guide which covers:
- Laravel, Symfony, CakePHP, Yii Framework 1.x, BEAR.Sunday, and Slim Framework
- Three usage patterns (Reflection, Direct Object Creation, Spread Operator)
- Testing examples and best practices
Ray.InputQuery converts flat query data into typed PHP objects automatically.
Define your input class with the #[Input] attribute on parameters that come from query data:
use Ray\InputQuery\Attribute\Input;
final class UserInput
{
public function __construct(
#[Input] public readonly string $name,
#[Input] public readonly string $email
) {}
}Create input objects from query data:
use Ray\InputQuery\InputQuery;
use Ray\Di\Injector;
$injector = new Injector();
$inputQuery = new InputQuery($injector);
// Create object directly from array
$user = $inputQuery->create(UserInput::class, [
'name' => 'John Doe',
'email' => 'john@example.com'
]);
echo $user->name; // John Doe
echo $user->email; // john@example.com
// Method argument resolution from $_POST
$method = new ReflectionMethod(UserController::class, 'register');
$args = $inputQuery->getArguments($method, $_POST);
$result = $method->invokeArgs($controller, $args);
// Or with PSR-7 Request
$args = $inputQuery->getArguments($method, $request->getParsedBody());
$result = $method->invokeArgs($controller, $args);Ray.InputQuery automatically creates nested objects from flat query data:
final class TodoInput
{
public function __construct(
#[Input] public readonly string $title,
#[Input] public readonly UserInput $assignee // Nested input
) {}
}
$todo = $inputQuery->create(TodoInput::class, [
'title' => 'Buy milk',
'assigneeId' => '123',
'assigneeName' => 'John',
'assigneeEmail' => 'john@example.com'
]);
echo $todo->title; // Buy milk
echo $todo->assignee->name; // JohnRay.InputQuery supports arrays and ArrayObject collections with the item parameter:
use Ray\InputQuery\Attribute\Input;
final class UserInput
{
public function __construct(
#[Input] public readonly string $id,
#[Input] public readonly string $name
) {}
}
final class UserListController
{
/**
* @param list<UserInput> $users
*/
public function updateUsers(
#[Input(item: UserInput::class)] array $users // Array of UserInput objects
) {
foreach ($users as $user) {
echo $user->name; // Each element is a UserInput instance
}
}
/**
* @param ArrayObject<int, UserInput> $users
*/
public function processUsers(
#[Input(item: UserInput::class)] ArrayObject $users // ArrayObject collection
) {
// $users is an ArrayObject containing UserInput instances
}
}Arrays should be submitted as indexed arrays. Here's how to structure HTML forms and the resulting data:
<!-- HTML Form -->
<form method="post">
<input name="users[0][id]" value="1">
<input name="users[0][name]" value="Jingu">
<input name="users[1][id]" value="2">
<input name="users[1][name]" value="Horikawa">
</form>This will be received as:
$data = [
'users' => [
['id' => '1', 'name' => 'Jingu'],
['id' => '2', 'name' => 'Horikawa']
]
];
$result = $method->invokeArgs($controller, $inputQuery->getArguments($method, $data));
// Arguments automatically resolved as UserInput objectsFor simple arrays like checkboxes or multi-select:
<form method="post">
<!-- Checkbox group -->
<input name="hobbies[]" type="checkbox" value="music">
<input name="hobbies[]" type="checkbox" value="sports">
<input name="hobbies[]" type="checkbox" value="reading">
<!-- Multi-select -->
<select name="categories[]" multiple>
<option value="tech">Technology</option>
<option value="business">Business</option>
<option value="lifestyle">Lifestyle</option>
</select>
</form>This will be received as:
$data = [
'hobbies' => ['music', 'sports'], // Only checked values
'categories' => ['tech', 'lifestyle'] // Only selected values
];
// In your controller
/**
* @param list<string> $hobbies
* @param list<string> $categories
*/
public function updatePreferences(
#[Input] array $hobbies, // Simple string array
#[Input] array $categories // Simple string array
) {
// Direct array of strings, no object conversion needed
}Note: For non-array parameters, use flat naming without brackets:
<!-- Single object properties -->
<input name="customerName" value="Jingu">
<input name="customerEmail" value="jingu@example.com">Custom ArrayObject subclasses are also supported:
final class UserCollection extends ArrayObject
{
public function getFirst(): ?UserInput
{
return $this[0] ?? null;
}
}
/**
* @param array<UserInput> $users
*/
public function handleUsers(
#[Input(item: UserInput::class)] UserCollection $users
) {
$firstUser = $users->getFirst(); // Custom method available
}Parameters without the #[Input] attribute are resolved via dependency injection:
use Ray\Di\Di\Named;
final class OrderInput
{
public function __construct(
#[Input] public readonly string $orderId, // From query
#[Input] public readonly CustomerInput $customer, // From query
#[Named('tax.rate')] private float $taxRate, // From DI
private LoggerInterface $logger // From DI
) {}
}All query keys are normalized to camelCase:
user_name→userNameuser-name→userNameUserName→userName
Ray.InputQuery provides comprehensive file upload support through integration with Koriym.FileUpload:
composer require koriym/file-uploadFor file uploads, use the dedicated #[InputFile] attribute which provides validation options:
use Koriym\FileUpload\FileUpload;
use Koriym\FileUpload\ErrorFileUpload;
use Ray\InputQuery\Attribute\InputFile;
final class UserProfileInput
{
public function __construct(
#[Input] public readonly string $name,
#[Input] public readonly string $email,
#[InputFile(
maxSize: 5 * 1024 * 1024, // 5MB
allowedTypes: ['image/jpeg', 'image/png'],
allowedExtensions: ['jpg', 'jpeg', 'png']
)]
public readonly FileUpload|ErrorFileUpload $avatar,
#[InputFile] public readonly FileUpload|ErrorFileUpload|null $banner = null,
) {}
}
// Method usage example - Direct attribute approachFile upload handling is designed to be test-friendly:
- Production - FileUpload library handles file uploads automatically
- Testing - Direct FileUpload object injection for easy mocking
// Production usage - FileUpload library handles file uploads automatically
$input = $inputQuery->create(UserProfileInput::class, $_POST);
// FileUpload objects are created automatically from uploaded files
// Testing usage - inject mock FileUpload objects directly for easy testing
$mockAvatar = FileUpload::create([
'name' => 'test.jpg',
'type' => 'image/jpeg',
'size' => 1024,
'tmp_name' => '/tmp/test',
'error' => UPLOAD_ERR_OK,
]);
$input = $inputQuery->create(UserProfileInput::class, [
'name' => 'Test User',
'email' => 'test@example.com',
'avatar' => $mockAvatar,
'banner' => null
]);Support for multiple file uploads using array types with validation:
final class GalleryInput
{
/**
* @param list<FileUpload|ErrorFileUpload> $images
*/
public function __construct(
#[Input] public readonly string $title,
#[InputFile(
maxSize: 10 * 1024 * 1024, // 10MB per file
allowedTypes: ['image/*']
)]
public readonly array $images,
) {}
}
// Method usage example
class GalleryController
{
public function createGallery(GalleryInput $input): void
{
$savedImages = [];
foreach ($input->images as $image) {
if ($image instanceof FileUpload) {
$savedImages[] = $this->saveFile($image, 'gallery/');
} elseif ($image instanceof ErrorFileUpload) {
// Log error but continue with other images
$this->logger->warning('Image upload failed: ' . $image->message);
}
}
$this->galleryService->create($input->title, $savedImages);
}
}
// Production usage - FileUpload library handles multiple files automatically
$input = $inputQuery->create(GalleryInput::class, $_POST);
// Array of FileUpload objects created automatically from uploaded files
// Testing usage - inject array of mock FileUpload objects for easy testing
$mockImages = [
FileUpload::create(['name' => 'image1.jpg', ...]),
FileUpload::create(['name' => 'image2.png', ...])
];
$input = $inputQuery->create(GalleryInput::class, [
'title' => 'My Gallery',
'images' => $mockImages
]);Ray.InputQuery is designed as a foundation library to be used by:
- Ray.MediaQuery - For database query integration
- BEAR.Resource - For REST resource integration
This project maintains high quality standards:
- 100% Code Coverage - Achieved through public interface tests only
- Static Analysis - Psalm and PHPStan at maximum levels
- Test Design - No private method tests, ensuring maintainability
- Type Safety - Comprehensive Psalm type annotations
- PHP 8.1+
- ray/di ^2.0
MIT