modrestservice.class.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. <?php
  2. /*
  3. * This file is part of the MODX Revolution package.
  4. *
  5. * Copyright (c) MODX, LLC
  6. *
  7. * For complete copyright and license information, see the COPYRIGHT and LICENSE
  8. * files found in the top-level directory of this distribution.
  9. *
  10. */
  11. require_once realpath(dirname(__FILE__).'/modrestcontroller.class.php');
  12. /**
  13. * A MODX-powered REST service class for dynamic REST API applications. Uses controller classes to handle routing
  14. * requests. Also supports xml/json/qs formats, and path/to/object/id routes.
  15. *
  16. * @package modx
  17. * @subpackage rest
  18. */
  19. class modRestService {
  20. /** @var modX $modx A reference to the modX instance */
  21. public $modx;
  22. /** @var array $config The configuration array */
  23. public $config = array();
  24. /** @var modRestServiceRequest $request The REST request object for this service */
  25. public $request;
  26. /** @var modRestServiceResponse $response The REST response object for this service */
  27. public $response;
  28. /** @var int|string $requestPrimaryKey The primary key requested on the object/id route */
  29. public $requestPrimaryKey;
  30. /**
  31. * @param modX $modx
  32. * @param array $config
  33. */
  34. public function __construct(modX &$modx,array $config = array()) {
  35. $this->modx =& $modx;
  36. $this->config = array_merge(array(
  37. 'basePath' => $this->modx->getOption('base_path',null,MODX_BASE_PATH),
  38. 'collectionResultsKey' => 'results',
  39. 'collectionTotalKey' => 'total',
  40. 'controllerClassPrefix' => 'modRestController',
  41. 'controllerClassSeparator' => '',
  42. 'defaultAction' => 'index',
  43. 'defaultResponseFormat' => 'json',
  44. 'defaultFailureStatusCode' => 200,
  45. 'defaultSuccessStatusCode' => 200,
  46. 'errorMessageSeparator' => ' ',
  47. 'exitOnResponse' => true,
  48. 'propertyLimit' => 'limit',
  49. 'propertyOffset' => 'start',
  50. 'propertySearch' => 'search',
  51. 'propertySort' => 'sort',
  52. 'propertySortDir' => 'dir',
  53. 'requestParameter' => '_rest',
  54. 'responseErrorsKey' => 'errors',
  55. 'responseMessageKey' => 'message',
  56. 'responseObjectKey' => 'object',
  57. 'responseSuccessKey' => 'success',
  58. 'trimParameters' => false,
  59. 'xmlRootNode' => 'response',
  60. ),$config);
  61. $this->modx->getService('lexicon','modLexicon');
  62. if ($this->modx->lexicon) {
  63. $this->modx->lexicon->load('rest');
  64. }
  65. }
  66. /**
  67. * Get a configuration option for this service
  68. *
  69. * @param string $key
  70. * @param mixed $default
  71. * @return mixed
  72. */
  73. public function getOption($key,$default = null) {
  74. return array_key_exists($key,$this->config) ? $this->config[$key] : $default;
  75. }
  76. /**
  77. * Check permissions for the request.
  78. *
  79. * @return boolean
  80. */
  81. public function checkPermissions() {
  82. return true;
  83. }
  84. /**
  85. * Prepare the request object, setting the method, headers, format and parameters
  86. */
  87. public function prepare() {
  88. $requestParameter = $this->getOption('requestParameter','_rest');
  89. $this->request = new modRestServiceRequest($this);
  90. $this->request->setAction();
  91. $this->request->setFormat($this->getOption('defaultResponseFormat','json'));
  92. $this->request->checkForSuffix();
  93. unset($_GET[$requestParameter]);
  94. $this->request->setMethod();
  95. $this->request->setHeaders();
  96. $this->request->setRequestParameters();
  97. }
  98. /**
  99. * Process the request, creating the controller and response objects, and then sending the processed
  100. * response back to the client. The controller is determined by the path passed to the request parameter, and
  101. * the controller's method is determined by the HTTP request method sent.
  102. */
  103. public function process() {
  104. try {
  105. $controllerName = $this->getController();
  106. if(null == $controllerName) {
  107. throw new Exception('Method not allowed', 405);
  108. }
  109. /** @var modRestController $controller */
  110. $controller = new ReflectionClass($controllerName);
  111. if (!$controller->isInstantiable()) {
  112. throw new Exception('Bad Request', 400);
  113. }
  114. $controller->properties = $this->request->parameters;
  115. $controller->headers = $this->request->headers;
  116. try {
  117. /** @var ReflectionMethod $method */
  118. $method = $controller->getMethod($this->request->method);
  119. } catch (ReflectionException $e) {
  120. throw new Exception('Unsupported HTTP method ' . $this->request->method, 405);
  121. }
  122. if (!$method->isStatic()) {
  123. $controller = $controller->newInstance($this->modx,$this->request,$this->config);
  124. $controller->setProperties($this->request->parameters);
  125. $controller->setHeaders($this->request->headers);
  126. if ($controller->isProtected() && $this->request->method != 'options') {
  127. if (!$controller->verifyAuthentication()) {
  128. throw new Exception('Unauthorized', 401);
  129. }
  130. }
  131. if (!empty($this->requestPrimaryKey)) {
  132. $controller->setProperty($controller->primaryKeyField,$this->requestPrimaryKey);
  133. }
  134. $controller->initialize();
  135. $method->invoke($controller);
  136. $this->response = new modRestServiceResponse($this,$controller->getResponse(),$controller->getResponseStatus());
  137. } else {
  138. throw new Exception('Static methods not supported in Controllers', 500);
  139. }
  140. if (empty($this->response)) {
  141. throw new Exception('Method not allowed', 405);
  142. }
  143. } catch (Exception $error) {
  144. $this->response = new modRestServiceResponse($this,array(
  145. 'success' => false,
  146. 'message' => $error->getMessage(),
  147. 'object' => array(),
  148. 'code' => $error->getCode(),
  149. ),$error->getCode());
  150. }
  151. $contentType = $this->getResponseContentType($this->request->format);
  152. $this->response->setContentType($contentType);
  153. $this->response->prepare();
  154. return $this->response->send();
  155. }
  156. /**
  157. * Get the Response content type based on the format passed
  158. *
  159. * @param string $format
  160. * @return string
  161. */
  162. public function getResponseContentType($format = 'json') {
  163. $supportedFormats = $this->getOption('supportedFormats','xml,json,qs');
  164. $supportedFormats = explode(',',$supportedFormats);
  165. if (!in_array($format,$supportedFormats)) {
  166. $contentType = $this->getOption('defaultResponseFormat','json');
  167. } else {
  168. $contentType = $format;
  169. }
  170. return trim($contentType);
  171. }
  172. /**
  173. * Get the correct controller path for the class
  174. *
  175. * @return string
  176. */
  177. protected function getController() {
  178. $expectedFile = trim($this->request->action,'/');
  179. $basePath = $this->getOption('basePath');
  180. $controllerClassPrefix = $this->getOption('controllerClassPrefix','modController');
  181. $controllerClassSeparator = $this->getOption('controllerClassSeparator','_');
  182. $controllerClassFilePostfix = $this->getOption('controllerClassFilePostfix','.php');
  183. /* handle [object]/[id] pathing */
  184. $expectedArray = explode('/',$expectedFile);
  185. if (empty($expectedArray)) $expectedArray = array(rtrim($expectedFile,'/').'/');
  186. $id = array_pop($expectedArray);
  187. if (!file_exists($basePath.$expectedFile.$controllerClassFilePostfix) && !empty($id)) {
  188. $expectedFile = implode('/',$expectedArray);
  189. if (empty($expectedFile)) {
  190. $expectedFile = $id;
  191. $id = null;
  192. }
  193. $this->requestPrimaryKey = $id;
  194. }
  195. foreach ($this->iterateDirectories($basePath.'/*'.$controllerClassFilePostfix, GLOB_NOSORT) as $controller) {
  196. $controller = $basePath != '/' ? str_replace($basePath,'',$controller) : $controller;
  197. $controller = trim($controller,'/');
  198. $controllerFile = str_replace(array($controllerClassFilePostfix),array(''),$controller);
  199. $controllerClass = str_replace(array('/',$controllerClassFilePostfix),array($controllerClassSeparator,''),$controller);
  200. if (strnatcasecmp($expectedFile, $controllerFile) == 0) {
  201. require_once $basePath.$controller;
  202. return $controllerClassPrefix . $controllerClassSeparator . $controllerClass;
  203. }
  204. }
  205. $this->modx->log(modX::LOG_LEVEL_INFO,'Could not find expected controller: '.$expectedFile);
  206. return null;
  207. }
  208. /**
  209. * Iterate across directories looking for files based on a pattern
  210. *
  211. * @param string $pattern
  212. * @param int $flags
  213. * @return array
  214. */
  215. public function iterateDirectories($pattern, $flags = 0) {
  216. $files = glob($pattern, $flags);
  217. $dirs = glob(dirname($pattern) . '/*', GLOB_ONLYDIR|GLOB_NOSORT);
  218. if ($dirs) {
  219. foreach ($dirs as $dir) {
  220. $files = array_merge($files, $this->iterateDirectories($dir . '/' . basename($pattern), $flags));
  221. }
  222. }
  223. return $files;
  224. }
  225. /**
  226. * Send either to the unauthorized page or exit out with a 401
  227. * @param bool $exit
  228. */
  229. public function sendUnauthorized($exit = true) {
  230. if (!$exit) {
  231. $this->modx->sendUnauthorizedPage();
  232. } else {
  233. header($_SERVER['SERVER_PROTOCOL'] . ' 401 Unauthorized');
  234. @session_write_close();
  235. exit(0);
  236. }
  237. }
  238. }
  239. /**
  240. * Request class for REST Service, which abstracts the incoming request
  241. *
  242. * @package modx
  243. * @subpackage rest
  244. */
  245. class modRestServiceRequest {
  246. /** @var \modRestService $service */
  247. public $service;
  248. /** @var string $action The action for the request */
  249. public $action = 'index';
  250. /** @var string $format The format the request is asking for */
  251. public $format = 'json';
  252. /** @var string $method The HTTP method the */
  253. public $method = 'GET';
  254. /** @var array $headers The HTTP headers on the request */
  255. public $headers = array();
  256. /** @var array $parameters The request parameters on the request */
  257. public $parameters = array();
  258. /**
  259. * @param modRestService $service A reference to the modRestService instance
  260. */
  261. function __construct(modRestService &$service) {
  262. $this->service = &$service;
  263. }
  264. /**
  265. * Set or determine the target action (controller) for this request
  266. *
  267. * @param string $action
  268. */
  269. public function setAction($action = '') {
  270. if (empty($action)) {
  271. $requestParameter = $this->service->getOption('requestParameter','_rest');
  272. $defaultAction = $this->service->getOption('defaultAction','index');
  273. $action = !empty($_GET[$requestParameter]) ? $_GET[$requestParameter] : $defaultAction;
  274. }
  275. $this->_trimString($action);
  276. $this->action = $action;
  277. }
  278. /**
  279. * Set the response format for this request
  280. *
  281. * @param string $format
  282. */
  283. public function setFormat($format = 'json') {
  284. $this->_trimString($format);
  285. $this->format = $format;
  286. }
  287. /**
  288. * Check for a format suffix (.json, .xml, etc) on the request, properly setting the format if found
  289. */
  290. public function checkForSuffix() {
  291. $checkForSuffix = $this->service->getOption('checkForSuffix', true);
  292. $formatPos = strpos($this->action,'.');
  293. if ($checkForSuffix && $formatPos !== false) {
  294. $this->format = substr($this->action,$formatPos+1);
  295. $this->action = substr($this->action,0,$formatPos);
  296. }
  297. }
  298. /**
  299. * Set or determine the HTTP request method for this request
  300. *
  301. * @param string $method
  302. */
  303. public function setMethod($method = '') {
  304. if (empty($method)) {
  305. $method = strtolower($_SERVER['REQUEST_METHOD']);
  306. }
  307. $this->_trimString($method);
  308. $this->method = $method;
  309. }
  310. /**
  311. * Set or collect the headers for this request
  312. *
  313. * @param array $headers
  314. */
  315. public function setHeaders(array $headers = array()) {
  316. if (empty($headers)) {
  317. if (function_exists('apache_request_headers')) {
  318. $this->headers = apache_request_headers();
  319. }
  320. $headers = array();
  321. $keys = preg_grep('{^HTTP_}i', array_keys($_SERVER));
  322. foreach ($keys as $val) {
  323. $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($val, 5)))));
  324. $headers[$key] = $_SERVER[$val];
  325. }
  326. }
  327. array_walk_recursive($headers,array('modRestServiceRequest','_trimString'));
  328. $this->headers = $headers;
  329. }
  330. /**
  331. * Set the REQUEST parameters for this request
  332. */
  333. public function setRequestParameters() {
  334. switch ($this->method) {
  335. case 'get':
  336. $this->parameters = $_GET;
  337. break;
  338. case 'post':
  339. $this->parameters = array_merge($_POST,$_GET,$this->_collectRequestParameters());
  340. $_REQUEST = $this->parameters;
  341. break;
  342. case 'put':
  343. $this->parameters = array_merge($_POST,$this->_collectRequestParameters());
  344. $_REQUEST = $this->parameters;
  345. break;
  346. case 'delete':
  347. $this->parameters = array_merge($_GET,$this->_collectRequestParameters());
  348. $_REQUEST = $this->parameters;
  349. break;
  350. default:
  351. break;
  352. }
  353. }
  354. /**
  355. * Properly get request parameters for various HTTP methods and content types
  356. * @return array
  357. */
  358. protected function _collectRequestParameters() {
  359. $filehandle = fopen('php://input', "r");
  360. $params = array();
  361. $contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '';
  362. $spPos = strpos($contentType, ';');
  363. if ($spPos !== false) {
  364. $contentType = substr($contentType, 0, $spPos);
  365. }
  366. switch ($contentType) {
  367. case 'image/jpeg':
  368. case 'image/png':
  369. case 'image/gif':
  370. $params['filehandle'] = $filehandle;
  371. break;
  372. case 'application/xml':
  373. case 'text/xml':
  374. $data = stream_get_contents($filehandle);
  375. fclose($filehandle);
  376. $xml = simplexml_load_string($data);
  377. $params = $this->_xml2array($xml);
  378. break;
  379. case 'application/json':
  380. case 'text/json':
  381. $data = stream_get_contents($filehandle);
  382. fclose($filehandle);
  383. $params = $this->service->modx->fromJSON($data);
  384. $params = (!is_array($params)) ? array() : $params;
  385. break;
  386. case 'application/x-www-form-urlencoded':
  387. default:
  388. $data = stream_get_contents($filehandle);
  389. fclose($filehandle);
  390. parse_str($data, $params);
  391. break;
  392. }
  393. if ($this->service->getOption('trimParameters', false)) {
  394. array_walk_recursive($this->parameters, array('modRestServiceRequest', '_trimString'));
  395. }
  396. return $params;
  397. }
  398. /**
  399. * Trim a value, assuming it is a string
  400. *
  401. * @static
  402. * @param mixed $value
  403. */
  404. public static function _trimString(&$value) {
  405. if (is_string($value)) {
  406. $value = trim($value);
  407. }
  408. }
  409. /**
  410. * Convert a SimpleXMLElement object to a multi-dimensional array
  411. *
  412. * @param SimpleXMLElement $xml
  413. * @param mixed $attributesKey
  414. * @param mixed $childrenKey
  415. * @param mixed $valueKey
  416. * @return array|null|string
  417. */
  418. protected function _xml2array(SimpleXMLElement $xml,$attributesKey=null,$childrenKey=null,$valueKey=null) {
  419. if ($childrenKey && !is_string($childrenKey)) $childrenKey = '@children';
  420. if ($attributesKey && !is_string($attributesKey)) $attributesKey = '@attributes';
  421. if ($valueKey && !is_string($valueKey)) $valueKey = '@values';
  422. $return = array();
  423. $name = $xml->getName();
  424. $_value = trim((string)$xml);
  425. if (!strlen($_value)) $_value = null;
  426. if ($_value !== null){
  427. if ($valueKey) {
  428. $return[$valueKey] = $_value;
  429. } else{
  430. $return = $_value;
  431. }
  432. }
  433. $children = array();
  434. $first = true;
  435. foreach ($xml->children() as $elementName => $child){
  436. $value = $this->_xml2array($child,$attributesKey, $childrenKey,$valueKey);
  437. if (isset($children[$elementName])) {
  438. if (is_array($children[$elementName])) {
  439. if ($first) {
  440. $temp = $children[$elementName];
  441. unset($children[$elementName]);
  442. $children[$elementName][] = $temp;
  443. $first = false;
  444. }
  445. $children[$elementName][] = $value;
  446. } else {
  447. $children[$elementName] = array($children[$elementName],$value);
  448. }
  449. } else {
  450. $children[$elementName] = $value;
  451. }
  452. }
  453. if ($children) {
  454. if ($childrenKey) {
  455. $return[$childrenKey] = $children;
  456. } else {
  457. $return = array_merge($return,$children);
  458. }
  459. }
  460. $attributes = array();
  461. foreach ($xml->attributes() as $name => $value) {
  462. $attributes[$name] = trim($value);
  463. }
  464. if ($attributes) {
  465. if ($attributesKey) {
  466. $return[$attributesKey] = $attributes;
  467. }
  468. else if (is_array($attributes) && is_array($return)) {
  469. $return = array_merge($return, $attributes);
  470. }
  471. }
  472. return $return;
  473. }
  474. }
  475. /**
  476. * The REST response class for the service
  477. *
  478. * @package modx
  479. * @subpackage rest
  480. */
  481. class modRestServiceResponse {
  482. /** @var string $body The data body of the response */
  483. public $body;
  484. /** @var int $status The status code of the response */
  485. public $status;
  486. /** @var string $contentType The string content type of the response */
  487. public $contentType = 'json';
  488. /** @var array $payload The data payload being sent as the response */
  489. protected $payload = array();
  490. /**
  491. * Map of formats to their parallel content types
  492. * @var array
  493. */
  494. protected static $contentTypes = array(
  495. 'xml' => 'application/xml',
  496. 'json' => 'application/json',
  497. 'qs' => 'text/plain'
  498. );
  499. /**
  500. * Dictionary of response codes and their text descriptions
  501. * @var array
  502. */
  503. protected static $responseCodes = array(
  504. 100 => 'Continue',
  505. 101 => 'Switching Protocols',
  506. 200 => 'OK',
  507. 201 => 'Created',
  508. 202 => 'Accepted',
  509. 203 => 'Non-Authoritative Information',
  510. 204 => 'No Content',
  511. 205 => 'Reset Content',
  512. 206 => 'Partial Content',
  513. 300 => 'Multiple Choices',
  514. 301 => 'Moved Permanently',
  515. 302 => 'Found',
  516. 303 => 'See Other',
  517. 304 => 'Not Modified',
  518. 305 => 'Use Proxy',
  519. 306 => '(Unused)',
  520. 307 => 'Temporary Redirect',
  521. 400 => 'Bad Request',
  522. 401 => 'Unauthorized',
  523. 402 => 'Payment Required',
  524. 403 => 'Forbidden',
  525. 404 => 'Not Found',
  526. 405 => 'Method Not Allowed',
  527. 406 => 'Not Acceptable',
  528. 407 => 'Proxy Authentication Required',
  529. 408 => 'Request Timeout',
  530. 409 => 'Conflict',
  531. 410 => 'Gone',
  532. 411 => 'Length Required',
  533. 412 => 'Precondition Failed',
  534. 413 => 'Request Entity Too Large',
  535. 414 => 'Request-URI Too Long',
  536. 415 => 'Unsupported Media Type',
  537. 416 => 'Requested Range Not Satisfiable',
  538. 417 => 'Expectation Failed',
  539. 500 => 'Internal Server Error',
  540. 501 => 'Not Implemented',
  541. 502 => 'Bad Gateway',
  542. 503 => 'Service Unavailable',
  543. 504 => 'Gateway Timeout',
  544. 505 => 'HTTP Version Not Supported',
  545. );
  546. /**
  547. * @param modRestService $service A reference to the modRestService instance
  548. * @param string $body The actual body of the response
  549. * @param string|int $status The status code for the response
  550. */
  551. function __construct(modRestService &$service,$body,$status) {
  552. $this->service = &$service;
  553. $this->body = $body;
  554. $this->status = $status;
  555. }
  556. /**
  557. * Set the content type for this response
  558. *
  559. * @param string $contentType
  560. */
  561. public function setContentType($contentType) {
  562. $this->contentType = $contentType;
  563. }
  564. /**
  565. * Prepare the response, properly formatting the body and generating the payload
  566. */
  567. public function prepare() {
  568. if (!empty($this->body)) {
  569. $this->payload = array('status' => $this->status, 'body' => $this->getFormattedBody());
  570. } else {
  571. $this->contentType = 'qs';
  572. $this->payload = array('status' => $this->status, 'body' => $this->body);
  573. }
  574. }
  575. /**
  576. * Format the body based on the content type of the response
  577. *
  578. * @access protected
  579. * @return string
  580. */
  581. protected function getFormattedBody() {
  582. switch ($this->contentType) {
  583. case 'xml':
  584. $data = $this->toXml($this->body);
  585. break;
  586. case 'qs':
  587. $data = http_build_query($this->body);
  588. break;
  589. case 'json':
  590. case 'js':
  591. default:
  592. $data = $this->service->modx->toJSON($this->body);
  593. break;
  594. }
  595. return $data;
  596. }
  597. /**
  598. * Send the response back to the client.
  599. */
  600. public function send() {
  601. $contentType = $this->getResponseContentType($this->contentType);
  602. $status = !empty($this->payload['status']) ? $this->payload['status'] : 200;
  603. $body = empty($this->payload['body']) ? '' : $this->payload['body'];
  604. $headers = $_SERVER['SERVER_PROTOCOL'] . ' ' . $status . ' ' . $this->getResponseCodeMessage($status);
  605. header($headers);
  606. header('Content-Type: ' . $contentType);
  607. echo $body;
  608. if ($this->service->getOption('exitOnResponse',true)) {
  609. @session_write_close();
  610. exit(0);
  611. }
  612. }
  613. /**
  614. * Get the proper response code message for the passed status code
  615. *
  616. * @param int $status
  617. * @return string
  618. */
  619. protected function getResponseCodeMessage($status) {
  620. return (isset(self::$responseCodes[$status])) ? self::$responseCodes[$status] : self::$responseCodes[500];
  621. }
  622. /**
  623. * Get the proper HTTP content type for the passed format
  624. *
  625. * @param string $format
  626. * @return string
  627. */
  628. protected function getResponseContentType($format = 'json') {
  629. return self::$contentTypes[$format];
  630. }
  631. /**
  632. * Convert an array to XML output
  633. *
  634. * @param array $data
  635. * @param string $version
  636. * @param string $encoding
  637. * @return string
  638. */
  639. protected function toXml($data, $version = '1.0', $encoding = 'UTF-8') {
  640. $xml = new XMLWriter;
  641. $xml->openMemory();
  642. $xml->startDocument($version, $encoding);
  643. $xml->startElement($this->service->getOption('xmlRootNode','response'));
  644. $this->_xml($xml, $data);
  645. $xml->endElement();
  646. return $xml->outputMemory(true);
  647. }
  648. /**
  649. * Helper method for converting an array to XML output
  650. *
  651. * @param XMLWriter $xml
  652. * @param mixed $data
  653. * @param string $old_key
  654. */
  655. protected function _xml(XMLWriter $xml, $data, $old_key = null) {
  656. foreach ($data as $key => $value){
  657. if (is_array($value)){
  658. if (!is_int($key)) {
  659. $xml->startElement($key);
  660. } else {
  661. $singleKey = trim($old_key,'s');
  662. $xml->startElement($singleKey);
  663. }
  664. $this->_xml($xml, $value, $key);
  665. $xml->endElement();
  666. continue;
  667. }
  668. $key = (is_int($key)) ? $old_key.$key : $key;
  669. if (!is_object($value)) {
  670. $xml->writeElement($key, $value);
  671. }
  672. }
  673. }
  674. }