/** * The xmlSerialize metod is called during xml writing. * * Use the $writer argument to write its own xml serialization. * * An important note: do _not_ create a parent element. Any element * implementing XmlSerializble should only ever write what's considered * its 'inner xml'. * * The parent of the current element is responsible for writing a * containing element. * * This allows serializers to be re-used for different element names. * * If you are opening new elements, you must also close them again. * * @param Writer $writer * @return void */ function xmlSerialize(Writer $writer) { $reader = new Reader(); // Wrapping the xml in a container, so root-less values can still be // parsed. $xml = <<<XML <?xml version="1.0"?> <xml-fragment xmlns="http://sabre.io/ns">{$this->getXml()}</xml-fragment> XML; $reader->xml($xml); $elementNamespace = null; while ($reader->read()) { if ($reader->depth < 1) { // Skipping the root node. continue; } switch ($reader->nodeType) { case Reader::ELEMENT: $writer->startElement($reader->getClark()); $empty = $reader->isEmptyElement; while ($reader->moveToNextAttribute()) { switch ($reader->namespaceURI) { case '': $writer->writeAttribute($reader->localName, $reader->value); break; case 'http://www.w3.org/2000/xmlns/': // Skip namespace declarations break; default: $writer->writeAttribute($reader->getClark(), $reader->value); break; } } if ($empty) { $writer->endElement(); } break; case Reader::CDATA: case Reader::TEXT: $writer->text($reader->value); break; case Reader::END_ELEMENT: $writer->endElement(); break; } } }
/** * This function behaves similar to Sabre\Xml\Reader::parseCurrentElement, * but instead of creating deep xml array structures, it will turn any * top-level element it doesn't recognize into either a string, or an * XmlFragment class. * * This method returns arn array with 2 properties: * * name - A clark-notation XML element name. * * value - The parsed value. * * @param Reader $reader * @return array */ private static function parseCurrentElement(Reader $reader) { $name = $reader->getClark(); if (array_key_exists($name, $reader->elementMap)) { $deserializer = $reader->elementMap[$name]; if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) { $value = call_user_func([$deserializer, 'xmlDeserialize'], $reader); } elseif (is_callable($deserializer)) { $value = call_user_func($deserializer, $reader); } else { $type = gettype($deserializer); if ($type === 'string') { $type .= ' (' . $deserializer . ')'; } elseif ($type === 'object') { $type .= ' (' . get_class($deserializer) . ')'; } throw new \LogicException('Could not use this type as a deserializer: ' . $type); } } else { $value = Complex::xmlDeserialize($reader); } return ['name' => $name, 'value' => $value]; }
/** * The deserialize method is called during xml parsing. * * This method is called staticly, this is because in theory this method * may be used as a type of constructor, or factory method. * * Often you want to return an instance of the current class, but you are * free to return other data as well. * * Important note 2: You are responsible for advancing the reader to the * next element. Not doing anything will result in a never-ending loop. * * If you just want to skip parsing for this element altogether, you can * just call $reader->next(); * * $reader->parseInnerTree() will parse the entire sub-tree, and advance to * the next element. * * @param Xml\Reader $reader * @return mixed */ static function xmlDeserialize(Xml\Reader $reader) { // If there's no children, we don't do anything. if ($reader->isEmptyElement) { $reader->next(); return []; } $values = []; $reader->read(); do { if ($reader->nodeType === Xml\Reader::ELEMENT) { $clark = $reader->getClark(); $values[$clark] = $reader->parseCurrentElement()['value']; } else { $reader->read(); } } while ($reader->nodeType !== Xml\Reader::END_ELEMENT); $reader->read(); return $values; }
/** * The deserialize method is called during xml parsing. * * This method is called statictly, this is because in theory this method * may be used as a type of constructor, or factory method. * * Often you want to return an instance of the current class, but you are * free to return other data as well. * * You are responsible for advancing the reader to the next element. Not * doing anything will result in a never-ending loop. * * If you just want to skip parsing for this element altogether, you can * just call $reader->next(); * * $reader->parseInnerTree() will parse the entire sub-tree, and advance to * the next element. * * @param Reader $reader * @return mixed */ static function xmlDeserialize(Reader $reader) { $reader->pushContext(); $reader->elementMap['{DAV:}propstat'] = 'Sabre\\Xml\\Element\\KeyValue'; // We are overriding the parser for {DAV:}prop. This deserializer is // almost identical to the one for Sabre\Xml\Element\KeyValue. // // The difference is that if there are any child-elements inside of // {DAV:}prop, that have no value, normally any deserializers are // called. But we don't want this, because a singular element without // child-elements implies 'no value' in {DAV:}prop, so we want to skip // deserializers and just set null for those. $reader->elementMap['{DAV:}prop'] = function (Reader $reader) { if ($reader->isEmptyElement) { $reader->next(); return []; } $values = []; $reader->read(); do { if ($reader->nodeType === Reader::ELEMENT) { $clark = $reader->getClark(); if ($reader->isEmptyElement) { $values[$clark] = null; $reader->next(); } else { $values[$clark] = $reader->parseCurrentElement()['value']; } } else { $reader->read(); } } while ($reader->nodeType !== Reader::END_ELEMENT); $reader->read(); return $values; }; $elems = $reader->parseInnerTree(); $reader->popContext(); $href = null; $propertyLists = []; $statusCode = null; foreach ($elems as $elem) { switch ($elem['name']) { case '{DAV:}href': $href = $elem['value']; break; case '{DAV:}propstat': $status = $elem['value']['{DAV:}status']; list(, $status, ) = explode(' ', $status, 3); $properties = isset($elem['value']['{DAV:}prop']) ? $elem['value']['{DAV:}prop'] : []; $propertyLists[$status] = $properties; break; case '{DAV:}status': list(, $statusCode, ) = explode(' ', $elem['value'], 3); break; } } return new self($href, $propertyLists, $statusCode); }
/** * Parse the curriculum definition file. * * By passing the official curriculum definition file (XML), this method * will parse it and return a curriculum definition it can understand and * treat. It mainly needs a "dictionary" of term types. * * @param string $curriculumXml * The curriculum definition file, in XML. * @param string $variant * (optional) The variant of the curriculum to parse. Defaults to 'V_EF'. * * @return array * An object with 2 properties: * - curriculum: A parsed and prepared curriculum tree. It uses * Educa\DSB\Client\Curriculum\Term\LP21Term elements to define * the curriculum tree. * - dictionary: A dictionary of term identifiers, with name and type * information for each one of them. * * @see \Educa\DSB\Client\Curriculum\LP21Curriculum::setCurriculumDictionary() */ public static function parseCurriculumXml($curriculumXml, $variant = 'V_EF') { $reader = new Reader(); // Prepare custom handlers for reading an XML node. See the Sabre\Xml // documentation for more information. $baseHandler = function ($reader) use($variant) { $node = new \stdClass(); // Fetch the attributes. We want the UUID attribute. $attributes = $reader->parseAttributes(); $node->uuid = trim($attributes['uuid']); // We derive the type from the node name. $node->type = strtolower(str_replace('{}', '', trim($reader->getClark()))); // Give a default description. $node->description = (object) array('de' => ''); // Fetch the descendants. $children = $reader->parseInnerTree(); if (!empty($children)) { $node->children = array(); foreach ($children as $child) { // Look for child types that are relevant for us. Some must // be parsed as child types of their own, others should be // treated as being part of the current node. if (in_array($child['name'], array('{}fach', '{}kompetenzbereich', '{}handlungs-themenaspekt', '{}kompetenz', '{}kompetenzstufe'))) { $node->children[] = $child; } elseif ($child['name'] == '{}bezeichnung') { $node->description = (object) array_reduce($child['value'], function ($carry, $item) { $langcode = strtolower(str_replace('{}', '', $item['name'])); $carry[$langcode] = $item['value']; return $carry; }, array()); } elseif ($child['name'] == '{}kantone') { $node->cantons = array_map('trim', explode(',', $child['value'])); } } } if (!empty($node->cantons) && !in_array($variant, $node->cantons)) { return null; } return $node; }; $kompetenzstufeHandler = function ($reader) use($variant) { $nodes = array(); $cycle = $url = $version = $code = null; // Fetch the descendants. $children = $reader->parseInnerTree(); if (!empty($children)) { foreach ($children as $child) { if ($child['name'] == '{}absaetze') { $nodes = $child['value']; } elseif ($child['name'] == '{}zyklus') { $cycle = trim($child['value']); } elseif ($child['name'] == '{}lehrplanversion') { $version = trim($child['value']); } elseif ($child['name'] == '{}kanton' && $child['attributes']['id'] == $variant) { foreach ($child['value'] as $grandChild) { if ($grandChild['name'] == '{}code') { $code = trim($grandChild['value']); } elseif ($grandChild['name'] == '{}url') { $url = trim($grandChild['value']); } } } } } // Map all the Kompetenzstufe properties to the child Absaetzen. return array_map(function ($node) use($cycle, $url, $version, $code) { if (isset($cycle)) { $node->cycle = $cycle; } if (isset($url)) { $node->url = $url; } if (isset($version)) { $node->version = $version; } if (isset($code)) { $node->code = $code; } return $node; }, $nodes); }; $absaetzeHandler = function ($reader) { $nodes = array(); // Fetch the descendants. $children = $reader->parseInnerTree(); if (!empty($children)) { foreach ($children as $child) { if ($child['name'] == '{}bezeichnung') { $node = new \stdClass(); // We treat it as a "Kompetenzstufe". $node->type = 'kompetenzstufe'; $node->description = (object) array_reduce($child['value'], function ($carry, $item) { $langcode = strtolower(str_replace('{}', '', $item['name'])); $carry[$langcode] = $item['value']; return $carry; }, array()); // The UUID is on the child Bezeichnung element, not our // own node. $node->uuid = $child['attributes']['uuid']; $nodes[] = $node; } } } return $nodes; }; // Register our handler for the following node types. All others will be // treated with the default one provided by Sabre\Xml, but we don't // really care. $reader->elementMap = ['{}fachbereich' => $baseHandler, '{}fach' => $baseHandler, '{}kompetenzbereich' => $baseHandler, '{}handlungs-themenaspekt' => $baseHandler, '{}kompetenz' => $baseHandler, '{}kompetenzstufe' => $kompetenzstufeHandler, '{}absaetze' => $absaetzeHandler]; // Parse the data. $reader->xml($curriculumXml); $data = $reader->parse(); // Prepare the dictionary. $dictionary = array(); // Prepare our root element. $root = new LP21Term('root', 'root'); // Now, recursively parse the tree, transforming it into a tree of // LP21Term instances. $recurse = function ($tree, $parent) use(&$recurse, &$dictionary) { foreach ($tree as $item) { // Fetch our nodes. $nodes = $item['value']; if (!is_array($nodes)) { $nodes = [$nodes]; } // Double check the format. Is this one of our nodes? foreach ($nodes as $node) { if (isset($node->uuid) && isset($node->type) && isset($node->description)) { $term = new LP21Term($node->type, $node->uuid, $node->description); $parent->addChild($term); // Add it to our dictionary. $dictionary[$node->uuid] = (object) array('name' => $node->description, 'type' => $node->type); // Do we have an objective code? if (!empty($node->code)) { $term->setCode($node->code); $dictionary[$node->uuid]->code = $node->code; } // Do we have any cantons information? if (!empty($node->cantons)) { $term->setCantons($node->cantons); $dictionary[$node->uuid]->cantons = $node->cantons; } // Do we have curriculum version information? if (!empty($node->version)) { $term->setVersion($node->version); $dictionary[$node->uuid]->version = $node->version; } // Do we have URL information? if (!empty($node->url)) { $term->setUrl($node->url); $dictionary[$node->uuid]->url = $node->url; } // Do we have cycle information? if (!empty($node->cycle)) { $cycles = str_split($node->cycle); $term->setCycles($cycles); $dictionary[$node->uuid]->cycles = $cycles; } if (!empty($node->children)) { $recurse($node->children, $term); } } } } }; $recurse($data['value'], $root); // Return the parsed data. return (object) array('curriculum' => $root, 'dictionary' => $dictionary); }
/** * The deserialize method is called during xml parsing. * * This method is called statictly, this is because in theory this method * may be used as a type of constructor, or factory method. * * Often you want to return an instance of the current class, but you are * free to return other data as well. * * Important note 2: You are responsible for advancing the reader to the * next element. Not doing anything will result in a never-ending loop. * * If you just want to skip parsing for this element altogether, you can * just call $reader->next(); * * $reader->parseSubTree() will parse the entire sub-tree, and advance to * the next element. * * @param Xml\Reader $reader * @return mixed */ static function xmlDeserialize(Xml\Reader $reader) { // If there's no children, we don't do anything. if ($reader->isEmptyElement) { $reader->next(); return []; } $reader->read(); $currentDepth = $reader->depth; $values = []; do { if ($reader->nodeType === Xml\Reader::ELEMENT) { $values[] = $reader->getClark(); } } while ($reader->depth >= $currentDepth && $reader->next()); $reader->next(); return $values; }
/** * The 'enum' deserializer parses elements into a simple list * without values or attributes. * * For example, Elements will parse: * * <?xml version="1.0"? > * <s:root xmlns:s="http://sabredav.org/ns"> * <s:elem1 /> * <s:elem2 /> * <s:elem3 /> * <s:elem4>content</s:elem4> * <s:elem5 attr="val" /> * </s:root> * * Into: * * [ * "{http://sabredav.org/ns}elem1", * "{http://sabredav.org/ns}elem2", * "{http://sabredav.org/ns}elem3", * "{http://sabredav.org/ns}elem4", * "{http://sabredav.org/ns}elem5", * ]; * * This is useful for 'enum'-like structures. * * If the $namespace argument is specified, it will strip the namespace * for all elements that match that. * * For example, * * enum($reader, 'http://sabredav.org/ns') * * would return: * * [ * "elem1", * "elem2", * "elem3", * "elem4", * "elem5", * ]; * * @param Reader $reader * @param string $namespace * @return string[] */ function enum(Reader $reader, $namespace = null) { // If there's no children, we don't do anything. if ($reader->isEmptyElement) { $reader->next(); return []; } $reader->read(); $currentDepth = $reader->depth; $values = []; do { if ($reader->nodeType !== Reader::ELEMENT) { continue; } if (!is_null($namespace) && $namespace === $reader->namespaceURI) { $values[] = $reader->localName; } else { $values[] = $reader->getClark(); } } while ($reader->depth >= $currentDepth && $reader->next()); $reader->next(); return $values; }