+++ /dev/null
-<?php
-
-/*
- * This file is part of the Behat.
- * (c) Konstantin Kudryashov <ever.zet@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Behat\Testwork\Argument;
-
-use ReflectionFunctionAbstract;
-use ReflectionClass;
-use ReflectionParameter;
-
-/**
- * Organises function arguments using its reflection.
- *
- * @author Konstantin Kudryashov <ever.zet@gmail.com>
- */
-final class MixedArgumentOrganiser implements ArgumentOrganiser
-{
- private $definedArguments = array();
-
- /**
- * Organises arguments using function reflection.
- *
- * @param ReflectionFunctionAbstract $function
- * @param mixed[] $arguments
- *
- * @return mixed[]
- */
- public function organiseArguments(ReflectionFunctionAbstract $function, array $arguments)
- {
- return $this->prepareArguments($function->getParameters(), $arguments);
- }
-
- /**
- * Prepares arguments based on provided parameters.
- *
- * @param ReflectionParameter[] $parameters
- * @param mixed[] $arguments
- *
- * @return mixed[]
- */
- private function prepareArguments(array $parameters, array $arguments)
- {
- $this->markAllArgumentsUndefined();
-
- list($named, $typehinted, $numbered) = $this->splitArguments($parameters, $arguments);
-
- $arguments =
- $this->prepareNamedArguments($parameters, $named) +
- $this->prepareTypehintedArguments($parameters, $typehinted) +
- $this->prepareNumberedArguments($parameters, $numbered) +
- $this->prepareDefaultArguments($parameters);
-
- return $this->reorderArguments($parameters, $arguments);
- }
-
- /**
- * Splits arguments into three separate arrays - named, numbered and typehinted.
- *
- * @param ReflectionParameter[] $parameters
- * @param mixed[] $arguments
- *
- * @return array
- */
- private function splitArguments(array $parameters, array $arguments)
- {
- $parameterNames = array_map(
- function (ReflectionParameter $parameter) {
- return $parameter->getName();
- },
- $parameters
- );
-
- $namedArguments = array();
- $numberedArguments = array();
- $typehintedArguments = array();
- foreach ($arguments as $key => $val) {
- if ($this->isStringKeyAndExistsInParameters($key, $parameterNames)) {
- $namedArguments[$key] = $val;
- } elseif ($this->isParameterTypehintedInArgumentList($parameters, $val)) {
- $typehintedArguments[] = $val;
- } else {
- $numberedArguments[] = $val;
- }
- }
-
- return array($namedArguments, $typehintedArguments, $numberedArguments);
- }
-
- /**
- * Checks that provided argument key is a string and it matches some parameter name.
- *
- * @param mixed $argumentKey
- * @param string[] $parameterNames
- *
- * @return Boolean
- */
- private function isStringKeyAndExistsInParameters($argumentKey, $parameterNames)
- {
- return is_string($argumentKey) && in_array($argumentKey, $parameterNames);
- }
-
- /**
- * Check if a given value is typehinted in the argument list.
- *
- * @param ReflectionParameter[] $parameters
- * @param mixed $value
- *
- * @return Boolean
- */
- private function isParameterTypehintedInArgumentList(array $parameters, $value)
- {
- if (!is_object($value)) {
- return false;
- }
-
- foreach ($parameters as $parameter) {
- if ($this->isValueMatchesTypehintedParameter($value, $parameter)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Checks if value matches typehint of provided parameter.
- *
- * @param object $value
- * @param ReflectionParameter $parameter
- *
- * @return Boolean
- */
- private function isValueMatchesTypehintedParameter($value, ReflectionParameter $parameter)
- {
- $typehintRefl = $parameter->getClass();
-
- return $typehintRefl && $typehintRefl->isInstance($value);
- }
-
- /**
- * Captures argument values based on their respective names.
- *
- * @param ReflectionParameter[] $parameters
- * @param mixed[] $namedArguments
- *
- * @return mixed[]
- */
- private function prepareNamedArguments(array $parameters, array $namedArguments)
- {
- $arguments = array();
-
- foreach ($parameters as $num => $parameter) {
- $name = $parameter->getName();
-
- if (array_key_exists($name, $namedArguments)) {
- $arguments[$name] = $namedArguments[$name];
- $this->markArgumentDefined($num);
- }
- }
-
- return $arguments;
- }
-
- /**
- * Captures argument values for typehinted arguments based on the given candidates.
- *
- * This method attempts to match up the best fitting arguments to each constructor argument.
- *
- * This case specifically fixes the issue where a constructor asks for a parent and child class,
- * as separate arguments, but both arguments could satisfy the first argument,
- * so they would both be passed in (overwriting each other).
- *
- * This will ensure that the children (exact class matches) are mapped first, and then other dependencies
- * are mapped sequentially (to arguments which they are an `instanceof`).
- *
- * As such, this requires two passes of the $parameters array to ensure it is mapped as accurately as possible.
- *
- * @param ReflectionParameter[] $parameters Reflection Parameters (constructor argument requirements)
- * @param mixed[] $typehintedArguments Resolved arguments
- *
- * @return mixed[] Ordered list of arguments, index is the constructor argument position, value is what will be injected
- */
- private function prepareTypehintedArguments(array $parameters, array $typehintedArguments)
- {
- $arguments = array();
-
- $candidates = $typehintedArguments;
-
- $this->applyPredicateToTypehintedArguments(
- $parameters,
- $candidates,
- $arguments,
- array($this, 'classMatchingPredicateForTypehintedArguments')
- );
-
- // This iteration maps up everything else, providing the argument is an instanceof the parameter.
- $this->applyPredicateToTypehintedArguments(
- $parameters,
- $candidates,
- $arguments,
- array($this, 'isInstancePredicateForTypehintedArguments')
- );
-
- return $arguments;
- }
-
- /**
- * Filtered out superfluous parameters for matching up typehinted arguments.
- *
- * @param ReflectionParameter[] $parameters Constructor Arguments
- * @return ReflectionParameter[] Filtered $parameters
- */
- private function filterApplicableTypehintedParameters(array $parameters)
- {
- $filtered = array();
-
- foreach ($parameters as $num => $parameter) {
- if ($this->isArgumentDefined($num)) {
- continue;
- }
-
- $reflectionClass = $parameter->getClass();
-
- if (!$reflectionClass) {
- continue;
- }
-
- $filtered[$num] = $parameter;
- }
-
- return $filtered;
- }
-
- /**
- * Applies a predicate for each candidate when matching up typehinted arguments.
- * This passes through to another loop of the candidates in @matchParameterToCandidateUsingPredicate,
- * because this method is "too complex" with two loops...
- *
- * @param ReflectionParameter[] $parameters Reflection Parameters (constructor argument requirements)
- * @param mixed[] &$candidates Resolved arguments
- * @param mixed[] &$arguments Argument mapping
- * @param callable $predicate Callable predicate to apply to each candidate
- * @return void
- */
- private function applyPredicateToTypehintedArguments(
- array $parameters,
- array &$candidates,
- array &$arguments,
- callable $predicate
- ) {
- $filtered = $this->filterApplicableTypehintedParameters($parameters);
-
- foreach ($filtered as $num => $parameter) {
- $this->matchParameterToCandidateUsingPredicate($parameter, $candidates, $arguments, $predicate);
- }
- }
-
- /**
- * Applies a predicate for each candidate when matching up typehinted arguments.
- * This helps to avoid repetition when looping them, as multiple passes are needed over the parameters / candidates.
- *
- * @param ReflectionParameter $parameter Reflection Parameter (constructor argument requirements)
- * @param mixed[] &$candidates Resolved arguments
- * @param mixed[] &$arguments Argument mapping
- * @param callable $predicate Callable predicate to apply to each candidate
- * @return boolean Returns true if a candidate has been matched to the given parameter, otherwise false
- */
- public function matchParameterToCandidateUsingPredicate(
- ReflectionParameter $parameter,
- array &$candidates,
- array &$arguments,
- callable $predicate
- ) {
- foreach ($candidates as $candidateIndex => $candidate) {
- if (call_user_func_array($predicate, array($parameter->getClass(), $candidate))) {
- $num = $parameter->getPosition();
-
- $arguments[$num] = $candidate;
-
- $this->markArgumentDefined($num);
-
- unset($candidates[$candidateIndex]);
-
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Typehinted argument predicate to check if the argument and parameter classes match equally.
- *
- * @param ReflectionClass $reflectionClass Typehinted argument
- * @param mixed $candidate Resolved argument
- * @return boolean
- */
- private function classMatchingPredicateForTypehintedArguments(ReflectionClass $reflectionClass, $candidate)
- {
- return $reflectionClass->getName() === get_class($candidate);
- }
-
- /**
- * Typehinted argument predicate to check if the argument is an instance of the parameter.
- *
- * @param ReflectionClass $reflectionClass Typehinted argument
- * @param mixed $candidate Resolved argument
- * @return boolean
- */
- private function isInstancePredicateForTypehintedArguments(ReflectionClass $reflectionClass, $candidate)
- {
- return $reflectionClass->isInstance($candidate);
- }
-
- /**
- * Captures argument values for undefined arguments based on their respective numbers.
- *
- * @param ReflectionParameter[] $parameters
- * @param mixed[] $numberedArguments
- *
- * @return mixed[]
- */
- private function prepareNumberedArguments(array $parameters, array $numberedArguments)
- {
- $arguments = array();
-
- $increment = 0;
- foreach ($parameters as $num => $parameter) {
- if ($this->isArgumentDefined($num)) {
- continue;
- }
-
- if (array_key_exists($increment, $numberedArguments)) {
- $arguments[$num] = $numberedArguments[$increment++];
- $this->markArgumentDefined($num);
- }
- }
-
- return $arguments;
- }
-
- /**
- * Captures argument values for undefined arguments based on parameters defaults.
- *
- * @param ReflectionParameter[] $parameters
- *
- * @return mixed[]
- */
- private function prepareDefaultArguments(array $parameters)
- {
- $arguments = array();
-
- foreach ($parameters as $num => $parameter) {
- if ($this->isArgumentDefined($num)) {
- continue;
- }
-
- if ($parameter->isDefaultValueAvailable()) {
- $arguments[$num] = $parameter->getDefaultValue();
- $this->markArgumentDefined($num);
- }
- }
-
- return $arguments;
- }
-
- /**
- * Reorders arguments based on their respective parameters order.
- *
- * @param ReflectionParameter[] $parameters
- * @param array $arguments
- *
- * @return mixed[]
- */
- private function reorderArguments(array $parameters, array $arguments)
- {
- $orderedArguments = array();
-
- foreach ($parameters as $num => $parameter) {
- $name = $parameter->getName();
-
- if (array_key_exists($num, $arguments)) {
- $orderedArguments[$num] = $arguments[$num];
- } elseif (array_key_exists($name, $arguments)) {
- $orderedArguments[$name] = $arguments[$name];
- }
- }
-
- return $orderedArguments;
- }
-
- /**
- * Marks arguments at all positions as undefined.
- *
- * This is used to share state between get*Arguments() methods.
- */
- private function markAllArgumentsUndefined()
- {
- $this->definedArguments = array();
- }
-
- /**
- * Marks an argument at provided position as defined.
- *
- * @param integer $position
- */
- private function markArgumentDefined($position)
- {
- $this->definedArguments[$position] = true;
- }
-
- /**
- * Checks if an argument at provided position is defined.
- *
- * @param integer $position
- *
- * @return Boolean
- */
- private function isArgumentDefined($position)
- {
- return isset($this->definedArguments[$position]);
- }
-}