Security update for permissions_by_term
[yaffs-website] / vendor / behat / behat / src / Behat / Testwork / Argument / MixedArgumentOrganiser.php
1 <?php
2
3 /*
4  * This file is part of the Behat.
5  * (c) Konstantin Kudryashov <ever.zet@gmail.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 namespace Behat\Testwork\Argument;
12
13 use ReflectionFunctionAbstract;
14 use ReflectionClass;
15 use ReflectionParameter;
16
17 /**
18  * Organises function arguments using its reflection.
19  *
20  * @author Konstantin Kudryashov <ever.zet@gmail.com>
21  */
22 final class MixedArgumentOrganiser implements ArgumentOrganiser
23 {
24     private $definedArguments = array();
25
26     /**
27      * Organises arguments using function reflection.
28      *
29      * @param ReflectionFunctionAbstract $function
30      * @param mixed[]                    $arguments
31      *
32      * @return mixed[]
33      */
34     public function organiseArguments(ReflectionFunctionAbstract $function, array $arguments)
35     {
36         return $this->prepareArguments($function->getParameters(), $arguments);
37     }
38
39     /**
40      * Prepares arguments based on provided parameters.
41      *
42      * @param ReflectionParameter[] $parameters
43      * @param mixed[]               $arguments
44      *
45      * @return mixed[]
46      */
47     private function prepareArguments(array $parameters, array $arguments)
48     {
49         $this->markAllArgumentsUndefined();
50
51         list($named, $typehinted, $numbered) = $this->splitArguments($parameters, $arguments);
52
53         $arguments =
54             $this->prepareNamedArguments($parameters, $named) +
55             $this->prepareTypehintedArguments($parameters, $typehinted) +
56             $this->prepareNumberedArguments($parameters, $numbered) +
57             $this->prepareDefaultArguments($parameters);
58
59         return $this->reorderArguments($parameters, $arguments);
60     }
61
62     /**
63      * Splits arguments into three separate arrays - named, numbered and typehinted.
64      *
65      * @param ReflectionParameter[] $parameters
66      * @param mixed[]               $arguments
67      *
68      * @return array
69      */
70     private function splitArguments(array $parameters, array $arguments)
71     {
72         $parameterNames = array_map(
73             function (ReflectionParameter $parameter) {
74                 return $parameter->getName();
75             },
76             $parameters
77         );
78
79         $namedArguments = array();
80         $numberedArguments = array();
81         $typehintedArguments = array();
82         foreach ($arguments as $key => $val) {
83             if ($this->isStringKeyAndExistsInParameters($key, $parameterNames)) {
84                 $namedArguments[$key] = $val;
85             } elseif ($this->isParameterTypehintedInArgumentList($parameters, $val)) {
86                 $typehintedArguments[] = $val;
87             } else {
88                 $numberedArguments[] = $val;
89             }
90         }
91
92         return array($namedArguments, $typehintedArguments, $numberedArguments);
93     }
94
95     /**
96      * Checks that provided argument key is a string and it matches some parameter name.
97      *
98      * @param mixed    $argumentKey
99      * @param string[] $parameterNames
100      *
101      * @return Boolean
102      */
103     private function isStringKeyAndExistsInParameters($argumentKey, $parameterNames)
104     {
105         return is_string($argumentKey) && in_array($argumentKey, $parameterNames);
106     }
107
108     /**
109      * Check if a given value is typehinted in the argument list.
110      *
111      * @param  ReflectionParameter[] $parameters
112      * @param  mixed                 $value
113      *
114      * @return Boolean
115      */
116     private function isParameterTypehintedInArgumentList(array $parameters, $value)
117     {
118         if (!is_object($value)) {
119             return false;
120         }
121
122         foreach ($parameters as $parameter) {
123             if ($this->isValueMatchesTypehintedParameter($value, $parameter)) {
124                 return true;
125             }
126         }
127
128         return false;
129     }
130
131     /**
132      * Checks if value matches typehint of provided parameter.
133      *
134      * @param object              $value
135      * @param ReflectionParameter $parameter
136      *
137      * @return Boolean
138      */
139     private function isValueMatchesTypehintedParameter($value, ReflectionParameter $parameter)
140     {
141         $typehintRefl = $parameter->getClass();
142
143         return $typehintRefl && $typehintRefl->isInstance($value);
144     }
145
146     /**
147      * Captures argument values based on their respective names.
148      *
149      * @param ReflectionParameter[] $parameters
150      * @param mixed[]               $namedArguments
151      *
152      * @return mixed[]
153      */
154     private function prepareNamedArguments(array $parameters, array $namedArguments)
155     {
156         $arguments = array();
157
158         foreach ($parameters as $num => $parameter) {
159             $name = $parameter->getName();
160
161             if (array_key_exists($name, $namedArguments)) {
162                 $arguments[$name] = $namedArguments[$name];
163                 $this->markArgumentDefined($num);
164             }
165         }
166
167         return $arguments;
168     }
169
170     /**
171      * Captures argument values for typehinted arguments based on the given candidates.
172      *
173      * This method attempts to match up the best fitting arguments to each constructor argument.
174      *
175      * This case specifically fixes the issue where a constructor asks for a parent and child class,
176      * as separate arguments, but both arguments could satisfy the first argument,
177      * so they would both be passed in (overwriting each other).
178      *
179      * This will ensure that the children (exact class matches) are mapped first, and then other dependencies
180      * are mapped sequentially (to arguments which they are an `instanceof`).
181      *
182      * As such, this requires two passes of the $parameters array to ensure it is mapped as accurately as possible.
183      *
184      * @param ReflectionParameter[] $parameters          Reflection Parameters (constructor argument requirements)
185      * @param mixed[]               $typehintedArguments Resolved arguments
186      *
187      * @return mixed[] Ordered list of arguments, index is the constructor argument position, value is what will be injected
188      */
189     private function prepareTypehintedArguments(array $parameters, array $typehintedArguments)
190     {
191         $arguments = array();
192
193         $candidates = $typehintedArguments;
194
195         $this->applyPredicateToTypehintedArguments(
196             $parameters,
197             $candidates,
198             $arguments,
199             array($this, 'classMatchingPredicateForTypehintedArguments')
200         );
201
202         // This iteration maps up everything else, providing the argument is an instanceof the parameter.
203         $this->applyPredicateToTypehintedArguments(
204             $parameters,
205             $candidates,
206             $arguments,
207             array($this, 'isInstancePredicateForTypehintedArguments')
208         );
209
210         return $arguments;
211     }
212
213     /**
214      * Filtered out superfluous parameters for matching up typehinted arguments.
215      *
216      * @param  ReflectionParameter[] $parameters Constructor Arguments
217      * @return ReflectionParameter[]             Filtered $parameters
218      */
219     private function filterApplicableTypehintedParameters(array $parameters)
220     {
221         $filtered = array();
222
223         foreach ($parameters as $num => $parameter) {
224             if ($this->isArgumentDefined($num)) {
225                 continue;
226             }
227
228             $reflectionClass = $parameter->getClass();
229
230             if (!$reflectionClass) {
231                 continue;
232             }
233
234             $filtered[$num] = $parameter;
235         }
236
237         return $filtered;
238     }
239
240     /**
241      * Applies a predicate for each candidate when matching up typehinted arguments.
242      * This passes through to another loop of the candidates in @matchParameterToCandidateUsingPredicate,
243      * because this method is "too complex" with two loops...
244      *
245      * @param  ReflectionParameter[] $parameters Reflection Parameters (constructor argument requirements)
246      * @param  mixed[]               &$candidates Resolved arguments
247      * @param  mixed[]               &$arguments  Argument mapping
248      * @param  callable              $predicate   Callable predicate to apply to each candidate
249      * @return void
250      */
251     private function applyPredicateToTypehintedArguments(
252         array $parameters,
253         array &$candidates,
254         array &$arguments,
255         callable $predicate
256     ) {
257         $filtered = $this->filterApplicableTypehintedParameters($parameters);
258
259         foreach ($filtered as $num => $parameter) {
260             $this->matchParameterToCandidateUsingPredicate($parameter, $candidates, $arguments, $predicate);
261         }
262     }
263
264     /**
265      * Applies a predicate for each candidate when matching up typehinted arguments.
266      * This helps to avoid repetition when looping them, as multiple passes are needed over the parameters / candidates.
267      *
268      * @param  ReflectionParameter $parameter   Reflection Parameter (constructor argument requirements)
269      * @param  mixed[]             &$candidates Resolved arguments
270      * @param  mixed[]             &$arguments  Argument mapping
271      * @param  callable            $predicate   Callable predicate to apply to each candidate
272      * @return boolean Returns true if a candidate has been matched to the given parameter, otherwise false
273      */
274     public function matchParameterToCandidateUsingPredicate(
275         ReflectionParameter $parameter,
276         array &$candidates,
277         array &$arguments,
278         callable $predicate
279     ) {
280         foreach ($candidates as $candidateIndex => $candidate) {
281             if (call_user_func_array($predicate, array($parameter->getClass(), $candidate))) {
282                 $num = $parameter->getPosition();
283
284                 $arguments[$num] = $candidate;
285
286                 $this->markArgumentDefined($num);
287
288                 unset($candidates[$candidateIndex]);
289
290                 return true;
291             }
292         }
293
294         return false;
295     }
296
297     /**
298      * Typehinted argument predicate to check if the argument and parameter classes match equally.
299      *
300      * @param  ReflectionClass $reflectionClass Typehinted argument
301      * @param  mixed           $candidate       Resolved argument
302      * @return boolean
303      */
304     private function classMatchingPredicateForTypehintedArguments(ReflectionClass $reflectionClass, $candidate)
305     {
306         return $reflectionClass->getName() === get_class($candidate);
307     }
308
309     /**
310      * Typehinted argument predicate to check if the argument is an instance of the parameter.
311      *
312      * @param  ReflectionClass $reflectionClass Typehinted argument
313      * @param  mixed           $candidate       Resolved argument
314      * @return boolean
315      */
316     private function isInstancePredicateForTypehintedArguments(ReflectionClass $reflectionClass, $candidate)
317     {
318         return $reflectionClass->isInstance($candidate);
319     }
320
321     /**
322      * Captures argument values for undefined arguments based on their respective numbers.
323      *
324      * @param ReflectionParameter[] $parameters
325      * @param mixed[]               $numberedArguments
326      *
327      * @return mixed[]
328      */
329     private function prepareNumberedArguments(array $parameters, array $numberedArguments)
330     {
331         $arguments = array();
332
333         $increment = 0;
334         foreach ($parameters as $num => $parameter) {
335             if ($this->isArgumentDefined($num)) {
336                 continue;
337             }
338
339             if (array_key_exists($increment, $numberedArguments)) {
340                 $arguments[$num] = $numberedArguments[$increment++];
341                 $this->markArgumentDefined($num);
342             }
343         }
344
345         return $arguments;
346     }
347
348     /**
349      * Captures argument values for undefined arguments based on parameters defaults.
350      *
351      * @param ReflectionParameter[] $parameters
352      *
353      * @return mixed[]
354      */
355     private function prepareDefaultArguments(array $parameters)
356     {
357         $arguments = array();
358
359         foreach ($parameters as $num => $parameter) {
360             if ($this->isArgumentDefined($num)) {
361                 continue;
362             }
363
364             if ($parameter->isDefaultValueAvailable()) {
365                 $arguments[$num] = $parameter->getDefaultValue();
366                 $this->markArgumentDefined($num);
367             }
368         }
369
370         return $arguments;
371     }
372
373     /**
374      * Reorders arguments based on their respective parameters order.
375      *
376      * @param ReflectionParameter[] $parameters
377      * @param array                 $arguments
378      *
379      * @return mixed[]
380      */
381     private function reorderArguments(array $parameters, array $arguments)
382     {
383         $orderedArguments = array();
384
385         foreach ($parameters as $num => $parameter) {
386             $name = $parameter->getName();
387
388             if (array_key_exists($num, $arguments)) {
389                 $orderedArguments[$num] = $arguments[$num];
390             } elseif (array_key_exists($name, $arguments)) {
391                 $orderedArguments[$name] = $arguments[$name];
392             }
393         }
394
395         return $orderedArguments;
396     }
397
398     /**
399      * Marks arguments at all positions as undefined.
400      *
401      * This is used to share state between get*Arguments() methods.
402      */
403     private function markAllArgumentsUndefined()
404     {
405         $this->definedArguments = array();
406     }
407
408     /**
409      * Marks an argument at provided position as defined.
410      *
411      * @param integer $position
412      */
413     private function markArgumentDefined($position)
414     {
415         $this->definedArguments[$position] = true;
416     }
417
418     /**
419      * Checks if an argument at provided position is defined.
420      *
421      * @param integer $position
422      *
423      * @return Boolean
424      */
425     private function isArgumentDefined($position)
426     {
427         return isset($this->definedArguments[$position]);
428     }
429 }