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() . ' |
|||
0 ignored issues
–
show
The method
getName() does not exist on ReflectionType . It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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 |