Passed
Push — main ( ba6688...9c4ee4 )
by Sammy
07:19 queued 05:00
created

LeMarchand::cascadeNamespace()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

257
                        $construction_args [] = $this->get($param->getType()->/** @scrutinizer ignore-call */ getName());
Loading history...
258
                    } else {
259
                        $setting = 'settings.Constructor.' . $constructor->class . '.' . $param->getName();
260
                        $construction_args [] = $this->getSettings($setting);
261
                    }
262
                // } catch (NotFoundExceptionInterface $e) {
263
                //     dd($e);
264
                // }
265
            }
266
        }
267
        return $construction_args;
268
    }
269
}
270