Skip to content

Commit 47ac480

Browse files
koriymclaude
andcommitted
Add demo execution to CI workflow
This ensures the demo remains functional across all supported PHP versions. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 75089b7 commit 47ac480

27 files changed

+622
-0
lines changed

.claude/settings.local.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(composer test:*)",
5+
"Bash(composer install:*)",
6+
"Bash(composer:*)"
7+
],
8+
"deny": []
9+
}
10+
}

.github/workflows/continuous-integration.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ jobs:
4949

5050
- name: Run test suite
5151
run: ./vendor/bin/phpunit
52+
53+
- name: Run demo
54+
run: php demo/run.php

demo/BlogController.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\InputQuery\Demo;
6+
7+
use Ray\Di\Di\Named;
8+
use Ray\InputQuery\Attribute\Input;
9+
10+
interface LoggerInterface
11+
{
12+
public function log(string $message): void;
13+
}
14+
15+
final class ConsoleLogger implements LoggerInterface
16+
{
17+
public function log(string $message): void
18+
{
19+
echo "[LOG] " . $message . "\n";
20+
}
21+
}
22+
23+
final class BlogController
24+
{
25+
public function createPost(
26+
#[Input] BlogPost $post,
27+
ConsoleLogger $logger,
28+
#[Named('app.version')] string $version
29+
): string {
30+
$logger->log("Creating blog post: {$post->title} (App v{$version})");
31+
32+
return "Blog post created successfully!\n\n" . $post->getPostSummary();
33+
}
34+
35+
public function updateProfile(
36+
#[Input] UserProfile $profile,
37+
ConsoleLogger $logger
38+
): string {
39+
$logger->log("Updating user profile: {$profile->name}");
40+
41+
return "Profile updated successfully!\n\n" . $profile->getDisplayInfo();
42+
}
43+
}

demo/BlogPost.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\InputQuery\Demo;
6+
7+
use Ray\InputQuery\Attribute\Input;
8+
9+
final class BlogPost
10+
{
11+
public function __construct(
12+
#[Input] public readonly string $title,
13+
#[Input] public readonly string $content,
14+
#[Input] public readonly Author $author,
15+
#[Input] public readonly ?string $category = null,
16+
#[Input] public readonly bool $published = false
17+
) {}
18+
19+
public function getPostSummary(): string
20+
{
21+
$status = $this->published ? 'Published' : 'Draft';
22+
$category = $this->category ?? 'Uncategorized';
23+
$preview = substr($this->content, 0, 100) . '...';
24+
25+
return sprintf(
26+
"Title: %s\nAuthor: %s <%s>\nCategory: %s\nStatus: %s\nPreview: %s",
27+
$this->title,
28+
$this->author->name,
29+
$this->author->email,
30+
$category,
31+
$status,
32+
$preview
33+
);
34+
}
35+
}
36+
37+
final class Author
38+
{
39+
public function __construct(
40+
#[Input] public readonly string $name,
41+
#[Input] public readonly string $email,
42+
#[Input] public readonly string $id = 'unknown'
43+
) {}
44+
}

demo/UserProfile.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\InputQuery\Demo;
6+
7+
use Ray\InputQuery\Attribute\Input;
8+
9+
final class UserProfile
10+
{
11+
public function __construct(
12+
#[Input] public readonly string $name,
13+
#[Input] public readonly string $email,
14+
#[Input] public readonly int $age = 25,
15+
#[Input] public readonly ?string $bio = null,
16+
#[Input] public readonly bool $isPublic = true
17+
) {}
18+
19+
public function getDisplayInfo(): string
20+
{
21+
$visibility = $this->isPublic ? 'Public' : 'Private';
22+
$bio = $this->bio ?? 'No bio available';
23+
24+
return sprintf(
25+
"Name: %s\nEmail: %s\nAge: %d\nBio: %s\nProfile: %s",
26+
$this->name,
27+
$this->email,
28+
$this->age,
29+
$bio,
30+
$visibility
31+
);
32+
}
33+
}

