Test Failed
Push — master ( 1a9c2f...e3b3ac )
by Jelmer
01:56
created

DiCompiler::validateServiceDefinitionParameters()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 6
nc 4
nop 1
dl 0
loc 10
ccs 3
cts 3
cp 1
crap 5
rs 9.6111
c 0
b 0
f 0
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 14
    private string $parentDi;
15
16 14
    public function __construct(string $parentDi)
17
    {
18
        $this->parentDi = $parentDi;
19 12
    }
20
21 12
    private function getCompiledName(): string
22
    {
23
        return $this->parentDi . self::COMPILED_EXTENSION;
24 6
    }
25
26 6
    public function compiledClassExists(): bool
27
    {
28
        return class_exists($this->getCompiledName());
29 5
    }
30
31 5
    public function compile(): void
32 1
    {
33
        if ($this->compiledClassExists()) {
34
            throw new \RuntimeException('Cannot recompile already compiled container');
35 4
        }
36
37
        eval(substr($this->generateCode(), 32));
38 5
    }
39
40 5
    public function generateCode(): string
41 5
    {
42 5
        $parent = new ReflectionClass($this->parentDi);
43 5
44
        $code = $this->generateHeader($parent);
45
46 5
        $methods = $parent->getMethods();
47
        foreach ($methods as $method) {
48 5
            $code .= $this->processMethod($method);
49 5
        }
50
        return $code . $this->generateFooter();
51 5
    }
52
53 5
    private function generateHeader(ReflectionClass $parent)
54
    {
55 5
        return
56
'<?php declare(strict_types=1);
57
58
namespace ' . $parent->getNamespaceName() . ';
59
60
use ' . $parent->getName() . ';
61
62
class ' . $parent->getShortName() . self::COMPILED_EXTENSION . ' extends ' . $parent->getShortName() . '
63
{
64
    private array $__services = [];
65 5
66
    private function __service(string $method, ?string $instanceName = null)
67
    {
68 5
        $suffix = is_null($instanceName) ? \'\' : \'.\' . $instanceName;
69
        return $this->__services[$method . $suffix] ?? ($this->__services[$method . $suffix] = parent::{$method}($instanceName));
70 5
    }
71
72 5
';
73 5
    }
74 2
75
    public function processMethod(ReflectionMethod $method): string
76 5
    {
77 1
        // Decide if the method needs to be overloaded
78
        if (substr($method->getName(), 0, 3) !== 'get') {
79 4
            return '';
80 1
        }
81
82 3
        // Run validations
83 3
        $this->validateServiceDefinitionReturnType($method);
84
        $this->validateServiceDefinitionParameters($method);
85 3
86 1
        // Generate method-overload code
87
        return '
88
    public function ' . $method->getName() . '(?string $instanceName = null): \\' . $method->getReturnType()->getName() . '
0 ignored issues
show
Bug introduced by
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

88
    public function ' . $method->getName() . '(?string $instanceName = null): \\' . $method->getReturnType()->/** @scrutinizer ignore-call */ getName() . '
Loading history...
89
    {
90 2
        return $this->__service(\'' . $method->getName() . '\', $instanceName);
91 2
    }
92 2
';
93
    }
94
95 2
    private function validateServiceDefinitionReturnType(ReflectionMethod $method): void
96 2
    {
97
        // It must have a return type, and the return type must only define a single class or interface
98 2
        if (!$method->hasReturnType()) {
99
            throw new RuntimeException('Service definitions must have return types');
100 2
        }
101
102
        $returnType = $method->getReturnType();
103 2
        if (!$returnType instanceof ReflectionNamedType) {
104
            throw new RuntimeException('Service definitions must define only a single class or interface returntype');
105
        }
106 2
        if ($returnType->isBuiltin()) {
107
            throw new RuntimeException('Service definitions must return objects');
108 2
        }
109
    }
110
111 6
    private function validateServiceDefinitionParameters(ReflectionMethod $method): void
112
    {
113 6
        if ($method->getNumberOfParameters() > 1) {
114 6
            throw new RuntimeException('Service definitions cannot take more than a name parameter');
115
        }
116
        if ($method->getNumberOfParameters() === 1) {
117
            $parameter = $method->getParameters()[0];
118
119
            if (!is_a($parameter->getType(), ReflectionNamedType::class) || $parameter->getType()->getName() !== 'string') {
120
                throw new \RuntimeException('Service definitions are only allowed a single named nullable string argument.');
121
            }
122
        }
123
    }
124
125
    private function generateFooter(): string
126
    {
127
        return PHP_EOL . '}' . PHP_EOL;
128
    }
129
130
    public function newInstance(array ...$args): mixed
131
    {
132
        $reflectedClass = new ReflectionClass($this->getCompiledName());
133
        return $reflectedClass->newInstance(...$args);
134
    }
135
}
136