Skip to content

Latest commit

 

History

History
453 lines (350 loc) · 12.4 KB

File metadata and controls

453 lines (350 loc) · 12.4 KB

Ray.InputQuery

Continuous Integration Type Coverage codecov

Structured input objects from HTTP with 100% test coverage.

Overview

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, assigneeName fields automatically compose UserInput objects
  • 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
}

Installation

composer require ray/input-query

Demo

To see file upload integration in action:

php -S localhost:8080 -t demo/

Then visit http://localhost:8080 in your browser.

Documentation

Comprehensive documentation including design philosophy, AI prompts for development assistance, and sample data examples can be found in the docs/ directory.

Framework Integration

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

Usage

Ray.InputQuery converts flat query data into typed PHP objects automatically.

Basic Usage

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);

Nested Objects

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;  // John

Array Support

Ray.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
    }
}

Query data format for arrays

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 objects

Simple array values (e.g., checkboxes)

For 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">

ArrayObject Inheritance Support

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
}

Mixed with Dependency Injection

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
    ) {}
}

Key Normalization

All query keys are normalized to camelCase:

  • user_nameuserName
  • user-nameuserName
  • UserNameuserName

File Upload Integration

Ray.InputQuery provides comprehensive file upload support through integration with Koriym.FileUpload:

composer require koriym/file-upload

Using #[InputFile] Attribute

For 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 approach

Test-Friendly Design

File 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
]);

Multiple File Uploads

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
]);

Integration

Ray.InputQuery is designed as a foundation library to be used by:

Project Quality

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

Requirements

  • PHP 8.1+
  • ray/di ^2.0

License

MIT