From 25da68e77f96e99955900192b5bff21782cc73d7 Mon Sep 17 00:00:00 2001 From: rojaro <> Date: Sat, 27 Mar 2010 23:07:16 +0000 Subject: added TRpcClient and TRpcServer - fixing #180 --- framework/Util/TRpcClient.php | 355 +++++++++++++++++++++++ framework/Web/Services/TRpcService.php | 513 +++++++++++++++++++++++++++++++++ 2 files changed, 868 insertions(+) create mode 100644 framework/Util/TRpcClient.php create mode 100644 framework/Web/Services/TRpcService.php (limited to 'framework') diff --git a/framework/Util/TRpcClient.php b/framework/Util/TRpcClient.php new file mode 100644 index 00000000..22af2d55 --- /dev/null +++ b/framework/Util/TRpcClient.php @@ -0,0 +1,355 @@ + + * @link http://www.pradosoft.com/ + * @copyright 2010 Bigpoint GmbH + * @license http://www.pradosoft.com/license/ + * @version $Id: TRpcClient.php 137 2010-03-27 22:13:36Z rrogge $ + * @since 3.2 + */ + +/** + * TRpcClient class + * + * Note: When using setIsNotification(true), *every* following request is also + * considered to be a notification until you use setIsNotification(false). + * + * Usage: + * + * First, you can use the factory: + *
+ * $_rpcClient = TRpcClient::create('xml', 'http://host/server');
+ * $_result = $_rpcClient->remoteMethodName($param, $otherParam);
+ * 
+ * + * or as oneliner: + *
+ * $_result = TRpcClient::create('json', 'http://host/server')->remoteMethod($param, ...);
+ * 
+ * + * Second, you can also use the specific implementation directly: + *
+ * $_rpcClient = new TXmlRpcClient('http://host/server');
+ * $_result = $_rpcClient->remoteMethod($param, ...);
+ * 
+ * + * or as oneliner: + *
+ * $_result = TXmlRpcClient('http://host/server')->hello();
+ * 
+ * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Util + * @since 3.2 + */ + +class TRpcClient extends TApplicationComponent +{ + /** + * @var string url of the RPC server + */ + private $_serverUrl; + + /** + * @var boolean whether the request is a notification and therefore should not care about the result (default: false) + */ + private $_isNotification = false; + + // magics + + /** + * @param string url to RPC server + * @param boolean whether requests are considered to be notifications (completely ignoring the response) (default: false) + */ + public function __construct($serverUrl, $isNotification = false) + { + $this->_serverUrl = $serverUrl; + $this->_isNotification = TPropertyValue::ensureBoolean($isNotification); + } + + // methods + + /** + * Creates an instance of the requested RPC client type + * @return TRpcClient instance + * @throws TApplicationException if an unsupported RPC client type was specified + */ + public static function create($type, $serverUrl, $isNotification = false) + { + if(($_handler = constant('TRpcClientTypesEnumerable::'.strtoupper($type))) === null) + throw new TApplicationException('rpcclient_unsupported_handler'); + + return new $_handler($serverUrl, $isNotification); + } + + /** + * Creates a stream context resource + * @param mixed $content + * @param string $contentType mime type + */ + protected function createStreamContext($content, $contentType) + { + return stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'header' => "Content-Type: {$contentType}", + 'content' => $content + ) + )); + } + + /** + * Performs the actual request + * @param string RPC server URL + * @param array payload data + * @param string request mime type + */ + protected function performRequest($serverUrl, $payload, $mimeType) + { + if(($_response = file_get_contents($serverUrl, false, $this->createStreamContext($payload, $mimeType))) === false) + throw new TRpcClientRequestException('RPC request failed'); + + return $_response; + } + + // getter/setter + + /** + * @return boolean whether requests are considered to be notifications (completely ignoring the response) + */ + public function getIsNotification() + { + return $this->_isNotification; + } + + /** + * @param string boolean whether the requests are considered to be notifications (completely ignoring the response) (default: false) + */ + public function setIsNotification($bool) + { + $this->_isNotification = TPropertyValue::ensureBoolean($bool); + } + + /** + * @return string url of the RPC server + */ + public function getServerUrl() + { + return $this->_serverUrl; + } + + /** + * @param string url of the RPC server + */ + public function setServerUrl($value) + { + $this->_serverUrl = $value; + } +} + +/** + * TRpcClientTypesEnumerable class + * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Util + * @since 3.2 + */ + +class TRpcClientTypesEnumerable extends TEnumerable +{ + const JSON = 'TJsonRpcClient'; + const XML = 'TXmlRpcClient'; +} + +/** + * TRpcClientRequestException class + * + * This Exception is fired if the RPC request fails because of transport problems e.g. when + * there is no RPC server responding on the given remote host. + * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Util + * @since 3.2 + */ + +class TRpcClientRequestException extends TApplicationException +{ +} + +/** + * TRpcClientResponseException class + * + * This Exception is fired when the + * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Util + * @since 3.2 + */ + +class TRpcClientResponseException extends TApplicationException +{ + /** + * @param string error message + * @param integer error code (optional) + */ + public function __construct($errorMessage, $errorCode = null) + { + $this->setErrorCode($errorCode); + + parent::__construct($errorMessage); + } +} + +/** + * TJsonRpcClient class + * + * Note: When using setIsNotification(true), *every* following request is also + * considered to be a notification until you use setIsNotification(false). + * + * Usage: + *
+ * $_rpcClient = new TJsonRpcClient('http://host/server');
+ * $_result = $_rpcClient->remoteMethod($param, $otherParam);
+ * // or
+ * $_result = TJsonRpcClient::create('http://host/server')->remoteMethod($param, $otherParam);
+ * 
+ * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Util + * @since 3.2 + */ + +class TJsonRpcClient extends TRpcClient +{ + // magics + + /** + * @param string RPC method name + * @param array RPC method parameters + * @return mixed RPC request result + * @throws TRpcClientRequestException if the client fails to connect to the server + * @throws TRpcClientResponseException if the response represents an RPC fault + */ + public function __call($method, $parameters) + { + // send request + $_response = $this->performRequest($this->getServerUrl(), $this->encodeRequest($method, $parameters), 'application/json'); + + // skip response handling if the request was just a notification request + if($this->isNotification) + return true; + + // decode response + $_response = json_decode($_response, true); + + // handle error response + if(!is_null($_response['error'])) + throw new TRpcClientResponseException($_response['error']); + + return $_response['result']; + } + + // methods + + /** + * @param string method name + * @param array method parameters + */ + public function encodeRequest($method, $parameters) + { + static $_requestId; + $_requestId = ($_requestId === null) ? 1 : $_requestId + 1; + + return json_encode(array( + 'method' => $method, + 'params' => $parameters, + 'id' => $this->isNotification ? null : $_requestId + )); + } + + /** + * Creates an instance of TJsonRpcClient + * @param string url of the rpc server + * @param boolean whether the requests are considered to be notifications (completely ignoring the response) (default: false) + */ + public static function create($serverUrl, $isNotification = false) + { + return new self($serverUrl, $isNotification); + } +} + +/** + * TXmlRpcClient class + * + * Note: When using setIsNotification(true), *every* following request is also + * considered to be a notification until you use setIsNotification(false). + * + * Usage: + *
+ * $_rpcClient = new TXmlRpcClient('http://remotehost/rpcserver');
+ * $_rpcClient->remoteMethod($param, $otherParam);
+ * 
+ * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Util + * @since 3.2 + */ + +class TXmlRpcClient extends TRpcClient +{ + // magics + + /** + * @param string RPC method name + * @param array RPC method parameters + * @return mixed RPC request result + * @throws TRpcClientRequestException if the client fails to connect to the server + * @throws TRpcClientResponseException if the response represents an RPC fault + */ + public function __call($method, $parameters) + { + // send request + $_response = $this->performRequest($this->getServerUrl(), $this->encodeRequest($method, $parameters), 'text/xml'); + + // skip response handling if the request was just a notification request + if($this->isNotification) + return true; + + // decode response + $_response = xmlrpc_decode($_response); + + // handle error response + if(xmlrpc_is_fault($_response)) + throw new TRpcClientResponseException($_response['faultString'], $_response['faultCode']); + + return $_response; + } + + // methods + + /** + * @param string method name + * @param array method parameters + */ + public function encodeRequest($method, $parameters) + { + return xmlrpc_encode_request($method, $parameters); + } + + /** + * Creates an instance of TXmlRpcClient + * @param string url of the rpc server + * @param boolean whether the requests are considered to be notifications (completely ignoring the response) (default: false) + */ + public static function create($serverUrl, $isNotification = false) + { + return new self($serverUrl, $isNotification); + } +} diff --git a/framework/Web/Services/TRpcService.php b/framework/Web/Services/TRpcService.php new file mode 100644 index 00000000..78fc927a --- /dev/null +++ b/framework/Web/Services/TRpcService.php @@ -0,0 +1,513 @@ + + * @link http://www.pradosoft.com/ + * @copyright 2010 Bigpoint GmbH + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @since 3.2 + */ + +/** + * TRpcService class + * + * Usage: + * + * + * + * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + **/ +class TRpcService extends TService +{ + /** + * @const string base api provider class which every API must extend + */ + const BASE_API_PROVIDER = 'TRpcApiProvider'; + + /** + * @const string base RPC server implementation + */ + const BASE_RPC_SERVER = 'TRpcServer'; + + /** + * @var array containing mimetype to protocol handler mappings + */ + protected $protocolHandlers = array( + 'application/json' => 'TJsonRpcProtocol', + 'text/xml' => 'TXmlRpcProtocol' + ); + + /** + * @var array containing API provider and their configured properties + */ + protected $apiProviders = array(); + + // methods + + /** + * Creates the API provider instance for the current request + * @param TRpcProtocol $protocolHandler instance + * @param string $providerId + */ + public function createApiProvider(TRpcProtocol $protocolHandler, $providerId) + { + $_properties = $this->apiProviders[$providerId]; + + if(($_providerClass = $_properties->remove('class')) === null) + throw new TConfigurationException('rpcservice_apiprovider_required'); + + prado::using($_providerClass); + + $_className = ($_pos = strrpos($_providerClass, '.')) !== false ? substr($_providerClass, $_pos + 1) : $_providerClass; + if(!is_subclass_of($_className, self::BASE_API_PROVIDER)) + throw new TConfigurationException('rpcservice_apiprovider_invalid'); + + if(($_rpcServerClass = $_properties->remove('server')) === null) + $_rpcServerClass = self::BASE_RPC_SERVER; + + $_apiProvider = new $_className(new $_rpcServerClass($protocolHandler)); + $_apiProvider->setId($providerId); + + foreach($_properties as $_key => $_value) + $_apiProvider->setSubProperty($_key, $_value); + + return $_apiProvider; + } + + /** + * Initializes the service + * @param TXmlElement $config containing the module configuration + */ + public function init($config) + { + $this->loadConfig($config); + } + + /** + * @param TXmlElement $xml configuration + */ + public function loadConfig(TXmlElement $xml) + { + foreach($xml->getElementsByTagName('rpcapi') as $_apiProviderXml) + { + $_properties = $_apiProviderXml->getAttributes(); + + if(($_id = $_properties->remove('id')) === null || $_id == "") + throw new TConfigurationException('rpcservice_apiproviderid_required'); + + if(isset($this->apiProviders[$_id])) + throw new TConfigurationException('rpcservice_apiproviderid_duplicated'); + + $this->apiProviders[$_id] = $_properties; + } + } + + /** + * Runs the service + */ + public function run() + { + $_request = $this->getRequest(); + + if(($_providerId = $_request->getServiceParameter()) == "") + throw new THttpException(400, 'RPC API-Provider id required'); + + if(($_method = $_request->getRequestType()) != 'POST') + throw new THttpException(405, 'Invalid request method "'.$_method.'"!'); // TODO Exception muss "Allow POST" Header setzen + + if(($_mimeType = $_request->getContentType()) === null) + throw new THttpException(406, 'Content-Type is missing!'); // TODO Exception muss gültige Content-Type werte zurück geben + + if(!in_array($_mimeType, array_keys($this->protocolHandlers))) + throw new THttpException(406, 'Unsupported Content-Type!'); // TODO see previous + + $_protocolHandlerClass = $this->protocolHandlers[$_mimeType]; + $_protocolHandler = new $_protocolHandlerClass; + + if(($_result = $this->createApiProvider($_protocolHandler, $_providerId)->processRequest()) !== null) + { + $_response = $this->getResponse(); + $_protocolHandler->createResponseHeaders($_response); + $_response->write($_result); + } + } +} + +/** + * TRpcServer class + * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + **/ +class TRpcServer extends TModule +{ + /** + * @var TRpcProtocol instance + */ + protected $handler; + + /** + * Constructor + * @param TRpcProtocol $protocolHandler instance + */ + public function __construct(TRpcProtocol $protocolHandler) + { + $this->handler = $protocolHandler; + } + + /** + * @param string $methodName + * @param array $methodDetails + */ + public function addRpcMethod($methodName, $methodDetails) + { + $this->handler->addMethod($methodName, $methodDetails); + } + + /** + * @return string request payload + */ + public function getPayload() + { + return file_get_contents('php://input'); + } + + /** + * @return string rpc response + */ + public function processRequest() + { + try + { + return $this->handler->callMethod($this->getPayload()); + } + catch(TRpcException $e) + { + return $this->handler->createErrorResponse($e); + } + } +} + +/** + * TRpcException class + * + * A TRpcException represents a RPC fault i.e. an error that is caused by the input data + * sent from the client. + * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + */ +class TRpcException extends TException +{ + public function __construct($message, $errorCode = -1) + { + $this->setErrorCode($errorCode); + + parent::__construct($message); + } +} + +/** + * TRpcApiProvider class + * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + */ +abstract class TRpcApiProvider extends TModule +{ + /** + * @var TRpcServer instance + */ + protected $rpcServer; + + // abstracts + + abstract public function registerMethods(); + + // methods + + public function __construct(TRpcServer $rpcServer) + { + $this->rpcServer = $rpcServer; + + foreach($this->registerMethods() as $_methodName => $_methodDetails) + $this->rpcServer->addRpcMethod($_methodName, $_methodDetails); + } + + public function processRequest() + { + return $this->rpcServer->processRequest(); + } + + // getter/setter + + public function getRpcServer() + { + return $this->rpcServer; + } +} + +/** + * TRpcProtocol class + * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + **/ +abstract class TRpcProtocol +{ + /** + * @var array containis the mapping from RPC method names to the actual handlers + */ + protected $rpcMethods = array(); + + // abstracts + + abstract public function callMethod($requestPayload); + abstract public function createErrorResponse($exception); + abstract public function createResponseHeaders($response); + abstract public function encode($data); + abstract public function decode($data); + + // methods + + /** + * Registers a new RPC method and handler details + * @param string $methodName + * @param array $handlerDetails containing the callback handler + */ + public function addMethod($methodName, $handlerDetails) + { + $this->rpcMethods[$methodName] = $handlerDetails; + } + + /** + * Calls the callback handler for the given method + * @param string $methodName of the RPC + * @param array $parameters for the callback handler as provided by the client + * @return mixed whatever the callback handler returns + */ + public function callApiMethod($methodName, $parameters) + { + if(!isset($this->rpcMethods[$methodName])) + throw new TRpcException('Method "'.$methodName.'" not found'); + + return call_user_func_array($this->rpcMethods[$methodName]['method'], $parameters); + } +} + +/** + * TJsonRpcProtocol class + * + * Implements the JSON RPC protocol + * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + */ +class TJsonRpcProtocol extends TRpcProtocol +{ + // methods + + /** + * Handles the RPC request + * @param string $requestPayload + * @return string JSON RPC response + */ + public function callMethod($requestPayload) + { + $_request = $this->decode($requestPayload); + + try + { + return $this->encode(array( + 'result' => $this->callApiMethod($_request['method'], $_request['params']), + 'error' => null + )); + } + catch(TRpcException $e) + { + return $this->createErrorResponse($e); + } + catch(Exception $e) + { + prado::log(); + + return $this->createErrorResponse(new TRpcException('An internal error occured')); + } + } + + /** + * Turns the given exception into an JSON RPC fault + * @param TRpcException $exception + * @return string JSON RPC fault + */ + public function createErrorResponse(TRpcException $exception) + { + return $this->encode(array( + 'faultCode' => $exception->getCode(), + 'faultString' => $exception->getMessage() + )); + } + + /** + * Sets the correct response headers + * @param THttpResponse $response + */ + public function createResponseHeaders($response) + { + $response->setContentType('application/json'); + $response->setCharset('UTF-8'); + } + + /** + * Decodes JSON encoded data into PHP data + * @param string $data in JSON format + * @return array PHP data + */ + public function decode($data) + { + return json_decode($data, true); + } + + /** + * Encodes PHP data into JSON data + * @param mixed PHP data + * @return string JSON encoded PHP data + */ + public function encode($data) + { + return json_encode($data); + } +} + +/** + * TXmlRpcProtocol class + * + * Implements the XML RPC protocol + * + * @author Robin J. Rogge + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + */ +class TXmlRpcProtocol extends TRpcProtocol +{ + /** + * @var XML RPC server resource + */ + private $_xmlrpcServer; + + // magics + + /** + * Constructor + */ + public function __construct() + { + $this->_xmlrpcServer = xmlrpc_server_create(); + } + + /** + * Destructor + */ + public function __destruct() + { + xmlrpc_server_destroy($this->_xmlrpcServer); + } + + // methods + + /** + * Registers a new RPC method and handler details + * @param string $methodName + * @param array $handlerDetails containing the callback handler + */ + public function addMethod($methodName, $methodDetails) + { + parent::addMethod($methodName, $methodDetails); + + xmlrpc_server_register_method($this->_xmlrpcServer, $methodName, array($this, 'callApiMethod')); + } + + // methods + + /** + * Handles the RPC request + * @param string $requestPayload + * @return string XML RPC response + */ + public function callMethod($requestPayload) + { + try + { + return xmlrpc_server_call_method($this->_xmlrpcServer, $requestPayload, null); + } + catch(TRpcException $e) + { + return $this->createErrorResponse($e); + } + catch(Exception $e) + { + prado::log(); + + return $this->createErrorResponse(new TRpcException('An internal error occured')); + } + } + + /** + * Turns the given exception into an XML RPC fault + * @param TRpcException $exception + * @return string XML RPC fault + */ + public function createErrorResponse(TRpcException $exception) + { + return $this->encode(array( + 'faultCode' => $exception->getCode(), + 'faultString' => $exception->getMessage() + )); + } + + /** + * Sets the correct response headers + * @param THttpResponse $response + */ + public function createResponseHeaders($response) + { + $response->setContentType('text/xml'); + $response->setCharset('UTF-8'); + } + + /** + * Decodes XML encoded data into PHP data + * @param string $data in XML format + * @return array PHP data + */ + public function decode($data) + { + return xmlrpc_decode($data); + } + + /** + * Encodes PHP data into XML data + * @param mixed PHP data + * @return string XML encoded PHP data + */ + public function encode($data) + { + return xmlrpc_encode($data); + } +} -- cgit v1.2.3