Skip to content

Commit

Permalink
Handle plain array collections and improve object/array processing
Browse files Browse the repository at this point in the history
  • Loading branch information
magicsunday committed May 21, 2021
1 parent 40d8da1 commit b04008c
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 56 deletions.
93 changes: 75 additions & 18 deletions src/JsonMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

use function array_key_exists;
use function in_array;
use function is_array;

/**
* JsonMapper
Expand Down Expand Up @@ -86,7 +87,7 @@ public function __construct(
$this->extractor = $extractor;
$this->accessor = $accessor;
$this->nameConverter = $nameConverter;
$this->defaultType = new Type('string');
$this->defaultType = new Type(Type::BUILTIN_TYPE_STRING);
$this->classMap = $classMap;
}

Expand Down Expand Up @@ -125,7 +126,7 @@ public function addCustomClassMapEntry(string $className, Closure $closure): Jso
* @param string $className The class name of the initial element
* @param null|string $collectionClassName The class name of a collection used to assign the initial elements
*
* @return null|object|object[]
* @return null|mixed
*
* @throws InvalidArgumentException
* @throws DomainException
Expand All @@ -134,15 +135,31 @@ public function map($json, string $className, string $collectionClassName = null
{
$this->assertClassesExists($className, $collectionClassName);

if ($collectionClassName) {
// Map all elements of the JSON array to this collection
return $this->makeInstance(
$collectionClassName,
$this->asCollection(
// Handle collections
if ($this->isArrayOrObject($json)) {
if ($collectionClassName) {
// Map array into collection class if given
return $this->makeInstance(
$collectionClassName,
$this->asCollection(
$json,
new Type(
Type::BUILTIN_TYPE_OBJECT,
false,
$className
)
)
);
}

// Handle plain array collections
if ($this->isNumericIndexArray($json)) {
// Map all elements of the JSON array to an array
return $this->asCollection(
$json,
new Type('object', false, $className, false)
)
);
new Type(Type::BUILTIN_TYPE_OBJECT, false, $className)
);
}
}

$properties = $this->getProperties($className);
Expand All @@ -166,6 +183,42 @@ public function map($json, string $className, string $collectionClassName = null
return $entity;
}

/**
* Returns TRUE if the given json contains integer property keys.
*
* @param mixed $json
*
* @return bool
*/
private function isNumericIndexArray($json): bool
{
foreach ($json as $propertyName => $propertyValue) {
if (is_int($propertyName)) {
return true;
}
}

return false;
}

/**
* Returns TRUE if the given json is an array or object.
*
* @param mixed $json
*
* @return bool
*/
private function isArrayOrObject($json): bool
{
foreach ($json as $propertyValue) {
if (!is_array($propertyValue) && !is_object($propertyValue)) {
return false;
}
}

return true;
}

/**
* Assert that the given classes exists.
*
Expand All @@ -192,9 +245,9 @@ private function assertClassesExists(string $className, string $collectionClassN
* @param string|Closure $className The class to instantiate
* @param mixed $constructorArguments,... The arguments for the constructor
*
* @return object
* @return mixed
*/
private function makeInstance($className, ...$constructorArguments): object
private function makeInstance($className, ...$constructorArguments)
{
return new $className(...$constructorArguments);
}
Expand Down Expand Up @@ -242,7 +295,7 @@ private function getType(string $className, string $propertyName): Type
* @param mixed $json
* @param Type $type
*
* @return null|array|bool|float|mixed
* @return null|mixed
*
* @throws DomainException
*/
Expand All @@ -253,7 +306,7 @@ private function getValue($json, Type $type)
$collection = $this->asCollection($json, $collectionType);

// Create a new instance of the collection class
if ($type->getBuiltinType() === 'object') {
if ($type->getBuiltinType() === Type::BUILTIN_TYPE_OBJECT) {
return $this->makeInstance(
$this->getClassName($type),
$collection
Expand All @@ -270,7 +323,7 @@ private function getValue($json, Type $type)

$builtinType = $type->getBuiltinType();

if ($builtinType === 'object') {
if ($builtinType === Type::BUILTIN_TYPE_OBJECT) {
return $this->asObject($json, $type);
}

Expand Down Expand Up @@ -312,12 +365,16 @@ private function getClassName(Type $type)
* @param mixed $json
* @param Type $type
*
* @return mixed[]
* @return null|array<mixed>
*
* @throws DomainException
*/
private function asCollection($json, Type $type): array
private function asCollection($json, Type $type): ?array
{
if ($json === null) {
return null;
}

$collection = [];

foreach ($json as $key => $value) {
Expand All @@ -333,7 +390,7 @@ private function asCollection($json, Type $type): array
* @param mixed $json
* @param Type $type
*
* @return mixed
* @return null|mixed
*
* @throws DomainException
*/
Expand Down
95 changes: 76 additions & 19 deletions test/JsonMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,32 +114,28 @@ public function mapArrayOrCollection(string $jsonString): void
self::assertSame('Item 2', $iterator->current()->name);
}

/**
* @return string[][]
*/
public function mapArrayOrCollectionWithStringKeysJsonDataProvider(): array
{
return [
'mapCollectionWithStringKeys' => [
DataProvider::mapCollectionWithStringKeys(),
],
];
}

/**
* Tests mapping an array or collection of objects.
*
* @dataProvider mapArrayOrCollectionWithStringKeysJsonDataProvider
* @test
*
* @param string $jsonString
*/
public function mapArrayOrCollectionWithStringKeys(string $jsonString): void
public function mapArrayOrCollectionWithStringKeys(): void
{
/** @var Collection<Base> $result */
$result = $this->getJsonMapper()
->map(
$this->getJsonArray($jsonString),
$this->getJsonArray(
<<<JSON
{
"foo": {
"name": "Item 1"
},
"bar": {
"name": "Item 2"
}
}
JSON
),
Base::class,
Collection::class
);
Expand Down Expand Up @@ -268,7 +264,15 @@ public function mapCustomType(string $jsonString): void
->addType(
CustomConstructor::class,
static function ($value): ?CustomConstructor {
return $value ? new CustomConstructor($value['name']) : null;
if (is_array($value) && $value['name']) {
return new CustomConstructor($value['name']);
}

if ($value->name) {
return new CustomConstructor($value->name);
}

return null;
}
)
->map(
Expand Down Expand Up @@ -356,7 +360,7 @@ public function mapObjectUsingCustomClassName(string $jsonString): void
Person::class,
// Map each entry of the collection to a separate class
static function ($value): string {
if ($value['is_vip']) {
if ((is_array($value) && $value['is_vip']) || (($value instanceof \stdClass) && $value->is_vip)) {
return VipPerson::class;
}

Expand Down Expand Up @@ -451,4 +455,57 @@ public function checkCamelCasePropertyConverter(): void
self::assertInstanceOf(Base::class, $result);
self::assertSame('Private property value', $result->getPrivateProperty());
}

/**
* Tests mapping a JSON array with objects into a plain PHP array with objects of given class.
*
* @test
*/
public function mapArrayOfObjects(): void
{
$result = $this->getJsonMapper()
->map(
$this->getJsonArray(<<<JSON
[
{
"name": "foo"
},
{
"name": "bar"
}
]
JSON
),
Base::class
);

self::assertIsArray($result);
self::assertContainsOnlyInstancesOf(Base::class, $result);
self::assertSame('foo', $result[0]->name);
self::assertSame('bar', $result[1]->name);
}

/**
* Tests mapping a JSON object into an PHP object ignoring a given collection class as the
* JSON does not contain a collection.
*
* @test
*/
public function mapSingleObjectWithGivenCollection(): void
{
$result = $this->getJsonMapper()
->map(
$this->getJsonArray(<<<JSON
{
"name": "foo"
}
JSON
),
Base::class,
Collection::class
);

self::assertInstanceOf(Base::class, $result);
self::assertSame('foo', $result->name);
}
}
8 changes: 0 additions & 8 deletions test/Provider/DataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,6 @@ public static function mapCollectionJson(): string
return (string) file_get_contents(__DIR__ . '/_files/MapCollection.json');
}

/**
* @return string
*/
public static function mapCollectionWithStringKeys(): string
{
return (string) file_get_contents(__DIR__ . '/_files/MapCollectionWithStringKeys.json');
}

/**
* @return string
*/
Expand Down
8 changes: 0 additions & 8 deletions test/Provider/_files/MapCollectionWithStringKeys.json

This file was deleted.

6 changes: 3 additions & 3 deletions test/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ protected function getJsonMapper(array $classMap = []): JsonMapper
*
* @param string $jsonString
*
* @return mixed[]
* @return object[]|object
*/
protected function getJsonArray(string $jsonString): array
protected function getJsonArray(string $jsonString)
{
try {
return json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
return json_decode($jsonString, false, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
return [];
}
Expand Down

0 comments on commit b04008c

Please sign in to comment.