Skip to content

Commit

Permalink
JSON-RPC changes: notification & batch support;
Browse files Browse the repository at this point in the history
bugfixes for short DTO class names (without namespace) in phpDoc
  • Loading branch information
cranetm committed Dec 9, 2014
1 parent 7fd02b6 commit 2e65be2
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 65 deletions.
114 changes: 70 additions & 44 deletions JsonRpc2/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Controller extends \yii\web\Controller
'return' => []
];

/** @var array Contains parsed JSON-RPC 2.0 request object*/
/** @var \stdClass Contains parsed JSON-RPC 2.0 request object*/
private $requestObject;

/** @var array Use as 'result' when Action returns null */
Expand All @@ -38,14 +38,43 @@ class Controller extends \yii\web\Controller
* @see createAction()
*/
public function runAction($id, $params = [])
{
$this->initRequest($id);

try {
$requestObject = Json::decode(file_get_contents('php://input'), false);
} catch (InvalidParamException $e) {
$requestObject = null;
}
$isBatch = is_array($requestObject);
$requests = $isBatch ? $requestObject : [$requestObject];
$resultData = null;
foreach ($requests as $request) {
if($response = $this->getActionResponse($request))
$resultData[] = $response;
}

$response = new Response();
$response->format = Response::FORMAT_JSON;
$response->data = $isBatch ? $resultData : current($resultData);
return $response;
}

