| <?php declare(strict_types=1); |
|
|
| namespace PhpDocReader; |
|
|
| use PhpDocReader\PhpParser\UseStatementParser; |
| use ReflectionClass; |
| use ReflectionMethod; |
| use ReflectionParameter; |
| use ReflectionProperty; |
| use Reflector; |
|
|
| |
| |
| |
| class PhpDocReader |
| { |
| |
| private $parser; |
|
|
| private const PRIMITIVE_TYPES = [ |
| 'bool' => 'bool', |
| 'boolean' => 'bool', |
| 'string' => 'string', |
| 'int' => 'int', |
| 'integer' => 'int', |
| 'float' => 'float', |
| 'double' => 'float', |
| 'array' => 'array', |
| 'object' => 'object', |
| 'callable' => 'callable', |
| 'resource' => 'resource', |
| 'mixed' => 'mixed', |
| 'iterable' => 'iterable', |
| ]; |
|
|
| |
| private $ignorePhpDocErrors; |
|
|
| |
| |
| |
| public function __construct(bool $ignorePhpDocErrors = false) |
| { |
| $this->parser = new UseStatementParser; |
| $this->ignorePhpDocErrors = $ignorePhpDocErrors; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getPropertyType(ReflectionProperty $property): ?string |
| { |
| return $this->readPropertyType($property, true); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getPropertyClass(ReflectionProperty $property): ?string |
| { |
| return $this->readPropertyType($property, false); |
| } |
|
|
| private function readPropertyType(ReflectionProperty $property, bool $allowPrimitiveTypes): ?string |
| { |
| |
| $docComment = $property->getDocComment(); |
| if (! $docComment) { |
| return null; |
| } |
| if (preg_match('/@var\s+([^\s]+)/', $docComment, $matches)) { |
| [, $type] = $matches; |
| } else { |
| return null; |
| } |
|
|
| |
| if (isset(self::PRIMITIVE_TYPES[$type])) { |
| if ($allowPrimitiveTypes) { |
| return self::PRIMITIVE_TYPES[$type]; |
| } |
| return null; |
| } |
|
|
| |
| if (! preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) { |
| return null; |
| } |
|
|
| $class = $property->getDeclaringClass(); |
|
|
| |
| if ($type[0] !== '\\') { |
| |
| $resolvedType = $this->tryResolveFqn($type, $class, $property); |
|
|
| if (! $resolvedType && ! $this->ignorePhpDocErrors) { |
| throw new AnnotationException(sprintf( |
| 'The @var annotation on %s::%s contains a non existent class "%s". ' |
| . 'Did you maybe forget to add a "use" statement for this annotation?', |
| $class->name, |
| $property->getName(), |
| $type |
| )); |
| } |
|
|
| $type = $resolvedType; |
| } |
|
|
| if (! $this->ignorePhpDocErrors && ! $this->classExists($type)) { |
| throw new AnnotationException(sprintf( |
| 'The @var annotation on %s::%s contains a non existent class "%s"', |
| $class->name, |
| $property->getName(), |
| $type |
| )); |
| } |
|
|
| |
| $type = is_string($type) ? ltrim($type, '\\') : null; |
|
|
| return $type; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getParameterType(ReflectionParameter $parameter): ?string |
| { |
| return $this->readParameterClass($parameter, true); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getParameterClass(ReflectionParameter $parameter): ?string |
| { |
| return $this->readParameterClass($parameter, false); |
| } |
|
|
| private function readParameterClass(ReflectionParameter $parameter, bool $allowPrimitiveTypes): ?string |
| { |
| |
| $parameterType = $parameter->getType(); |
| if ($parameterType && $parameterType instanceof \ReflectionNamedType && ! $parameterType->isBuiltin()) { |
| return $parameterType->getName(); |
| } |
|
|
| $parameterName = $parameter->name; |
| |
| $method = $parameter->getDeclaringFunction(); |
| $docComment = $method->getDocComment(); |
| if (! $docComment) { |
| return null; |
| } |
| if (preg_match('/@param\s+([^\s]+)\s+\$' . $parameterName . '/', $docComment, $matches)) { |
| [, $type] = $matches; |
| } else { |
| return null; |
| } |
|
|
| |
| if (isset(self::PRIMITIVE_TYPES[$type])) { |
| if ($allowPrimitiveTypes) { |
| return self::PRIMITIVE_TYPES[$type]; |
| } |
| return null; |
| } |
|
|
| |
| if (! preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) { |
| return null; |
| } |
|
|
| $class = $parameter->getDeclaringClass(); |
|
|
| |
| if ($type[0] !== '\\') { |
| |
| $resolvedType = $this->tryResolveFqn($type, $class, $parameter); |
|
|
| if (! $resolvedType && ! $this->ignorePhpDocErrors) { |
| throw new AnnotationException(sprintf( |
| 'The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s". ' |
| . 'Did you maybe forget to add a "use" statement for this annotation?', |
| $parameterName, |
| $class->name, |
| $method->name, |
| $type |
| )); |
| } |
|
|
| $type = $resolvedType; |
| } |
|
|
| if (! $this->ignorePhpDocErrors && ! $this->classExists($type)) { |
| throw new AnnotationException(sprintf( |
| 'The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s"', |
| $parameterName, |
| $class->name, |
| $method->name, |
| $type |
| )); |
| } |
|
|
| |
| $type = is_string($type) ? ltrim($type, '\\') : null; |
|
|
| return $type; |
| } |
|
|
| |
| |
| |
| |
| |
| private function tryResolveFqn(string $type, ReflectionClass $class, Reflector $member): ?string |
| { |
| $alias = ($pos = strpos($type, '\\')) === false ? $type : substr($type, 0, $pos); |
| $loweredAlias = strtolower($alias); |
|
|
| |
| $uses = $this->parser->parseUseStatements($class); |
|
|
| if (isset($uses[$loweredAlias])) { |
| |
| if ($pos !== false) { |
| return $uses[$loweredAlias] . substr($type, $pos); |
| } |
| return $uses[$loweredAlias]; |
| } |
|
|
| if ($this->classExists($class->getNamespaceName() . '\\' . $type)) { |
| return $class->getNamespaceName() . '\\' . $type; |
| } |
|
|
| if (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) { |
| |
| return $uses['__NAMESPACE__'] . '\\' . $type; |
| } |
|
|
| if ($this->classExists($type)) { |
| |
| return $type; |
| } |
|
|
| |
| return $this->tryResolveFqnInTraits($type, $class, $member); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| private function tryResolveFqnInTraits(string $type, ReflectionClass $class, Reflector $member): ?string |
| { |
| |
| $traits = []; |
|
|
| |
| while ($class) { |
| $traits = array_merge($traits, $class->getTraits()); |
| $class = $class->getParentClass(); |
| } |
|
|
| foreach ($traits as $trait) { |
| |
| if ($member instanceof ReflectionProperty && ! $trait->hasProperty($member->name)) { |
| continue; |
| } |
| if ($member instanceof ReflectionMethod && ! $trait->hasMethod($member->name)) { |
| continue; |
| } |
| if ($member instanceof ReflectionParameter && ! $trait->hasMethod($member->getDeclaringFunction()->name)) { |
| continue; |
| } |
|
|
| |
| $resolvedType = $this->tryResolveFqn($type, $trait, $member); |
|
|
| if ($resolvedType) { |
| return $resolvedType; |
| } |
| } |
|
|
| return null; |
| } |
|
|
| private function classExists(string $class): bool |
| { |
| return class_exists($class) || interface_exists($class); |
| } |
| } |
|
|