Issues (2)

src/DiCompiler.php (2 issues)

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
The use of eval() is discouraged.
Loading history...
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 ignore-call  annotation

96
    public function ' . $method->getName() . '(?string $instanceName = null): \\' . $method->getReturnType()->/** @scrutinizer ignore-call */ getName() . '
Loading history...
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