Passed
Push — main ( f9bc30...9f095f )
by Sammy
01:20
created

LeMarchand.php (2 issues)

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
89
    public function get($configuration)
90
    {
91
        if (!is_string($configuration)) {
92
            throw new ContainerException($configuration);
93
        }
94
95
        $ret = null;
96
97
        if ($this->isFirstLevelKey($configuration)) {
98
            $ret = $this->configurations[$configuration];
99
        }
100
        elseif ($this->isSettings($configuration)) {
101
            $ret = $this->getSettings($configuration);
102
        }
103
        elseif (class_exists($configuration)) {
104
            $ret = $this->getInstance($configuration);
105
        }
106
        elseif ($this->isInterface($configuration)) {
107
            $ret = $this->wireInstance($configuration);
108
        }
109
        elseif ($this->hasModelOrController($configuration)) {
0 ignored issues
show
The method hasModelOrController() does not exist on HexMakina\LeMarchand\LeMarchand. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

109
        elseif ($this->/** @scrutinizer ignore-call */ hasModelOrController($configuration)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
110
            // 5. is it cascadable ?
111
            preg_match(self::RX_MVC, $configuration, $m);
112
113
            $class_name = $this->cascadeNamespace($m[1] . '\\' . $m[2]);
114
115
            if($this->hasClassNameModifier($configuration)){
116
              $ret = $class_name;
117
            }
118
            elseif($this->hasNewInstanceModifier($configuration)){
119
              $ret = $this->makeInstance($class_name);
120
            }
121
            $ret = $this->getInstance($class_name);
122
        }
123
124
        if(is_null($ret))
125
          throw new NotFoundException($configuration);
126
127
        return $ret;
128
    }
129
130
    private function isFirstLevelKey($configuration){
131
      return isset($this->configurations[$configuration]);
132
    }
133
134
    private function isSettings($configuration){
135
      return preg_match(self::RX_SETTINGS, $configuration) === 1;
136
    }
137
138
    private function isInterface($configuration){
139
      return preg_match(self::RX_INTERFACE, $configuration) === 1;
140
    }
141
142
    private function isModelOrController($configuration){
0 ignored issues
show
The method isModelOrController() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
143
      return preg_match(self::RX_MVC, $configuration) === 1;
144
    }
145
146
    private function hasClassNameModifier($configuration){
147
      return strpos($configuration, '::class') !== false;
148
    }
149
150
    private function hasNewInstanceModifier($configuration){
151
      return strpos($configuration, '::new') !== false;
152
    }
153
154
    private function getSettings($setting)
155
    {
156
        // vd(__FUNCTION__);
157
        $ret = $this->configurations;
158
159
      //dot based hierarchy, parse and climb
160
        foreach (explode('.', $setting) as $k) {
161
            if (!isset($ret[$k])) {
162
                throw new NotFoundException($setting);
163
            }
164
            $ret = $ret[$k];
165
        }
166
167
        return $ret;
168
    }
169
170
    private function resolved($clue, $solution = null)
171
    {
172
        if (!is_null($solution)) {
173
            $this->resolved_cache[$clue] = $solution;
174
        }
175
        // vd($clue, __FUNCTION__);
176
        return $this->resolved_cache[$clue] ?? null;
177
    }
178
179
    private function isResolved($clue): bool
180
    {
181
        return isset($this->resolved_cache[$clue]);
182
    }
183
184
    private function cascadeNamespace($class_name)
185
    {
186
        if ($this->isResolved($class_name)) {
187
            return $this->resolved($class_name);
188
        }
189
190
        // not fully namespaced, lets cascade
191
        foreach ($this->namespace_cascade as $ns) {
192
            if (class_exists($fully_namespaced = $ns . $class_name)) {
193
                $this->resolved($class_name, $fully_namespaced);
194
                return $fully_namespaced;
195
            }
196
        }
197
        throw new NotFoundException($class_name);
198
    }
199
200
    private function wireInstance($interface)
201
    {
202
        if (!isset($this->interface_wiring[$interface])) {
203
            throw new NotFoundException($interface);
204
        }
205
206
        $wire = $this->interface_wiring[$interface];
207
208
        // interface + constructor params
209
        if ($this->hasEmbeddedConstructorParameters($wire)) {
210
            $class = array_shift($wire);
211
            $args = $wire;
212
        } else {
213
            $class = $wire;
214
            $args = null;
215
        }
216
217
        if ($this->isResolved($class) && $this->hasPrivateContructor($class)) {
218
            return $this->resolved($class);
219
        }
220
221
        return $this->getInstance($class, $args);
222
    }
223
224
    private function hasPrivateContructor($class_name): bool
225
    {
226
        $rc = new \ReflectionClass($class_name);
227
        return !is_null($constructor = $rc->getConstructor()) && $constructor->isPrivate();
228
    }
229
230
    private function hasEmbeddedConstructorParameters($wire)
231
    {
232
        return is_array($wire);
233
    }
234
235
    private function getInstance($class, $construction_args = [])
236
    {
237
        if (isset($this->instance_cache[$class])) {
238
            return $this->instance_cache[$class];
239
        }
240
241
        return $this->makeInstance($class, $construction_args);
242
    }
243
244
    private function makeInstance($class, $construction_args = [])
245
    {
246
        try {
247
            $rc = new \ReflectionClass($class);
248
            $instance = null;
249
250
            if (!is_null($constructor = $rc->getConstructor())) {
251
                $construction_args = $this->getConstructorParameters($constructor, $construction_args);
252
253
                if ($constructor->isPrivate()) { // singleton ?
254
                  // first argument is the static instance-making method
255
                    $singleton_method = $rc->getMethod(array_shift($construction_args));
256
                  // invoke the method with remaining constructor args
257
                    $instance = $this->resolved($class, $singleton_method->invoke(null, $construction_args));
258
                } else {
259
                    $instance = $rc->newInstanceArgs($construction_args);
260
                }
261
            } else {
262
                $instance = $rc->newInstanceArgs();
263
            }
264
265
            if ($rc->hasMethod('set_container')) {
266
                $instance->set_container($this);
267
            }
268
            $this->instance_cache[$class] = $instance;
269
270
            return $instance;
271
        } catch (\ReflectionException $e) {
272
            throw new ContainerException($e->getMessage());
273
        }
274
    }
275
276
    private function getConstructorParameters(\ReflectionMethod $constructor, $construction_args = [])
277
    {
278
      // vd(__FUNCTION__);
279
280
        if (empty($construction_args)) {
281
            foreach ($constructor->getParameters() as $param) {
282
                // try {
283
                if ($param->getType()) {
284
                    $construction_args [] = $this->get($param->getType()->getName());
285
                } else {
286
                    $setting = 'settings.Constructor.' . $constructor->class . '.' . $param->getName();
287
                    $construction_args [] = $this->getSettings($setting);
288
                }
289
                // } catch (NotFoundExceptionInterface $e) {
290
                //     dd($e);
291
                // }
292
            }
293
        }
294
        return $construction_args;
295
    }
296
}
297