Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to typehint for the type (array/hash) of the root item to be serialized #728

Merged
merged 8 commits into from
Apr 21, 2017
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 @@ -80,6 +80,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 @@ -89,7 +91,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 @@ -266,6 +267,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