summaryrefslogtreecommitdiff
path: root/framework
diff options
context:
space:
mode:
authorrojaro <>2010-03-27 23:07:16 +0000
committerrojaro <>2010-03-27 23:07:16 +0000
commit25da68e77f96e99955900192b5bff21782cc73d7 (patch)
tree78c43816dbe7054049c4a54cd4191ffb6207cca1 /framework
parent28b3fab134ef34a215ded1ad06a440a5b4db5610 (diff)
added TRpcClient and TRpcServer - fixing #180
Diffstat (limited to 'framework')
-rw-r--r--framework/Util/TRpcClient.php355
-rw-r--r--framework/Web/Services/TRpcService.php513
2 files changed, 868 insertions, 0 deletions
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 @@
+<?php
+
+/**
+ * @author Robin J. Rogge <rrogge@bigpoint.net>
+ * @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:
+ * <pre>
+ * $_rpcClient = TRpcClient::create('xml', 'http://host/server');
+ * $_result = $_rpcClient->remoteMethodName($param, $otherParam);
+ * </pre>
+ *
+ * or as oneliner:
+ * <pre>
+ * $_result = TRpcClient::create('json', 'http://host/server')->remoteMethod($param, ...);
+ * </pre>
+ *
+ * Second, you can also use the specific implementation directly:
+ * <pre>
+ * $_rpcClient = new TXmlRpcClient('http://host/server');
+ * $_result = $_rpcClient->remoteMethod($param, ...);
+ * </pre>
+ *
+ * or as oneliner:
+ * <pre>
+ * $_result = TXmlRpcClient('http://host/server')->hello();
+ * </pre>
+ *
+ * @author Robin J. Rogge <rrogge@bigpoint.net>
+ * @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 <rrogge@bigpoint.net>
+ * @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 <rrogge@bigpoint.net>
+ * @version $Id$
+ * @package System.Util
+ * @since 3.2
+ */
+
+class TRpcClientRequestException extends TApplicationException
+{
+}
+
+/**
+ * TRpcClientResponseException class
+ *
+ * This Exception is fired when the
+ *
+ * @author Robin J. Rogge <rrogge@bigpoint.net>
+ * @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:
+ * <pre>
+ * $_rpcClient = new TJsonRpcClient('http://host/server');
+ * $_result = $_rpcClient->remoteMethod($param, $otherParam);
+ * // or
+ * $_result = TJsonRpcClient::create('http://host/server')->remoteMethod($param, $otherParam);
+ * </pre>
+ *
+ * @author Robin J. Rogge <rrogge@bigpoint.net>
+ * @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:
+ * <pre>
+ * $_rpcClient = new TXmlRpcClient('http://remotehost/rpcserver');
+ * $_rpcClient->remoteMethod($param, $otherParam);
+ * </pre>
+ *
+ * @author Robin J. Rogge <rrogge@bigpoint.net>
+ * @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 @@
+<?php
+
+/**
+ * @author Robin J. Rogge <rrogge@bigpoint.net>
+ * @link http://www.pradosoft.com/
+ * @copyright 2010 Bigpoint GmbH
+ * @license http://www.pradosoft.com/license/
+ * @version $Id$
+ * @since 3.2
+ */
+
+/**
+ * TRpcService class
+ *
+ * Usage:
+ * <service id="rpc" class="TRpcService">
+ * <rpcapi id="myapi" Class="MyApi" />
+ * </service>
+ *
+ * @author Robin J. Rogge <rrogge@bigpoint.net>
+ * @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 <rrogge@bigpoint.net>
+ * @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 <rrogge@bigpoint.net>
+ * @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 <rrogge@bigpoint.net>
+ * @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 <rrogge@bigpoint.net>
+ * @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 <rrogge@bigpoint.net>
+ * @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 <rrogge@bigpoint.net>
+ * @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);
+ }
+}