Service.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. declare(strict_types=1);
  3. namespace Sabre\Xml;
  4. /**
  5. * XML parsing and writing service.
  6. *
  7. * You are encouraged to make an instance of this for your application and
  8. * potentially extend it, as a central API point for dealing with xml and
  9. * configuring the reader and writer.
  10. *
  11. * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
  12. * @author Evert Pot (http://evertpot.com/)
  13. * @license http://sabre.io/license/ Modified BSD License
  14. */
  15. class Service
  16. {
  17. /**
  18. * This is the element map. It contains a list of XML elements (in clark
  19. * notation) as keys and PHP class names as values.
  20. *
  21. * The PHP class names must implement Sabre\Xml\Element.
  22. *
  23. * Values may also be a callable. In that case the function will be called
  24. * directly.
  25. *
  26. * @var array
  27. */
  28. public $elementMap = [];
  29. /**
  30. * This is a list of namespaces that you want to give default prefixes.
  31. *
  32. * You must make sure you create this entire list before starting to write.
  33. * They should be registered on the root element.
  34. *
  35. * @var array
  36. */
  37. public $namespaceMap = [];
  38. /**
  39. * This is a list of custom serializers for specific classes.
  40. *
  41. * The writer may use this if you attempt to serialize an object with a
  42. * class that does not implement XmlSerializable.
  43. *
  44. * Instead it will look at this classmap to see if there is a custom
  45. * serializer here. This is useful if you don't want your value objects
  46. * to be responsible for serializing themselves.
  47. *
  48. * The keys in this classmap need to be fully qualified PHP class names,
  49. * the values must be callbacks. The callbacks take two arguments. The
  50. * writer class, and the value that must be written.
  51. *
  52. * function (Writer $writer, object $value)
  53. *
  54. * @var array
  55. */
  56. public $classMap = [];
  57. /**
  58. * A bitmask of the LIBXML_* constants.
  59. *
  60. * @var int
  61. */
  62. public $options = 0;
  63. /**
  64. * Returns a fresh XML Reader.
  65. */
  66. public function getReader(): Reader
  67. {
  68. $r = new Reader();
  69. $r->elementMap = $this->elementMap;
  70. return $r;
  71. }
  72. /**
  73. * Returns a fresh xml writer.
  74. */
  75. public function getWriter(): Writer
  76. {
  77. $w = new Writer();
  78. $w->namespaceMap = $this->namespaceMap;
  79. $w->classMap = $this->classMap;
  80. return $w;
  81. }
  82. /**
  83. * Parses a document in full.
  84. *
  85. * Input may be specified as a string or readable stream resource.
  86. * The returned value is the value of the root document.
  87. *
  88. * Specifying the $contextUri allows the parser to figure out what the URI
  89. * of the document was. This allows relative URIs within the document to be
  90. * expanded easily.
  91. *
  92. * The $rootElementName is specified by reference and will be populated
  93. * with the root element name of the document.
  94. *
  95. * @param string|resource $input
  96. *
  97. * @return array|object|string
  98. *
  99. * @throws ParseException
  100. */
  101. public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null)
  102. {
  103. if (!is_string($input)) {
  104. // Unfortunately the XMLReader doesn't support streams. When it
  105. // does, we can optimize this.
  106. if (is_resource($input)) {
  107. $input = (string) stream_get_contents($input);
  108. } else {
  109. // Input is not a string and not a resource.
  110. // Therefore, it has to be a closed resource.
  111. // Effectively empty input has been passed in.
  112. $input = '';
  113. }
  114. }
  115. // If input is empty, then it's safe to throw an exception
  116. if (empty($input)) {
  117. throw new ParseException('The input element to parse is empty. Do not attempt to parse');
  118. }
  119. $r = $this->getReader();
  120. $r->contextUri = $contextUri;
  121. $r->XML($input, null, $this->options);
  122. $result = $r->parse();
  123. $rootElementName = $result['name'];
  124. return $result['value'];
  125. }
  126. /**
  127. * Parses a document in full, and specify what the expected root element
  128. * name is.
  129. *
  130. * This function works similar to parse, but the difference is that the
  131. * user can specify what the expected name of the root element should be,
  132. * in clark notation.
  133. *
  134. * This is useful in cases where you expected a specific document to be
  135. * passed, and reduces the amount of if statements.
  136. *
  137. * It's also possible to pass an array of expected rootElements if your
  138. * code may expect more than one document type.
  139. *
  140. * @param string|string[] $rootElementName
  141. * @param string|resource $input
  142. *
  143. * @return array|object|string
  144. *
  145. * @throws ParseException
  146. */
  147. public function expect($rootElementName, $input, ?string $contextUri = null)
  148. {
  149. if (!is_string($input)) {
  150. // Unfortunately the XMLReader doesn't support streams. When it
  151. // does, we can optimize this.
  152. if (is_resource($input)) {
  153. $input = (string) stream_get_contents($input);
  154. } else {
  155. // Input is not a string and not a resource.
  156. // Therefore, it has to be a closed resource.
  157. // Effectively empty input has been passed in.
  158. $input = '';
  159. }
  160. }
  161. // If input is empty, then it's safe to throw an exception
  162. if (empty($input)) {
  163. throw new ParseException('The input element to parse is empty. Do not attempt to parse');
  164. }
  165. $r = $this->getReader();
  166. $r->contextUri = $contextUri;
  167. $r->XML($input, null, $this->options);
  168. $rootElementName = (array) $rootElementName;
  169. foreach ($rootElementName as &$rEl) {
  170. if ('{' !== $rEl[0]) {
  171. $rEl = '{}'.$rEl;
  172. }
  173. }
  174. $result = $r->parse();
  175. if (!in_array($result['name'], $rootElementName, true)) {
  176. throw new ParseException('Expected '.implode(' or ', $rootElementName).' but received '.$result['name'].' as the root element');
  177. }
  178. return $result['value'];
  179. }
  180. /**
  181. * Generates an XML document in one go.
  182. *
  183. * The $rootElement must be specified in clark notation.
  184. * The value must be a string, an array or an object implementing
  185. * XmlSerializable. Basically, anything that's supported by the Writer
  186. * object.
  187. *
  188. * $contextUri can be used to specify a sort of 'root' of the PHP application,
  189. * in case the xml document is used as a http response.
  190. *
  191. * This allows an implementor to easily create URI's relative to the root
  192. * of the domain.
  193. *
  194. * @param string|array|object|XmlSerializable $value
  195. *
  196. * @return string
  197. */
  198. public function write(string $rootElementName, $value, ?string $contextUri = null)
  199. {
  200. $w = $this->getWriter();
  201. $w->openMemory();
  202. $w->contextUri = $contextUri;
  203. $w->setIndent(true);
  204. $w->startDocument();
  205. $w->writeElement($rootElementName, $value);
  206. return $w->outputMemory();
  207. }
  208. /**
  209. * Map an XML element to a PHP class.
  210. *
  211. * Calling this function will automatically set up the Reader and Writer
  212. * classes to turn a specific XML element to a PHP class.
  213. *
  214. * For example, given a class such as :
  215. *
  216. * class Author {
  217. * public $firstName;
  218. * public $lastName;
  219. * }
  220. *
  221. * and an XML element such as:
  222. *
  223. * <author xmlns="http://example.org/ns">
  224. * <firstName>...</firstName>
  225. * <lastName>...</lastName>
  226. * </author>
  227. *
  228. * These can easily be mapped by calling:
  229. *
  230. * $service->mapValueObject('{http://example.org}author', 'Author');
  231. */
  232. public function mapValueObject(string $elementName, string $className)
  233. {
  234. list($namespace) = self::parseClarkNotation($elementName);
  235. $this->elementMap[$elementName] = function (Reader $reader) use ($className, $namespace) {
  236. return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace);
  237. };
  238. $this->classMap[$className] = function (Writer $writer, $valueObject) use ($namespace) {
  239. return \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace);
  240. };
  241. $this->valueObjectMap[$className] = $elementName;
  242. }
  243. /**
  244. * Writes a value object.
  245. *
  246. * This function largely behaves similar to write(), except that it's
  247. * intended specifically to serialize a Value Object into an XML document.
  248. *
  249. * The ValueObject must have been previously registered using
  250. * mapValueObject().
  251. *
  252. * @param object $object
  253. *
  254. * @throws \InvalidArgumentException
  255. */
  256. public function writeValueObject($object, ?string $contextUri = null)
  257. {
  258. if (!isset($this->valueObjectMap[get_class($object)])) {
  259. throw new \InvalidArgumentException('"'.get_class($object).'" is not a registered value object class. Register your class with mapValueObject.');
  260. }
  261. return $this->write(
  262. $this->valueObjectMap[get_class($object)],
  263. $object,
  264. $contextUri
  265. );
  266. }
  267. /**
  268. * Parses a clark-notation string, and returns the namespace and element
  269. * name components.
  270. *
  271. * If the string was invalid, it will throw an InvalidArgumentException.
  272. *
  273. * @throws \InvalidArgumentException
  274. */
  275. public static function parseClarkNotation(string $str): array
  276. {
  277. static $cache = [];
  278. if (!isset($cache[$str])) {
  279. if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) {
  280. throw new \InvalidArgumentException('\''.$str.'\' is not a valid clark-notation formatted string');
  281. }
  282. $cache[$str] = [
  283. $matches[1],
  284. $matches[2],
  285. ];
  286. }
  287. return $cache[$str];
  288. }
  289. /**
  290. * A list of classes and which XML elements they map to.
  291. */
  292. protected $valueObjectMap = [];
  293. }