diff --git a/composer.json b/composer.json index 1c154df..b82274d 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,8 @@ "require": { "php": "^7.1 || ^8.0", "netresearch/jsonmapper": "^1.0 || ^2.0", - "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + "phpdocumentor/reflection": "^4.0" + }, "require-dev": { "phpunit/phpunit": "^7.0 || ^8.0" diff --git a/lib/Dispatcher.php b/lib/Dispatcher.php index 5f045df..cc73367 100644 --- a/lib/Dispatcher.php +++ b/lib/Dispatcher.php @@ -3,13 +3,9 @@ namespace AdvancedJsonRpc; +use AdvancedJsonRpc\Reflection; use JsonMapper; use JsonMapper_Exception; -use phpDocumentor\Reflection\DocBlockFactory; -use phpDocumentor\Reflection\Types; -use ReflectionException; -use ReflectionMethod; -use ReflectionNamedType; class Dispatcher { @@ -24,21 +20,14 @@ class Dispatcher private $delimiter; /** - * method => ReflectionMethod[] - * - * @var ReflectionMethod - */ - private $methods; - - /** - * @var \phpDocumentor\Reflection\DocBlockFactory + * @var Reflection\ReflectionInterface */ - private $docBlockFactory; + private $reflection; /** - * @var \phpDocumentor\Reflection\Types\ContextFactory + * @var JsonMapper */ - private $contextFactory; + private $mapper; /** * @param object $target The target object that should receive the method calls @@ -48,9 +37,8 @@ public function __construct($target, $delimiter = '->') { $this->target = $target; $this->delimiter = $delimiter; - $this->docBlockFactory = DocBlockFactory::createInstance(); - $this->contextFactory = new Types\ContextFactory(); $this->mapper = new JsonMapper(); + $this->reflection = new Reflection\NativeReflection(); } /** @@ -81,33 +69,19 @@ public function dispatch($msg) } $obj = $obj->$part; } - if (!isset($this->methods[$msg->method])) { - try { - $method = new ReflectionMethod($obj, $fn); - $this->methods[$msg->method] = $method; - } catch (ReflectionException $e) { - throw new Error($e->getMessage(), ErrorCode::METHOD_NOT_FOUND, null, $e); - } - } - $method = $this->methods[$msg->method]; - $parameters = $method->getParameters(); - if ($method->getDocComment()) { - $docBlock = $this->docBlockFactory->create( - $method->getDocComment(), - $this->contextFactory->createFromReflector($method->getDeclaringClass()) - ); - $paramTags = $docBlock->getTagsByName('param'); - } + + $method = $this->reflection->getMethodDetails($msg->method, $obj, $fn); + $args = []; if (isset($msg->params)) { // Find out the position if (is_array($msg->params)) { $args = $msg->params; } else if (is_object($msg->params)) { - foreach ($parameters as $pos => $parameter) { + foreach ($method->getParameters() as $pos => $parameter) { $value = null; foreach(get_object_vars($msg->params) as $key => $val) { - if ($parameter->name === $key) { + if ($parameter->getName() === $key) { $value = $val; break; } @@ -117,46 +91,25 @@ public function dispatch($msg) } else { throw new Error('Params must be structured or omitted', ErrorCode::INVALID_REQUEST); } + foreach ($args as $position => $value) { try { // If the type is structured (array or object), map it with JsonMapper if (is_object($value)) { // Does the parameter have a type hint? - $param = $parameters[$position]; + $param = $method->getParameter($position); if ($param->hasType()) { - $paramType = $param->getType(); - if ($paramType instanceof ReflectionNamedType) { - // We have object data to map and want the class name. - // This should not include the `?` if the type was nullable. - $class = $paramType->getName(); - } else { - // Fallback for php 7.0, which is still supported (and doesn't have nullable). - $class = (string)$paramType; - } + $class = $param->getType()->getName(); $value = $this->mapper->map($value, new $class()); } - } else if (is_array($value) && isset($docBlock)) { - // Get the array type from the DocBlock - $type = $paramTags[$position]->getType(); - // For union types, use the first one that is a class array (often it is SomeClass[]|null) - if ($type instanceof Types\Compound) { - for ($i = 0; $t = $type->get($i); $i++) { - if ( - $t instanceof Types\Array_ - && $t->getValueType() instanceof Types\Object_ - && (string)$t->getValueType() !== 'object' - ) { - $class = (string)$t->getValueType()->getFqsen(); - $value = $this->mapper->mapArray($value, [], $class); - break; - } - } - } else if ($type instanceof Types\Array_) { - $class = (string)$type->getValueType()->getFqsen(); - $value = $this->mapper->mapArray($value, [], $class); - } else { - throw new Error('Type is not matching @param tag', ErrorCode::INVALID_PARAMS); + } else if (is_array($value)) { + if (!$method->hasParameter($position)) { + throw new Error('Type information is missing', ErrorCode::INVALID_PARAMS); } + + // Get the array type from the DocBlock + $class = $method->getParameter($position)->getType()->getName(); + $value = $this->mapper->mapArray($value, [], $class); } } catch (JsonMapper_Exception $e) { throw new Error($e->getMessage(), ErrorCode::INVALID_PARAMS, null, $e); diff --git a/lib/Reflection/Dto/Method.php b/lib/Reflection/Dto/Method.php new file mode 100644 index 0000000..0fc3c1d --- /dev/null +++ b/lib/Reflection/Dto/Method.php @@ -0,0 +1,31 @@ +parameters = $parameters; + } + + public function getParameters(): array + { + return $this->parameters; + } + + public function hasParameter(int $name): bool + { + return array_key_exists($name, $this->parameters); + } + + public function getParameter(int $name): Parameter + { + return $this->parameters[$name]; + } +} diff --git a/lib/Reflection/Dto/Parameter.php b/lib/Reflection/Dto/Parameter.php new file mode 100644 index 0000000..6754a3b --- /dev/null +++ b/lib/Reflection/Dto/Parameter.php @@ -0,0 +1,35 @@ +name = $name; + $this->type = $type; + } + + public function getName(): string + { + return $this->name; + } + + public function getType(): Type + { + return $this->type; + } + + public function hasType(): bool + { + return $this->type !== null; + } +} diff --git a/lib/Reflection/Dto/Type.php b/lib/Reflection/Dto/Type.php new file mode 100644 index 0000000..11d0134 --- /dev/null +++ b/lib/Reflection/Dto/Type.php @@ -0,0 +1,21 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/lib/Reflection/NativeReflection.php b/lib/Reflection/NativeReflection.php new file mode 100644 index 0000000..dc7281e --- /dev/null +++ b/lib/Reflection/NativeReflection.php @@ -0,0 +1,117 @@ +docBlockFactory = DocBlockFactory::createInstance(); + $this->contextFactory = new Types\ContextFactory(); + } + + public function getMethodDetails($rpcMethod, $target, $nativeMethod): Method + { + if (array_key_exists($rpcMethod, $this->methods)) { + return $this->methods[$rpcMethod]; + } + + try { + $nativeMethod = new ReflectionMethod($target, $nativeMethod); + } catch (ReflectionException $e) { + throw new Error($e->getMessage(), ErrorCode::METHOD_NOT_FOUND, null, $e); + } + + $parameters = array_map(function($p) { return $this->mapNativeReflectionParameterToParameter($p); }, $nativeMethod->getParameters()); + + if ($nativeMethod->getDocComment()) { + $docBlock = $this->docBlockFactory->create( + $nativeMethod->getDocComment(), + $this->contextFactory->createFromReflector($nativeMethod->getDeclaringClass()) + ); + + /* Improve types from the doc block */ + if ($docBlock !== null) { + $docBlockParameters = []; + + foreach ($docBlock->getTagsByName('param') as $param) { + $docBlockParameters[$param->getVariableName()] = $this->mapDocBlockTagToParameter($param); + } + + foreach ($parameters as $position => $param) { + if (array_key_exists($param->getName(), $docBlockParameters) && $docBlockParameters[$param->getName()]->hasType()) + { + $parameters[$position] = $docBlockParameters[$param->getName()]; + } + } + } + } + + $method = new Method($parameters); + + $this->methods[$rpcMethod] = $method; + + return $method; + } + + private function mapNativeReflectionParameterToParameter(\ReflectionParameter $native): Parameter + { + $type = $this->mapNativeReflectionTypeToType($native->getType()); + return new Parameter($native->getName(), $type); + } + + private function mapDocBlockTagToParameter(Tag $tag): Parameter + { + $type = $tag->getType(); + // For union types, use the first one that is a class array (often it is SomeClass[]|null) + if ($type instanceof Types\Compound) { + for ($i = 0; $t = $type->get($i); $i++) { + if ( + $t instanceof Types\Array_ + && $t->getValueType() instanceof Types\Object_ + && (string)$t->getValueType() !== 'object' + ) { + return new Parameter($tag->getVariableName(), new Type((string)$t->getValueType()->getFqsen())); + } + } + } else if ($type instanceof Types\Array_) { + return new Parameter($tag->getVariableName(), new Type((string)$type->getValueType()->getFqsen())); + } + } + + /** @return Type|null */ + private function mapNativeReflectionTypeToType(\ReflectionType $native = null) + { + if ($native instanceof ReflectionNamedType && $native->getName() !== '') { + // We have object data to map and want the class name. + // This should not include the `?` if the type was nullable. + return new Type($native->getName()); + } + if ((string) $native !== '') { + // Fallback for php 7.0, which is still supported (and doesn't have nullable). + return new Type((string) $native); + } + } +} diff --git a/lib/Reflection/PhpDocumentorReflection.php b/lib/Reflection/PhpDocumentorReflection.php new file mode 100644 index 0000000..67d6c30 --- /dev/null +++ b/lib/Reflection/PhpDocumentorReflection.php @@ -0,0 +1,92 @@ +projectFactory = ProjectFactory::createInstance(); + } + + public function getMethodDetails($rpcMethod, $target, $nativeMethodName): Method + { + $nativeMethod = new ReflectionMethod($target, $nativeMethodName); + $projectFiles = [new LocalFile($nativeMethod->getFileName())]; + /** @var Project $project */ + $project = $this->projectFactory->create('php-advanced-json-rpc', $projectFiles); + + /** @var \phpDocumentor\Reflection\Php\Class_ $class */ + $class = $project->getFiles()[$nativeMethod->getFileName()]->getClasses()['\\' . $nativeMethod->class]; /* @todo add error handling of multiple classes in single file */ + $methodName = '\\' . $nativeMethod->class . '::' . $nativeMethod->getName() . '()'; + $method = $class->getMethods()[$methodName]; /* @todo add error handling for missing method */ + + $parameters = array_map(function ($a) { return $this->mapPhpDocumentorReflectionParameterToParameter($a); }, $method->getArguments()); + + /* Improve types from the doc block */ + $docBlock = $method->getDocBlock(); + if ($docBlock !== null) { + $docBlockParameters = []; + + foreach ($method->getDocBlock()->getTagsByName('param') as $param) { + $docBlockParameters[$param->getVariableName()] = $this->mapDocBlockTagToParameter($param); + } + + foreach ($parameters as $position => $param) { + if (array_key_exists($param->getName(), $docBlockParameters) && $docBlockParameters[$param->getName()]->hasType()) + { + $parameters[$position] = $docBlockParameters[$param->getName()]; + } + } + } + + return new Method($parameters); + } + + private function mapPhpDocumentorReflectionParameterToParameter(Argument $argument): Parameter + { + $phpDocumentorType = $argument->getType(); + if ($phpDocumentorType === null) { + return new Parameter($argument->getName()); + } + + return new Parameter($argument->getName(), new Type((string) $phpDocumentorType)); + } + + private function mapDocBlockTagToParameter(Tag $tag): Parameter + { + $type = $tag->getType(); + // For union types, use the first one that is a class array (often it is SomeClass[]|null) + if ($type instanceof Types\Compound) { + for ($i = 0; $t = $type->get($i); $i++) { + if ( + $t instanceof Types\Array_ + && $t->getValueType() instanceof Types\Object_ + && (string)$t->getValueType() !== 'object' + ) { + return new Parameter($tag->getVariableName(), new Type((string)$t->getValueType()->getFqsen())); + } + } + } else if ($type instanceof Types\Array_) { + return new Parameter($tag->getVariableName(), new Type((string)$type->getValueType()->getFqsen())); + } + } +} diff --git a/lib/Reflection/ReflectionInterface.php b/lib/Reflection/ReflectionInterface.php new file mode 100644 index 0000000..541d19f --- /dev/null +++ b/lib/Reflection/ReflectionInterface.php @@ -0,0 +1,12 @@ +