Passed
Push — main ( 65d534...8369ee )
by Sammy
01:17
created

LeMarchand::makeInstance()   A

Complexity

Conditions 5
Paths 26

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 18
c 2
b 0
f 1
dl 0
loc 29
rs 9.3554
cc 5
nc 26
nop 2
1
<?php
2
3
namespace HexMakina\LeMarchand;
4
5
use Psr\Container\ContainerInterface;
6
use Psr\Container\ContainerExceptionInterface;
7
use Psr\Container\NotFoundExceptionInterface;
8
9
class LeMarchand implements ContainerInterface
10
{
11
    private static $instance = null;
12
    // stores all the settings
13
    private $configurations = [];
14
15
    // stores the namespace cascade
16
    private $namespace_cascade = [];
17
18
    // stores the interface to class wiring
19
    private $interface_wiring = [];
20
21
    // store the resolved names for performance
22
    private $resolved_cache = [];
23
24
    // stores the automatically created instances, by class name
25
    private $instance_cache = [];
26
27
28
    public const RX_SETTINGS = '/^settings\./';
29
30
    public const RX_MVC = '/(Models|Controllers)\\\([a-zA-Z]+)(::class|::new)?/';
31
32
    public const RX_INTERFACE = '/([a-zA-Z]+)Interface$/';
33
34
35
    public static function box($settings = null): ContainerInterface
36
    {
37
        if (is_null(self::$instance)) {
38
            if (is_array($settings)) {
39
                return (self::$instance = new LeMarchand($settings));
40
            }
41
            throw new ContainerException('UNABLE_TO_OPEN_BOX');
42
        }
43
44
        return self::$instance;
45
    }
46
47
48
    private function __construct($settings)
49
    {
50
        if (isset($settings[__CLASS__])) {
51
            $this->namespace_cascade = $settings[__CLASS__]['cascade'] ?? [];
52
            $this->interface_wiring = $settings[__CLASS__]['wiring'] ?? [];
53
            unset($settings[__CLASS__]);
54
        }
55
        $this->configurations['settings'] = $settings;
56
    }
57
58
    public function __debugInfo(): array
59
    {
60
        $dbg = get_object_vars($this);
61
62
        foreach ($dbg['instance_cache'] as $class => $instance) {
63
            $dbg['instance_cache'][$class] = true;
64
        }
65
66
        foreach ($dbg['interface_wiring'] as $interface => $wire) {
67
            if (is_array($wire)) {
68
                $wire = array_shift($wire) . ' --array #' . count($wire);
69
            }
70
            $dbg['interface_wiring'][$interface] = $wire;
71
        }
72
73
        return $dbg;
74
    }
75
76
    public function has($configuration)
77
    {
78
        try {
79
            $this->get($configuration);
80
            return true;
81
        } catch (NotFoundExceptionInterface $e) {
82
            return false;
83
        } catch (ContainerExceptionInterface $e) {
84
            return false;
85
        }
86
    }
87
88
    public function get($configuration)
89
    {
90
        if (!is_string($configuration)) {
91
            throw new ContainerException($configuration);
92
        }
93
        // 1. is it a first level key ?
94
        if (isset($this->configurations[$configuration])) {
95
            return $this->configurations[$configuration];
96
        }
97
98
        // 2. is it configuration data ?
99
        if (preg_match(self::RX_SETTINGS, $configuration) === 1) {
100
            return $this->getSettings($configuration);
101
        }
102
103
        // 3. is it an existing class ?
104
        if (class_exists($configuration)) {
105
            return $this->getInstance($configuration);
106
        }
107
108
        // 4. is it an interface ?
109
        if (preg_match(self::RX_INTERFACE, $configuration) === 1) {
110
            return $this->wireInstance($configuration);
111
        }
112
113
        // 5. is it cascadable ?
114
        if (preg_match(self::RX_MVC, $configuration, $m) === 1) {
115
            $class_name = $this->cascadeNamespace($m[1] . '\\' . $m[2]);
116
117
            if (!isset($m[3])) {
118
                return $this->getInstance($class_name);
119
            }
120
121
            if ($m[3] === '::class') {
122
                return $class_name;
123
            }
124
125
            if ($m[3] === '::new') {
126
                return $this->makeInstance($class_name);
127
            }
128
        }
129
130
        throw new NotFoundException($configuration);
131
    }
132
133
134
    private function getSettings($setting)
135
    {
136
        // vd(__FUNCTION__);
137
        $ret = $this->configurations;
138
139
      //dot based hierarchy, parse and climb
140
        foreach (explode('.', $setting) as $k) {
141
            if (!isset($ret[$k])) {
142
                throw new NotFoundException($setting);
143
            }
144
            $ret = $ret[$k];
145
        }
146
147
        return $ret;
148
    }
149
150
    private function resolved($clue, $solution = null)
151
    {
152
        if (!is_null($solution)) {
153
            $this->resolved_cache[$clue] = $solution;
154
        }
155
        // vd($clue, __FUNCTION__);
156
        return $this->resolved_cache[$clue] ?? null;
157
    }
158
159
    private function isResolved($clue): bool
160
    {
161
        return isset($this->resolved_cache[$clue]);
162
    }
163
164
    private function cascadeNamespace($class_name)
165
    {
166
        if ($this->isResolved($class_name)) {
167
            return $this->resolved($class_name);
168
        }
169
170
        // not fully namespaced, lets cascade
171
        foreach ($this->namespace_cascade as $ns) {
172
            if (class_exists($fully_namespaced = $ns . $class_name)) {
173
                $this->resolved($class_name, $fully_namespaced);
174
                return $fully_namespaced;
175
            }
176
        }
177
        throw new NotFoundException($class_name);
178
    }
179
180
    private function wireInstance($interface)
181
    {
182
        if (!isset($this->interface_wiring[$interface])) {
183
            throw new NotFoundException($interface);
184
        }
185
186
        $wire = $this->interface_wiring[$interface];
187
188
        // interface + constructor params
189
        if ($this->hasEmbeddedConstructorParameters($wire)) {
190
            $class = array_shift($wire);
191
            $args = $wire;
192
        } else {
193
            $class = $wire;
194
            $args = null;
195
        }
196
197
        if ($this->isResolved($class) && $this->hasPrivateContructor($class)) {
198
            return $this->resolved($class);
199
        }
200
201
        return $this->getInstance($class, $args);
202
    }
203
204
    private function hasPrivateContructor($class_name): bool
205
    {
206
        $rc = new \ReflectionClass($class_name);
207
        return !is_null($constructor = $rc->getConstructor()) && $constructor->isPrivate();
208
    }
209
210
    private function hasEmbeddedConstructorParameters($wire)
211
    {
212
        return is_array($wire);
213
    }
214
215
    private function getInstance($class, $construction_args = [])
216
    {
217
        if (isset($this->instance_cache[$class])) {
218
            return $this->instance_cache[$class];
219
        }
220
221
        return $this->makeInstance($class, $construction_args);
222
    }
223
224
    private function makeInstance($class, $construction_args = [])
225
    {
226
        try {
227
            $rc = new \ReflectionClass($class);
228
            $instance = null;
229
230
            if (!is_null($constructor = $rc->getConstructor())) {
231
                $construction_args = $this->getConstructorParameters($constructor, $construction_args);
232
233
                if ($constructor->isPrivate()) { // singleton ?
234
                  // first argument is the static instance-making method
235
                    $singleton_method = $rc->getMethod(array_shift($construction_args));
236
                  // invoke the method with remaining constructor args
237
                    $instance = $this->resolved($class, $singleton_method->invoke(null, $construction_args));
238
                } else {
239
                    $instance = $rc->newInstanceArgs($construction_args);
240
                }
241
            } else {
242
                $instance = $rc->newInstanceArgs();
243
            }
244
245
            if ($rc->hasMethod('set_container')) {
246
                $instance->set_container($this);
247
            }
248
            $this->instance_cache[$class] = $instance;
249
250
            return $instance;
251
        } catch (\ReflectionException $e) {
252
            throw new ContainerException($e->getMessage());
253
        }
254
    }
255
256
    private function getConstructorParameters(\ReflectionMethod $constructor, $construction_args = [])
257
    {
258
      // vd(__FUNCTION__);
259
260
        if (empty($construction_args)) {
261
            foreach ($constructor->getParameters() as $param) {
262
                // try {
263
                if ($param->getType()) {
264
                    $construction_args [] = $this->get($param->getType()->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

264
                    $construction_args [] = $this->get($param->getType()->/** @scrutinizer ignore-call */ getName());
Loading history...
265
                } else {
266
                    $setting = 'settings.Constructor.' . $constructor->class . '.' . $param->getName();
267
                    $construction_args [] = $this->getSettings($setting);
268
                }
269
                // } catch (NotFoundExceptionInterface $e) {
270
                //     dd($e);
271
                // }
272
            }
273
        }
274
        return $construction_args;
275
    }
276
}
277