<?php declare(strict_types=1); namespace Roave\BetterReflection\Reflection; use Closure; use Exception; use InvalidArgumentException; use LogicException; use OutOfBoundsException; use phpDocumentor\Reflection\Type; use PhpParser\Node; use PhpParser\Node\NullableType; use PhpParser\Node\Param as ParamNode; use PhpParser\Node\Stmt\Namespace_; use Roave\BetterReflection\NodeCompiler\CompileNodeToValue; use Roave\BetterReflection\NodeCompiler\CompilerContext; use Roave\BetterReflection\Reflection\Exception\Uncloneable; use Roave\BetterReflection\Reflection\StringCast\ReflectionParameterStringCast; use Roave\BetterReflection\Reflector\ClassReflector; use Roave\BetterReflection\Reflector\Reflector; use Roave\BetterReflection\TypesFinder\FindParameterType; use Roave\BetterReflection\Util\CalculateReflectionColum; use RuntimeException; use function assert; use function count; use function get_class; use function in_array; use function is_array; use function is_object; use function is_string; use function sprintf; use function strtolower; class ReflectionParameter { /** @var ParamNode */ private $node; /** @var Namespace_|null */ private $declaringNamespace; /** @var ReflectionFunctionAbstract */ private $function; /** @var int */ private $parameterIndex; /** @var scalar|array<scalar>|null */ private $defaultValue; /** @var bool */ private $isDefaultValueConstant = false; /** @var string|null */ private $defaultValueConstantName; /** @var Reflector */ private $reflector; private function __construct() { } /** * Create a reflection of a parameter using a class name * * @throws OutOfBoundsException */ public static function createFromClassNameAndMethod( string $className, string $methodName, string $parameterName ) : self { return ReflectionClass::createFromName($className) ->getMethod($methodName) ->getParameter($parameterName); } /** * Create a reflection of a parameter using an instance * * @param object $instance * * @throws OutOfBoundsException */ public static function createFromClassInstanceAndMethod( $instance, string $methodName, string $parameterName ) : self { return ReflectionClass::createFromInstance($instance) ->getMethod($methodName) ->getParameter($parameterName); } /** * Create a reflection of a parameter using a closure */ public static function createFromClosure(Closure $closure, string $parameterName) : ReflectionParameter { return ReflectionFunction::createFromClosure($closure) ->getParameter($parameterName); } /** * Create the parameter from the given spec. Possible $spec parameters are: * * - [$instance, 'method'] * - ['Foo', 'bar'] * - ['foo'] * - [function () {}] * * @param object[]|string[]|string|Closure $spec * * @throws Exception * @throws InvalidArgumentException */ public static function createFromSpec($spec, string $parameterName) : self { if (is_array($spec) && count($spec) === 2 && is_string($spec[1])) { if (is_object($spec[0])) { return self::createFromClassInstanceAndMethod($spec[0], $spec[1], $parameterName); } return self::createFromClassNameAndMethod($spec[0], $spec[1], $parameterName); } if (is_string($spec)) { return ReflectionFunction::createFromName($spec)->getParameter($parameterName); } if ($spec instanceof Closure) { return self::createFromClosure($spec, $parameterName); } throw new InvalidArgumentException('Could not create reflection from the spec given'); } public function __toString() : string { return ReflectionParameterStringCast::toString($this); } /** * @internal * * @param ParamNode $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver * @param Namespace_|null $declaringNamespace namespace of the declaring function/method */ public static function createFromNode( Reflector $reflector, ParamNode $node, ?Namespace_ $declaringNamespace, ReflectionFunctionAbstract $function, int $parameterIndex ) : self { $param = new self(); $param->reflector = $reflector; $param->node = $node; $param->declaringNamespace = $declaringNamespace; $param->function = $function; $param->parameterIndex = $parameterIndex; return $param; } private function parseDefaultValueNode() : void { if (! $this->isDefaultValueAvailable()) { throw new LogicException('This parameter does not have a default value available'); } $defaultValueNode = $this->node->default; if ($defaultValueNode instanceof Node\Expr\ClassConstFetch) { assert($defaultValueNode->class instanceof Node\Name); $className = $defaultValueNode->class->toString(); if ($className === 'self' || $className === 'static') { assert($defaultValueNode->name instanceof Node\Identifier); $constantName = $defaultValueNode->name->name; $className = $this->findParentClassDeclaringConstant($constantName); } $this->isDefaultValueConstant = true; assert($defaultValueNode->name instanceof Node\Identifier); $this->defaultValueConstantName = $className . '::' . $defaultValueNode->name->name; } if ($defaultValueNode instanceof Node\Expr\ConstFetch && ! in_array(strtolower($defaultValueNode->name->parts[0]), ['true', 'false', 'null'], true)) { $this->isDefaultValueConstant = true; $this->defaultValueConstantName = $defaultValueNode->name->parts[0]; $this->defaultValue = null; return; } $this->defaultValue = (new CompileNodeToValue())->__invoke( $defaultValueNode, new CompilerContext($this->reflector, $this->getDeclaringClass()) ); } /** * @throws LogicException */ private function findParentClassDeclaringConstant(string $constantName) : string { $method = $this->function; assert($method instanceof ReflectionMethod); $class = $method->getDeclaringClass(); do { if ($class->hasConstant($constantName)) { return $class->getName(); } $class = $class->getParentClass(); } while ($class); // note: this code is theoretically unreachable, so don't expect any coverage on it throw new LogicException(sprintf('Failed to find parent class of constant "%s".', $constantName)); } /** * Get the name of the parameter. */ public function getName() : string { assert(is_string($this->node->var->name)); return $this->node->var->name; } /** * Get the function (or method) that declared this parameter. */ public function getDeclaringFunction() : ReflectionFunctionAbstract { return $this->function; } /** * Get the class from the method that this parameter belongs to, if it * exists. * * This will return null if the declaring function is not a method. */ public function getDeclaringClass() : ?ReflectionClass { if ($this->function instanceof ReflectionMethod) { return $this->function->getDeclaringClass(); } return null; } /** * Is the parameter optional? * * Note this is distinct from "isDefaultValueAvailable" because you can have * a default value, but the parameter not be optional. In the example, the * $foo parameter isOptional() == false, but isDefaultValueAvailable == true * * @example someMethod($foo = 'foo', $bar) */ public function isOptional() : bool { return ((bool) $this->node->isOptional) || $this->isVariadic(); } /** * Does the parameter have a default, regardless of whether it is optional. * * Note this is distinct from "isOptional" because you can have * a default value, but the parameter not be optional. In the example, the * $foo parameter isOptional() == false, but isDefaultValueAvailable == true * * @example someMethod($foo = 'foo', $bar) */ public function isDefaultValueAvailable() : bool { return $this->node->default !== null; } /** * Get the default value of the parameter. * * @return scalar|array<scalar>|null * * @throws LogicException */ public function getDefaultValue() { $this->parseDefaultValueNode(); return $this->defaultValue; } /** * Does this method allow null for a parameter? */ public function allowsNull() : bool { if (! $this->hasType()) { return true; } if ($this->node->type instanceof NullableType) { return true; } if (! $this->isDefaultValueAvailable()) { return false; } return $this->getDefaultValue() === null; } /** * Get the DocBlock type hints as an array of strings. * * @return string[] */ public function getDocBlockTypeStrings() : array { $stringTypes = []; foreach ($this->getDocBlockTypes() as $type) { $stringTypes[] = (string) $type; } return $stringTypes; } /** * Get the types defined in the DocBlocks. This returns an array because * the parameter may have multiple (compound) types specified (for example * when you type hint pipe-separated "string|null", in which case this * would return an array of Type objects, one for string, one for null. * * @see getTypeHint() * * @return Type[] */ public function getDocBlockTypes() : array { return (new FindParameterType())->__invoke($this->function, $this->declaringNamespace, $this->node); } /** * Find the position of the parameter, left to right, starting at zero. */ public function getPosition() : int { return $this->parameterIndex; } /** * Get the ReflectionType instance representing the type declaration for * this parameter * * (note: this has nothing to do with DocBlocks). */ public function getType() : ?ReflectionType { $type = $this->node->type; if ($type === null) { return null; } if ($type instanceof NullableType) { $type = $type->type; } return ReflectionType::createFromTypeAndReflector((string) $type, $this->allowsNull(), $this->reflector); } /** * Does this parameter have a type declaration? * * (note: this has nothing to do with DocBlocks). */ public function hasType() : bool { return $this->node->type !== null; } /** * Set the parameter type declaration. */ public function setType(string $newParameterType) : void { $this->node->type = new Node\Name($newParameterType); } /** * Remove the parameter type declaration completely. */ public function removeType() : void { $this->node->type = null; } /** * Is this parameter an array? */ public function isArray() : bool { return strtolower((string) $this->getType()) === 'array'; } /** * Is this parameter a callable? */ public function isCallable() : bool { return strtolower((string) $this->getType()) === 'callable'; } /** * Is this parameter a variadic (denoted by ...$param). */ public function isVariadic() : bool { return $this->node->variadic; } /** * Is this parameter passed by reference (denoted by &$param). */ public function isPassedByReference() : bool { return $this->node->byRef; } public function canBePassedByValue() : bool { return ! $this->isPassedByReference(); } public function isDefaultValueConstant() : bool { $this->parseDefaultValueNode(); return $this->isDefaultValueConstant; } /** * @throws LogicException */ public function getDefaultValueConstantName() : string { $this->parseDefaultValueNode(); if (! $this->isDefaultValueConstant()) { throw new LogicException('This parameter is not a constant default value, so cannot have a constant name'); } return $this->defaultValueConstantName; } /** * Gets a ReflectionClass for the type hint (returns null if not a class) * * @throws RuntimeException */ public function getClass() : ?ReflectionClass { $className = $this->getClassName(); if ($className === null) { return null; } if (! $this->reflector instanceof ClassReflector) { throw new RuntimeException(sprintf( 'Unable to reflect class type because we were not given a "%s", but a "%s" instead', ClassReflector::class, get_class($this->reflector) )); } return $this->reflector->reflect($className); } private function getClassName() : ?string { if (! $this->hasType()) { return null; } $type = $this->getType(); assert($type instanceof ReflectionType); $typeHint = (string) $type; if ($typeHint === 'self') { $declaringClass = $this->getDeclaringClass(); assert($declaringClass instanceof ReflectionClass); return $declaringClass->getName(); } if ($typeHint === 'parent') { $declaringClass = $this->getDeclaringClass(); assert($declaringClass instanceof ReflectionClass); $parentClass = $declaringClass->getParentClass(); assert($parentClass instanceof ReflectionClass); return $parentClass->getName(); } if ($type->isBuiltin()) { return null; } return $typeHint; } /** * {@inheritdoc} * * @throws Uncloneable */ public function __clone() { throw Uncloneable::fromClass(self::class); } public function getStartColumn() : int { return CalculateReflectionColum::getStartColumn($this->function->getLocatedSource()->getSource(), $this->node); } public function getEndColumn() : int { return CalculateReflectionColum::getEndColumn($this->function->getLocatedSource()->getSource(), $this->node); } public function getAst() : ParamNode { return $this->node; } }