| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- <?php
- declare(strict_types=1);
- namespace Sabre\Xml;
- /**
- * XML parsing and writing service.
- *
- * You are encouraged to make an instance of this for your application and
- * potentially extend it, as a central API point for dealing with xml and
- * configuring the reader and writer.
- *
- * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
- * @author Evert Pot (http://evertpot.com/)
- * @license http://sabre.io/license/ Modified BSD License
- */
- class Service
- {
- /**
- * This is the element map. It contains a list of XML elements (in clark
- * notation) as keys and PHP class names as values.
- *
- * The PHP class names must implement Sabre\Xml\Element.
- *
- * Values may also be a callable. In that case the function will be called
- * directly.
- *
- * @var array
- */
- public $elementMap = [];
- /**
- * This is a list of namespaces that you want to give default prefixes.
- *
- * You must make sure you create this entire list before starting to write.
- * They should be registered on the root element.
- *
- * @var array
- */
- public $namespaceMap = [];
- /**
- * This is a list of custom serializers for specific classes.
- *
- * The writer may use this if you attempt to serialize an object with a
- * class that does not implement XmlSerializable.
- *
- * Instead it will look at this classmap to see if there is a custom
- * serializer here. This is useful if you don't want your value objects
- * to be responsible for serializing themselves.
- *
- * The keys in this classmap need to be fully qualified PHP class names,
- * the values must be callbacks. The callbacks take two arguments. The
- * writer class, and the value that must be written.
- *
- * function (Writer $writer, object $value)
- *
- * @var array
- */
- public $classMap = [];
- /**
- * A bitmask of the LIBXML_* constants.
- *
- * @var int
- */
- public $options = 0;
- /**
- * Returns a fresh XML Reader.
- */
- public function getReader(): Reader
- {
- $r = new Reader();
- $r->elementMap = $this->elementMap;
- return $r;
- }
- /**
- * Returns a fresh xml writer.
- */
- public function getWriter(): Writer
- {
- $w = new Writer();
- $w->namespaceMap = $this->namespaceMap;
- $w->classMap = $this->classMap;
- return $w;
- }
- /**
- * Parses a document in full.
- *
- * Input may be specified as a string or readable stream resource.
- * The returned value is the value of the root document.
- *
- * Specifying the $contextUri allows the parser to figure out what the URI
- * of the document was. This allows relative URIs within the document to be
- * expanded easily.
- *
- * The $rootElementName is specified by reference and will be populated
- * with the root element name of the document.
- *
- * @param string|resource $input
- *
- * @return array|object|string
- *
- * @throws ParseException
- */
- public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null)
- {
- if (!is_string($input)) {
- // Unfortunately the XMLReader doesn't support streams. When it
- // does, we can optimize this.
- if (is_resource($input)) {
- $input = (string) stream_get_contents($input);
- } else {
- // Input is not a string and not a resource.
- // Therefore, it has to be a closed resource.
- // Effectively empty input has been passed in.
- $input = '';
- }
- }
- // If input is empty, then it's safe to throw an exception
- if (empty($input)) {
- throw new ParseException('The input element to parse is empty. Do not attempt to parse');
- }
- $r = $this->getReader();
- $r->contextUri = $contextUri;
- $r->XML($input, null, $this->options);
- $result = $r->parse();
- $rootElementName = $result['name'];
- return $result['value'];
- }
- /**
- * Parses a document in full, and specify what the expected root element
- * name is.
- *
- * This function works similar to parse, but the difference is that the
- * user can specify what the expected name of the root element should be,
- * in clark notation.
- *
- * This is useful in cases where you expected a specific document to be
- * passed, and reduces the amount of if statements.
- *
- * It's also possible to pass an array of expected rootElements if your
- * code may expect more than one document type.
- *
- * @param string|string[] $rootElementName
- * @param string|resource $input
- *
- * @return array|object|string
- *
- * @throws ParseException
- */
- public function expect($rootElementName, $input, ?string $contextUri = null)
- {
- if (!is_string($input)) {
- // Unfortunately the XMLReader doesn't support streams. When it
- // does, we can optimize this.
- if (is_resource($input)) {
- $input = (string) stream_get_contents($input);
- } else {
- // Input is not a string and not a resource.
- // Therefore, it has to be a closed resource.
- // Effectively empty input has been passed in.
- $input = '';
- }
- }
- // If input is empty, then it's safe to throw an exception
- if (empty($input)) {
- throw new ParseException('The input element to parse is empty. Do not attempt to parse');
- }
- $r = $this->getReader();
- $r->contextUri = $contextUri;
- $r->XML($input, null, $this->options);
- $rootElementName = (array) $rootElementName;
- foreach ($rootElementName as &$rEl) {
- if ('{' !== $rEl[0]) {
- $rEl = '{}'.$rEl;
- }
- }
- $result = $r->parse();
- if (!in_array($result['name'], $rootElementName, true)) {
- throw new ParseException('Expected '.implode(' or ', $rootElementName).' but received '.$result['name'].' as the root element');
- }
- return $result['value'];
- }
- /**
- * Generates an XML document in one go.
- *
- * The $rootElement must be specified in clark notation.
- * The value must be a string, an array or an object implementing
- * XmlSerializable. Basically, anything that's supported by the Writer
- * object.
- *
- * $contextUri can be used to specify a sort of 'root' of the PHP application,
- * in case the xml document is used as a http response.
- *
- * This allows an implementor to easily create URI's relative to the root
- * of the domain.
- *
- * @param string|array|object|XmlSerializable $value
- *
- * @return string
- */
- public function write(string $rootElementName, $value, ?string $contextUri = null)
- {
- $w = $this->getWriter();
- $w->openMemory();
- $w->contextUri = $contextUri;
- $w->setIndent(true);
- $w->startDocument();
- $w->writeElement($rootElementName, $value);
- return $w->outputMemory();
- }
- /**
- * Map an XML element to a PHP class.
- *
- * Calling this function will automatically set up the Reader and Writer
- * classes to turn a specific XML element to a PHP class.
- *
- * For example, given a class such as :
- *
- * class Author {
- * public $firstName;
- * public $lastName;
- * }
- *
- * and an XML element such as:
- *
- * <author xmlns="http://example.org/ns">
- * <firstName>...</firstName>
- * <lastName>...</lastName>
- * </author>
- *
- * These can easily be mapped by calling:
- *
- * $service->mapValueObject('{http://example.org}author', 'Author');
- */
- public function mapValueObject(string $elementName, string $className)
- {
- list($namespace) = self::parseClarkNotation($elementName);
- $this->elementMap[$elementName] = function (Reader $reader) use ($className, $namespace) {
- return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace);
- };
- $this->classMap[$className] = function (Writer $writer, $valueObject) use ($namespace) {
- return \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace);
- };
- $this->valueObjectMap[$className] = $elementName;
- }
- /**
- * Writes a value object.
- *
- * This function largely behaves similar to write(), except that it's
- * intended specifically to serialize a Value Object into an XML document.
- *
- * The ValueObject must have been previously registered using
- * mapValueObject().
- *
- * @param object $object
- *
- * @throws \InvalidArgumentException
- */
- public function writeValueObject($object, ?string $contextUri = null)
- {
- if (!isset($this->valueObjectMap[get_class($object)])) {
- throw new \InvalidArgumentException('"'.get_class($object).'" is not a registered value object class. Register your class with mapValueObject.');
- }
- return $this->write(
- $this->valueObjectMap[get_class($object)],
- $object,
- $contextUri
- );
- }
- /**
- * Parses a clark-notation string, and returns the namespace and element
- * name components.
- *
- * If the string was invalid, it will throw an InvalidArgumentException.
- *
- * @throws \InvalidArgumentException
- */
- public static function parseClarkNotation(string $str): array
- {
- static $cache = [];
- if (!isset($cache[$str])) {
- if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) {
- throw new \InvalidArgumentException('\''.$str.'\' is not a valid clark-notation formatted string');
- }
- $cache[$str] = [
- $matches[1],
- $matches[2],
- ];
- }
- return $cache[$str];
- }
- /**
- * A list of classes and which XML elements they map to.
- */
- protected $valueObjectMap = [];
- }
|