-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
bf39466
commit c6959f4
Showing
7 changed files
with
431 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,150 @@ | ||
# PHP-XML | ||
# PHP-XML | ||
|
||
This is a library for serialization and deserialization between XML and PHP objects. The mapping between XML tag and PHP property is | ||
done using annotations inside PHPDoc comments. | ||
|
||
## Basic Usage | ||
|
||
Given the following classes: | ||
|
||
```php | ||
namespace App; | ||
|
||
class Films { | ||
/** | ||
* @var \App\Film[] | ||
* @serializedName Films | ||
* @serializedList Film | ||
*/ | ||
public $films; | ||
} | ||
``` | ||
|
||
```php | ||
namespace App; | ||
|
||
class Film { | ||
/** | ||
* Film title | ||
* | ||
* @var string|null | ||
* @serializedName Title | ||
*/ | ||
public $title; | ||
|
||
/** | ||
* Release year (USA) | ||
* | ||
* @var int|null | ||
* @serializedName ReleaseYear | ||
*/ | ||
public $releaseYear; | ||
} | ||
``` | ||
|
||
### Deserializing | ||
|
||
```php | ||
$body = <<<EOF | ||
<Films> | ||
<Film> | ||
<Title>Avengers: Assemble</Title> | ||
<ReleaseYear>2012</ReleaseYear> | ||
</Film> | ||
<Film> | ||
<Title>Avengers: Age of Ultron</Title> | ||
<ReleaseYear>2015</ReleaseYear> | ||
</Film> | ||
<Film> | ||
<Title>Avengers: Infinity War</Title> | ||
<ReleaseYear>2018</ReleaseYear> | ||
</Film> | ||
<Film> | ||
<Title>Avengers: Endgame</Title> | ||
<ReleaseYear>2019</ReleaseYear> | ||
</Film> | ||
</Films> | ||
EOF; | ||
|
||
$deserializer = new Deserializer(); | ||
try { | ||
$object = $deserializer->parse($body, Films::class); | ||
var_dump($object); | ||
} catch (ReflectionException $e) { | ||
var_dump($e); | ||
} | ||
``` | ||
|
||
Output | ||
```php | ||
object(App\Films)[7] | ||
public 'films' => | ||
array (size=4) | ||
0 => | ||
object(App\Film)[13] | ||
public 'title' => string 'Avengers: Assemble' (length=18) | ||
public 'releaseYear' => int 2012 | ||
1 => | ||
object(App\Film)[14] | ||
public 'title' => string 'Avengers: Age of Ultron' (length=23) | ||
public 'releaseYear' => int 2015 | ||
2 => | ||
object(App\Film)[15] | ||
public 'title' => string 'Avengers: Infinity War' (length=22) | ||
public 'releaseYear' => int 2018 | ||
3 => | ||
object(App\Film)[16] | ||
public 'title' => string 'Avengers: Endgame' (length=17) | ||
public 'releaseYear' => int 2019 | ||
``` | ||
|
||
### Serializing | ||
|
||
```php | ||
$films = new Films( | ||
[ | ||
new Film("Guardians of the Galaxy", 2014), | ||
new Film("Guardians of the Galaxy Vol. 2", 2017), | ||
] | ||
); | ||
|
||
$serializer = new Serializer(); | ||
try { | ||
$xml = $serializer->write($films, null); | ||
var_dump($xml); | ||
} catch (ReflectionException $e) { | ||
var_dump($e); | ||
} | ||
``` | ||
|
||
Output | ||
```xml | ||
<?xml version="1.0"?> | ||
<Films><Film><Title>Guardians of the Galaxy</Title><ReleaseYear>2014</ReleaseYear></Film><Film><Title>Guardians of the Galaxy Vol. 2</Title><ReleaseYear>2017</ReleaseYear></Film></Films> | ||
``` | ||
|
||
## Annotations | ||
|
||
- @var Defines the type of the property. | ||
- Note: Types must always be fully qualified e.g. ```@var \App\Film``` rather than ```@var Film``` | ||
- @serializedName Defines the XML tag for the property | ||
- @serializedList Defines the property as an array, and specifies the XML tag of the child elements | ||
- @cdata Specifies that the property should be wrapped in a CDATA tag (Used in serialization only) | ||
|
||
## Types | ||
|
||
This library currently supports the following types: | ||
|
||
- DateTime with the format ```Y-m-d\TH:i:s``` | ||
- Scalar types: string, int, float, double, bool | ||
- Custom classes and arrays of custom classes e.g. Foo, Foo[] | ||
|
||
## Possible improvements | ||
|
||
- Allow custom converters to be defined, such as int <-> bool, DateTime formats | ||
- Use [Doctrine Annotations](https://github.com/doctrine/annotations) | ||
|
||
## See Also | ||
|
||
- https://github.com/runz0rd/mapper-php | ||
- http://sabre.io/xml/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"name": "brightec/php-xml", | ||
"description": "Serialize and deserialize between XML and PHP objects", | ||
"license": "MIT", | ||
"require": { | ||
"php": "^7.2" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"PHP_XML\\": "src/" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
<?php | ||
|
||
namespace PHP_XML; | ||
|
||
use DateTime; | ||
use ReflectionClass; | ||
use ReflectionException; | ||
use XMLReader; | ||
|
||
class Deserializer { | ||
/** | ||
* @var XMLReader | ||
*/ | ||
private $reader; | ||
|
||
/** | ||
* @param string $xml | ||
* @param $class | ||
* @return mixed | ||
* @throws ReflectionException | ||
*/ | ||
public function parse(string $xml, $class) { | ||
$this->reader = new XMLReader(); | ||
$this->reader->xml($xml); | ||
return $this->process($this->reader, $class); | ||
} | ||
|
||
/** | ||
* @param XMLReader $reader | ||
* @param string $root Class or primitive type to process | ||
* @param array $tags | ||
* @param mixed $array | ||
* @return mixed | ||
* @throws ReflectionException | ||
*/ | ||
private function process(XMLReader $reader, $root, $tags = [], &$array = null) { | ||
if (Helpers::isTypeScalar($root)) { | ||
$propertyMap = []; | ||
} else { | ||
$reflectionClass = new ReflectionClass($root); | ||
$properties = $reflectionClass->getProperties(); | ||
$propertyMap = $this->createPropertyMap($properties); | ||
$object = $reflectionClass->newInstanceWithoutConstructor(); | ||
} | ||
|
||
$targetTagDepth = max(0, count($tags) - 1); | ||
|
||
while ($reader->read()) { | ||
$xmlProperty = $propertyMap[$reader->name] ?? null; | ||
|
||
if ($reader->nodeType == XMLReader::ELEMENT && !$reader->isEmptyElement) { | ||
$tags[] = $reader->name; | ||
|
||
if (!is_null($xmlProperty)) { | ||
if (isset($xmlProperty->annotations['var'])) { | ||
$type = Helpers::getTypeFromAnnotation($xmlProperty->annotations['var'][0]); | ||
|
||
if ($type == 'DateTime') { | ||
$value = DateTime::createFromFormat('Y-m-d\TH:i:s', strtok($reader->readString(), '.')); | ||
if (!$value) { | ||
$value = DateTime::createFromFormat('Y-m-d\TH:i:sP', strtok($reader->readString(), '.')); | ||
} | ||
} elseif (Helpers::isTypeScalar($type)) { | ||
$value = $reader->readString(); | ||
settype($value, $type); | ||
} else { | ||
$type = str_replace('[]', '', $type); | ||
|
||
if (isset($xmlProperty->annotations['serializedList'])) { | ||
$value = []; | ||
$this->process($reader, $type, ['array'], $value); | ||
$this->processEndTag($xmlProperty, $object, $value, $tags); | ||
} else { | ||
$value = $this->process($reader, $type, $tags); | ||
$this->processEndTag($xmlProperty, $object, $value, $tags); | ||
} | ||
} | ||
} | ||
} elseif (Helpers::isTypeScalar($root)) { | ||
$value = $reader->readString(); | ||
settype($value, $root); | ||
} | ||
} elseif ($reader->nodeType == XMLReader::END_ELEMENT) { | ||
$this->processEndTag($xmlProperty, $object ?? null, $value ?? null, $tags); | ||
|
||
if (!is_null($array) && count($tags) == $targetTagDepth + 1) { | ||
if (isset($object)) { | ||
$array[] = $object; | ||
$object = $reflectionClass->newInstanceWithoutConstructor(); | ||
} else { | ||
$array[] = $value; | ||
} | ||
} | ||
|
||
if (count($tags) == $targetTagDepth) { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
return $object ?? null; | ||
} | ||
|
||
private function processEndTag($xmlProperty, $object, $value, &$tags) { | ||
if (!is_null($xmlProperty)) { | ||
$xmlProperty->property->setValue($object, $value); | ||
} | ||
|
||
array_pop($tags); | ||
} | ||
|
||
/** | ||
* @param \ReflectionProperty[] $properties | ||
* @return array | ||
*/ | ||
private function createPropertyMap(array $properties): array { | ||
$propertyMap = []; | ||
foreach ($properties as $property) { | ||
$docComment = $property->getDocComment(); | ||
$annotations = Helpers::getAnnotations($docComment); | ||
$property->setAccessible(true); | ||
if (isset($annotations['serializedName']) && isset($annotations['var'])) { | ||
$xmlProperty = new XmlProperty(); | ||
$xmlProperty->property = $property; | ||
$xmlProperty->annotations = $annotations; | ||
|
||
$xmlKey = $annotations['serializedName'][0]; | ||
$propertyMap[$xmlKey] = $xmlProperty; | ||
} | ||
} | ||
return $propertyMap; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<?php | ||
|
||
namespace PHP_XML; | ||
|
||
class Helpers { | ||
public static function getAnnotations($docComment): array { | ||
if ($docComment === false) { | ||
return []; | ||
} | ||
|
||
$annotations = []; | ||
|
||
// Strip away the docblock header and footer | ||
// to ease parsing of one line annotations | ||
$docComment = substr($docComment, 3, -2); | ||
|
||
$re = '/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m'; | ||
if (preg_match_all($re, $docComment, $matches)) { | ||
$numMatches = count($matches[0]); | ||
|
||
for ($i = 0; $i < $numMatches; ++$i) { | ||
$annotations[$matches['name'][$i]][] = $matches['value'][$i]; | ||
} | ||
} | ||
|
||
return $annotations; | ||
} | ||
|
||
public static function getTypeFromAnnotation(string $annotation) { | ||
$type = $annotation; | ||
$parts = explode("|", $type); | ||
if (count($parts) > 1) { | ||
$type = $parts[0]; | ||
} | ||
return $type; | ||
} | ||
|
||
public static function isTypeScalar(string $type): bool { | ||
return in_array($type, ['string', 'int', 'float', 'double', 'bool']); | ||
} | ||
} |
Oops, something went wrong.