/**
* Runs and returns method response
* @param $requestObject
* @throws \Exception
* @throws \yii\web\HttpException
* @return Response
*/
private function getActionResponse($requestObject)
{
$error = null;
$result = [];
try {
$this->initRequest($id);
$this->parseAndValidateRequestObject();
$this->parseAndValidateRequestObject($requestObject);
ob_start();
$dirtyResult = parent::runAction($this->requestObject['method']);
$dirtyResult = parent::runAction($this->requestObject->method);
ob_clean();
$result = $this->validateResult($dirtyResult);
} catch (HttpException $e) {
Expand All @@ -56,23 +85,22 @@ public function runAction($id, $params = [])
$error = new Exception("Internal error", Exception::INTERNAL_ERROR);
}

$response = new Response();
$response->format = Response::FORMAT_JSON;
if (!isset($this->requestObject['id']) && empty($error))
return $response;
$responseData = [];
if (!isset($this->requestObject->id) && (empty($error) || $error->getCode() != Exception::PARSE_ERROR))
return $responseData;

$response->data = [
$responseData = [
'jsonrpc' => '2.0',
'id' => !empty($this->requestObject['id'])? $this->requestObject['id'] : null,
'id' => !empty($this->requestObject->id)? $this->requestObject->id : null,
];

if (!empty($error))
$response->data['error'] = $error->toArray();
$responseData['error'] = $error->toArray();

if (!empty($result) || is_array($result))
$response->data['result'] = $result;
$responseData['result'] = $result;

return $response;
return $responseData;
}

/**
Expand Down Expand Up @@ -118,7 +146,7 @@ public function bindActionParams($action, $params)

$this->parseMethodDocComment($method);
$this->validateActionParams();
$params = $this->requestObject['params'];
$params = $this->requestObject->params;

$args = [];
$missing = [];
Expand All @@ -127,17 +155,17 @@ public function bindActionParams($action, $params)

foreach ($method->getParameters() as $param) {
$name = $param->getName();
if (array_key_exists($name, $params)) {
if (property_exists($params, $name)) {
if ($param->isArray() || isset($paramsTypes[$name]) && strpos($paramsTypes[$name], "[]") !== false) { //changes for array types documented as square brackets
$args[] = $actionParams[$name] = is_array($params[$name]) ? $params[$name] : [$params[$name]];
} elseif (!is_array($params[$name])) {
$args[] = $actionParams[$name] = $params[$name];
$args[] = $actionParams[$name] = is_array($params->$name) ? $params->$name : [$params->$name];
} elseif (!is_array($params->$name)) {
$args[] = $actionParams[$name] = $params->$name;
} else {
throw new BadRequestHttpException(Yii::t('yii', 'Invalid data received for parameter "{param}".', [
throw new Exception(Yii::t('yii', 'Invalid data received for parameter "{param}".', [
'param' => $name,
]));
]), Exception::INVALID_REQUEST);
}
unset($params[$name]);
unset($params->$name);
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $actionParams[$name] = $param->getDefaultValue();
} else {
Expand All @@ -146,16 +174,16 @@ public function bindActionParams($action, $params)
}

if (!empty($missing)) {
throw new BadRequestHttpException(Yii::t('yii', 'Missing required parameters: {params}', [
throw new Exception(Yii::t('yii', 'Missing required parameters: {params}', [
'params' => implode(', ', $missing),
]));
]), Exception::INVALID_REQUEST);
}

$this->actionParams = $actionParams;

return $args;
} catch (BadRequestHttpException $e) {
throw new Exception("Invalid Params", Exception::INVALID_PARAMS);
throw new Exception("Invalid Request", Exception::INVALID_REQUEST);
}
}

Expand All @@ -172,18 +200,15 @@ private function initRequest($id)

/**
* Try to decode input json data and validate for required fields for JSON-RPC 2.0
* @param $requestObject string
* @throws Exception
*/
private function parseAndValidateRequestObject()
private function parseAndValidateRequestObject($requestObject)
{
$input = file_get_contents('php://input');
try {
$requestObject = Json::decode($input, true);
} catch (InvalidParamException $e) {
if (!is_object($requestObject))
throw new Exception("Parse error", Exception::PARSE_ERROR);
}

if (!isset($requestObject['jsonrpc']) || $requestObject['jsonrpc'] !== '2.0' || empty($requestObject['method']))
if (!isset($requestObject->jsonrpc) || $requestObject->jsonrpc !== '2.0' || empty($requestObject->method))
throw new Exception("Invalid Request", Exception::INVALID_REQUEST);

$this->requestObject = $requestObject;
Expand All @@ -195,20 +220,19 @@ private function parseAndValidateRequestObject()
*/
private function prepareActionParams($action)
{
if (is_object($this->requestObject->params))
return;

$method = $this->getMethodFromAction($action);
$methodParams = [];
$methodParams = new \stdClass();

$i=0;
foreach ($method->getParameters() as $param) {
$methodParams[$param->getName()] = $param->getName();
}

if (count(array_intersect_key($methodParams, $this->requestObject['params'])) === 0) {
$additionalParamsNumber = count($methodParams)-count($this->requestObject['params']);
$this->requestObject['params'] = array_combine(
$methodParams,
$additionalParamsNumber ? array_merge($this->requestObject['params'], array_fill(0, $additionalParamsNumber, null)) : $this->requestObject['params']
);
if (!isset($this->requestObject->params[$i])) continue;
$methodParams->{$param->getName()} = $this->requestObject->params[$i];
$i++;
}
$this->requestObject->params = $methodParams;
}

/**
Expand All @@ -217,11 +241,12 @@ private function prepareActionParams($action)
*/
private function validateActionParams()
{
foreach ($this->requestObject['params'] as $name=>$value) {
foreach ($this->requestObject->params as $name=>$value) {
if (!isset($this->methodInfo['params'][$name])) continue;
$paramInfo = $this->methodInfo['params'][$name];

$this->requestObject['params'][$name] = Helper::bringValueToType(
$this->requestObject->params->$name = Helper::bringValueToType(
$this,
$paramInfo['type'],
$value,
$paramInfo['isNullable'],
Expand Down Expand Up @@ -266,6 +291,7 @@ private function validateResult($result)
{
if (!empty($this->methodInfo['return'])) {
$result = Helper::bringValueToType(
$this,
$this->methodInfo['return']['type'],
$result,
$this->methodInfo['return']['isNullable'],
Expand Down Expand Up @@ -323,7 +349,7 @@ private function parseMethodDocComment($method)
if (strpos($tagMatches[0], "@inArray") === 0 && in_array($subject['type'], ['string', 'int'])) {
eval("\$parsedData = {$tagMatches[2]};");
if (!is_array($parsedData))
throw new Exception("Invalid syntax in @inArray{$tagMatches[2]}", Exception::INTERNAL_ERROR);
throw new Exception(sprintf("Invalid syntax in %s in tag @inArray{$tagMatches[2]}", get_class($this)), Exception::INTERNAL_ERROR);
$subject['restrictions'] = $parsedData;
} elseif (strpos($tagMatches[0], "@null") === 0) {
$subject['isNullable'] = true;
Expand Down
11 changes: 5 additions & 6 deletions JsonRpc2/Dto.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
class Dto {
public function __construct($data)
{
$this->setDataFromArray($data);
$this->setDataFromArray((array)$data);
}

public function setDataFromArray($data)
protected function setDataFromArray($data)
{
foreach (get_object_vars($this) as $name=>$defaultValue) {
$property = new ReflectionProperty(get_class($this), $name);
if (!$property->isPublic()) continue;

preg_match("/@var ([\w\\\\]+)/", $property->getDocComment(), $matches);
preg_match("/@var[ ]+([\w\\\\]+)/", $property->getDocComment(), $matches);
$type = !empty($matches) ? $matches[1] : false;
if (empty($type)) continue;

Expand All @@ -28,11 +28,10 @@ public function setDataFromArray($data)
if (!empty($matches) && in_array($type, ['string', 'int'])) {
eval("\$parsedData = {$matches[2]};");
if (!is_array($parsedData))
throw new Exception(get_class($this).": Invalid syntax in @inArray{$matches[2]}", Exception::INTERNAL_ERROR);
throw new Exception(get_class($this).": Invalid syntax in {$name} tag @inArray{$matches[2]}", Exception::INTERNAL_ERROR);
$restrictions = $parsedData;
}

$this->$name = Helper::bringValueToType($type, isset($data[$name]) ? $data[$name] : $defaultValue, $isNullable, $restrictions);
$this->$name = Helper::bringValueToType($this, $type, isset($data[$name]) ? $data[$name] : $defaultValue, $isNullable, $restrictions);
}
}
}
46 changes: 31 additions & 15 deletions JsonRpc2/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,62 @@ class Helper {

/**
* Recursively brings value to type
* @param $parent
* @param $type
* @param $value
* @param bool $isNullable
* @param array $restrictions
* @throws Exception
* @return mixed
*/
public static function bringValueToType($type, $value, $isNullable = false, $restrictions = [])
public static function bringValueToType($parent, $type, $value, $isNullable = false, $restrictions = [])
{
if ($isNullable && null === $value)
if ($isNullable && null === $value || empty($type))
return $value;

$typeParts = explode("[]", $type);
$type = current($typeParts);
$singleType = current($typeParts);
if (count($typeParts) > 2)
throw new Exception("Type '$type' is invalid", Exception::INTERNAL_ERROR);
throw new Exception(sprintf("In %s type '$type' is invalid", get_class($parent)), Exception::INTERNAL_ERROR);

//for array type
if (count($typeParts) === 2) {
if (!is_array($value))
throw new Exception("Invalid Params", Exception::INVALID_PARAMS);
if (!is_array($value)) {
if ($parent instanceof \JsonRpc2\Dto)
throw new Exception(sprintf("In %s value has type %s, but array expected", get_class($parent), gettype($value)), Exception::INTERNAL_ERROR);
else
throw new Exception("Value has type %s, but array expected", gettype($value), Exception::INTERNAL_ERROR);
}

foreach ($value as $key=>$childValue) {
$value[$key] = self::bringValueToType($type, $childValue, $isNullable);
$value[$key] = self::bringValueToType($parent, $singleType, $childValue, $isNullable);
}
return $value;
}

$class = new \ReflectionClass($parent);
if (0 !== strpos($type, "\\") && class_exists($class->getNamespaceName()."\\".$type)) {
$type = $class->getNamespaceName()."\\".$type;
}
if (class_exists($type)) {
if (!is_subclass_of($type, '\\JsonRpc2\\Dto'))
throw new Exception("Class '$type' MUST be instance of '\\JsonRpc2\\Dto'", Exception::INTERNAL_ERROR);
throw new Exception(sprintf("In %s class '%s' MUST be instance of '\\JsonRpc2\\Dto'", get_class($parent), $type), Exception::INTERNAL_ERROR);
return new $type($value);
} else {
switch ($type) {
case "string":
$value = (string)$value;
self::restrictValue($type, $value, $restrictions);
self::restrictValue($parent, $type, $value, $restrictions);
return $value;
break;
case "int":
$value = (int)$value;
self::restrictValue($type, $value, $restrictions);
self::restrictValue($parent, $type, $value, $restrictions);
return $value;
break;
case "float":
$value = (float)$value;
self::restrictValue($type, $value, $restrictions);
self::restrictValue($parent, $type, $value, $restrictions);
return $value;
break;
case "array":
Expand All @@ -66,14 +76,20 @@ public static function bringValueToType($type, $value, $isNullable = false, $res
}

/**
* @param $parent
* @param $type
* @param $value
* @param $restrictions
* @throws Exception if value does not belong to restrictions
* @throws Exception
*/
private static function restrictValue($type, $value, $restrictions)
private static function restrictValue($parent, $type, $value, $restrictions)
{
if (!empty($restrictions) && !in_array($value, $restrictions))
throw new Exception(sprintf("$type value '$value' is not allowed. Allowed values is '%s'", implode("','", $restrictions)), Exception::INVALID_PARAMS);
if (!empty($restrictions) && !in_array($value, $restrictions)) {
$message = sprintf("$type value '$value' is not allowed. Allowed values is '%s'", implode("','", $restrictions));
if ($parent instanceof \JsonRpc2\Dto)
throw new Exception("In class ".get_class($parent). "" . $message, Exception::INTERNAL_ERROR);
else
throw new Exception($message, Exception::INVALID_PARAMS);
}
}
}

0 comments on commit 2e65be2

Please sign in to comment.