Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Switch to php documentor reflection #44

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
89 changes: 21 additions & 68 deletions lib/Dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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
Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down
31 changes: 31 additions & 0 deletions lib/Reflection/Dto/Method.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace AdvancedJsonRpc\Reflection\Dto;

class Method
{
/** @var Parameter[] */
private $parameters;

public function __construct(array $parameters)
{
$this->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];
}
}
35 changes: 35 additions & 0 deletions lib/Reflection/Dto/Parameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace AdvancedJsonRpc\Reflection\Dto;


class Parameter
{
/** @var string */
private $name;
/** @var Type|null */
private $type;

public function __construct(string $name, Type $type = null)
{
$this->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;
}
}
21 changes: 21 additions & 0 deletions lib/Reflection/Dto/Type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace AdvancedJsonRpc\Reflection\Dto;

class Type
{
/** @var string */
private $name;

public function __construct(string $name)
{
$this->name = $name;
}

public function getName(): string
{
return $this->name;
}
}
117 changes: 117 additions & 0 deletions lib/Reflection/NativeReflection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

namespace AdvancedJsonRpc\Reflection;


use AdvancedJsonRpc\Error;
use AdvancedJsonRpc\ErrorCode;
use AdvancedJsonRpc\Reflection\Dto\Method;
use AdvancedJsonRpc\Reflection\Dto\Parameter;
use AdvancedJsonRpc\Reflection\Dto\Type;
use phpDocumentor\Reflection\DocBlock\Tag;
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\Types;
use ReflectionException;
use ReflectionMethod;
use ReflectionNamedType;

class NativeReflection implements ReflectionInterface
{
/** @var DocBlockFactory */
private $docBlockFactory;
/** @var \phpDocumentor\Reflection\Types\ContextFactory */
private $contextFactory;
/** @var Method[] */
private $methods = [];

public function __construct()
{
$this->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);
}
}
}
Loading