* @link http://www.pradosoft.com/
* @copyright 2010 Bigpoint GmbH
* @license http://www.pradosoft.com/license/
* @version $Id$
* @since 3.2
* @package System.Web.Services
*/
/**
* TRpcService class
*
* The TRpcService class is a generic class that can be extended and used to implement
* rpc services using different servers and protocols.
*
* A server is a {@link TModule} that must subclass {@link TRpcServer}: its role is
* to be an intermediate, moving data between the service and the provider. The base
* {@link TRpcServer} class should suit the most common needs, but can be sublassed for
* logging and debugging purposes, or to filter and modify the request/response on the fly.
*
* A protocol is a {@link TModule} that must subclass {@link TRpcProtocol}: its role is
* to implement the protocol that exposes the rpc api. Prado already implements two
* protocols: {@link TXmlRpcProtocol} for Xml-Rpc request and {@link TJsonRpcProtocol} for
* JSON-Rpc requests.
*
* A provider is a {@link TModule} that must subclass {@link TRpcApiProvider}: its role is
* to implement the methods that are available through the api. Each defined api must be
* a sublass of the abstract class {@link TRpcApiProvider} and implement its methods.
*
* The flow of requests and reponses is the following:
* Request <-> TRpcService <-> TRpcServer <-> TRpcProtocol <-> TRpcApiProvider <-> Response
*
* To define an rpc service, add the proper application configuration:
*
*
*
*
*
*
*
*
*
*
* An api can be registered adding a proper definition inside the service
* configuration. Each api definition must contain an id property and a class name
* expressed in namespace format. When the service receives a request for that api,
* the specified class will be instanciated in order to satisfy the request.
*
* @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);
$_providerClassName = ($_pos = strrpos($_providerClass, '.')) !== false ? substr($_providerClass, $_pos + 1) : $_providerClass;
if(!is_subclass_of($_providerClassName, self::BASE_API_PROVIDER))
throw new TConfigurationException('rpcservice_apiprovider_invalid');
if(($_rpcServerClass = $_properties->remove('server')) === null)
$_rpcServerClass = self::BASE_RPC_SERVER;
Prado::using($_rpcServerClass);
$_rpcServerClassName = ($_pos = strrpos($_rpcServerClass, '.')) !== false ? substr($_rpcServerClass, $_pos + 1) : $_rpcServerClass;
if($_rpcServerClassName!==self::BASE_RPC_SERVER && !is_subclass_of($_rpcServerClassName, self::BASE_RPC_SERVER))
throw new TConfigurationException('rpcservice_rpcserver_invalid');
$_apiProvider = new $_providerClassName(new $_rpcServerClassName($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);
}
/**
* Loads the service configuration
* @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
*
* TRpcServer is a class
*
* TRpcServer is the base class used to creare a server to be used in conjunction with
* {@link TRpcService}.
* The role of TRpcServer is to be an intermediate, moving data between the service and
* the provider. This base class should suit the most common needs, but can be sublassed for
* logging and debugging purposes, or to filter and modify the request/response on the fly.
*
* @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;
}
/**
* Registers the method in the protocol handler
* @param string $methodName
* @param array $methodDetails
*/
public function addRpcMethod($methodName, $methodDetails)
{
$this->handler->addMethod($methodName, $methodDetails);
}
/**
* Retrieves the request payload
* @return string request payload
*/
public function getPayload()
{
return file_get_contents('php://input');
}
/**
* Passes the request payload to the protocol handler and returns the result
* @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
*
* TRpcApiProvider is an abstract class the can be subclasses in order to implement an
* api for a {@link TRpcService}. A subclass of TRpcApiProvider must implement the
* {@link registerMethods} method in order to declare the available methods, their
* names and the associated callback.
*
*
* public function registerMethods()
* {
* return array(
* 'apiMethodName1' => array('method' => array($this, 'objectMethodName1')),
* 'apiMethodName2' => array('method' => array('ClassName', 'staticMethodName')),
* );
* }
*
*
* In this example, two api method have been defined. The first refers to an object
* method that must be implemented in the same class, the second to a static method
* implemented in a 'ClassName' class.
* In both cases, the method implementation will receive the request parameters as its
* method parameters. Since the number of received parameters depends on
* external-supplied data, it's adviced to use php's func_get_args() funtion to
* validate them.
*
* Providers must be registered in the service configuration in order to be available,
* as explained in {@link TRpcService}'s documentation.
*
* @author Robin J. Rogge
* @version $Id$
* @package System.Web.Services
* @since 3.2
*/
abstract class TRpcApiProvider extends TModule
{
/**
* @var TRpcServer instance
*/
protected $rpcServer;
/**
* Must return an array of the available methods
* @abstract
*/
abstract public function registerMethods();
/**
* Constructor: informs the rpc server of the registered methods
*/
public function __construct(TRpcServer $rpcServer)
{
$this->rpcServer = $rpcServer;
foreach($this->registerMethods() as $_methodName => $_methodDetails)
$this->rpcServer->addRpcMethod($_methodName, $_methodDetails);
}
/**
* Processes the request using the server
* @return processed request
*/
public function processRequest()
{
return $this->rpcServer->processRequest();
}
/**
* @return rpc server instance
*/
public function getRpcServer()
{
return $this->rpcServer;
}
}
/**
* TRpcProtocol class
*
* TRpcProtocol is the base class used to implement a protocol in a {@link TRpcService}.
* Prado already implements two protocols: {@link TXmlRpcProtocol} for Xml-Rpc request
* and {@link TJsonRpcProtocol} for JSON-Rpc requests.
*
* @author Robin J. Rogge
* @version $Id$
* @package System.Web.Services
* @since 3.2
**/
abstract class TRpcProtocol
{
/**
* @var array containing the mapping from RPC method names to the actual handlers
*/
protected $rpcMethods = array();
// abstracts
/**
* @param string request payload
* Processed the request ans returns the response, if any
* @return processed response
* @abstract
*/
abstract public function callMethod($requestPayload);
/**
* @param TRpcException the exception with error details
* Creates a proper response for an error condition
* @return a response representing the error
* @abstract
*/
abstract public function createErrorResponse(TRpcException $exception);
/**
* @param response
* Sets the needed headers for the response (eg: content-type, charset)
* @abstract
*/
abstract public function createResponseHeaders($response);
/**
* Encodes the response
* @param mixed reponse data
* @return string encoded response
* @abstract
*/
abstract public function encode($data);
/**
* Decodes the request payload
* @param string request payload
* @return mixed decoded request
* @abstract
*/
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');
if($parameters === null)
$parameters = array();
if(!is_array($parameters))
$parameters = array($parameters);
return call_user_func_array($this->rpcMethods[$methodName]['method'], $parameters);
}
}
/**
* TJsonRpcProtocol class
*
* TJsonRpcProtocol is a class that implements JSON-Rpc protocol in {@link TRpcService}.
* Both version 1.0 and 2.0 of the specification are implemented, and the server will try
* to answer using the same version of the protocol used by the requesting client.
*
* @author Robin J. Rogge
* @author Fabio Bas
* @version $Id$
* @package System.Web.Services
* @since 3.2
*/
class TJsonRpcProtocol extends TRpcProtocol
{
protected $_id=null;
protected $_specificationVersion=1.0;
/**
* Handles the RPC request
* @param string $requestPayload
* @return string JSON RPC response
*/
public function callMethod($requestPayload)
{
try
{
$_request = $this->decode($requestPayload);
if(isset($_request['jsonrpc']))
{
$this->_specificationVersion=$_request['jsonrpc'];
if($this->_specificationVersion > 2.0)
throw new TRpcException('Unsupported specification version', '-32600');
}
if(isset($_request['id']))
$this->_id=$_request['id'];
if(!isset($_request['method']))
throw new TRpcException('Missing request method', '-32600');
if(!isset($_request['params']))
$parameters = array();
else
$parameters = $_request['params'];
if(!is_array($parameters))
$parameters = array($parameters);
$ret = $this->callApiMethod($_request['method'], $parameters);
// a request without an id is a notification that doesn't need a response
if($this->_id !== null)
{
if($this->_specificationVersion==2.0)
{
return $this->encode(array(
'jsonrpc' => '2.0',
'id' => $this->_id,
'result' => $ret
));
} else {
return $this->encode(array(
'id' => $this->_id,
'result' => $this->callApiMethod($_request['method'], $_request['params']),
'error' => null
));
}
}
}
catch(TRpcException $e)
{
return $this->createErrorResponse($e);
}
catch(THttpException $e)
{
throw $e;
}
catch(Exception $e)
{
return $this->createErrorResponse(new TRpcException('An internal error occured', '-32603'));
}
}
/**
* Turns the given exception into an JSON RPC fault
* @param TRpcException $exception
* @return string JSON RPC fault
*/
public function createErrorResponse(TRpcException $exception)
{
if($this->_specificationVersion==2.0)
{
return $this->encode(array(
'id' => $this->_id,
'result' => null,
'error'=> array(
'code' => $exception->getCode(),
'message'=> $exception->getMessage(),
'data' => null,
)
));
} else {
return $this->encode(array(
'id' => $this->_id,
'error'=> array(
'code' => $exception->getCode(),
'message'=> $exception->getMessage(),
'data' => null,
)
));
}
}
/**
* 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)
{
$s = json_decode($data, true);
self::checkJsonError();
return $s;
}
/**
* Encodes PHP data into JSON data
* @param mixed PHP data
* @return string JSON encoded PHP data
*/
public function encode($data)
{
$s = json_encode($data);
self::checkJsonError();
return $s;
}
private static function checkJsonError()
{
$errnum = json_last_error();
if($errnum != JSON_ERROR_NONE)
throw new Exception("JSON error: $msg", $err);
}
/**
* Calls the callback handler for the given method
* Overrides parent implementation to correctly handle error codes
* @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', '-32601');
return call_user_func_array($this->rpcMethods[$methodName]['method'], $parameters);
}
}
/**
* TXmlRpcProtocol class
*
* TXmlRpcProtocol is a class that implements XML-Rpc protocol in {@link TRpcService}.
* It's basically a wrapper to the xmlrpc_server_* family of php methods.
*
* @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(THttpException $e)
{
throw $e;
}
catch(Exception $e)
{
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);
}
}