Created
November 10, 2011 12:57
-
-
Save bastman/1354794 to your computer and use it in GitHub Desktop.
advanced json-rpc server (php)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
JS Client using jquery ajax and json2.js | |
======================================== | |
App.invokeRpc = function(method, params, callback) | |
{ | |
var rpc = { | |
method:method, | |
params:params | |
}; | |
$.ajax({ | |
url:'rpc.php', | |
processData:false, | |
data:JSON.stringify(rpc), | |
type:'POST', | |
success:function(response) | |
{ | |
var resp = response; | |
try | |
{ | |
if(typeof(response) == 'string') | |
{ | |
resp = JSON.parse(response); | |
} | |
if(typeof(resp) != 'object') | |
{ | |
throw new Error("Invalid rpc response"); | |
} | |
} | |
catch(e) | |
{ | |
if(typeof(callback) == 'function') | |
{ | |
callback({ | |
result:null, | |
error:{ | |
message:'Invalid rpc response!' | |
} | |
}); | |
} | |
return; | |
} | |
if(typeof(callback) == 'function') | |
{ | |
callback(resp); | |
} | |
}, | |
error:function() | |
{ | |
callback({ | |
result:null, | |
error:{ | |
message:'XHR ERROR' | |
} | |
}); | |
} | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
the dispatcher | |
=============== | |
<?php | |
class Dispatcher | |
{ | |
/* | |
- advanced jsonrpc server (php): https://gist.github.com/gists/1354794 | |
- a simple jsonrpc server (php): https://gist.github.com/1344759 | |
- rpc client (js): https://gist.github.com/gists/1354794 | |
- rpc client (as3): https://gist.github.com/1371010 | |
EXAMPLE | |
======= | |
var rpc = { | |
"method":"MyService.foobar", | |
"params":["foo","bar","baz"] | |
} | |
*/ | |
const ERROR_DISPATCHER_FAILED = "RPC DISPATCHER FAILED"; | |
// ++++++++++++++++++++ config +++++++++++++++++++++++++++++ | |
/** | |
* register your service enpoints here: | |
* | |
* e.g. User.Events -> App_Rpc_Service_User_Events | |
* | |
* means | |
* rpc.method: User.Events.foo() | |
* php: App_Rpc_Service_User_Events::foo() | |
* @var array | |
*/ | |
protected $_services = array( | |
array( | |
"endpoint" => "User.Events", | |
"class" => "App_Rpc_Service_User_Events", | |
/* | |
"methods" => array( | |
"allow" => array( | |
"*" | |
), | |
"deny" => array( | |
"*myPrivateMethod" | |
), | |
), | |
*/ | |
), | |
array( | |
"endpoint" => "Events.Artists", | |
"class" => "App_Rpc_Service_Events_Artists", | |
/* | |
"methods" => array( | |
"allow" => array( | |
"*" | |
), | |
"deny" => array( | |
"*myPrivateMethod" | |
), | |
), | |
*/ | |
), | |
); | |
/** | |
* @var array | |
*/ | |
protected $_responseHeaders = array( | |
"Content-Type: application/json; charset=utf-8", | |
); | |
// ++++++++++++++++++ run ++++++++++++++++++++++++++++++++ | |
/** | |
* @throws Exception | |
* @return void | |
*/ | |
public function run() | |
{ | |
$this->_init(); | |
$this->_run(); | |
} | |
/** | |
* @param $className | |
* @return void | |
*/ | |
public function phpAutoLoader($className) | |
{ | |
$classPath = dirname(__FILE__); | |
$filename = str_replace( | |
"_", | |
"/", | |
$className | |
).".php"; | |
$location = $classPath."/".$filename; | |
require_once($location); | |
if(!class_exists($className)) { | |
throw new Exception("classloader failed"); | |
} | |
} | |
/** | |
* for php<5.3 | |
* @throws ErrorException | |
* @param $errno | |
* @param $errstr | |
* @param $errfile | |
* @param $errline | |
* @return void | |
*/ | |
public function phpErrorHandler($errno, $errstr, $errfile, $errline) | |
{ | |
// convert php notice to exception | |
throw new ErrorException($errstr, 0, $errno, $errfile, $errline); | |
} | |
/** | |
* @return Hardcoded shutdown handler to detect critical PHP errors. | |
*/ | |
public function phpShutdownHandler() | |
{ | |
$error = error_get_last(); | |
if ($error === null) { | |
// no error, we have a "normal" shut down (script is finished). | |
return; | |
} | |
$responseData = array( | |
"error" => array( | |
"class" => str_replace("_", ".", get_class($this)), | |
"message" => "rpc shutdown error" | |
), | |
"result" => null, | |
); | |
echo json_encode($responseData); | |
} | |
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
/** | |
* @throws ErrorException | |
* @return void | |
*/ | |
protected function _init() | |
{ | |
/* | |
RECOMMENDED SETUP | |
================= | |
*/ | |
ini_set("display_errors", true); | |
error_reporting(E_ALL|E_STRICT & ~E_NOTICE); | |
// turn on error exceptions | |
// php 5.3+ | |
/* | |
set_error_handler(function($errno, $errstr, $errfile, $errline){ | |
throw new ErrorException($errstr, 0, $errno, $errfile, $errline); | |
}); | |
*/ | |
// php < 5.3 | |
set_error_handler(array($this, "phpErrorHandler")); | |
// try catch fatal errors | |
register_shutdown_function(array($this, 'phpShutdownHandler')); | |
spl_autoload_register(array($this, 'phpAutoLoader')); | |
} | |
/** | |
* @param ReflectionClass $reflectionClass | |
* @param ReflectionMethod $reflectionMethod | |
* @param object $serviceInstance | |
* @param array $params | |
* @return void | |
*/ | |
protected function _onRpcBeforeInvoke( | |
ReflectionClass $reflectionClass, | |
ReflectionMethod $reflectionMethod, | |
$serviceInstance, | |
array $params | |
) | |
{ | |
// your hooks here | |
} | |
/** | |
* @param Exception $exception | |
* @return void | |
*/ | |
protected function _onRpcError(Exception $exception) | |
{ | |
// your hooks here | |
// you may want to log sth? | |
// you may want to sanitize error data before deliver to client? | |
$error = array( | |
"class" => str_replace("_", ".", get_class($exception)), | |
"message" => $exception->getMessage(), | |
); | |
$responseData = array( | |
"result" => null, | |
"error" => $error, | |
); | |
$this->_sendResponse($responseData); | |
} | |
/** | |
* @param mixed $result | |
* @return void | |
*/ | |
protected function _onRpcResult($result) | |
{ | |
// your hooks here | |
$responseData = array( | |
"result" => $result, | |
"error" => null | |
); | |
$this->_sendResponse($responseData); | |
} | |
/** | |
* @param Exception $e | |
* @return | |
*/ | |
protected function _onDispatcherError(Exception $e) | |
{ | |
// YOU HAVE FUCKING FATAL ERROR IN YOUR HANDLERS! | |
// FIX IT MONKEY! | |
$responseData = array( | |
"error" => array( | |
"class" => str_replace("_", ".", get_class($e)), | |
"message" => self::ERROR_DISPATCHER_FAILED, | |
), | |
"result" => null, | |
); | |
echo json_encode($responseData); | |
return; | |
} | |
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
protected function _run() | |
{ | |
$serviceResult = null; | |
$serviceError = null; | |
try { | |
$requestText = "" . $this->_fetchRequestText(); | |
$rpc = json_decode($requestText, true); | |
if (!is_array($rpc)) { | |
throw new Exception("Invalid rpc request"); | |
} | |
$rpcMethod = null; | |
if (array_key_exists("method", $rpc)) { | |
$rpcMethod = $rpc["method"]; | |
} | |
$rpcParams = $rpc["params"]; | |
if (array_key_exists("params", $rpc)) { | |
$rpcParams = $rpc["params"]; | |
if ($rpcParams === null) { | |
$rpcParams = array(); | |
} | |
} | |
if (!is_string($rpcMethod)) { | |
throw new Exception("Invalid rpc method"); | |
} | |
if (!is_array($rpcParams)) { | |
throw new Exception("Invalid rpc params"); | |
} | |
$rpcMethodParts = (array)explode(".", $rpcMethod); | |
$rpcMethodName = "".array_pop($rpcMethodParts); | |
$rpcMethodClass = implode(".", (array)$rpcMethodParts); | |
// locate class by convention? | |
// $rpcMethodClass = str_replace(".", "_", "".$rpcMethodClass); | |
$rpcMethodName = "".strtolower( | |
trim("".$rpcMethodName) | |
); | |
$rpcMethodClass = "".strtolower( | |
trim("".$rpcMethodClass) | |
); | |
$serviceClassName = null; | |
$serviceClassInfo = null; | |
$serviceMethodName = $rpcMethodName; | |
$servicesAvailable = (array)$this->_services; | |
foreach($servicesAvailable as $serviceInfo) { | |
$serviceEndpoint = "".strtolower( | |
trim("".$serviceInfo["endpoint"]) | |
); | |
if(strlen($serviceEndpoint)<1) { | |
throw new Exception("Invalid rpc server config"); | |
} | |
if($serviceEndpoint === $rpcMethodClass) { | |
$serviceClassName = "".strtolower( | |
trim("".$serviceInfo["class"]) | |
); | |
if(strlen($serviceClassName)<1) { | |
throw new Exception("Invalid rpc server config"); | |
} | |
$serviceClassInfo = $serviceInfo; | |
break; | |
} | |
} | |
try { | |
if(!class_exists($serviceClassName)) { | |
throw new Exception("Invalid rpc service class"); | |
} | |
$reflectionClass = new ReflectionClass($serviceClassName); | |
} catch (Exception $e) { | |
throw new Exception("Invalid rpc service class"); | |
} | |
if (!$reflectionClass->hasMethod($serviceMethodName)) { | |
throw new Exception("rpc method does not exist!"); | |
} | |
$reflectionMethod = $reflectionClass->getMethod( | |
$serviceMethodName | |
); | |
$this->_validateReflectionMethod($reflectionMethod); | |
$this->_validateServiceMethodName( | |
(array)$serviceClassInfo, $reflectionMethod | |
); | |
$serviceResult = $this->_invokeService( | |
$reflectionClass, | |
$reflectionMethod, | |
$serviceClassName, | |
(array)$rpcParams | |
); | |
} catch (Exception $e) { | |
$serviceError = $e; | |
} | |
try { | |
if ($serviceError instanceof Exception) { | |
$this->_onRpcError($serviceError); | |
} else { | |
$this->_onRpcResult($serviceResult); | |
} | |
} catch (Exception $e) { | |
// YOU HAVE FUCKING FATAL ERROR IN YOUR HANDLERS! FIX IT MONKEY! | |
$this->_onDispatcherError($e); | |
return; | |
} | |
return; | |
} | |
/** | |
* @param ReflectionClass $reflectionClass | |
* @param ReflectionMethod $reflectionMethod | |
* @param $serviceClassName | |
* @param array $params | |
* @return mixed | |
*/ | |
protected function _invokeService( | |
ReflectionClass $reflectionClass, | |
ReflectionMethod $reflectionMethod, | |
$serviceClassName, | |
array $params | |
) | |
{ | |
$service = new $serviceClassName(); | |
$this->_onRpcBeforeInvoke( | |
$reflectionClass, | |
$reflectionMethod, | |
$service, | |
(array)$params | |
); | |
$serviceResult = $reflectionMethod->invokeArgs( | |
$service, (array)$params | |
); | |
return $serviceResult; | |
} | |
/** | |
* @return string | |
*/ | |
protected function _fetchRequestText() | |
{ | |
$requestText = file_get_contents('php://input'); | |
return $requestText; | |
} | |
/** | |
* @param array $responseData | |
* @return void | |
*/ | |
protected function _sendResponse($responseData) | |
{ | |
if(!is_array($responseData)) { | |
$responseData = array( | |
"result" => null, | |
"error" => array( | |
"class" => str_replace("_", ".", get_class($this)), | |
"message" => "invalid rpc response data" | |
), | |
); | |
} | |
// json encode response | |
$responseText = null; | |
try { | |
$responseText = json_encode($responseData); | |
} catch(Exception $e) { | |
//NOP | |
} | |
if(!is_string($responseText)) { | |
$responseData = array( | |
"result" => "null", | |
"error" => array( | |
"class" => str_replace("_", ".", get_class($this)), | |
"message" => "rpc encode response failed", | |
), | |
); | |
$responseText = json_encode($responseData); | |
} | |
// send response headers | |
$this->_sendResponseHeaders(); | |
// send response text | |
echo "".$responseText; | |
} | |
/** | |
* @return void | |
*/ | |
protected function _sendResponseHeaders() | |
{ | |
// send response headers | |
$headers = $this->_responseHeaders; | |
if(!is_array($headers)) { | |
$headers = array(); | |
} | |
foreach($headers as $header) { | |
if (is_string($header)) { | |
try { | |
header($header); | |
} catch(Exception $e) { | |
//NOP | |
// ignore warnings "headers already sent" | |
} | |
} | |
} | |
} | |
/** | |
* @throws Exception | |
* @param ReflectionMethod $reflectionMethod | |
* @return void | |
*/ | |
protected function _validateReflectionMethod( | |
ReflectionMethod $reflectionMethod | |
) | |
{ | |
$reflectionMethodName = $reflectionMethod->getName(); | |
if ($reflectionMethodName[0] === "_") { | |
throw new Exception("rpc method is not invokable!"); | |
} | |
if (!$reflectionMethod->isPublic()) { | |
throw new Exception("rpc method is not invokable!"); | |
} | |
if ($reflectionMethod->isStatic()) { | |
throw new Exception("rpc method is not invokable!"); | |
} | |
if ($reflectionMethod->isAbstract()) { | |
throw new Exception("rpc method is not invokable!"); | |
} | |
if ($reflectionMethod->isInternal()) { | |
throw new Exception("rpc method is not invokable!"); | |
} | |
if(method_exists($reflectionMethod, "isConstructor")) { | |
if ($reflectionMethod->isConstructor()) { | |
throw new Exception("rpc method is not invokable!"); | |
} | |
} | |
if(method_exists($reflectionMethod, "isDestructor")) { | |
if ($reflectionMethod->isDestructor()) { | |
throw new Exception("rpc method is not invokable!"); | |
} | |
} | |
if(method_exists($reflectionMethod, "isClosure")) { | |
if ($reflectionMethod->isClosure()) { | |
throw new Exception("rpc method is not invokable!"); | |
} | |
} | |
if(method_exists($reflectionMethod, "isDeprecated")) { | |
if ($reflectionMethod->isDeprecated()) { | |
throw new Exception("rpc method is not invokable!"); | |
} | |
} | |
} | |
/** | |
* @throws Exception | |
* @param array $serviceInfo | |
* @param ReflectionMethod $reflectionMethod | |
* @return null | |
*/ | |
protected function _validateServiceMethodName( | |
array $serviceInfo, ReflectionMethod $reflectionMethod | |
) { | |
$result = null; | |
$methodName = $reflectionMethod->getName(); | |
if(!array_key_exists("methods", $serviceInfo)) { | |
return $result; | |
} | |
$methodsWhiteListed = null; | |
$methodsBlackListed = null; | |
if(array_key_exists("allow", $serviceInfo["methods"])) { | |
$methodsWhiteListed = $serviceInfo["methods"]["allow"]; | |
} | |
if(array_key_exists("deny", $serviceInfo["methods"])) { | |
$methodsBlackListed = $serviceInfo["methods"]["deny"]; | |
} | |
if($methodsWhiteListed===null) { | |
$methodsWhiteListed = array("*"); | |
} | |
if($methodsBlackListed===null) { | |
$methodsBlackListed = array(); | |
} | |
if(!is_array($methodsWhiteListed)) { | |
throw new Exception("Invalid rpc serviceConfig.methods.allow"); | |
} | |
if(!is_array($methodsBlackListed)) { | |
throw new Exception("Invalid rpc serviceConfig.methods.deny"); | |
} | |
$isWhiteListed = false; | |
foreach($methodsWhiteListed as $pattern) { | |
$isMatched = fnmatch($pattern, $methodName, FNM_CASEFOLD); | |
if($isMatched) { | |
$isWhiteListed = true; | |
break; | |
} | |
} | |
$isBlackListed = false; | |
foreach($methodsBlackListed as $pattern) { | |
$isMatched = fnmatch($pattern, $methodName, FNM_CASEFOLD); | |
if($isMatched) { | |
$isBlackListed = true; | |
break; | |
} | |
} | |
$isAllowed = ( | |
($isWhiteListed) | |
&& (!$isBlackListed) | |
); | |
if(!$isAllowed) { | |
throw new Exception("rpc method not allowed"); | |
} | |
return $result; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment