Passed
Branch main (9b2adc)
by Sammy
07:01 queued 05:15
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
        foreach($dbg['interface_wiring'] as $interface => $wire)
65
          $dbg['interface_wiring'][$interface] = is_array($wire) ? array_shift($wire) . ' --array #' .count($wire) : $wire;
66
67
        return $dbg;
68
    }
69
70
    public function has($configuration)
71
    {
72
        try {
73
            $this->get($configuration);
74
            return true;
75
        } catch (NotFoundExceptionInterface $e) {
76
            return false;
77
        } catch (ContainerExceptionInterface $e) {
78
            return false;
79
        }
80
    }
81
82
83
    public function get($configuration)
84
    {
85
        if (!is_string($configuration)) {
86
            throw new LamentException($configuration);
87
        }
88
        // 1. is it a first level key ?
89
        if (isset($this->configurations[$configuration])) {
90
            return $this->configurations[$configuration];
91
        }
92
93
        // 2. is it configuration data ?
94
        if (preg_match(self::RX_SETTINGS, $configuration, $m) === 1) {
95
            return $this->getSettings($configuration);
96
        }
97
98
        // 3. is it an existing class
99
        if (class_exists($configuration)) {
100
            return $this->getInstance($configuration);
101
        }
102
103
104
        // vdt($configuration, __FUNCTION__);
105
        if (preg_match(self::RX_MVC, $configuration, $m) === 1) {
106
            $class_name = $this->cascadeNamespace($m[1] . '\\' . $m[2]);
107
            // vd($configuration, $class_name);
108
109
            if (!isset($m[3])) {
110
                return $this->getInstance($class_name);
111
            }
112
113
            if ($m[3] === '::class') {
114
                return $class_name;
115
            }
116
117
            if ($m[3] === '::new') {
118
                return $this->makeInstance($class_name);
119
            }
120
        }
121
122
        // if it is an interface, we respond with an instance
123
        if (preg_match(self::RX_INTERFACE, $configuration, $m) === 1) {
124
            return $this->wireInstance($configuration);
125
        }
126
127
        throw new ConfigurationException($configuration);
128
    }
129
130
131
    private function getSettings($setting)
132
    {
133
        // vd(__FUNCTION__);
134
        $ret = $this->configurations;
135
136
      //dot based hierarchy, parse and climb
137
        foreach (explode('.', $setting) as $k) {
138
            if (!isset($ret[$k])) {
139
                throw new ConfigurationException($setting);
140
            }
141
            $ret = $ret[$k];
142
        }
143
144
        return $ret;
145
    }
146
147
    private function resolved($clue, $solution = null)
148
    {
149
        if (!is_null($solution)) {
150
            $this->resolved_cache[$clue] = $solution;
151
        }
152
        // vd($clue, __FUNCTION__);
153
        return $this->resolved_cache[$clue] ?? null;
154
    }
155
156
    private function isResolved($clue): bool
157
    {
158
        return isset($this->resolved_cache[$clue]);
159
    }
160
161
    private function cascadeNamespace($class_name)
162
    {
163
        if ($this->isResolved($class_name)) {
164
            return $this->resolved($class_name);
165
        }
166
167
        // not fully namespaced, lets cascade
168
        foreach ($this->namespace_cascade as $ns) {
169
            if (class_exists($fully_namespaced = $ns . $class_name)) {
170
                $this->resolved($class_name, $fully_namespaced);
171
                return $fully_namespaced;
172
            }
173
        }
174
        throw new ConfigurationException($class_name);
175
    }
176
177
    private function wireInstance($interface)
178
    {
179
        // vd($interface, __FUNCTION__);
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