@@ -24,7 +24,7 @@ $title = $data['title'] ?? '';
2424$assigneeId = $data['assigneeId'] ?? '';
2525$assigneeName = $data['assigneeName'] ?? '';
2626$assigneeEmail = $data['assigneeEmail'] ?? '';
27-
27+ ```
2828
2929** Ray.InputQuery Solution:**
3030``` php
@@ -48,6 +48,14 @@ public function createTodo(TodoInput $input) {
4848composer require ray/input-query
4949```
5050
51+ ### Optional: File Upload Support
52+
53+ For file upload functionality, also install:
54+
55+ ``` bash
56+ composer require koriym/file-upload
57+ ```
58+
5159## Demo
5260
5361To see file upload integration in action:
@@ -100,7 +108,7 @@ $injector = new Injector();
100108$inputQuery = new InputQuery($injector);
101109
102110// Create object directly from array
103- $user = $inputQuery->create (UserInput::class, [
111+ $user = $inputQuery->newInstance (UserInput::class, [
104112 'name' => 'John Doe',
105113 'email' => 'john@example.com'
106114]);
@@ -123,23 +131,34 @@ $result = $method->invokeArgs($controller, $args);
123131Ray.InputQuery automatically creates nested objects from flat query data:
124132
125133``` php
126- final class TodoInput
134+ final class AddressInput
127135{
128136 public function __construct(
129- #[Input] public readonly string $title,
130- #[Input] public readonly UserInput $assignee // Nested input
137+ #[Input] public readonly string $street,
138+ #[Input] public readonly string $city,
139+ #[Input] public readonly string $zip
131140 ) {}
132141}
133142
134- $todo = $inputQuery->create(TodoInput::class, [
135- 'title' => 'Buy milk',
136- 'assigneeId' => '123',
137- 'assigneeName' => 'John',
138- 'assigneeEmail' => 'john@example.com'
143+ final class UserInput
144+ {
145+ public function __construct(
146+ #[Input] public readonly string $name,
147+ #[Input] public readonly string $email,
148+ #[Input] public readonly AddressInput $address // Nested input
149+ ) {}
150+ }
151+
152+ $user = $inputQuery->newInstance(UserInput::class, [
153+ 'name' => 'John Doe',
154+ 'email' => 'john@example.com',
155+ 'addressStreet' => '123 Main St',
156+ 'addressCity' => 'Tokyo',
157+ 'addressZip' => '100-0001'
139158]);
140159
141- echo $todo->title ; // Buy milk
142- echo $todo->assignee->name; // John
160+ echo $user->name ; // John Doe
161+ echo $user->address->street; // 123 Main St
143162```
144163
145164### Array Support
@@ -289,14 +308,71 @@ Parameters without the `#[Input]` attribute are resolved via dependency injectio
289308``` php
290309use Ray\Di\Di\Named;
291310
292- final class OrderInput
311+ interface AddressServiceInterface
312+ {
313+ public function findByZip(string $zip): Address;
314+ }
315+
316+
317+ interface TicketFactoryInterface
318+ {
319+ public function create(string $eventId, string $ticketId): Ticket;
320+ }
321+
322+ final class EventBookingInput
293323{
294324 public function __construct(
295- #[Input] public readonly string $orderId, // From query
296- #[Input] public readonly CustomerInput $customer, // From query
297- #[Named('tax.rate')] private float $taxRate, // From DI
298- private LoggerInterface $logger // From DI
299- ) {}
325+ #[Input] public readonly string $ticketId, // From query - raw ID
326+ #[Input] public readonly string $email, // From query
327+ #[Input] public readonly string $zip, // From query
328+ #[Named('event_id')] private string $eventId, // From DI
329+ private TicketFactoryInterface $ticketFactory, // From DI
330+ private AddressServiceInterface $addressService, // From DI
331+ ) {
332+ // Create complete Ticket object from ID (includes validation, expiry, etc.)
333+ $this->ticket = $this->ticketFactory->create($eventId, $ticketId);
334+ // Fully validated immutable ticket object created!
335+
336+ if (!$this->ticket->isValid) {
337+ throw new InvalidTicketException(
338+ "Ticket {$ticketId} is invalid: {$this->ticket->getInvalidReason()}"
339+ );
340+ }
341+
342+ // Get address from zip
343+ $this->address = $this->addressService->findByZip($zip);
344+ }
345+
346+ public readonly Ticket $ticket; // Complete ticket object with ID, status, etc.
347+ public readonly Address $address; // Structured address object
348+ }
349+
350+ // DI configuration
351+ $injector = new Injector(new class extends AbstractModule {
352+ protected function configure(): void
353+ {
354+ $this->bind(TicketFactoryInterface::class)->to(TicketFactory::class); // Can swap with mock in tests
355+ $this->bind(AddressServiceInterface::class)->to(AddressService::class);
356+ $this->bind()->annotatedWith('event_id')->toInstance('ray-event-2025');
357+ }
358+ });
359+
360+ $inputQuery = new InputQuery($injector);
361+
362+ // Usage - Factory automatically creates complete objects from IDs
363+ try {
364+ $booking = $inputQuery->newInstance(EventBookingInput::class, [
365+ 'ticketId' => 'TKT-2024-001',
366+ 'email' => 'user@example.com',
367+ 'zip' => '100-0001'
368+ ]);
369+
370+ // $booking->ticket is a Ticket object with ID and validation status
371+ echo "Ticket ID: " . $booking->ticket->id; // Only valid ticket ID
372+
373+ } catch (InvalidTicketException $e) {
374+ // Handle expired or invalid tickets
375+ echo "Booking failed: " . $e->getMessage();
300376}
301377```
302378
@@ -316,6 +392,15 @@ Ray.InputQuery provides comprehensive file upload support through integration wi
316392composer require koriym/file-upload
317393```
318394
395+ When using file upload features, instantiate InputQuery with FileUploadFactory:
396+
397+ ``` php
398+ use Ray\InputQuery\InputQuery;
399+ use Ray\InputQuery\FileUploadFactory;
400+
401+ $inputQuery = new InputQuery($injector, new FileUploadFactory());
402+ ```
403+
319404### Using #[ InputFile] Attribute
320405
321406For file uploads, use the dedicated ` #[InputFile] ` attribute which provides validation options:
@@ -352,7 +437,7 @@ File upload handling is designed to be test-friendly:
352437
353438``` php
354439// Production usage - FileUpload library handles file uploads automatically
355- $input = $inputQuery->create (UserProfileInput::class, $_POST);
440+ $input = $inputQuery->newInstance (UserProfileInput::class, $_POST);
356441// FileUpload objects are created automatically from uploaded files
357442
358443// Testing usage - inject mock FileUpload objects directly for easy testing
@@ -364,7 +449,7 @@ $mockAvatar = FileUpload::create([
364449 'error' => UPLOAD_ERR_OK,
365450]);
366451
367- $input = $inputQuery->create (UserProfileInput::class, [
452+ $input = $inputQuery->newInstance (UserProfileInput::class, [
368453 'name' => 'Test User',
369454 'email' => 'test@example.com',
370455 'avatar' => $mockAvatar,
@@ -412,7 +497,7 @@ class GalleryController
412497}
413498
414499// Production usage - FileUpload library handles multiple files automatically
415- $input = $inputQuery->create (GalleryInput::class, $_POST);
500+ $input = $inputQuery->newInstance (GalleryInput::class, $_POST);
416501// Array of FileUpload objects created automatically from uploaded files
417502
418503// Testing usage - inject array of mock FileUpload objects for easy testing
@@ -421,33 +506,8 @@ $mockImages = [
421506 FileUpload::create(['name' => 'image2.png', ...])
422507];
423508
424- $input = $inputQuery->create (GalleryInput::class, [
509+ $input = $inputQuery->newInstance (GalleryInput::class, [
425510 'title' => 'My Gallery',
426511 'images' => $mockImages
427512]);
428513```
429-
430- ## Integration
431-
432- Ray.InputQuery is designed as a foundation library to be used by:
433-
434- - [ Ray.MediaQuery] ( https://github.com/ray-di/Ray.MediaQuery ) - For database query integration
435- - [ BEAR.Resource] ( https://github.com/bearsunday/BEAR.Resource ) - For REST resource integration
436-
437- ## Project Quality
438-
439- This project maintains high quality standards:
440-
441- - ** 100% Code Coverage** - Achieved through public interface tests only
442- - ** Static Analysis** - Psalm and PHPStan at maximum levels
443- - ** Test Design** - No private method tests, ensuring maintainability
444- - ** Type Safety** - Comprehensive Psalm type annotations
445-
446- ## Requirements
447-
448- - PHP 8.1+
449- - ray/di ^2.0
450-
451- ## License
452-
453- MIT
0 commit comments