Skip to content

Commit 1fccdeb

Browse files
authored
Merge pull request #7 from ray-di/feature/newinstance-api-improvement
feat: improve API with newInstance method and optional FileUploadFactory
2 parents 137905a + a36e0f6 commit 1fccdeb

24 files changed

+569
-118
lines changed

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,16 @@
55
/phpunit.xml
66
/.phpunit.result.cache
77
/.phpcs-cache
8+
9+
# Uploaded files
10+
demo/uploads/
11+
uploads/
12+
13+
# OS generated files
14+
.DS_Store
15+
.DS_Store?
16+
._*
17+
.Spotlight-V100
18+
.Trashes
19+
ehthumbs.db
20+
Thumbs.db

README.md

Lines changed: 107 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -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) {
4848
composer 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

5361
To 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);
123131
Ray.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
290309
use 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
316392
composer 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

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

composer-require-checker.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"symbol-whitelist" : [
33
"Koriym\\FileUpload\\FileUpload",
4-
"Koriym\\FileUpload\\ErrorFileUpload"
4+
"Koriym\\FileUpload\\ErrorFileUpload",
5+
"Koriym\\FileUpload\\AbstractFileUpload"
56
],
67
"php-core-extensions" : [
78
"Core",

demo/ArrayDemo.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public function listUsers(
3232
echo " [$index] ID: {$user->id}, Name: {$user->name}\n";
3333
}
3434
}
35-
35+
3636
public function listUsersAsArrayObject(
3737
#[Input(item: User::class)]
3838
ArrayObject $users
@@ -67,4 +67,4 @@ public function listUsersAsArrayObject(
6767
// ArrayObject example
6868
$method = new ReflectionMethod($controller, 'listUsersAsArrayObject');
6969
$args = $inputQuery->getArguments($method, $query);
70-
$controller->listUsersAsArrayObject(...$args);
70+
$controller->listUsersAsArrayObject(...$args);

demo/csv/AgeGroup.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\InputQuery\Demo;
6+
7+
/**
8+
* Age Grouping Service - Stateful Singleton Service
9+
*
10+
* Key Learning Points:
11+
* 1. Services can be injected into Input objects
12+
* 2. Singleton services maintain state across multiple Input object creations
13+
* 3. Business logic is separated from Input objects (Single Responsibility)
14+
* 4. Input objects can collaborate with services during construction
15+
* 5. Services accumulate knowledge as Input objects are created
16+
*/
17+
final class AgeGroup
18+
{
19+
/** @var array<string, int> */
20+
private array $groups = [
21+
'under_25' => 0,
22+
'25_35' => 0,
23+
'36_50' => 0,
24+
'over_50' => 0,
25+
];
26+
27+
public function addAge(?int $age): void
28+
{
29+
if ($age === null) {
30+
return;
31+
}
32+
33+
if ($age < 25) {
34+
$this->groups['under_25']++;
35+
} elseif ($age <= 35) {
36+
$this->groups['25_35']++;
37+
} elseif ($age <= 50) {
38+
$this->groups['36_50']++;
39+
} else {
40+
$this->groups['over_50']++;
41+
}
42+
}
43+
44+
/** @return array<string, int> */
45+
public function getGroups(): array
46+
{
47+
return $this->groups;
48+
}
49+
50+
public function getTotalCount(): int
51+
{
52+
return array_sum($this->groups);
53+
}
54+
}

demo/csv/AgeInput.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\InputQuery\Demo;
6+
7+
use Ray\InputQuery\Attribute\Input;
8+
9+
/**
10+
* Age Value Object with Domain Validation
11+
*
12+
* Key Learning Points:
13+
* 1. Input objects can contain domain validation logic
14+
* 2. Constructor validation ensures "creation = validity" principle
15+
* 3. Ray.InputQuery automatically converts string to int before passing to constructor
16+
* 4. If validation fails, object creation fails - no invalid objects exist
17+
*/
18+
final class AgeInput
19+
{
20+
public function __construct(
21+
#[Input] public readonly int $age, // Ray.InputQuery converts CSV string "28" to int 28
22+
){
23+
// Domain validation: Age must be non-negative
24+
// This demonstrates the "creation = validity" principle
25+
if ($age < 0) {
26+
throw new \InvalidArgumentException('Age must be a non-negative integer.');
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)