AbstractContainer::internalMake()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 9
rs 10
1
<?php
2
/*
3
 * @copyright (c) 2019 Mendel <[email protected]>
4
 * @license see license.txt
5
 */
6
namespace drycart\di;
7
8
/**
9
 * Main code for instantiate our object, magic call etc
10
 * Abstract, need configuration logic
11
 * @author Mendel <[email protected]>
12
 */
13
abstract class AbstractContainer extends AbstractParametersContainer implements DiInterface
14
{
15
    /**
16
     * Contain our configuration data
17
     * @var array 
18
     */
19
    protected $config = [];
20
    
21
    /**
22
     * Contain stored object (for services)
23
     * @var array
24
     */
25
    private $storage = [];
26
    
27
    private $cachedConfig = [];
28
        
29
    /**
30
     * Get prepared config for selected class
31
     * Add default value for system fields if empty
32
     * @param string $id 
33
     * @param bool $required need check if exist, and throw if not
34
     * @return array
35
     */
36
    protected function classConfig(string $id, bool $required = false) : ?array
37
    {
38
        if (!isset($this->cachedConfig[$id])) {
39
            $config = $this->internalConfig($id);
40
            $this->initRequired($config['#required'] ?? []);
41
            unset($config['#required']);
42
            $this->cachedConfig[$id] = $config;
43
        }
44
        if($required and is_null($this->cachedConfig[$id])) {
45
            throw new NotFoundException('Not exist class: '.$id);
46
        }
47
        return $this->cachedConfig[$id];
48
    }
49
    
50
    protected function internalConfig(string $id) : ?array
51
    {
52
        if (!isset($this->config[$id]) and !class_exists($id)) {
53
            return null;
54
        }
55
        $parentConfig = $this->getParentConfig($id);
56
        //
57
        $currentConfig = $this->config[$id] ?? [];
58
        if (empty($currentConfig['#class']) and !$this->isAlias($id)) {
59
            $currentConfig['#class'] = $id;
60
        }
61
        //
62
        $config = array_merge($parentConfig, $currentConfig);
63
        if (!isset($config['#singleton'])) {
64
            $config['#singleton'] = false;
65
        }
66
        return $config;
67
    }
68
    
69
    protected function getParentConfig(string $id) : array 
70
    {
71
        if ($this->isAlias($id)) {
72
            $parentId = $this->getAliaseParent($id);
73
        } else {
74
            $parents = class_parents($id);
75
            $parentId = reset($parents);
76
        }
77
        //
78
        if(!is_string($parentId)) {
79
            return [];
80
        }
81
        return $this->internalConfig($parentId);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->internalConfig($parentId) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
82
    }
83
84
    /**
85
     * Singleton wrapper for internal use
86
     * @param string $id
87
     * @param array $config
88
     * @return mixed
89
     */
90
    protected function internalSingleton($id, array $config)
91
    {
92
        if (!isset($this->storage[$id])) {
93
            $this->storage[$id] = $this->internalMake($id, $config);
94
        } 
95
        return $this->storage[$id];
96
    }
97
    
98
    /**
99
     * Create new object using our core
100
     * @param string $id class name
101
     * @param array $config
102
     * @param array $parameters parameters from request
103
     * @return mixed
104
     * @throws ContainerException
105
     */
106
    protected function internalMake($id, array $config, array $parameters = [])
107
    {
108
        $fullParameters = array_merge($parameters, $config);
109
        $obj = $this->getObject($fullParameters);
110
        //
111
        if (!$this->isAlias($id) and !is_a($obj, $id)) {
112
            throw new ContainerException('Wrong class, will be '.$id);
113
        }
114
        return $obj;
115
    }
116
117
    protected function isAlias(string $id) : bool
118
    {
119
        return (substr($id, 0, 1) == '#');
120
    }
121
    
122
    protected function getAliaseParent(string $id) : ?string
123
    {
124
        $parts = explode(':', $id, 2);
125
        if(count($parts) == 1) {
126
            return null;
127
        }
128
        return $parts[0];
129
    }
130
    
131
    protected function initRequired(array $requirement) : void
132
    {
133
        foreach ($requirement as $className) {
134
            if (!isset($this->storage[$className])) {
135
                $this->singleton($className);
136
            }
137
        }
138
    }
139
    
140
    /**
141
     * Get reflector for method/function... i.e. callable
142
     * @2do: check if it need, maybe it can be done whithout this?
143
     * @param callable $callable
144
     * @return array
145
     */
146
    protected function getCallableDependency(callable $callable) : array
147
    {
148
        if (is_array($callable)) {
149
            $className = get_class($callable[0]);
150
            $classReflector = new \ReflectionClass($className);
151
            return $classReflector->getMethod($callable[1])->getParameters();
152
        } else {
153
            return (new \ReflectionFunction($callable))->getParameters();
154
        }
155
    }
156
    
157
    /**
158
     * Prepare parameters for callable (add dependency)
159
     * @param callable $callable
160
     * @param array $parameters assoc array of parameters
161
     * @return array prepared parameters
162
     */
163
    protected function callableParameters(callable $callable, array $parameters) : array
164
    {
165
        $dependency = $this->getCallableDependency($callable);
166
        return $this->prepareParameters($dependency, $parameters);
167
    }
168
    
169
    private $reflectionCache = [];
170
    private function getClassDependency(string $className) : array
171
    {
172
        if (!class_exists($className)) {
173
            // Not NotFoundException => it is missconfiguration, i.e. wrong class at config
174
            throw new ContainerException('Class need to instantiate not exist '.$className);
175
        }
176
        if (!isset($this->reflectionCache[$className])) {
177
            $reflector = new \ReflectionClass($className);
178
            $constructor = $reflector->getConstructor();
179
            if (!is_null($constructor)) {
180
                $this->reflectionCache[$className] = $constructor->getParameters();
181
            } else {
182
                $this->reflectionCache[$className] = [];
183
            }
184
        }
185
        return $this->reflectionCache[$className];
186
    }
187
    
188
    /**
189
     * Instantiate object using parameters array
190
     * @param array $parameters
191
     * @return mixed
192
     */
193
    private function getObject(array $parameters = [])
194
    {
195
        if (isset($parameters['#factory'])) {
196
            return call_user_func_array($parameters['#factory'], [$parameters, $this]);
197
        }
198
        $className = $parameters['#class'];
199
        //
200
        $dependency = $this->getClassDependency($className);
201
        if (!empty($dependency)) {
202
            $preparedParameters = $this->prepareParameters($dependency, $parameters);
203
        } else {
204
            $preparedParameters = [];
205
        }
206
        return new $className(...$preparedParameters);
207
    }
208
}
209