Passed
Push — master ( 24b216...683720 )
by Jelmer
11:28
created

DiCompiler::newInstance()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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

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