1 | <?php declare(strict_types=1); |
||
2 | |||
3 | namespace jschreuder\MiddleDi; |
||
4 | |||
5 | use ReflectionClass; |
||
6 | use ReflectionMethod; |
||
7 | use ReflectionNamedType; |
||
8 | use RuntimeException; |
||
9 | |||
10 | final class DiCompiler implements DiCompilerInterface |
||
11 | { |
||
12 | const COMPILED_EXTENSION = '__Compiled'; |
||
13 | |||
14 | private string $parentDi; |
||
15 | |||
16 | 17 | public function __construct(string $parentDi) |
|
17 | { |
||
18 | 17 | $this->parentDi = $parentDi; |
|
19 | } |
||
20 | |||
21 | 15 | private function getCompiledName(): string |
|
22 | { |
||
23 | 15 | return $this->parentDi . self::COMPILED_EXTENSION; |
|
24 | } |
||
25 | |||
26 | 15 | public function compiledClassExists(): bool |
|
27 | { |
||
28 | 15 | return class_exists($this->getCompiledName()); |
|
29 | } |
||
30 | |||
31 | 14 | public function compile(): static |
|
32 | { |
||
33 | 14 | if ($this->compiledClassExists()) { |
|
34 | 1 | throw new \RuntimeException('Cannot recompile already compiled container'); |
|
35 | } |
||
36 | |||
37 | 14 | eval(substr($this->generateCode(), 31)); |
|
0 ignored issues
–
show
introduced
by
![]() |
|||
38 | |||
39 | 9 | return $this; |
|
40 | } |
||
41 | |||
42 | 15 | public function generateCode(): string |
|
43 | { |
||
44 | 15 | $parent = new ReflectionClass($this->parentDi); |
|
45 | |||
46 | 15 | $code = $this->generateHeader($parent); |
|
47 | |||
48 | 15 | $methods = $parent->getMethods(); |
|
49 | 15 | foreach ($methods as $method) { |
|
50 | 15 | $code .= $this->processMethod($method); |
|
51 | } |
||
52 | 10 | return $code . $this->generateFooter(); |
|
53 | } |
||
54 | |||
55 | 15 | private function generateHeader(ReflectionClass $parent) |
|
56 | { |
||
57 | 15 | $code = '<?php declare(strict_types=1); |
|
58 | 15 | '; |
|
59 | |||
60 | 15 | if ($parent->getNamespaceName()) { |
|
61 | 14 | $code .= 'namespace ' . $parent->getNamespaceName() . '; |
|
62 | |||
63 | 14 | use ' . $parent->getName() . '; |
|
64 | 14 | '; |
|
65 | } |
||
66 | |||
67 | 15 | $code .= ' |
|
68 | 15 | class ' . $parent->getShortName() . self::COMPILED_EXTENSION . ' extends ' . $parent->getShortName() . ' |
|
69 | { |
||
70 | private array $__services = []; |
||
71 | |||
72 | private function __service(string $method, ?string $instanceName = null) |
||
73 | { |
||
74 | $suffix = is_null($instanceName) ? \'\' : \'.\' . $instanceName; |
||
75 | return $this->__services[$method . $suffix] ?? ($this->__services[$method . $suffix] = parent::{$method}($instanceName)); |
||
76 | } |
||
77 | |||
78 | 15 | '; |
|
79 | |||
80 | 15 | return $code; |
|
81 | } |
||
82 | |||
83 | 15 | public function processMethod(ReflectionMethod $method): string |
|
84 | { |
||
85 | // Decide if the method needs to be overloaded |
||
86 | 15 | if (substr($method->getName(), 0, 3) !== 'get') { |
|
87 | 10 | return ''; |
|
88 | } |
||
89 | |||
90 | // Run validations |
||
91 | 15 | $this->validateServiceDefinitionReturnType($method); |
|
92 | 12 | $this->validateServiceDefinitionParameters($method); |
|
93 | |||
94 | // Generate method-overload code |
||
95 | 10 | return ' |
|
96 | 10 | public function ' . $method->getName() . '(?string $instanceName = null): \\' . $method->getReturnType()->getName() . ' |
|
97 | { |
||
98 | 10 | return $this->__service(\'' . $method->getName() . '\', $instanceName); |
|
99 | } |
||
100 | 10 | '; |
|
101 | } |
||
102 | |||
103 | 15 | private function validateServiceDefinitionReturnType(ReflectionMethod $method): void |
|
104 | { |
||
105 | // It must have a return type, and the return type must only define a single class or interface |
||
106 | 15 | if (!$method->hasReturnType()) { |
|
107 | 1 | throw new RuntimeException('Service definitions must have return types'); |
|
108 | } |
||
109 | |||
110 | 14 | $returnType = $method->getReturnType(); |
|
111 | 14 | if (!$returnType instanceof ReflectionNamedType) { |
|
112 | 1 | throw new RuntimeException('Service definitions must define only a single class or interface returntype'); |
|
113 | } |
||
114 | 13 | if ($returnType->isBuiltin()) { |
|
115 | 1 | throw new RuntimeException('Service definitions must return objects'); |
|
116 | } |
||
117 | } |
||
118 | |||
119 | 12 | private function validateServiceDefinitionParameters(ReflectionMethod $method): void |
|
120 | { |
||
121 | 12 | if ($method->getNumberOfParameters() > 1) { |
|
122 | 1 | throw new RuntimeException('Service definitions cannot take more than a name parameter'); |
|
123 | } |
||
124 | 11 | if ($method->getNumberOfParameters() === 1) { |
|
125 | 11 | $parameter = $method->getParameters()[0]; |
|
126 | |||
127 | 11 | if (!is_a($parameter->getType(), ReflectionNamedType::class) || $parameter->getType()->getName() !== 'string') { |
|
128 | 1 | throw new \RuntimeException('Service definitions are only allowed a single named nullable string argument.'); |
|
129 | } |
||
130 | } |
||
131 | } |
||
132 | |||
133 | 10 | private function generateFooter(): string |
|
134 | { |
||
135 | 10 | return PHP_EOL . '}' . PHP_EOL; |
|
136 | } |
||
137 | |||
138 | 7 | public function newInstance(array ...$args): mixed |
|
139 | { |
||
140 | 7 | $reflectedClass = new ReflectionClass($this->getCompiledName()); |
|
141 | 7 | return $reflectedClass->newInstance(...$args); |
|
142 | } |
||
143 | } |
||
144 |