Skip to content

Commit

Permalink
The library updated with new functionalities
Browse files Browse the repository at this point in the history
- adds new `Serializer\XmlSerializer` class
- adds new functions: json_serialize(), json_unserialize(), xml_serialize(), xml_unserialize(), php_serialize() and php_unserialize()
- updates the composer.json (some extensions requirements, new tags, branch alias)
- updates the unit test (some files are renamed)
  • Loading branch information
kodeart authored Aug 30, 2018
1 parent 22e0c31 commit 2c94738
Show file tree
Hide file tree
Showing 16 changed files with 481 additions and 14 deletions.
7 changes: 6 additions & 1 deletion Serializer/JsonSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ final class JsonSerializer implements StringSerializable
*/
private $options;

/**
* JsonSerializer constructor.
*
* @param int $options [optional] JSON encode options
*/
public function __construct(int $options = null)
{
$this->options = $options ??
Expand All @@ -33,7 +38,7 @@ public function serialize($value): string

public function unserialize(string $value)
{
$json = json_decode($value, true);
$json = json_decode(utf8_encode($value), true, 512, JSON_BIGINT_AS_STRING);

if (JSON_ERROR_NONE !== json_last_error()) {
throw KodedException::generic(json_last_error_msg());
Expand Down
172 changes: 172 additions & 0 deletions Serializer/XmlSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php

/*
* This file is part of the Koded package.
*
* (c) Mihail Binev <mihail@kodeart.com>
*
* Please view the LICENSE distributed with this source code
* for the full copyright and license information.
*
*/

namespace Koded\Stdlib\Serializer;

use DateTime;
use DateTimeInterface;
use DOMDocument;
use DOMElement;
use Exception;
use Koded\Stdlib\Interfaces\StringSerializable;

/**
* Class XmlSerializer is heavily copied from excellent
* Propel 3 runtime parser (XmlParser) and modified.
*
*/
final class XmlSerializer implements StringSerializable
{

private $root;

public function __construct(string $root)
{
$this->root = $root;
}

/**
* @param iterable $data
*
* @return string XML
*/
public function serialize($data): string
{
$xml = new DOMDocument('1.0', 'UTF-8');
$xml->preserveWhiteSpace = false;
$xml->formatOutput = true;

$root = $xml->createElement($this->root);
$xml->appendChild($root);
$this->parseFromArray($data, $root);

return $xml->saveXML();
}

/**
* @param string $document XML string
*
* @return array
*/
public function unserialize(string $document)
{
$xml = new DOMDocument('1.0', 'UTF-8');

try {
$xml->loadXML(utf8_encode($document));
} catch (Exception $e) {
return [];
}

return $this->parseFromElement($xml->documentElement);
}

private function parseFromArray(iterable $data, DOMElement $element): DOMElement
{
foreach ($data as $key => $value) {
if (is_numeric($key)) {
$key = $element->nodeName;
if ('s' === mb_substr($key, -1, 1)) {
$key = mb_substr($key, 0, mb_strlen($key) - 1);
}
}

try {
$child = $element->ownerDocument->createElement($key);
} catch (Exception $e) {
error_log(sprintf('[%s] thrown while parsing the data into XML, with message "%s" for the key %s and value %s',
get_class($e),
$e->getMessage(),
var_export($key, true),
var_export($value, true)
));
continue;
}

if (is_array($value)) {
$child = $this->parseFromArray($value, $child);
} elseif (is_string($value)) {
$value = htmlentities($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$child->appendChild($child->ownerDocument->createCDATASection($value));
} elseif ($value instanceof DateTimeInterface) {
$child->setAttribute('type', 'xsd:dateTime');
$child->appendChild($child->ownerDocument->createTextNode($value->format(DateTime::ISO8601)));
} elseif (is_object($value)) {
$child->setAttribute('type', 'xsd:token');
$child->appendChild($child->ownerDocument->createCDATASection(serialize($value)));
} else {
$child->appendChild($child->ownerDocument->createTextNode($value));
}

$element->appendChild($child);
}

return $element;
}

private function parseFromElement(DOMElement $element): array
{
$result = [];
$names = [];

/** @var DOMElement $node */
foreach ($element->childNodes as $node) {
if (XML_TEXT_NODE === $node->nodeType) {
continue;
}

$name = $node->nodeName;

if (isset($names[$name])) {
if (isset($result[$name])) {
$result[$names[$name]] = $result[$name];
unset($result[$name]);
}

$names[$name] += 1;
$index = $names[$name];
} else {
$names[$name] = 0;
$index = $name;
}

$hasChildNodes = $node->hasChildNodes();

if (false === $hasChildNodes) {
$result[$index] = null;
} elseif ('xsd:token' === $node->getAttribute('type')) {
$result[$index] = unserialize($node->firstChild->textContent);
} elseif ($hasChildNodes && false === $this->hasOnlyTextNodes($node)) {
$result[$index] = $this->parseFromElement($node);
} elseif ($hasChildNodes && XML_CDATA_SECTION_NODE === $node->firstChild->nodeType) {
$result[$index] = html_entity_decode($node->firstChild->textContent, ENT_QUOTES | ENT_HTML5);
} elseif ('xsd:dateTime' === $node->getAttribute('type')) {
$result[$index] = new DateTime($node->textContent);
} else {
$result[$index] = $node->textContent;
}
}

return $result;
}

private function hasOnlyTextNodes(DOMElement $node): bool
{
foreach ($node->childNodes as $child) {
if (($child->nodeType !== XML_CDATA_SECTION_NODE) && ($child->nodeType !== XML_TEXT_NODE)) {
return false;
}
}

return true;
}
}
4 changes: 2 additions & 2 deletions Tests/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public function test_should_load_env_file_and_trim_the_namespace()

public function test_should_load_from_env_variable()
{
putenv('CONFIG_FILE=Tests/fixtures/nested_array.php');
putenv('CONFIG_FILE=Tests/fixtures/nested-array.php');
$config = new Config;
$config->fromEnvVariable('CONFIG_FILE');

Expand Down Expand Up @@ -204,6 +204,6 @@ class MockOtherConfigInstance extends Config
public function __construct()
{
parent::__construct();
$this->fromPhpFile(__DIR__ . '/fixtures/nested_array.php');
$this->fromPhpFile(__DIR__ . '/fixtures/nested-array.php');
}
}
2 changes: 1 addition & 1 deletion Tests/ExtendedArgumentsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public function test_flatten($input)

public function test_frakked_up_keys()
{
$data = include __DIR__ . '/fixtures/nested_array.php';
$data = include __DIR__ . '/fixtures/nested-array.php';

$arguments = new ExtendedArguments($data);
$this->assertSame($data, $arguments->toArray(), 'The data is intact');
Expand Down
2 changes: 1 addition & 1 deletion Tests/ImmutableObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,6 @@ public function test_should_filter_out_the_data()

protected function setUp()
{
$this->SUT = new Immutable(require __DIR__ . '/fixtures/nested_array.php');
$this->SUT = new Immutable(require __DIR__ . '/fixtures/nested-array.php');
}
}
42 changes: 42 additions & 0 deletions Tests/JsonSerializerFunctionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Koded\Stdlib;

use Koded\Exceptions\KodedException;
use Koded\Stdlib\Serializer\JsonSerializerTest;
use PHPUnit\Framework\TestCase;

class JsonSerializerFunctionsTest extends TestCase
{

/** @dataProvider data */
public function test_serialize_to_json($data)
{
$this->assertEquals(JsonSerializerTest::SERIALIZED_JSON, json_serialize($data));
}

public function test_unserialize_json()
{
$this->assertEquals(
json_decode(JsonSerializerTest::SERIALIZED_JSON, true),
json_unserialize(JsonSerializerTest::SERIALIZED_JSON)
);
}

public function test_unserialize_error()
{
$this->expectException(KodedException::class);
$this->expectExceptionMessage('[Exception] Syntax error');

json_unserialize('');
}

public function data()
{
return [
[
require __DIR__ . '/fixtures/config-test.php'
]
];
}
}
31 changes: 31 additions & 0 deletions Tests/PhpSerializerFunctionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Koded\Stdlib;

use PHPUnit\Framework\TestCase;

class PhpSerializerFunctionsTest extends TestCase
{

/** @var array */
private $original;

/** @var string */
private $serialized;

public function test_serialize_php()
{
$this->assertEquals($this->serialized, php_serialize($this->original));
}

public function test_unserialize_php()
{
$this->assertEquals($this->original, php_unserialize($this->serialized));
}

protected function setUp()
{
$this->original = require __DIR__ . '/fixtures/config-test.php';
$this->serialized = php_serialize($this->original);
}
}
44 changes: 44 additions & 0 deletions Tests/Serializer/XmlSerializerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Koded\Stdlib\Serializer;

use PHPUnit\Framework\TestCase;

class XmlSerializerTest extends TestCase
{

const XML_FILE = __DIR__ . '/../fixtures/error-message.xml';
const PHP_FILE = __DIR__ . '/../fixtures/error-message.php';

/** @var XmlSerializer */
private $SUT;

public function test_serialize()
{
$xml = $this->SUT->serialize(require self::PHP_FILE);
$this->assertXmlStringEqualsXmlFile(self::XML_FILE, $xml);
}

public function test_unserialize()
{
$array = $this->SUT->unserialize(file_get_contents(self::XML_FILE));
$this->assertEquals(require self::PHP_FILE, $array);
}

public function test_unserialize_error_should_return_empty_array()
{
$this->assertSame([], $this->SUT->unserialize(''));
}

public function test_frankenstein_array()
{
$array = require __DIR__ . '/../fixtures/nested-array.php';
$this->SUT->serialize($array);
$this->assertEquals(require __DIR__ . '/../fixtures/nested-array.php', $array);
}

protected function setUp()
{
$this->SUT = new XmlSerializer('payload');
}
}
31 changes: 31 additions & 0 deletions Tests/XmlSerializerFunctionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Koded\Stdlib;

use Koded\Stdlib\Serializer\XmlSerializerTest;
use PHPUnit\Framework\TestCase;

class XmlSerializerFunctionsTest extends TestCase
{

public function test_serialize_to_xml()
{
$this->assertXmlStringEqualsXmlFile(
XmlSerializerTest::XML_FILE,
xml_serialize('payload', require XmlSerializerTest::PHP_FILE)
);
}

public function test_unserialize()
{
$this->assertEquals(
require XmlSerializerTest::PHP_FILE,
xml_unserialize('payload', file_get_contents(XmlSerializerTest::XML_FILE))
);
}

public function test_unserialize_error_should_return_empty_array()
{
$this->assertSame([], xml_unserialize('', ''));
}
}
Loading

0 comments on commit 2c94738

Please sign in to comment.