Skip to content

Commit

Permalink
Add library and README
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisleversuch committed Oct 24, 2019
1 parent bf39466 commit c6959f4
Show file tree
Hide file tree
Showing 7 changed files with 431 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
151 changes: 150 additions & 1 deletion README.md
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/
13 changes: 13 additions & 0 deletions composer.json
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/"
}
}
}
133 changes: 133 additions & 0 deletions src/Deserializer.php
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;
}
}
41 changes: 41 additions & 0 deletions src/Helpers.php
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']);
}
}
Loading

0 comments on commit c6959f4

Please sign in to comment.