Skip to content

Commit 9e9f01c

Browse files
koriymclaude
andcommitted
Implement semantic bindings with Value Objects
Convert string-based binding information to structured VOs: - Add BindingInfo and AopInfo VOs in Bindings/ namespace - Replace string parsing with direct VO access in ModuleBindings - Extend SpyCompiler to provide AopInfo directly from dependencies - Update JSON schema to reflect new VO structure - Maintain type safety and machine-readable binding information 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f85c539 commit 9e9f01c

File tree

10 files changed

+525
-21
lines changed

10 files changed

+525
-21
lines changed

binding-schema.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Ray.Di BindingInfo Schema",
4+
"description": "Schema for Ray.Di BindingInfo Value Objects",
5+
"type": "array",
6+
"items": {
7+
"$ref": "#/definitions/bindingInfo"
8+
},
9+
"definitions": {
10+
"bindingInfo": {
11+
"type": "object",
12+
"required": ["interface", "named", "type", "target"],
13+
"properties": {
14+
"interface": {
15+
"type": "string",
16+
"description": "The interface or class being bound"
17+
},
18+
"named": {
19+
"type": ["string", "null"],
20+
"description": "Named binding identifier (null if not named)"
21+
},
22+
"type": {
23+
"type": "string",
24+
"enum": ["to", "toProvider", "toInstance"],
25+
"description": "The type of binding"
26+
},
27+
"target": {
28+
"oneOf": [
29+
{
30+
"type": "string",
31+
"description": "Target class name for 'to' or 'toProvider' bindings"
32+
},
33+
{
34+
"type": ["string", "number", "boolean", "null"],
35+
"description": "Instance value for 'toInstance' bindings"
36+
}
37+
],
38+
"description": "The target of the binding"
39+
},
40+
"aop": {
41+
"type": "object",
42+
"description": "AOP method bindings from AopInfo VO",
43+
"additionalProperties": {
44+
"type": "array",
45+
"items": {
46+
"type": "string",
47+
"description": "Interceptor class name"
48+
},
49+
"description": "List of interceptors for this method"
50+
}
51+
}
52+
},
53+
"additionalProperties": false
54+
}
55+
}
56+
}

