Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2025-07-07

### Added
- `ToArrayInterface` and `ToArray` implementation for flattening objects to flat associative arrays

## [0.1.0] - 2025-07-07

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

[0.1.0]: https://github.com/ray-di/Ray.InputQuery/releases/tag/0.1.0
[0.1.0]: https://github.com/ray-di/Ray.InputQuery/releases/tag/0.1.0
267 changes: 209 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,83 +4,58 @@
[![Type Coverage](https://shepherd.dev/github/ray-di/Ray.InputQuery/coverage.svg)](https://shepherd.dev/github/ray-di/Ray.InputQuery)
[![codecov](https://codecov.io/gh/ray-di/Ray.InputQuery/branch/main/graph/badge.svg)](https://codecov.io/gh/ray-di/Ray.InputQuery)

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

## 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.
## Quick Example

**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:**
```php
// Manual parameter extraction and object construction
$data = $request->getParsedBody(); // or $_POST
$title = $data['title'] ?? '';
$assigneeId = $data['assigneeId'] ?? '';
$assigneeName = $data['assigneeName'] ?? '';
$assigneeEmail = $data['assigneeEmail'] ?? '';
```
// HTTP Request: ?name=John&email=john@example.com&addressStreet=123 Main St&addressCity=Tokyo

**Ray.InputQuery Solution:**
```php
// Declarative structure definition
final class TodoInput {
// Automatically becomes:
final class AddressInput {
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
#[Input] public readonly string $street,
#[Input] public readonly string $city
) {}
}

public function createTodo(TodoInput $input) {
// $input automatically structured from request data
final class UserInput {
public function __construct(
#[Input] public readonly string $name,
#[Input] public readonly string $email,
#[Input] public readonly AddressInput $address // Nested object!
) {}
}
```

## Installation

```bash
composer require ray/input-query
$user = $inputQuery->newInstance(UserInput::class, $_GET);
echo $user->name; // "John"
echo $user->address->street; // "123 Main St"
```

### Optional: File Upload Support
**Key Point**: `addressStreet` and `addressCity` automatically compose the `AddressInput` object.

For file upload functionality, also install:
## Overview

```bash
composer require koriym/file-upload
```
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.

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

### Web Demo

To see file upload integration in action:
## Installation

```bash
php -S localhost:8080 -t demo/
composer require ray/input-query
```

Then visit [http://localhost:8080](http://localhost:8080) in your browser.

### Console Demos
### Optional: File Upload Support

Run various examples from the command line:
For file upload functionality, also install:

```bash
# Basic examples with nested objects and DI
php demo/run.php

# Array processing demo
php demo/ArrayDemo.php

# CSV file processing with batch operations
php demo/csv/run.php
composer require koriym/file-upload
```

## Documentation
Expand Down Expand Up @@ -136,10 +111,6 @@ 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);
```

Expand Down Expand Up @@ -528,3 +499,183 @@ $input = $inputQuery->newInstance(GalleryInput::class, [
'images' => $mockImages
]);
```

## Converting Objects to Arrays

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:

### Basic ToArray Usage

```php
use Ray\InputQuery\ToArray;

final class CustomerInput
{
public function __construct(
#[Input] public readonly string $name,
#[Input] public readonly string $email,
) {}
}

final class OrderInput
{
public function __construct(
#[Input] public readonly string $id,
#[Input] public readonly CustomerInput $customer,
#[Input] public readonly array $items,
) {}
}

// Create nested input object
$orderInput = new OrderInput(
id: 'ORD-001',
customer: new CustomerInput(name: 'John Doe', email: 'john@example.com'),
items: [['product' => 'laptop', 'quantity' => 1]]
);

// Convert to flat array for SQL
$toArray = new ToArray();
$params = $toArray($orderInput);

// Result:
// [
// 'id' => 'ORD-001',
// 'name' => 'John Doe', // Flattened from customer
// 'email' => 'john@example.com', // Flattened from customer
// 'items' => [['product' => 'laptop', 'quantity' => 1]] // Arrays preserved
// ]
```

### SQL Param¥¥eter Binding

The flattened arrays work seamlessly with Aura.Sql and other SQL libraries:

```php
// Using with Aura.Sql
$sql = "INSERT INTO orders (id, customer_name, customer_email) VALUES (:id, :name, :email)";
$statement = $pdo->prepare($sql);
$statement->execute($params);

// Arrays are preserved for IN clauses
$productIds = $params['productIds']; // [1, 2, 3]
$sql = "SELECT * FROM products WHERE id IN (?)";
$statement = $pdo->prepare($sql);
$statement->execute([$productIds]); // Aura.Sql handles array expansion

// Other use cases
return new JsonResponse($params); // API responses
$this->logger->info('Order data', $params); // Logging
```

### Property Name Conflicts

When flattened properties have the same name, later values overwrite earlier ones:

```php
final class OrderInput
{
public function __construct(
#[Input] public readonly string $id, // 'ORD-001'
#[Input] public readonly CustomerInput $customer, // Has 'id' property: 'CUST-123'
) {}
}

$params = $toArray($orderInput);
// Result: ['id' => 'CUST-123'] // Customer ID overwrites order ID
```

### Key Features

- **Recursive Flattening**: Nested objects with `#[Input]` parameters are automatically flattened
- **Array Preservation**: Arrays remain intact for SQL IN clauses (Aura.Sql compatible)
- **Property Conflicts**: Later properties overwrite earlier ones
- **Public Properties Only**: Private/protected properties are ignored
- **Type Safety**: Maintains type information through transformation

### Complex Example

```php
final class AddressInput
{
public function __construct(
#[Input] public readonly string $street,
#[Input] public readonly string $city,
#[Input] public readonly string $country,
) {}
}

final class CustomerInput
{
public function __construct(
#[Input] public readonly string $name,
#[Input] public readonly string $email,
#[Input] public readonly AddressInput $address,
) {}
}

final class OrderInput
{
public function __construct(
#[Input] public readonly string $orderId,
#[Input] public readonly CustomerInput $customer,
#[Input] public readonly AddressInput $shipping,
#[Input] public readonly array $productIds,
) {}
}

$order = new OrderInput(
orderId: 'ORD-001',
customer: new CustomerInput(
name: 'John Doe',
email: 'john@example.com',
address: new AddressInput(street: '123 Main St', city: 'Tokyo', country: 'Japan')
),
shipping: new AddressInput(street: '456 Oak Ave', city: 'Osaka', country: 'Japan'),
productIds: ['PROD-1', 'PROD-2', 'PROD-3']
);

$params = $toArray($order);
// Result:
// [
// 'orderId' => 'ORD-001',
// 'name' => 'John Doe',
// 'email' => 'john@example.com',
// 'street' => '456 Oak Ave', // Shipping address overwrites customer address
// 'city' => 'Osaka', // Shipping address overwrites customer address
// 'country' => 'Japan', // Same value, so no visible conflict
// 'productIds' => ['PROD-1', 'PROD-2', 'PROD-3'] // Array preserved
// ]

// Use the flattened data
$orderId = $params['orderId'];
$customerName = $params['name'];
$shippingAddress = "{$params['street']}, {$params['city']}, {$params['country']}";
$productIds = $params['productIds']; // Array preserved
```

## Demo

### Web Demo

To see file upload integration in action:

```bash
php -S localhost:8080 -t demo/
```

Then visit [http://localhost:8080](http://localhost:8080) in your browser.

### Console Demos

Run various examples from the command line:

```bash
# Basic examples with nested objects and DI
php demo/run.php

# Array processing demo
php demo/ArrayDemo.php

# CSV file processing with batch operations
php demo/csv/run.php
```
54 changes: 54 additions & 0 deletions src/ToArray.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Ray\InputQuery;

use Override;
use ReflectionClass;
use ReflectionProperty;

use function is_object;

final class ToArray implements ToArrayInterface
{
/**
* {@inheritDoc}
*/
#[Override]
public function __invoke(object $input): array
{
return $this->extractProperties($input);
}

/** @return array<string, mixed> */
private function extractProperties(object $object): array
{
$result = [];
$reflection = new ReflectionClass($object);

foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
/** @var mixed $value */
$value = $property->getValue($object);
$name = $property->getName();

if (is_object($value)) {
// Recursively extract nested objects
$nestedProperties = $this->extractProperties($value);
/** @var mixed $nestedValue */
foreach ($nestedProperties as $nestedName => $nestedValue) {
/** @psalm-suppress MixedAssignment */
$result[$nestedName] = $nestedValue;
}

continue;
}

// Keep arrays and scalar values as-is
/** @psalm-suppress MixedAssignment */
$result[$name] = $value;
}

return $result;
}
}
17 changes: 17 additions & 0 deletions src/ToArrayInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Ray\InputQuery;

interface ToArrayInterface
{
/**
* Convert Input object to flat associative array
*
* @param object $input Input object with #[Input] attributes
*
* @return array<string, mixed> Flat associative array
*/
public function __invoke(object $input): array;
}
Loading