Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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;
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
}

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