Skip to content

Commit 74d0fe6

Browse files
authored
Merge pull request #9 from ray-di/flattern
feat: add ToArray functionality for flattening Input objects
2 parents 3f5f77b + 9a4ccd3 commit 74d0fe6

File tree

5 files changed

+449
-59
lines changed

5 files changed

+449
-59
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.0] - 2025-07-07
9+
10+
### Added
11+
- `ToArrayInterface` and `ToArray` implementation for flattening objects to flat associative arrays
12+
813
## [0.1.0] - 2025-07-07
914

1015
### Added
@@ -37,4 +42,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3742
- symfony/polyfill-php83 ^1.28
3843
- koriym/file-upload ^0.2.0 (optional, for file upload support)
3944

40-
[0.1.0]: https://github.com/ray-di/Ray.InputQuery/releases/tag/0.1.0
45+
[0.1.0]: https://github.com/ray-di/Ray.InputQuery/releases/tag/0.1.0

README.md

Lines changed: 209 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,83 +4,58 @@
44
[![Type Coverage](https://shepherd.dev/github/ray-di/Ray.InputQuery/coverage.svg)](https://shepherd.dev/github/ray-di/Ray.InputQuery)
55
[![codecov](https://codecov.io/gh/ray-di/Ray.InputQuery/branch/main/graph/badge.svg)](https://codecov.io/gh/ray-di/Ray.InputQuery)
66

7-
Structured input objects from HTTP with 100% test coverage.
7+
Convert HTTP query parameters into hierarchical PHP objects automatically.
88

9-
## Overview
10-
11-
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.
9+
## Quick Example
1210

13-
**Core Mechanism:**
14-
- **Attribute-Based Control** - `#[Input]` explicitly marks query-sourced parameters
15-
- **Prefix-Based Nesting** - `assigneeId`, `assigneeName` fields automatically compose `UserInput` objects
16-
- **Type-Safe Conversion** - Leverages PHP's type system for automatic scalar conversion
17-
- **DI Integration** - Parameters without `#[Input]` are resolved from dependency injection
18-
19-
**The Problem:**
2011
```php
21-
// Manual parameter extraction and object construction
22-
$data = $request->getParsedBody(); // or $_POST
23-
$title = $data['title'] ?? '';
24-
$assigneeId = $data['assigneeId'] ?? '';
25-
$assigneeName = $data['assigneeName'] ?? '';
26-
$assigneeEmail = $data['assigneeEmail'] ?? '';
27-
```
12+
// HTTP Request: ?name=John&email=john@example.com&addressStreet=123 Main St&addressCity=Tokyo
2813

29-
**Ray.InputQuery Solution:**
30-
```php
31-
// Declarative structure definition
32-
final class TodoInput {
14+
// Automatically becomes:
15+
final class AddressInput {
3316
public function __construct(
34-
#[Input] public readonly string $title,
35-
#[Input] public readonly UserInput $assignee, // Auto-composed from assigneeId, assigneeName, assigneeEmail
36-
private LoggerInterface $logger // From DI container
17+
#[Input] public readonly string $street,
18+
#[Input] public readonly string $city
3719
) {}
3820
}
3921

40-
public function createTodo(TodoInput $input) {
41-
// $input automatically structured from request data
22+
final class UserInput {
23+
public function __construct(
24+
#[Input] public readonly string $name,
25+
#[Input] public readonly string $email,
26+
#[Input] public readonly AddressInput $address // Nested object!
27+
) {}
4228
}
43-
```
4429

45-
## Installation
46-
47-
```bash
48-
composer require ray/input-query
30+
$user = $inputQuery->newInstance(UserInput::class, $_GET);
31+
echo $user->name; // "John"
32+
echo $user->address->street; // "123 Main St"
4933
```
5034

51-
### Optional: File Upload Support
35+
**Key Point**: `addressStreet` and `addressCity` automatically compose the `AddressInput` object.
5236

53-
For file upload functionality, also install:
37+
## Overview
5438

55-
```bash
56-
composer require koriym/file-upload
57-
```
39+
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.
5840

59-
## Demo
41+
**Core Features:**
42+
- **Automatic Nesting** - Prefix-based parameters create hierarchical objects
43+
- **Type Safety** - Leverages PHP's type system for automatic conversion
44+
- **DI Integration** - Mix query parameters with dependency injection
45+
- **Validation** - Type constraints ensure data integrity
6046

61-
### Web Demo
62-
63-
To see file upload integration in action:
47+
## Installation
6448

6549
```bash
66-
php -S localhost:8080 -t demo/
50+
composer require ray/input-query
6751
```
6852

69-
Then visit [http://localhost:8080](http://localhost:8080) in your browser.
70-
71-
### Console Demos
53+
### Optional: File Upload Support
7254

73-
Run various examples from the command line:
55+
For file upload functionality, also install:
7456

7557
```bash
76-
# Basic examples with nested objects and DI
77-
php demo/run.php
78-
79-
# Array processing demo
80-
php demo/ArrayDemo.php
81-
82-
# CSV file processing with batch operations
83-
php demo/csv/run.php
58+
composer require koriym/file-upload
8459
```
8560

8661
## Documentation
@@ -136,10 +111,6 @@ echo $user->email; // john@example.com
136111
// Method argument resolution from $_POST
137112
$method = new ReflectionMethod(UserController::class, 'register');
138113
$args = $inputQuery->getArguments($method, $_POST);
139-
$result = $method->invokeArgs($controller, $args);
140-
141-
// Or with PSR-7 Request
142-
$args = $inputQuery->getArguments($method, $request->getParsedBody());
143114
$result = $method->invokeArgs($controller, $args);
144115
```
145116

@@ -528,3 +499,183 @@ $input = $inputQuery->newInstance(GalleryInput::class, [
528499
'images' => $mockImages
529500
]);
530501
```
502+
503+
## Converting Objects to Arrays
504+
505+
Ray.InputQuery provides the `ToArray` functionality to convert objects with `#[Input]` parameters into flat associative arrays, primarily for SQL parameter binding with libraries like Aura.Sql:
506+
507+
### Basic ToArray Usage
508+
509+
```php
510+
use Ray\InputQuery\ToArray;
511+
512+
final class CustomerInput
513+
{
514+
public function __construct(
515+
#[Input] public readonly string $name,
516+
#[Input] public readonly string $email,
517+
) {}
518+
}
519+
520+
final class OrderInput
521+
{
522+
public function __construct(
523+
#[Input] public readonly string $id,
524+
#[Input] public readonly CustomerInput $customer,
525+
#[Input] public readonly array $items,
526+
) {}
527+
}
528+
529+
// Create nested input object
530+
$orderInput = new OrderInput(
531+
id: 'ORD-001',
532+
customer: new CustomerInput(name: 'John Doe', email: 'john@example.com'),
533+
items: [['product' => 'laptop', 'quantity' => 1]]
534+
);
535+
536+
// Convert to flat array for SQL
537+
$toArray = new ToArray();
538+
$params = $toArray($orderInput);
539+
540+
// Result:
541+
// [
542+
// 'id' => 'ORD-001',
543+
// 'name' => 'John Doe', // Flattened from customer
544+
// 'email' => 'john@example.com', // Flattened from customer
545+
// 'items' => [['product' => 'laptop', 'quantity' => 1]] // Arrays preserved
546+
// ]
547+
```
548+
549+
### SQL Param¥¥eter Binding
550+
551+
The flattened arrays work seamlessly with Aura.Sql and other SQL libraries:
552+
553+
```php
554+
// Using with Aura.Sql
555+
$sql = "INSERT INTO orders (id, customer_name, customer_email) VALUES (:id, :name, :email)";
556+
$statement = $pdo->prepare($sql);
557+
$statement->execute($params);
558+
559+
// Arrays are preserved for IN clauses
560+
$productIds = $params['productIds']; // [1, 2, 3]
561+
$sql = "SELECT * FROM products WHERE id IN (?)";
562+
$statement = $pdo->prepare($sql);
563+
$statement->execute([$productIds]); // Aura.Sql handles array expansion
564+
565+
// Other use cases
566+
return new JsonResponse($params); // API responses
567+
$this->logger->info('Order data', $params); // Logging
568+
```
569+
570+
### Property Name Conflicts
571+
572+
When flattened properties have the same name, later values overwrite earlier ones:
573+
574+
```php
575+
final class OrderInput
576+
{
577+
public function __construct(
578+
#[Input] public readonly string $id, // 'ORD-001'
579+
#[Input] public readonly CustomerInput $customer, // Has 'id' property: 'CUST-123'
580+
) {}
581+
}
582+
583+
$params = $toArray($orderInput);
584+
// Result: ['id' => 'CUST-123'] // Customer ID overwrites order ID
585+
```
586+
587+
### Key Features
588+
589+
- **Recursive Flattening**: Nested objects with `#[Input]` parameters are automatically flattened
590+
- **Array Preservation**: Arrays remain intact for SQL IN clauses (Aura.Sql compatible)
591+
- **Property Conflicts**: Later properties overwrite earlier ones
592+
- **Public Properties Only**: Private/protected properties are ignored
593+
- **Type Safety**: Maintains type information through transformation
594+
595+
### Complex Example
596+
597+
```php
598+
final class AddressInput
599+
{
600+
public function __construct(
601+
#[Input] public readonly string $street,
602+
#[Input] public readonly string $city,
603+
#[Input] public readonly string $country,
604+
) {}
605+
}
606+
607+
final class CustomerInput
608+
{
609+
public function __construct(
610+
#[Input] public readonly string $name,
611+
#[Input] public readonly string $email,
612+
#[Input] public readonly AddressInput $address,
613+
) {}
614+
}
615+
616+
final class OrderInput
617+
{
618+
public function __construct(
619+
#[Input] public readonly string $orderId,
620+
#[Input] public readonly CustomerInput $customer,
621+
#[Input] public readonly AddressInput $shipping,
622+
#[Input] public readonly array $productIds,
623+
) {}
624+
}
625+
626+
$order = new OrderInput(
627+
orderId: 'ORD-001',
628+
customer: new CustomerInput(
629+
name: 'John Doe',
630+
email: 'john@example.com',
631+
address: new AddressInput(street: '123 Main St', city: 'Tokyo', country: 'Japan')
632+
),
633+
shipping: new AddressInput(street: '456 Oak Ave', city: 'Osaka', country: 'Japan'),
634+
productIds: ['PROD-1', 'PROD-2', 'PROD-3']
635+
);
636+
637+
$params = $toArray($order);
638+
// Result:
639+
// [
640+
// 'orderId' => 'ORD-001',
641+
// 'name' => 'John Doe',
642+
// 'email' => 'john@example.com',
643+
// 'street' => '456 Oak Ave', // Shipping address overwrites customer address
644+
// 'city' => 'Osaka', // Shipping address overwrites customer address
645+
// 'country' => 'Japan', // Same value, so no visible conflict
646+
// 'productIds' => ['PROD-1', 'PROD-2', 'PROD-3'] // Array preserved
647+
// ]
648+
649+
// Use the flattened data
650+
$orderId = $params['orderId'];
651+
$customerName = $params['name'];
652+
$shippingAddress = "{$params['street']}, {$params['city']}, {$params['country']}";
653+
$productIds = $params['productIds']; // Array preserved
654+
```
655+
656+
## Demo
657+
658+
### Web Demo
659+
660+
To see file upload integration in action:
661+
662+
```bash
663+
php -S localhost:8080 -t demo/
664+
```
665+
666+
Then visit [http://localhost:8080](http://localhost:8080) in your browser.
667+
668+
### Console Demos
669+
670+
Run various examples from the command line:
671+
672+
```bash
673+
# Basic examples with nested objects and DI
674+
php demo/run.php
675+
676+
# Array processing demo
677+
php demo/ArrayDemo.php
678+
679+
# CSV file processing with batch operations
680+
php demo/csv/run.php
681+
```

src/ToArray.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;
6+
7+
use Override;
8+
use ReflectionClass;
9+
use ReflectionProperty;
10+
11+
use function is_object;
12+
13+
final class ToArray implements ToArrayInterface
14+
{
15+
/**
16+
* {@inheritDoc}
17+
*/
18+
#[Override]
19+
public function __invoke(object $input): array
20+
{
21+
return $this->extractProperties($input);
22+
}
23+
24+
/** @return array<string, mixed> */
25+
private function extractProperties(object $object): array
26+
{
27+
$result = [];
28+
$reflection = new ReflectionClass($object);
29+
30+
foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
31+
/** @var mixed $value */
32+
$value = $property->getValue($object);
33+
$name = $property->getName();
34+
35+
if (is_object($value)) {
36+
// Recursively extract nested objects
37+
$nestedProperties = $this->extractProperties($value);
38+
/** @var mixed $nestedValue */
39+
foreach ($nestedProperties as $nestedName => $nestedValue) {
40+
/** @psalm-suppress MixedAssignment */
41+
$result[$nestedName] = $nestedValue;
42+
}
43+
44+
continue;
45+
}
46+
47+
// Keep arrays and scalar values as-is
48+
/** @psalm-suppress MixedAssignment */
49+
$result[$name] = $value;
50+
}
51+
52+
return $result;
53+
}
54+
}

src/ToArrayInterface.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\InputQuery;
6+
7+
interface ToArrayInterface
8+
{
9+
/**
10+
* Convert Input object to flat associative array
11+
*
12+
* @param object $input Input object with #[Input] attributes
13+
*
14+
* @return array<string, mixed> Flat associative array
15+
*/
16+
public function __invoke(object $input): array;
17+
}

0 commit comments

Comments
 (0)