summaryrefslogtreecommitdiff
path: root/framework/Web/Services/TRpcService.php
blob: 9903fefd6d9af400bbdeef2e5180df7abb3b7ae3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
<?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
 * @package Prado\Web\Services
 */

namespace Prado\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:
 *
 * <code>
 * <service id="rpc" class="System.Web.Services.TRpcService">
 * 	 <rpcapi id="customers" class="Application.Api.CustomersApi" />
 *   <modules>
 *     <!--  register any module needed by the service here -->
 *   </modules>
 * </service>
 * </code>
 *
 * An api can be registered adding a proper <rpcapi ..> 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 <rrogge@bigpoint.net>
 * @version $Id$
 * @package Prado\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);
		}
	}
}