demo-bindings.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Ray\Di\AbstractModule;
6+
use Ray\Di\Container;
7+
use Ray\Di\Injector;
8+
use Ray\Di\ModuleBindings;
9+
use Ray\Di\ModuleString;
10+
use Ray\Di\Fake\FakeAopInterface;
11+
use Ray\Di\Fake\FakeAop;
12+
use Ray\Di\FakeDoubleInterceptor;
13+
14+
require __DIR__ . '/vendor/autoload.php';
15+
16+
// Example module with AOP
17+
class DemoAopModule extends AbstractModule
18+
{
19+
protected function configure(): void
20+
{
21+
// Standard binding
22+
$this->bind(FakeAopInterface::class)->to(FakeAop::class);
23+
24+
// Named binding
25+
$this->bind(FakeAopInterface::class)
26+
->annotatedWith('special')
27+
->to(FakeAop::class);
28+
29+
// AOP binding - bind interceptor to all methods of FakeAop
30+
$this->bindInterceptor(
31+
$this->matcher->subclassesOf(FakeAop::class),
32+
$this->matcher->any(),
33+
[FakeDoubleInterceptor::class]
34+
);
35+
}
36+
}
37+
38+
// Create injector and get container
39+
$module = new DemoAopModule();
40+
$injector = new Injector($module);
41+
$container = $injector->getContainer();
42+
43+
// Get pointcuts for AOP
44+
$pointcuts = $container->getPointcuts();
45+
46+
echo "=== Traditional ModuleString Output ===\n";
47+
$moduleString = new ModuleString();
48+
$output = $moduleString($container, $pointcuts);
49+
echo $output . "\n\n";
50+
51+
echo "=== Machine-Readable ModuleBindings Output ===\n";
52+
$moduleBindings = new ModuleBindings();
53+
$bindings = $moduleBindings($container, $pointcuts);
54+
55+
// Display as formatted JSON
56+
$json = json_encode($bindings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
57+
echo $json . "\n\n";
58+
59+
// Show specific examples
60+
echo "=== Individual Binding Examples ===\n";
61+
foreach ($bindings as $binding) {
62+
echo sprintf(
63+
"Interface: %s\n Named: %s\n Type: %s\n Target: %s\n",
64+
$binding['interface'],
65+
$binding['named'] ?? '(none)',
66+
$binding['type'],
67+
is_string($binding['target']) ? $binding['target'] : json_encode($binding['target'])
68+
);
69+
70+
if (isset($binding['aop'])) {
71+
echo " AOP Bindings:\n";
72+
foreach ($binding['aop'] as $method => $interceptors) {
73+
echo sprintf(
74+
" Method '%s': %s\n",
75+
$method,
76+
implode(', ', $interceptors)
77+
);
78+
}
79+
}
80+
echo "\n";
81+
}

demo-vo.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
require __DIR__ . '/vendor/autoload.php';
4+
5+
use Ray\Di\AbstractModule;
6+
use Ray\Di\ModuleBindings;
7+
8+
$module = new class extends AbstractModule {
9+
protected function configure(): void
10+
{
11+
$this->bind('')->annotatedWith('app_name')->toInstance('MyApp');
12+
$this->bind('')->annotatedWith('version')->toInstance('1.0.0');
13+
}
14+
};
15+
16+
$container = $module->getContainer();
17+
$moduleBindings = new ModuleBindings();
18+
$bindings = $moduleBindings($container, []);
19+
20+
echo "=== BindingInfo Value Objects ===\n";
21+
foreach ($bindings as $binding) {
22+
echo sprintf(
23+
"Interface: '%s', Named: '%s', Type: %s, Target: %s\n",
24+
$binding->interface,
25+
$binding->named ?? 'null',
26+
$binding->type,
27+
json_encode($binding->target)
28+
);
29+
}
30+
31+
echo "\n=== JSON Serialized ===\n";
32+
echo json_encode($bindings, JSON_PRETTY_PRINT);

src/di/BindInterface.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\Di;
6+
7+
interface BindInterface
8+
{
9+
public function __toString(): string;
10+
11+
public function annotatedWith(string $name): self;
12+
13+
/**
14+
* @param class-string $class
15+
*/
16+
public function to(string $class): self;
17+
18+
/**
19+
* @param class-string<T> $class
20+
*
21+
* @template T of object
22+
*/
23+
public function toConstructor(string $class, string $name, ?InjectionPoints $injectionPoints = null, ?string $postConstruct = null): self;
24+
25+
/**
26+
* @phpstan-param class-string $provider
27+
*/
28+
public function toProvider(string $provider, string $context = ''): self;
29+
30+
public function toInstance(object $instance): self;
31+
32+
public function toNull(): self;
33+
34+
public function in(string $scope): self;
35+
36+
public function getBound(): DependencyInterface;
37+
38+
public function setBound(DependencyInterface $bound): void;
39+
}

src/di/Bindings/AopInfo.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\Di\Bindings;
6+
7+
/**
8+
* Value object representing AOP binding information
9+
*/
10+
final class AopInfo
11+
{
12+
/** @var array<string, list<string>> */
13+
public $methodBindings;
14+
15+
/**
16+
* @param array<string, list<string>|list<object>> $methodBindings
17+
*/
18+
public function __construct(array $methodBindings)
19+
{
20+
// Convert objects to string representations if needed
21+
$stringBindings = [];
22+
foreach ($methodBindings as $method => $interceptors) {
23+
$stringBindings[$method] = array_map('strval', $interceptors);
24+
}
25+
$this->methodBindings = $stringBindings;
26+
}
27+
28+
/**
29+
* Check if any AOP bindings exist
30+
*/
31+
public function hasBindings(): bool
32+
{
33+
return !empty($this->methodBindings);
34+
}
35+
36+
/**
37+
* Convert to string representation for compatibility
38+
*/
39+
public function __toString(): string
40+
{
41+
if (!$this->hasBindings()) {
42+
return '';
43+
}
44+
45+
$log = ' (aop)';
46+
foreach ($this->methodBindings as $method => $interceptors) {
47+
$log .= \sprintf(
48+
' +%s(%s)',
49+
$method,
50+
\implode(', ', $interceptors)
51+
);
52+
}
53+
54+
return $log;
55+
}
56+
}

src/di/Bindings/BindingInfo.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ray\Di\Bindings;
6+
7+
use JsonSerializable;
8+
9+
/**
10+
* Value object representing a dependency injection binding
11+
*/
12+
final class BindingInfo implements JsonSerializable
13+
{
14+
/** @var string */
15+
public $interface;
16+
17+
/** @var string|null */
18+
public $named;
19+
20+
/** @var string */
21+
public $type;
22+
23+
/** @var mixed */
24+
public $target;
25+
26+
/** @var array<string, list<string>>|null */
27+
public $aop;
28+
29+
/**
30+
* @param string $interface
31+
* @param string|null $named
32+
* @param string $type
33+
* @param mixed $target
34+
* @param array<string, list<string>>|null $aop
35+
*/
36+
public function __construct(
37+
string $interface,
38+
?string $named,
39+
string $type,
40+
$target,
41+
?array $aop = null
42+
) {
43+
$this->interface = $interface;
44+
$this->named = $named;
45+
$this->type = $type;
46+
$this->target = $target;
47+
$this->aop = $aop;
48+
}
49+
50+
/**
51+
* @return array<string, mixed>
52+
*/
53+
public function jsonSerialize(): array
54+
{
55+
$data = [
56+
'interface' => $this->interface,
57+
'named' => $this->named,
58+
'type' => $this->type,
59+
'target' => $this->target,
60+
];
61+
62+
if ($this->aop !== null) {
63+
$data['aop'] = $this->aop;
64+
}
65+
66+
return $data;
67+
}
68+
}

src/di/Dependency.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Ray\Aop\CompilerInterface;
99
use Ray\Aop\MethodInterceptor;
1010
use Ray\Aop\WeavedInterface;
11+
use Ray\Di\Bindings\AopInfo;
1112
use ReflectionClass;
1213
use ReflectionMethod;
1314

@@ -33,6 +34,9 @@ final class Dependency implements DependencyInterface, AcceptInterface
3334
/** @var ?mixed */
3435
private $instance;
3536

37+
/** @var ?AopInfo */
38+
private $aopInfo;
39+
3640
public function __construct(NewInstance $newInstance, ?ReflectionMethod $postConstruct = null)
3741
{
3842
$this->newInstance = $newInstance;
@@ -142,10 +146,23 @@ public function weaveAspects(CompilerInterface $compiler, array $pointcuts): voi
142146
return;
143147
}
144148

149+
// Store AOP info for later retrieval
150+
if ($compiler instanceof SpyCompiler) {
151+
$this->aopInfo = $compiler->getAopInfo($bind);
152+
}
153+
145154
$class = $compiler->compile($className, $bind);
146155
$this->newInstance->weaveAspects($class, $bind);
147156
}
148157

158+
/**
159+
* Get AOP information if available
160+
*/
161+
public function getAopInfo(): ?AopInfo
162+
{
163+
return $this->aopInfo;
164+
}
165+
149166
/** @inheritDoc */
150167
public function accept(VisitorInterface $visitor)
151168
{

0 commit comments

Comments
 (0)