Skip to content
Merged
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"allow": [
"Bash(composer test:*)",
"Bash(composer install:*)",
"Bash(composer:*)"
"Bash(composer:*)",
"Bash(./vendor/bin/phpunit:*)"
],
"deny": []
}
Expand Down
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,74 @@ echo $todo->title; // Buy milk
echo $todo->assignee->name; // John
```

### Array Support

Ray.InputQuery supports arrays and ArrayObject collections with the `item` parameter:

```php
use Ray\InputQuery\Attribute\Input;

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

final class UserListController
{
public function updateUsers(
#[Input(item: UserInput::class)] array $users // Array of UserInput objects
) {
foreach ($users as $user) {
echo $user->name; // Each element is a UserInput instance
}
}

public function processUsers(
#[Input(item: UserInput::class)] ArrayObject $users // ArrayObject collection
) {
// $users is an ArrayObject containing UserInput instances
}
}
```

Query data format for arrays:

```php
$data = [
'users' => [
['id' => '1', 'name' => 'John'],
['id' => '2', 'name' => 'Jane'],
['id' => '3', 'name' => 'Bob']
]
];

$args = $inputQuery->getArguments($method, $data);
// $args[0] will be an array of UserInput objects
```

**ArrayObject Inheritance Support:**

Custom ArrayObject subclasses are also supported:

```php
final class UserCollection extends ArrayObject
{
public function getFirst(): ?UserInput
{
return $this[0] ?? null;
}
}

public function handleUsers(
#[Input(item: UserInput::class)] UserCollection $users
) {
$firstUser = $users->getFirst(); // Custom method available
}
```

### Mixed with Dependency Injection

Parameters without the `#[Input]` attribute are resolved via dependency injection:
Expand Down
70 changes: 70 additions & 0 deletions demo/ArrayDemo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

require dirname(__DIR__) . '/vendor/autoload.php';

use Ray\Di\Injector;
use Ray\InputQuery\Attribute\Input;
use Ray\InputQuery\InputQuery;

// User input class
final class User
{
public function __construct(
#[Input]
public readonly string $id,
#[Input]
public readonly string $name
) {
}
}

// Controller with array handling
final class UserController
{
public function listUsers(
#[Input(item: User::class)]
array $users
): void {
echo "Array of users:\n";
foreach ($users as $index => $user) {
echo " [$index] ID: {$user->id}, Name: {$user->name}\n";
}
}

public function listUsersAsArrayObject(
#[Input(item: User::class)]
ArrayObject $users
): void {
echo "\nArrayObject of users:\n";
foreach ($users as $index => $user) {
echo " [$index] ID: {$user->id}, Name: {$user->name}\n";
}
}
}

// Demo
$injector = new Injector();
$inputQuery = new InputQuery($injector);

// Sample query data (like from $_POST)
$query = [
'users' => [
['id' => '1', 'name' => 'jingu'],
['id' => '2', 'name' => 'horikawa'],
['id' => '3', 'name' => 'tanaka']
]
];

$controller = new UserController();

// Array example
$method = new ReflectionMethod($controller, 'listUsers');
$args = $inputQuery->getArguments($method, $query);
$controller->listUsers(...$args);

// ArrayObject example
$method = new ReflectionMethod($controller, 'listUsersAsArrayObject');
$args = $inputQuery->getArguments($method, $query);
$controller->listUsersAsArrayObject(...$args);
4 changes: 4 additions & 0 deletions src/Attribute/Input.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@
#[Attribute(Attribute::TARGET_PARAMETER)]
final class Input
{
public function __construct(
public readonly string|null $item = null,
) {
}
}
79 changes: 79 additions & 0 deletions src/InputQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Ray\InputQuery;

use ArrayObject;
use InvalidArgumentException;
use Override;
use Ray\Di\Di\Named;
Expand All @@ -16,14 +17,17 @@
use ReflectionNamedType;
use ReflectionParameter;

use function array_key_exists;
use function assert;
use function class_exists;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_numeric;
use function is_scalar;
use function is_string;
use function is_subclass_of;
use function lcfirst;
use function sprintf;
use function str_replace;
Expand Down Expand Up @@ -100,13 +104,54 @@ private function resolveParameter(ReflectionParameter $param, array $query): mix
}

if ($type->isBuiltin()) {
// Check if it's an array type with item specification
if ($type->getName() === 'array') {
$inputAttribute = $inputAttributes[0]->newInstance();
if ($inputAttribute->item !== null) {
assert(class_exists($inputAttribute->item));
$itemClass = $inputAttribute->item;

/** @var class-string<T> $itemClass */
return $this->createArrayOfInputs($paramName, $query, $itemClass);
}
}

// Scalar type with #[Input]
/** @psalm-suppress MixedAssignment $value */

$value = $query[$paramName] ?? $this->getDefaultValue($param);

return $this->convertScalar($value, $type);
}

// Check if it's ArrayObject or its subclass with item specification
$className = $type->getName();
if (class_exists($className) && is_subclass_of($className, ArrayObject::class)) {
$inputAttribute = $inputAttributes[0]->newInstance();
if ($inputAttribute->item !== null) {
assert(class_exists($inputAttribute->item));
/** @var class-string<T> $itemClass */
$itemClass = $inputAttribute->item;
$array = $this->createArrayOfInputs($paramName, $query, $itemClass);
$reflectionClass = new ReflectionClass($className);

return $reflectionClass->newInstance($array);
}
}

// Check if it's ArrayObject itself with item specification
if ($className === ArrayObject::class) {
$inputAttribute = $inputAttributes[0]->newInstance();
if ($inputAttribute->item !== null) {
assert(class_exists($inputAttribute->item));
/** @var class-string<T> $itemClass */
$itemClass = $inputAttribute->item;
$array = $this->createArrayOfInputs($paramName, $query, $itemClass);

return new ArrayObject($array);
}
}

// Object type with #[Input] - create nested
$nestedQuery = $this->extractNestedQuery($paramName, $query);

Expand Down Expand Up @@ -245,4 +290,38 @@ private function toCamelCase(string $string): string

return lcfirst($string);
}

/**
* @param array<string, mixed> $query
* @param class-string<T> $itemClass
*
* @return array<mixed>
*/
private function createArrayOfInputs(string $paramName, array $query, string $itemClass): array
{
if (! array_key_exists($paramName, $query)) {
return [];
}

/** @var mixed $arrayData */
$arrayData = $query[$paramName];

if (! is_array($arrayData)) {
return [];
}

$result = [];
/** @var mixed $itemData */
foreach ($arrayData as $key => $itemData) {
if (is_array($itemData)) {
// Query parameters from HTTP requests have string keys
/** @psalm-var array<string, mixed> $itemData */
/** @phpstan-var array<string, mixed> $itemData */
$result[$key] = $this->create($itemClass, $itemData);
}
}

return $result;
}

}
Loading
Loading