From d662bcbde3d85bec93c15bfccbc97f0f7fddcb54 Mon Sep 17 00:00:00 2001 From: Nikolay Somov Date: Sun, 17 Dec 2017 15:16:24 +0200 Subject: [PATCH 1/2] Added xml annotations for document: version, encoding, namespaces --- README.md | 3 + composer.json | 1 + src/Annotation/XmlAnnotationEnum.php | 16 ++ src/Mapper/XmlModelMapper.php | 242 ++++++++++++++++++--------- tests/Mapper/XmlModelMapperTest2.php | 55 ++++++ tests/XmlTestUrl.php | 20 +++ tests/XmlTestUrlSet.php | 43 +++++ tests/testFiles/valid2.xml | 1 + 8 files changed, 302 insertions(+), 79 deletions(-) create mode 100644 src/Annotation/XmlAnnotationEnum.php create mode 100644 tests/Mapper/XmlModelMapperTest2.php create mode 100644 tests/XmlTestUrl.php create mode 100644 tests/XmlTestUrlSet.php create mode 100644 tests/testFiles/valid2.xml diff --git a/README.md b/README.md index deec981..32b4d5c 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,9 @@ Heres a list of annotations we can use in models: | @required | property | Used to declare that a property is required for a specific action (validation). Use without the action to make the property always required | | @rule | property | Used to enforce specific rules and filters on the property value (validation). You can use the predefined rules or create and import your own | | @xmlRoot | class | Used to name the xml root name of the element (only for xml mapping) | +| @xmlVersion | class | Version of xml document (only for xml mapping) | +| @xmlEncoding | class | Encoding document. Default utf-8. (only for xml mapping) | +| @xmlNamespaces| class | Document namespaces. Without prefix xmlns. Example : "http://www.sitemaps.org/schemas/sitemap/0.9" xhtml="http://www.w3.org/1999/xhtml". (only for xml mapping) | | @xmlAttribute | property | Used to declare that a property is a xml attribute for the element. Also used for namespaces (only for xml mapping) | | @xmlNodeValue | property | Used to declare that a property contains the text node value (only for xml mapping). Please see the models in tests/ (XmlTestModel) | diff --git a/composer.json b/composer.json index 43f0ea7..d65f6ab 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "src/" ] }, + "autoload-dev": { "classmap": [ "tests/" diff --git a/src/Annotation/XmlAnnotationEnum.php b/src/Annotation/XmlAnnotationEnum.php new file mode 100644 index 0000000..bed9a45 --- /dev/null +++ b/src/Annotation/XmlAnnotationEnum.php @@ -0,0 +1,16 @@ +loadXML($xml); - if(!$xmlLoadSuccess) { + if (!$xmlLoadSuccess) { throw new ModelMapperException('Invalid xml provided.'); } $domElement = $domDocument->documentElement; @@ -47,31 +52,32 @@ public function map($source, $model) { * @param object $model * @return object */ - protected function mapModel($source, $model) { - if(!is_object($source) || Validation::isEmpty($source)) { + protected function mapModel($source, $model) + { + if (!is_object($source) || Validation::isEmpty($source)) { throw new \InvalidArgumentException('Source must be an object with properties.'); } - if(!is_object($model) || Validation::isEmpty($model)) { + if (!is_object($model) || Validation::isEmpty($model)) { throw new \InvalidArgumentException('Model must be an object with properties.'); } $modelClass = new ModelClass($model); - foreach($modelClass->getProperties() as $property) { + foreach ($modelClass->getProperties() as $property) { - if($property->getDocBlock()->hasAnnotation(AnnotationEnum::XML_ATTRIBUTE)) { + if ($property->getDocBlock()->hasAnnotation(AnnotationEnum::XML_ATTRIBUTE)) { $mappedValue = null; $attributesKey = self::ATTR_KEY; - if(isset($source->$attributesKey) && isset($source->$attributesKey[$property->getName()])) { + if (isset($source->$attributesKey) && isset($source->$attributesKey[$property->getName()])) { $mappedValue = $source->$attributesKey[$property->getName()]; } $property->setPropertyValue($mappedValue); continue; } - if($property->getDocBlock()->hasAnnotation(AnnotationEnum::XML_NODE_VALUE)) { + if ($property->getDocBlock()->hasAnnotation(AnnotationEnum::XML_NODE_VALUE)) { $mappedValue = null; $valueKey = self::VALUE_KEY; - if(isset($source->$valueKey)) { + if (isset($source->$valueKey)) { $mappedValue = $source->$valueKey; } $property->setPropertyValue($mappedValue); @@ -90,17 +96,18 @@ protected function mapModel($source, $model) { * @param \DOMNode $domElement * @return \stdClass */ - protected function domNodeToObject(\DOMNode $domElement) { + protected function domNodeToObject(\DOMNode $domElement) + { $object = new \stdClass(); $result = null; $result = $this->mapAttributes($domElement, $object); $result = $this->mapNamespaces($domElement, $object); - for($i = 0; $i < $domElement->childNodes->length; $i++) { + for ($i = 0; $i < $domElement->childNodes->length; $i++) { $element = $domElement->childNodes->item($i); $isElementArray = Xml::isDomNodeArray($element->parentNode, $element->nodeName); - switch($element->nodeType) { + switch ($element->nodeType) { case XML_ELEMENT_NODE: $result = $this->mapDomElement($element, $object, $isElementArray); break; @@ -118,9 +125,10 @@ protected function domNodeToObject(\DOMNode $domElement) { * @param $object * @return \stdClass */ - protected function mapAttributes(\DOMNode $domElement, $object) { + protected function mapAttributes(\DOMNode $domElement, $object) + { $attributesKey = self::ATTR_KEY; - for($i = 0; $i < $domElement->attributes->length; $i++) { + for ($i = 0; $i < $domElement->attributes->length; $i++) { $key = $domElement->attributes->item($i)->nodeName; $value = $domElement->attributes->item($i)->nodeValue; $object->$attributesKey[$key] = $value; @@ -134,14 +142,15 @@ protected function mapAttributes(\DOMNode $domElement, $object) { * @param $object * @return \stdClass */ - protected function mapNamespaces(\DOMNode $domElement, $object) { + protected function mapNamespaces(\DOMNode $domElement, $object) + { $elementNamespaces = $this->getNameSpaces($domElement); $parentNamespaces = $this->getNameSpaces($domElement->parentNode); $newNamespaces = array_diff($elementNamespaces, $parentNamespaces); unset($newNamespaces['xmlns:xml']); $attributesKey = self::ATTR_KEY; - foreach($newNamespaces as $key => $value) { + foreach ($newNamespaces as $key => $value) { $object->$attributesKey[$key] = $value; } @@ -152,9 +161,10 @@ protected function mapNamespaces(\DOMNode $domElement, $object) { * @param \DOMNode $domElement * @return array */ - protected function getNameSpaces(\DOMNode $domElement) { + protected function getNameSpaces(\DOMNode $domElement) + { $namespaces = array(); - if(!is_null($domElement->ownerDocument)) { + if (!is_null($domElement->ownerDocument)) { $xpath = new \DOMXPath($domElement->ownerDocument); /** @var \DOMNode $node */ foreach ($xpath->query('namespace::*', $domElement) as $node) { @@ -171,13 +181,13 @@ protected function getNameSpaces(\DOMNode $domElement) { * @param bool $isElementArray * @return mixed */ - protected function mapDomElement(\DOMNode $element, $object, $isElementArray) { + protected function mapDomElement(\DOMNode $element, $object, $isElementArray) + { $value = $this->domNodeToObject($element); $key = $element->nodeName; - if($isElementArray) { + if ($isElementArray) { Iteration::pushArrayValue($object, $key, $value); - } - else { + } else { $object->$key = $value; } $result = $object; @@ -191,18 +201,19 @@ protected function mapDomElement(\DOMNode $element, $object, $isElementArray) { * @param bool $isElementArray * @return mixed */ - protected function mapDomText(\DOMNode $element, $object, $isElementArray) { + protected function mapDomText(\DOMNode $element, $object, $isElementArray) + { $value = Iteration::typeFilter($element->nodeValue); $result = $value; $attributesKey = self::ATTR_KEY; $valueKey = self::VALUE_KEY; - if(isset($object->$attributesKey)) { + if (isset($object->$attributesKey)) { $result = clone $object; $result->$valueKey = $value; } - if($isElementArray) { + if ($isElementArray) { $result = Iteration::pushArrayValue($object, $valueKey, $result); } @@ -214,14 +225,44 @@ protected function mapDomText(\DOMNode $element, $object, $isElementArray) { * @param object $model * @return string */ - public function unmap($model) { + public function unmap($model) + { + $modelClass = new ModelClass($model); + $source = $this->unmapModel($model); - $modelClass = new ModelClass($model); $rootName = $modelClass->getRootName(); - $xml = $this->objectToXml($source, $rootName); - return $xml; + $document = $this->getDocument( + $rootName, + $this->getAnnotation($modelClass, XmlAnnotationEnum::XML_VERSION, '1.0'), + $this->getAnnotation($modelClass, XmlAnnotationEnum::XML_ENCODING, 'utf-8'), + $this->getAnnotation($modelClass, XmlAnnotationEnum::XML_NAMESPACES, null, + function ($value) { + if (preg_match_all('/(([a-z]+)\=|)\"(.*?)\"/', $value, $m)) { + return array_combine(array_map(function ($a) { + return 'xmlns' . ((!empty($a)) ? ':' . $a : ''); + }, $m[2]), $m[3]); + } + } + + ) + ); + + return $this->objectToXml($source, $document); + } + + private function getAnnotation(ModelClass $classInfo, $xmlAnnotationName, $default = null, $annotationHandler = null) + { + $value = $default; + if ($classInfo->getDocBlock()->hasAnnotation($xmlAnnotationName) && + !Validation::isEmpty($classInfo->getDocBlock()->getAnnotation($xmlAnnotationName))) { + $value = $classInfo->getDocBlock()->getFirstAnnotation($xmlAnnotationName); + if (isset($annotationHandler) && is_callable($annotationHandler)) { + $value = call_user_func_array($annotationHandler, ['value' => $value]); + } + } + return $value; } /** @@ -229,26 +270,27 @@ public function unmap($model) { * @param object $model * @return \stdClass */ - protected function unmapModel($model) { - if(!is_object($model) || Validation::isEmpty($model)) { + protected function unmapModel($model) + { + if (!is_object($model) || Validation::isEmpty($model)) { throw new \InvalidArgumentException('Model must be an object with properties.'); } $modelClass = new ModelClass($model); $unmappedObject = new \stdClass(); - foreach($modelClass->getProperties() as $property) { + + foreach ($modelClass->getProperties() as $property) { $propertyKey = $property->getName(); $propertyValue = $property->getPropertyValue(); - if(Validation::isEmpty($propertyValue)) { + if (Validation::isEmpty($propertyValue)) { continue; } - - if($property->getDocBlock()->hasAnnotation(AnnotationEnum::XML_ATTRIBUTE)) { + if ($property->getDocBlock()->hasAnnotation(AnnotationEnum::XML_ATTRIBUTE)) { $attributeKey = self::ATTR_KEY; $unmappedObject->$attributeKey[$propertyKey] = $propertyValue; continue; } - if($property->getDocBlock()->hasAnnotation(AnnotationEnum::XML_NODE_VALUE)) { + if ($property->getDocBlock()->hasAnnotation(AnnotationEnum::XML_NODE_VALUE)) { $valueKey = self::VALUE_KEY; $unmappedObject->$valueKey = $propertyValue; continue; @@ -261,32 +303,66 @@ protected function unmapModel($model) { } /** - * @param object $source - * @param string $elementName - * @return string - * @throws ModelMapperException + * @param string $root + * @param string $version + * @param string $encoding + * @param null $namespaces + * @return \DOMDocument */ - protected function objectToXml($source, $elementName) { - $elementXml = '<'.$elementName.'>'; + protected function getDocument($root, $version = "1.0", $encoding = "UTF-8", $namespaces = null) + { $domDocument = new \DOMDocument(); - $domDocument->loadXML($elementXml); - $domElement = $domDocument->documentElement; + $domDocument->version = $version; + $domDocument->encoding = $encoding; + + $domElement = $domDocument->createElement($root); + + if (isset($namespaces)) { + foreach ($namespaces as $qualifiedName => $namespaceURI) { + $domElement->setAttributeNS('http://www.w3.org/2000/xmlns/', + $qualifiedName, $namespaceURI); + } + } + $domDocument->appendChild($domElement); + + return $domDocument; + } + + /** + * @param $source + * @param \DOMElement $domElement + * @internal param \DOMElement $element + */ + protected function processTheSources($source, \DOMElement $domElement) + { $this->addDomElementAttributes($source, $domElement); $valueKey = self::VALUE_KEY; - if(isset($source->$valueKey)) { - if(is_bool($source->$valueKey)) { + if (isset($source->$valueKey)) { + if (is_bool($source->$valueKey)) { $source->$valueKey = ($source->$valueKey) ? 'true' : 'false'; } $domElement->nodeValue = $source->$valueKey; - } - else { + } else { foreach ($source as $key => $value) { $this->populateDomElementByType($domElement, $key, $value); } } + } + + /** + * @param object $source + * @param $domDocument + * @return string + */ + protected function objectToXml($source, $domDocument) + { + + $domElement = $domDocument->documentElement; + + $this->processTheSources($source, $domElement); $xml = $domElement->ownerDocument->saveXML(); $xml = str_replace("\n", "", $xml); @@ -299,14 +375,13 @@ protected function objectToXml($source, $elementName) { * @param string $key * @param mixed $value */ - protected function populateDomElementByType(\DOMElement $domElement, $key, $value) { - if(is_object($value)) { + protected function populateDomElementByType(\DOMElement $domElement, $key, $value) + { + if (is_object($value)) { $this->addDomElementObject($domElement, $key, $value); - } - elseif(is_array($value)) { + } elseif (is_array($value)) { $this->addDomElementArray($domElement, $key, $value); - } - else { + } else { $this->addDomElement($domElement, $key, $value); } } @@ -315,10 +390,11 @@ protected function populateDomElementByType(\DOMElement $domElement, $key, $valu * @param object $source * @param \DOMElement $domElement */ - protected function addDomElementAttributes($source, \DOMElement $domElement) { + protected function addDomElementAttributes($source, \DOMElement $domElement) + { $attributesKey = self::ATTR_KEY; - if(isset($source->$attributesKey) && !Validation::isEmpty($source->$attributesKey)){ - foreach($source->$attributesKey as $attrKey => $attrValue) { + if (isset($source->$attributesKey) && !Validation::isEmpty($source->$attributesKey)) { + foreach ($source->$attributesKey as $attrKey => $attrValue) { $domElement->setAttribute($attrKey, $attrValue); } unset($source->$attributesKey); @@ -330,7 +406,8 @@ protected function addDomElementAttributes($source, \DOMElement $domElement) { * @param string $key * @param object $value */ - protected function addDomElementObject(\DOMElement $domElement, $key, $value) { + protected function addDomElementObject(\DOMElement $domElement, $key, $value) + { $child = $this->createDomNode($domElement->ownerDocument, $key, $value); $domElement->appendChild($child); } @@ -340,8 +417,9 @@ protected function addDomElementObject(\DOMElement $domElement, $key, $value) { * @param $key * @param array $value */ - protected function addDomElementArray(\DOMElement $domElement, $key, array $value) { - foreach($value as $arrayKey => $arrayValue) { + protected function addDomElementArray(\DOMElement $domElement, $key, array $value) + { + foreach ($value as $arrayKey => $arrayValue) { $this->populateDomElementByType($domElement, $key, $arrayValue); } } @@ -351,26 +429,33 @@ protected function addDomElementArray(\DOMElement $domElement, $key, array $valu * @param string $key * @param mixed $value */ - protected function addDomElement(\DOMElement $domElement, $key, $value) { - $child = $this->createDomElement($key, $value); + protected function addDomElement(\DOMElement $domElement, $key, $value) + { + $child = $this->createDomElement($key, $value, $domElement->ownerDocument); $domElement->appendChild($child); } /** * @param $name * @param $value - * @param null $uri + * @param \DOMDocument $document * @return \DOMElement * @throws ModelMapperException + * @internal param null $uri */ - protected function createDomElement($name, $value, $uri = null) { - if(!Xml::isValidElementName($name)) { - throw new ModelMapperException('Property name "' . $name . '" contains invalid xml element characters.'); - } - if(is_bool($value)) { + protected function createDomElement($name, $value, \DOMDocument $document) + { + + if (is_bool($value)) { $value = ($value) ? 'true' : 'false'; } - $element = new \DOMElement($name, $value, $uri); + + try { + $element = $document->createElement($name, $value); + } catch (\DOMException $e) { + throw new ModelMapperException('Property name "' . $name . '" contains invalid xml element characters.' + . $e->getMessage(), 0, $e); + } return $element; } @@ -381,12 +466,10 @@ protected function createDomElement($name, $value, $uri = null) { * @param object $value * @return \DOMNode */ - protected function createDomNode(\DOMDocument $domDocument, $name, $value) { - $xmlValue = $this->objectToXml($value, $name); - $domDoc = new \DOMDocument(); - $domDoc->loadXML($xmlValue); - $node = $domDocument->importNode($domDoc->documentElement, true); - + protected function createDomNode(\DOMDocument $domDocument, $name, $value) + { + $node = $domDocument->createElement($name); + $this->processTheSources($value, $node); return $node; } @@ -397,9 +480,10 @@ protected function createDomNode(\DOMDocument $domDocument, $name, $value) { * @param mixed $value * @return mixed */ - protected function mapPropertyByType(ModelPropertyType $propertyType, $value) { + protected function mapPropertyByType(ModelPropertyType $propertyType, $value) + { $value = Iteration::typeFilter($value); - if($propertyType->getActualType() === TypeEnum::ARR && !is_array($value) && !is_null($value)) { + if ($propertyType->getActualType() === TypeEnum::ARR && !is_array($value) && !is_null($value)) { $value = array($value); } diff --git a/tests/Mapper/XmlModelMapperTest2.php b/tests/Mapper/XmlModelMapperTest2.php new file mode 100644 index 0000000..89daba0 --- /dev/null +++ b/tests/Mapper/XmlModelMapperTest2.php @@ -0,0 +1,55 @@ +xmlMapper = new XmlModelMapper(); + parent::setUp(); + } + + /** + * @param $expectedModel + * @param $xml + * @dataProvider validValues + */ + public function testMap($expectedModel, $xml) { + $actualModel = $this->xmlMapper->map($xml, new XmlTestUrlSet()); + $this->assertEquals($expectedModel, $actualModel); + } + + + /** + * @param $model + * @param $expectedXml + * @dataProvider validValues + */ + public function testUnmap($model, $expectedXml) { + $actualXml = $this->xmlMapper->unmap($model); + $this->assertEquals($expectedXml, $actualXml); + } + + public function validValues() { + + $model = new XmlTestUrlSet(); + + $xml = Xml::loadFromFile(__DIR__ . '/../testFiles/valid2.xml'); + + return array( + array($model, $xml) + ); + } +} diff --git a/tests/XmlTestUrl.php b/tests/XmlTestUrl.php new file mode 100644 index 0000000..1efda3f --- /dev/null +++ b/tests/XmlTestUrl.php @@ -0,0 +1,20 @@ +loc = 'loc_value_1'; + + $this->url[] = $url; + + $url = new XmlTestUrl(); + $url->loc = 'loc_value_2'; + + $this->url[] = $url; + + + } + +} \ No newline at end of file diff --git a/tests/testFiles/valid2.xml b/tests/testFiles/valid2.xml new file mode 100644 index 0000000..788e949 --- /dev/null +++ b/tests/testFiles/valid2.xml @@ -0,0 +1 @@ +loc_value_1linkloc_value_2link \ No newline at end of file From 260921782579c8816a13426a5ed9ab58dcf6c25f Mon Sep 17 00:00:00 2001 From: =Somov Nikolay <=somov.nn@gmail.com> Date: Fri, 9 Mar 2018 16:52:11 +0200 Subject: [PATCH 2/2] Fixed parsing namespace annotation --- src/Mapper/XmlModelMapper.php | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Mapper/XmlModelMapper.php b/src/Mapper/XmlModelMapper.php index 2d4cd1a..87f16ed 100644 --- a/src/Mapper/XmlModelMapper.php +++ b/src/Mapper/XmlModelMapper.php @@ -239,10 +239,13 @@ public function unmap($model) $this->getAnnotation($modelClass, XmlAnnotationEnum::XML_ENCODING, 'utf-8'), $this->getAnnotation($modelClass, XmlAnnotationEnum::XML_NAMESPACES, null, function ($value) { - if (preg_match_all('/(([a-z]+)\=|)\"(.*?)\"/', $value, $m)) { - return array_combine(array_map(function ($a) { - return 'xmlns' . ((!empty($a)) ? ':' . $a : ''); - }, $m[2]), $m[3]); + if (preg_match_all('/(([a-z]+:|)([a-zA-z]+)\=|)\"(.*?)\"/', $value, $m)) { + return array_combine( + array_map(function ($name, $prefix) { + return (!empty($prefix) ? rtrim($prefix, ':') : 'xmlns') + . (!empty($name) ? ':' . rtrim($name, ':') : ''); + }, $m[3], $m[2]) + , $m[4]); } } @@ -252,8 +255,12 @@ function ($value) { return $this->objectToXml($source, $document); } - private function getAnnotation(ModelClass $classInfo, $xmlAnnotationName, $default = null, $annotationHandler = null) - { + private function getAnnotation( + ModelClass $classInfo, + $xmlAnnotationName, + $default = null, + $annotationHandler = null + ) { $value = $default; if ($classInfo->getDocBlock()->hasAnnotation($xmlAnnotationName) && !Validation::isEmpty($classInfo->getDocBlock()->getAnnotation($xmlAnnotationName))) { @@ -319,8 +326,13 @@ protected function getDocument($root, $version = "1.0", $encoding = "UTF-8", $na if (isset($namespaces)) { foreach ($namespaces as $qualifiedName => $namespaceURI) { - $domElement->setAttributeNS('http://www.w3.org/2000/xmlns/', - $qualifiedName, $namespaceURI); + try { + $domElement->setAttributeNS('http://www.w3.org/2000/xmlns/', + $qualifiedName, $namespaceURI); + } catch (\DOMException $e) { + $domElement->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', + $qualifiedName, $namespaceURI); + } } } $domDocument->appendChild($domElement);