demo/run.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
require_once __DIR__ . '/../vendor/autoload.php';
6+
require_once __DIR__ . '/UserProfile.php';
7+
require_once __DIR__ . '/BlogPost.php';
8+
require_once __DIR__ . '/BlogController.php';
9+
10+
use Ray\Di\AbstractModule;
11+
use Ray\Di\Injector;
12+
use Ray\InputQuery\Demo\BlogController;
13+
use Ray\InputQuery\Demo\BlogPost;
14+
use Ray\InputQuery\Demo\UserProfile;
15+
use Ray\InputQuery\InputQuery;
16+
17+
echo "=== Ray.InputQuery Demo ===\n\n";
18+
19+
// Setup dependency injection
20+
$injector = new Injector(new class extends AbstractModule {
21+
protected function configure(): void
22+
{
23+
$this->bind()->annotatedWith('app.version')->toInstance('1.0.0');
24+
}
25+
});
26+
27+
$inputQuery = new InputQuery($injector);
28+
29+
echo "1. Simple User Profile Creation\n";
30+
echo "================================\n";
31+
32+
// Simulate form data for user profile
33+
$userFormData = [
34+
'name' => 'John Doe',
35+
'email' => 'john@example.com',
36+
'age' => '30',
37+
'bio' => 'Full-stack developer passionate about clean code',
38+
'isPublic' => '1'
39+
];
40+
41+
$userProfile = $inputQuery->create(UserProfile::class, $userFormData);
42+
echo $userProfile->getDisplayInfo() . "\n\n";
43+
44+
echo "2. Nested Object Creation (Blog Post with Author)\n";
45+
echo "=================================================\n";
46+
47+
// Simulate form data with nested author information
48+
$blogFormData = [
49+
'title' => 'Understanding Ray.InputQuery',
50+
'content' => 'Ray.InputQuery is a powerful library that transforms flat query data into type-safe PHP objects. It provides seamless integration with dependency injection and supports complex nested object creation...',
51+
'category' => 'Technology',
52+
'published' => '1',
53+
'authorName' => 'Jane Smith',
54+
'authorEmail' => 'jane@example.com',
55+
'authorId' => 'user123'
56+
];
57+
58+
$blogPost = $inputQuery->create(BlogPost::class, $blogFormData);
59+
echo $blogPost->getPostSummary() . "\n\n";
60+
61+
echo "3. Controller Method Arguments with DI\n";
62+
echo "======================================\n";
63+
64+
// Test controller method with mixed Input and DI parameters
65+
$controller = new BlogController();
66+
67+
// Blog post creation
68+
$createMethod = new ReflectionMethod(BlogController::class, 'createPost');
69+
$createArgs = $inputQuery->getArguments($createMethod, $blogFormData);
70+
$result1 = $createMethod->invokeArgs($controller, $createArgs);
71+
echo $result1 . "\n\n";
72+
73+
// Profile update
74+
$updateMethod = new ReflectionMethod(BlogController::class, 'updateProfile');
75+
$updateArgs = $inputQuery->getArguments($updateMethod, $userFormData);
76+
$result2 = $updateMethod->invokeArgs($controller, $updateArgs);
77+
echo $result2 . "\n\n";
78+
79+
echo "4. Scalar Type Conversions\n";
80+
echo "==========================\n";
81+
82+
// Demonstrate automatic type conversion
83+
$scalarData = [
84+
'name' => 'Test User',
85+
'email' => 'test@example.com',
86+
'age' => '25', // string -> int
87+
'isPublic' => 'true' // string -> bool
88+
];
89+
90+
$profile = $inputQuery->create(UserProfile::class, $scalarData);
91+
echo "Converted types:\n";
92+
echo "- age (string '25' -> int): " . var_export($profile->age, true) . " (" . gettype($profile->age) . ")\n";
93+
echo "- isPublic (string 'true' -> bool): " . var_export($profile->isPublic, true) . " (" . gettype($profile->isPublic) . ")\n\n";
94+
95+
echo "5. Default Values\n";
96+
echo "=================\n";
97+
98+
// Minimal data - other fields will use defaults
99+
$minimalData = [
100+
'name' => 'Minimal User',
101+
'email' => 'minimal@example.com'
102+
];
103+
104+
$minimalProfile = $inputQuery->create(UserProfile::class, $minimalData);
105+
echo "Profile with defaults:\n";
106+
echo $minimalProfile->getDisplayInfo() . "\n\n";
107+
108+
echo "6. Key Normalization (snake_case to camelCase)\n";
109+
echo "===============================================\n";
110+
111+
// Form data with snake_case keys
112+
$snakeCaseData = [
113+
'title' => 'Snake Case Test',
114+
'content' => 'Testing snake_case to camelCase conversion',
115+
'author_name' => 'Snake Author', // author_name -> authorName
116+
'author_email' => 'snake@example.com', // author_email -> authorEmail
117+
'author_id' => 'snake123' // author_id -> authorId
118+
];
119+
120+
$snakeCasePost = $inputQuery->create(BlogPost::class, $snakeCaseData);
121+
echo "Successfully converted snake_case keys:\n";
122+
echo $snakeCasePost->getPostSummary() . "\n\n";
123+
124+
echo "Demo completed successfully! 🎉\n";

src/Attribute/Input.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\InputQuery\Attribute;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_PARAMETER)]
10+
final class Input
11+
{
12+
}

src/InputQueryInterface.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\InputQuery;
6+
7+
use ReflectionMethod;
8+
9+
/** @template T of object */
10+
interface InputQueryInterface
11+
{
12+
/**
13+
* Get method arguments from query data
14+
*
15+
* @param array<string, mixed> $query
16+
*
17+
* @return array<mixed>
18+
*/
19+
public function getArguments(ReflectionMethod $method, array $query): array;
20+
21+
/**
22+
* Create object from query data
23+
*
24+
* @param class-string<T> $class
25+
* @param array<string, mixed> $query
26+
*
27+
* @return T
28+
*/
29+
public function create(string $class, array $query): object;
30+
}

tests/Fake/AuthorInput.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\InputQuery\Fake;
6+
7+
use Ray\InputQuery\Attribute\Input;
8+
9+
final class AuthorInput
10+
{
11+
public function __construct(
12+
#[Input] public readonly string $name,
13+
#[Input] public readonly string $email,
14+
#[Input] public readonly ?string $id = null
15+
) {}
16+
}

tests/Fake/CustomTypeInput.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\InputQuery\Fake;
6+
7+
use Ray\InputQuery\Attribute\Input;
8+
9+
final class CustomTypeInput
10+
{
11+
public function __construct(
12+
#[Input] public readonly string $name,
13+
#[Input] public readonly mixed $customValue = null // This will trigger default case in convertScalar
14+
) {}
15+
}

0 commit comments

Comments
 (0)