Passed
Push — main ( b79f9e...612a22 )
by Sammy
02:02
created

LeMarchand.php (1 issue)

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

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