Skip to content

Commit

Permalink
Merge pull request #728 from schmittjoh/first-typehint
Browse files Browse the repository at this point in the history
Allow to typehint for the type (array/hash) of the root item to be serialized
  • Loading branch information
goetas authored Apr 21, 2017
2 parents b8683d2 + 9eebede commit 1d963f9
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 5 deletions.
47 changes: 47 additions & 0 deletions doc/cookbook/arrays.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Serailizing arrays and hashes
=============================

Introduction
------------
Serializing arrays and hashes (a concept that in PHP has not explicit boundaries)
can be challenging. The serializer offers via ``@Type`` annotation different options
to configure its behavior, but if we try to serialize directly an array
(not as a property of an object), we need to use context information to determine the
array "type"

Examples
--------

In case of a JSON serialization:

.. code-block :: php
<?php
// default (let the PHP's json_encode function decide)
$serializer->serialize([1, 2]); // [1, 2]
$serializer->serialize(['a', 'b']); // ['a', 'b']
$serializer->serialize(['c' => 'd']); // {"c" => "d"}
// same as default (let the PHP's json_encode function decide)
$serializer->serialize([1, 2], SerializationContext::create()->setInitialType('array')); // [1, 2]
$serializer->serialize([1 => 2], SerializationContext::create()->setInitialType('array')); // {"1": 2}
$serializer->serialize(['a', 'b'], SerializationContext::create()->setInitialType('array')); // ['a', 'b']
$serializer->serialize(['c' => 'd'], SerializationContext::create()->setInitialType('array')); // {"c" => "d"}
// typehint as strict array, keys will be always discarded
$serializer->serialize([], SerializationContext::create()->setInitialType('array<integer>')); // []
$serializer->serialize([1, 2], SerializationContext::create()->setInitialType('array<integer>')); // [1, 2]
$serializer->serialize(['a', 'b'], SerializationContext::create()->setInitialType('array<integer>')); // ['a', 'b']
$serializer->serialize(['c' => 'd'], SerializationContext::create()->setInitialType('array<string>')); // ["d"]
// typehint as hash, keys will be always considered
$serializer->serialize([], SerializationContext::create()->setInitialType('array<integer,integer>')); // {}
$serializer->serialize([1, 2], SerializationContext::create()->setInitialType('array<integer,integer>')); // {"0" : 1, "1" : 2}
$serializer->serialize(['a', 'b'], SerializationContext::create()->setInitialType('array<integer,integer>')); // {"0" : "a", "1" : "b"}
$serializer->serialize(['c' => 'd'], SerializationContext::create()->setInitialType('array<string,string>')); // {"d" : "d"}
.. note ::
This applies only for the JSON and YAML serialization.
10 changes: 8 additions & 2 deletions src/JMS/Serializer/GenericSerializationVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,15 @@ public function visitDouble($data, array $type, Context $context)
*/
public function visitArray($data, array $type, Context $context)
{
$this->dataStack->push($data);

$isHash = isset($type['params'][1]);

if (null === $this->root) {
$this->root = array();
$this->root = $isHash ? new \ArrayObject() : array();
$rs = &$this->root;
} else {
$rs = array();
$rs = $isHash ? new \ArrayObject() : array();
}

$isList = isset($type['params'][0]) && ! isset($type['params'][1]);
Expand All @@ -114,6 +118,8 @@ public function visitArray($data, array $type, Context $context)
}
}

$this->dataStack->pop();

return $rs;
}

Expand Down
26 changes: 26 additions & 0 deletions src/JMS/Serializer/SerializationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class SerializationContext extends Context
/** @var \SplStack */
private $visitingStack;

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

public static function create()
{
return new self();
Expand Down Expand Up @@ -115,4 +120,25 @@ public function getVisitingSet()
{
return $this->visitingSet;
}

/**
* @param string $type
* @return $this
*/
public function setInitialType($type)
{
$this->initialType = $type;
$this->attributes->set('initial_type', $type);
return $this;
}

/**
* @return string|null
*/
public function getInitialType()
{
return $this->initialType
? $this->initialType
: $this->attributes->containsKey('initial_type') ? $this->attributes->get('initial_type')->get() : null;
}
}
8 changes: 6 additions & 2 deletions src/JMS/Serializer/Serializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ public function serialize($data, $format, SerializationContext $context = null)

return $this->serializationVisitors->get($format)
->map(function(VisitorInterface $visitor) use ($context, $data, $format) {
$this->visit($visitor, $context, $visitor->prepare($data), $format);
$type = $context->getInitialType() !== null ? $this->typeParser->parse($context->getInitialType()) : null;

$this->visit($visitor, $context, $visitor->prepare($data), $format, $type);

return $visitor->getResult();
})
Expand Down Expand Up @@ -143,7 +145,9 @@ public function toArray($data, SerializationContext $context = null)

