4 * This file is part of the Behat.
5 * (c) Konstantin Kudryashov <ever.zet@gmail.com>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
11 namespace Behat\Testwork\Argument;
13 use ReflectionFunctionAbstract;
15 use ReflectionParameter;
18 * Organises function arguments using its reflection.
20 * @author Konstantin Kudryashov <ever.zet@gmail.com>
22 final class MixedArgumentOrganiser implements ArgumentOrganiser
24 private $definedArguments = array();
27 * Organises arguments using function reflection.
29 * @param ReflectionFunctionAbstract $function
30 * @param mixed[] $arguments
34 public function organiseArguments(ReflectionFunctionAbstract $function, array $arguments)
36 return $this->prepareArguments($function->getParameters(), $arguments);
40 * Prepares arguments based on provided parameters.
42 * @param ReflectionParameter[] $parameters
43 * @param mixed[] $arguments
47 private function prepareArguments(array $parameters, array $arguments)
49 $this->markAllArgumentsUndefined();
51 list($named, $typehinted, $numbered) = $this->splitArguments($parameters, $arguments);
54 $this->prepareNamedArguments($parameters, $named) +
55 $this->prepareTypehintedArguments($parameters, $typehinted) +
56 $this->prepareNumberedArguments($parameters, $numbered) +
57 $this->prepareDefaultArguments($parameters);
59 return $this->reorderArguments($parameters, $arguments);
63 * Splits arguments into three separate arrays - named, numbered and typehinted.
65 * @param ReflectionParameter[] $parameters
66 * @param mixed[] $arguments
70 private function splitArguments(array $parameters, array $arguments)
72 $parameterNames = array_map(
73 function (ReflectionParameter $parameter) {
74 return $parameter->getName();
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;
88 $numberedArguments[] = $val;
92 return array($namedArguments, $typehintedArguments, $numberedArguments);
96 * Checks that provided argument key is a string and it matches some parameter name.
98 * @param mixed $argumentKey
99 * @param string[] $parameterNames
103 private function isStringKeyAndExistsInParameters($argumentKey, $parameterNames)
105 return is_string($argumentKey) && in_array($argumentKey, $parameterNames);
109 * Check if a given value is typehinted in the argument list.
111 * @param ReflectionParameter[] $parameters
112 * @param mixed $value
116 private function isParameterTypehintedInArgumentList(array $parameters, $value)
118 if (!is_object($value)) {
122 foreach ($parameters as $parameter) {
123 if ($this->isValueMatchesTypehintedParameter($value, $parameter)) {
132 * Checks if value matches typehint of provided parameter.
134 * @param object $value
135 * @param ReflectionParameter $parameter
139 private function isValueMatchesTypehintedParameter($value, ReflectionParameter $parameter)
141 $typehintRefl = $parameter->getClass();
143 return $typehintRefl && $typehintRefl->isInstance($value);
147 * Captures argument values based on their respective names.
149 * @param ReflectionParameter[] $parameters
150 * @param mixed[] $namedArguments
154 private function prepareNamedArguments(array $parameters, array $namedArguments)
156 $arguments = array();
158 foreach ($parameters as $num => $parameter) {
159 $name = $parameter->getName();
161 if (array_key_exists($name, $namedArguments)) {
162 $arguments[$name] = $namedArguments[$name];
163 $this->markArgumentDefined($num);
171 * Captures argument values for typehinted arguments based on the given candidates.
173 * This method attempts to match up the best fitting arguments to each constructor argument.
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).
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`).
182 * As such, this requires two passes of the $parameters array to ensure it is mapped as accurately as possible.
184 * @param ReflectionParameter[] $parameters Reflection Parameters (constructor argument requirements)
185 * @param mixed[] $typehintedArguments Resolved arguments
187 * @return mixed[] Ordered list of arguments, index is the constructor argument position, value is what will be injected
189 private function prepareTypehintedArguments(array $parameters, array $typehintedArguments)
191 $arguments = array();
193 $candidates = $typehintedArguments;
195 $this->applyPredicateToTypehintedArguments(
199 array($this, 'classMatchingPredicateForTypehintedArguments')
202 // This iteration maps up everything else, providing the argument is an instanceof the parameter.
203 $this->applyPredicateToTypehintedArguments(
207 array($this, 'isInstancePredicateForTypehintedArguments')
214 * Filtered out superfluous parameters for matching up typehinted arguments.
216 * @param ReflectionParameter[] $parameters Constructor Arguments
217 * @return ReflectionParameter[] Filtered $parameters
219 private function filterApplicableTypehintedParameters(array $parameters)
223 foreach ($parameters as $num => $parameter) {
224 if ($this->isArgumentDefined($num)) {
228 $reflectionClass = $parameter->getClass();
230 if (!$reflectionClass) {
234 $filtered[$num] = $parameter;
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...
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
251 private function applyPredicateToTypehintedArguments(
257 $filtered = $this->filterApplicableTypehintedParameters($parameters);
259 foreach ($filtered as $num => $parameter) {
260 $this->matchParameterToCandidateUsingPredicate($parameter, $candidates, $arguments, $predicate);
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.
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
274 public function matchParameterToCandidateUsingPredicate(
275 ReflectionParameter $parameter,
280 foreach ($candidates as $candidateIndex => $candidate) {
281 if (call_user_func_array($predicate, array($parameter->getClass(), $candidate))) {
282 $num = $parameter->getPosition();
284 $arguments[$num] = $candidate;
286 $this->markArgumentDefined($num);
288 unset($candidates[$candidateIndex]);
298 * Typehinted argument predicate to check if the argument and parameter classes match equally.
300 * @param ReflectionClass $reflectionClass Typehinted argument
301 * @param mixed $candidate Resolved argument
304 private function classMatchingPredicateForTypehintedArguments(ReflectionClass $reflectionClass, $candidate)
306 return $reflectionClass->getName() === get_class($candidate);
310 * Typehinted argument predicate to check if the argument is an instance of the parameter.
312 * @param ReflectionClass $reflectionClass Typehinted argument
313 * @param mixed $candidate Resolved argument
316 private function isInstancePredicateForTypehintedArguments(ReflectionClass $reflectionClass, $candidate)
318 return $reflectionClass->isInstance($candidate);
322 * Captures argument values for undefined arguments based on their respective numbers.
324 * @param ReflectionParameter[] $parameters
325 * @param mixed[] $numberedArguments
329 private function prepareNumberedArguments(array $parameters, array $numberedArguments)
331 $arguments = array();
334 foreach ($parameters as $num => $parameter) {
335 if ($this->isArgumentDefined($num)) {
339 if (array_key_exists($increment, $numberedArguments)) {
340 $arguments[$num] = $numberedArguments[$increment++];
341 $this->markArgumentDefined($num);
349 * Captures argument values for undefined arguments based on parameters defaults.
351 * @param ReflectionParameter[] $parameters
355 private function prepareDefaultArguments(array $parameters)
357 $arguments = array();
359 foreach ($parameters as $num => $parameter) {
360 if ($this->isArgumentDefined($num)) {
364 if ($parameter->isDefaultValueAvailable()) {
365 $arguments[$num] = $parameter->getDefaultValue();
366 $this->markArgumentDefined($num);
374 * Reorders arguments based on their respective parameters order.
376 * @param ReflectionParameter[] $parameters
377 * @param array $arguments
381 private function reorderArguments(array $parameters, array $arguments)
383 $orderedArguments = array();
385 foreach ($parameters as $num => $parameter) {
386 $name = $parameter->getName();
388 if (array_key_exists($num, $arguments)) {
389 $orderedArguments[$num] = $arguments[$num];
390 } elseif (array_key_exists($name, $arguments)) {
391 $orderedArguments[$name] = $arguments[$name];
395 return $orderedArguments;
399 * Marks arguments at all positions as undefined.
401 * This is used to share state between get*Arguments() methods.
403 private function markAllArgumentsUndefined()
405 $this->definedArguments = array();
409 * Marks an argument at provided position as defined.
411 * @param integer $position
413 private function markArgumentDefined($position)
415 $this->definedArguments[$position] = true;
419 * Checks if an argument at provided position is defined.
421 * @param integer $position
425 private function isArgumentDefined($position)
427 return isset($this->definedArguments[$position]);