PluginProbe ʕ •ᴥ•ʔ
Presto Player / 4.3.3
Presto Player v4.3.3
4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / vendor / level-2 / dice / Dice.php
presto-player / vendor / level-2 / dice Last commit date
Extra 5 years ago Loader 5 years ago tests 2 years ago .gitignore 3 years ago Dice.php 2 years ago README.md 5 years ago composer.json 5 years ago phpunit.xml 5 years ago
Dice.php
279 lines
1 <?php
2 /* @description Dice - A minimal Dependency Injection Container for PHP
3 * @author Tom Butler tom@r.je
4 * @copyright 2012-2020 Tom Butler <tom@r.je> | https://r.je/dice
5 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
6 * @version 4.0 */
7 namespace PrestoPlayer\Dice;
8 class Dice {
9 const CONSTANT = 'Dice::CONSTANT';
10 const GLOBAL = 'Dice::GLOBAL';
11 const INSTANCE = 'Dice::INSTANCE';
12 const CHAIN_CALL = 'Dice::CHAIN_CALL';
13 const SELF = 'Dice::SELF';
14 /**
15 * @var array $rules Rules which have been set using addRule()
16 */
17 private $rules = [];
18
19 /**
20 * @var array $cache A cache of closures based on class name so each class is only reflected once
21 */
22 private $cache = [];
23
24 /**
25 * @var array $instances Stores any instances marked as 'shared' so create() can return the same instance
26 */
27 private $instances = [];
28
29 /**
30 * Add a rule $rule to the class $name
31 * @param string $name The name of the class to add the rule for
32 * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules.
33 */
34 public function addRule(string $name, array $rule): self {
35 $dice = clone $this;
36 $this->addRuleTo($dice, $name, $rule);
37 return $dice;
38 }
39
40 /**
41 * Add rules as array. Useful for JSON loading $dice->addRules(json_decode(file_get_contents('foo.json'));
42 * @param array Rules in a single array [name => $rule] format
43 */
44 public function addRules($rules): self {
45 if (is_string($rules)) $rules = json_decode(file_get_contents($rules), true);
46 $dice = clone $this;
47 foreach ($rules as $name => $rule) $this->addRuleTo($dice,$name, $rule);
48 return $dice;
49 }
50
51 private function addRuleTo(Dice $dice, string $name, array $rule) {
52 if (isset($rule['instanceOf']) && (!array_key_exists('inherit', $rule) || $rule['inherit'] === true ))
53 $rule = array_replace_recursive($dice->getRule($rule['instanceOf']), $rule);
54 //Allow substitutions rules to be defined with a leading a slash
55 if (isset($rule['substitutions'])) foreach($rule['substitutions'] as $key => $value) $rule['substitutions'][ltrim($key, '\\')] = $value;
56 //Clear any existing instance or cache for this class
57 unset($dice->instances[$name], $dice->cache[$name]);
58 $dice->rules[ltrim(strtolower($name), '\\')] = array_replace_recursive($dice->getRule($name), $rule);
59 }
60
61 /**
62 * Returns the rule that will be applied to the class $name when calling create()
63 * @param string name The name of the class to get the rules for
64 * @return array The rules for the specified class
65 */
66 public function getRule(string $name): array {
67 $lcName = strtolower(ltrim($name, '\\'));
68 if (isset($this->rules[$lcName])) return $this->rules[$lcName];
69
70 foreach ($this->rules as $key => $rule) { // Find a rule which matches the class described in $name where:
71 if (empty($rule['instanceOf']) // It's not a named instance, the rule is applied to a class name
72 && $key !== '*' // It's not the default rule
73 && is_subclass_of($name, $key) // The rule is applied to a parent class
74 && (!array_key_exists('inherit', $rule) || $rule['inherit'] === true )) // And that rule should be inherited to subclasses
75 return $rule;
76 }
77 // No rule has matched, return the default rule if it's set
78 return isset($this->rules['*']) ? $this->rules['*'] : [];
79 }
80
81 /**
82 * Returns a fully constructed object based on $name using $args and $share as constructor arguments if supplied
83 * @param string name The name of the class to instantiate
84 * @param array $args An array with any additional arguments to be passed into the constructor upon instantiation
85 * @param array $share a list of defined in shareInstances for objects higher up the object graph, should only be used internally
86 * @return object A fully constructed object based on the specified input arguments
87 */
88 public function create(string $name, array $args = [], array $share = []) {
89 // Is there a shared instance set? Return it. Better here than a closure for this, calling a closure is slower.
90 if (!empty($this->instances[$name])) return $this->instances[$name];
91
92 // Create a closure for creating the object if there isn't one already
93 if (empty($this->cache[$name])) $this->cache[$name] = $this->getClosure(ltrim($name, '\\'), $this->getRule($name));
94
95 // Call the cached closure which will return a fully constructed object of type $name
96 return $this->cache[$name]($args, $share);
97 }
98
99 /**
100 * Returns a closure for creating object $name based on $rule, caching the reflection object for later use
101 * @param string $name the Name of the class to get the closure for
102 * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules.
103 * @return callable A closure
104 */
105 private function getClosure(string $name, array $rule) {
106 // Reflect the class and constructor, this should only ever be done once per class and get cached
107 $class = new \ReflectionClass(isset($rule['instanceOf']) ? $rule['instanceOf'] : $name);
108 $constructor = $class->getConstructor();
109
110 // Create parameter generating function in order to cache reflection on the parameters. This way $reflect->getParameters() only ever gets called once
111 $params = $constructor ? $this->getParams($constructor, $rule) : null;
112 //PHP throws a fatal error rather than an exception when trying to instantiate an interface, detect it and throw an exception instead
113 if ($class->isInterface()) $closure = function() {
114 throw new \InvalidArgumentException('Cannot instantiate interface');
115 };
116 // Get a closure based on the type of object being created: Shared, normal or constructorless
117 else if ($params) $closure = function (array $args, array $share) use ($class, $params) {
118 // This class has depenencies, call the $params closure to generate them based on $args and $share
119 return new $class->name(...$params($args, $share));
120 };
121 else $closure = function () use ($class) { // No constructor arguments, just instantiate the class
122 return new $class->name;
123 };
124
125 if (!empty($rule['shared'])) $closure = function (array $args, array $share) use ($class, $name, $constructor, $params, $closure) {
126 //Internal classes may not be able to be constructed without calling the constructor and will not suffer from #7, construct them normally.
127 if ($class->isInternal()) $this->instances[$class->name] = $this->instances['\\' . $class->name] = $closure($args, $share);
128 else {
129 //Otherwise, create the class without calling the constructor (and write to \$name and $name, see issue #68)
130 $this->instances[$name] = $this->instances['\\' . $name] = $class->newInstanceWithoutConstructor();
131 // Now call this constructor after constructing all the dependencies. This avoids problems with cyclic references (issue #7)
132 if ($constructor) $constructor->invokeArgs($this->instances[$name], $params($args, $share));
133 }
134 return $this->instances[$name];
135 };
136 // If there are shared instances, create them and merge them with shared instances higher up the object graph
137 if (isset($rule['shareInstances'])) $closure = function(array $args, array $share) use ($closure, $rule) {
138 foreach($rule['shareInstances'] as $instance) $share[] = $this->create($instance, [], $share);
139 return $closure($args, $share);
140 };
141 // When $rule['call'] is set, wrap the closure in another closure which will call the required methods after constructing the object
142 // By putting this in a closure, the loop is never executed unless call is actually set
143 return isset($rule['call']) ? function (array $args, array $share) use ($closure, $class, $rule, $name) {
144 // Construct the object using the original closure
145 $object = $closure($args, $share);
146
147 foreach ($rule['call'] as $call) {
148 // Generate the method arguments using getParams() and call the returned closure
149 $params = $this->getParams($class->getMethod($call[0]), ['shareInstances' => isset($rule['shareInstances']) ? $rule['shareInstances'] : [] ])(($this->expand(isset($call[1]) ? $call[1] : [])), $share);
150 $return = $object->{$call[0]}(...$params);
151 if (isset($call[2])) {
152 if ($call[2] === self::CHAIN_CALL) {
153 if (!empty($rule['shared'])) $this->instances[$name] = $return;
154 if (is_object($return)) $class = new \ReflectionClass(get_class($return));
155 $object = $return;
156 }
157 else if (is_callable($call[2])) call_user_func($call[2], $return);
158 }
159 }
160 return $object;
161 } : $closure;
162 }
163
164 /**
165 * Looks for Dice::INSTANCE, Dice::GLOBAL or Dice::CONSTANT array keys in $param and when found returns an object based on the value see {@link https:// r.je/dice.html#example3-1}
166 * @param mixed $param Either a string or an array,
167 * @param array $share Array of instances from 'shareInstances', required for calls to `create`
168 * @param bool $createFromString
169 * @return mixed
170 */
171 private function expand($param, array $share = [], bool $createFromString = false) {
172 if (is_array($param)) {
173 //if a rule specifies Dice::INSTANCE, look up the relevant instance
174 if (isset($param[self::INSTANCE])) {
175 if ($param[self::INSTANCE] === self::SELF) return $this;
176 //Check for 'params' which allows parameters to be sent to the instance when it's created
177 //Either as a callback method or to the constructor of the instance
178 $args = isset($param['params']) ? $this->expand($param['params']) : [];
179
180 //Support Dice::INSTANCE by creating/fetching the specified instance
181 if (is_array($param[self::INSTANCE])) $param[self::INSTANCE][0] = $this->expand($param[self::INSTANCE][0], $share, true);
182 if (is_callable($param[self::INSTANCE])) return call_user_func($param[self::INSTANCE], ...$args);
183 else return $this->create($param[self::INSTANCE], array_merge($args, $share));
184 }
185 else if (isset($param[self::GLOBAL])) return $GLOBALS[$param[self::GLOBAL]];
186 else if (isset($param[self::CONSTANT])) return constant($param[self::CONSTANT]);
187 else foreach ($param as $name => $value) $param[$name] = $this->expand($value, $share);
188 }
189
190 return is_string($param) && $createFromString ? $this->create($param) : $param;
191 }
192 /**
193 * Looks through the array $search for any object which can be used to fulfil $param
194 The original array $search is modifed so must be passed by reference.
195
196 */
197 private function matchParam(\ReflectionParameter $param, $class, array &$search) {
198 foreach ($search as $i => $arg) {
199 if ($class && ($arg instanceof $class || ($arg === null && $param->allowsNull()))) {
200 // The argument matched, return it and remove it from $search so it won't wrongly match another parameter
201 return array_splice($search, $i, 1)[0];
202 }
203 }
204 return false;
205 }
206 /**
207 * Returns a closure that generates arguments for $method based on $rule and any $args passed into the closure
208 * @param object $method An instance of ReflectionMethod (see: {@link http:// php.net/manual/en/class.reflectionmethod.php})
209 * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules.
210 * @return callable A closure that uses the cached information to generate the arguments for the method
211 */
212 private function getParams(\ReflectionMethod $method, array $rule) {
213 // Cache some information about the parameter in $paramInfo so (slow) reflection isn't needed every time
214 $paramInfo = [];
215 foreach ($method->getParameters() as $param) {
216 $type = $param->getType();
217
218 $class = $type instanceof \ReflectionNamedType && !$type->isBuiltIn() ? $type->getName() : null;
219
220 $paramInfo[] = [$class, $param, isset($rule['substitutions']) && array_key_exists($class, $rule['substitutions'])];
221 }
222
223 // Return a closure that uses the cached information to generate the arguments for the method
224 return function (array $args, array $share = []) use ($paramInfo, $rule) {
225 // If the rule has construtParams set, construct any classes reference and use them as $args
226 if (isset($rule['constructParams'])) $args = array_merge($args, $this->expand($rule['constructParams'], $share));
227
228 // Array of matched parameters
229 $parameters = [];
230
231 // Fnd a value for each method argument
232 foreach ($paramInfo as list($class, $param, $sub)) {
233 // Loop through $args and see whether or not each value can match the current parameter based on type hint
234 if ($args && ($match = $this->matchParam($param, $class, $args)) !== false) {
235 $parameters[] = $match;
236 }
237 // Do the same with $share
238 else if (($copy = $share) && ($match = $this->matchParam($param, $class, $copy)) !== false) {
239 $parameters[] = $match;
240 }
241 // When nothing from $args or $share matches but a class is type hinted, create an instance to use, using a substitution if set
242 else if ($class) try {
243 if ($sub) {
244 $parameters[] = $this->expand($rule['substitutions'][$class], $share, true);
245 }
246 else {
247 $parameters[] = !$param->allowsNull() ? $this->create($class, [], $share) : null;
248 }
249 }
250 catch (\InvalidArgumentException $e) {
251 }
252 // Support PHP 7 scalar type hinting, is_a('string', 'foo') doesn't work so this is a hacky AF workaround: call_user_func('is_' . $type, '')
253
254 //Find a match in $args for scalar types
255 else if ($args && $param->getType()) {
256 for ($i = 0; $i < count($args); $i++) {
257 if (call_user_func('is_' . $param->getType()->getName(), $args[$i])) {
258 $parameters[] = array_splice($args, $i, 1)[0];
259 break;
260 }
261 }
262 }
263 else if ($args) {
264 $parameters[] = $this->expand(array_shift($args));
265 }
266 // For variadic parameters, provide remaining $args
267 else if ($param->isVariadic()) {
268 $parameters = array_merge($parameters, $args);
269 }
270 // There's no type hint and nothing left in $args, provide the default value or null
271 else {
272 $parameters[] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null;
273 }
274 }
275 return $parameters;
276 };
277 }
278 }
279