return $this->serializationVisitors->get('json')
->map(function(JsonSerializationVisitor $visitor) use ($context, $data) {
$this->visit($visitor, $context, $data, 'json');
$type = $context->getInitialType() !== null ? $this->typeParser->parse($context->getInitialType()) : null;

$this->visit($visitor, $context, $data, 'json', $type);
$result = $this->convertArrayObjects($visitor->getRoot());

if ( ! is_array($result)) {
Expand Down
4 changes: 3 additions & 1 deletion src/JMS/Serializer/YamlSerializationVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ public function visitString($data, array $type, Context $context)
*/
public function visitArray($data, array $type, Context $context)
{
$isHash = isset($type['params'][1]);

$count = $this->writer->changeCount;
$isList = (isset($type['params'][0]) && ! isset($type['params'][1]))
|| array_keys($data) === range(0, count($data) - 1);
Expand All @@ -90,7 +92,7 @@ public function visitArray($data, array $type, Context $context)
continue;
}

if ($isList) {
if ($isList && !$isHash) {
$this->writer->writeln('-');
} else {
$this->writer->writeln(Inline::dump($k).':');
Expand Down
12 changes: 12 additions & 0 deletions tests/JMS/Serializer/Tests/Serializer/ContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,18 @@ public function testCanVisitScalars($scalar)
$context->stopVisiting($scalar);
}

public function testInitialTypeCompatibility()
{
$context = SerializationContext::create();
$context->setInitialType('foo');
$this->assertEquals('foo', $context->getInitialType());
$this->assertEquals('foo', $context->attributes->get('initial_type')->get());

$context = SerializationContext::create();
$context->attributes->set('initial_type', 'foo');
$this->assertEquals('foo', $context->getInitialType());
}

public function testSerializeNullOption()
{
$context = SerializationContext::create();
Expand Down
68 changes: 68 additions & 0 deletions tests/JMS/Serializer/Tests/Serializer/JsonSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use JMS\Serializer\VisitorInterface;
use JMS\Serializer\Tests\Fixtures\Author;
use JMS\Serializer\Tests\Fixtures\AuthorList;
use JMS\Serializer\SerializationContext;

class JsonSerializationTest extends BaseSerializationTest
{
Expand Down Expand Up @@ -268,6 +269,73 @@ public function testSerializeArrayWithEmptyObject()
$this->assertEquals('{"0":{}}', $this->serialize(array(new \stdClass())));
}

public function testSerializeRootArrayWithDefinedKeys()
{
$author1 = new Author("Jim");
$author2 = new Author("Mark");

$data = array(
'jim' => $author1,
'mark' => $author2,
);

$this->assertEquals('{"jim":{"full_name":"Jim"},"mark":{"full_name":"Mark"}}', $this->serializer->serialize($data, $this->getFormat(), SerializationContext::create()->setInitialType('array')));
$this->assertEquals('[{"full_name":"Jim"},{"full_name":"Mark"}]', $this->serializer->serialize($data, $this->getFormat(), SerializationContext::create()->setInitialType('array<JMS\Serializer\Tests\Fixtures\Author>')));
$this->assertEquals('{"jim":{"full_name":"Jim"},"mark":{"full_name":"Mark"}}', $this->serializer->serialize($data, $this->getFormat(), SerializationContext::create()->setInitialType('array<string,JMS\Serializer\Tests\Fixtures\Author>')));

$data = array(
$author1,
$author2,
);
$this->assertEquals('[{"full_name":"Jim"},{"full_name":"Mark"}]', $this->serializer->serialize($data, $this->getFormat(), SerializationContext::create()->setInitialType('array')));
$this->assertEquals('{"0":{"full_name":"Jim"},"1":{"full_name":"Mark"}}', $this->serializer->serialize($data, $this->getFormat(), SerializationContext::create()->setInitialType('array<int,JMS\Serializer\Tests\Fixtures\Author>')));
$this->assertEquals('{"0":{"full_name":"Jim"},"1":{"full_name":"Mark"}}', $this->serializer->serialize($data, $this->getFormat(), SerializationContext::create()->setInitialType('array<string,JMS\Serializer\Tests\Fixtures\Author>')));
}

public function getTypeHintedArrays()
{
return [

[[1, 2], '[1,2]', null],
[['a', 'b'], '["a","b"]', null],
[['a' => 'a', 'b' => 'b'], '{"a":"a","b":"b"}', null],

[[], '[]', null],
[[], '[]', SerializationContext::create()->setInitialType('array')],
[[], '[]', SerializationContext::create()->setInitialType('array<integer>')],
[[], '{}', SerializationContext::create()->setInitialType('array<string,integer>')],


[[1, 2], '[1,2]', SerializationContext::create()->setInitialType('array')],
[[1 => 1, 2 => 2], '{"1":1,"2":2}', SerializationContext::create()->setInitialType('array')],
[[1 => 1, 2 => 2], '[1,2]', SerializationContext::create()->setInitialType('array<integer>')],
[['a', 'b'], '["a","b"]', SerializationContext::create()->setInitialType('array<string>')],

[[1 => 'a', 2 => 'b'], '["a","b"]', SerializationContext::create()->setInitialType('array<string>')],
[['a' => 'a', 'b' => 'b'], '["a","b"]', SerializationContext::create()->setInitialType('array<string>')],


[[1,2], '{"0":1,"1":2}', SerializationContext::create()->setInitialType('array<integer,integer>')],
[[1,2], '{"0":1,"1":2}', SerializationContext::create()->setInitialType('array<string,integer>')],
[[1,2], '{"0":"1","1":"2"}', SerializationContext::create()->setInitialType('array<string,string>')],


[['a', 'b'], '{"0":"a","1":"b"}', SerializationContext::create()->setInitialType('array<integer,string>')],
[['a' => 'a', 'b' => 'b'], '{"a":"a","b":"b"}', SerializationContext::create()->setInitialType('array<string,string>')],
];
}

/**
* @dataProvider getTypeHintedArrays
* @param array $array
* @param string $expected
* @param SerializationContext|null $context
*/
public function testTypeHintedArraySerialization(array $array, $expected, $context = null)
{
$this->assertEquals($expected, $this->serialize($array, $context));
}

protected function getFormat()
{
return 'json';
Expand Down
46 changes: 46 additions & 0 deletions tests/JMS/Serializer/Tests/Serializer/YamlSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
namespace JMS\Serializer\Tests\Serializer;

use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\SerializationContext;

class YamlSerializationTest extends BaseSerializationTest
{
Expand Down Expand Up @@ -56,6 +57,51 @@ protected function getContent($key)
return file_get_contents($file);
}

public function getTypeHintedArrays()
{
return [

[[1, 2], "- 1\n- 2\n", null],
[['a', 'b'], "- a\n- b\n", null],
[['a' => 'a', 'b' => 'b'], "a: a\nb: b\n", null],

[[], " []\n", null],
[[], " []\n", SerializationContext::create()->setInitialType('array')],
[[], " []\n", SerializationContext::create()->setInitialType('array<integer>')],
[[], " {}\n", SerializationContext::create()->setInitialType('array<string,integer>')],


[[1, 2], "- 1\n- 2\n", SerializationContext::create()->setInitialType('array')],
[[1 => 1, 2 => 2], "1: 1\n2: 2\n", SerializationContext::create()->setInitialType('array')],
[[1 => 1, 2 => 2], "- 1\n- 2\n", SerializationContext::create()->setInitialType('array<integer>')],
[['a', 'b'], "- a\n- b\n", SerializationContext::create()->setInitialType('array<string>')],

[[1 => 'a', 2 => 'b'], "- a\n- b\n", SerializationContext::create()->setInitialType('array<string>')],
[['a' => 'a', 'b' => 'b'], "- a\n- b\n", SerializationContext::create()->setInitialType('array<string>')],


[[1,2], "0: 1\n1: 2\n", SerializationContext::create()->setInitialType('array<integer,integer>')],
[[1,2], "0: 1\n1: 2\n", SerializationContext::create()->setInitialType('array<string,integer>')],
[[1,2], "0: 1\n1: 2\n", SerializationContext::create()->setInitialType('array<string,string>')],


[['a', 'b'], "0: a\n1: b\n", SerializationContext::create()->setInitialType('array<integer,string>')],
[['a' => 'a', 'b' => 'b'], "a: a\nb: b\n", SerializationContext::create()->setInitialType('array<string,string>')],
];
}

/**
* @dataProvider getTypeHintedArrays
* @param array $array
* @param string $expected
* @param SerializationContext|null $context
*/
public function testTypeHintedArraySerialization(array $array, $expected, $context = null)
{
$this->assertEquals($expected, $this->serialize($array, $context));
}


protected function getFormat()
{
return 'yml';
Expand Down

0 comments on commit 1d963f9

Please sign in to comment.