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

Add nested mapping support via new ObjectMap class #97

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 1.5.1 under development

- no changes in this release.
- New #63: Add nested mapping support via new `ObjectMap` class (@vjik)

## 1.5.0 September 17, 2024

Expand Down
36 changes: 35 additions & 1 deletion docs/guide/en/mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,40 @@ $post = $hydrator->create(Post::class, new ArrayData($data, $map));

This way we take `header` key for `title` and `text` key for `body`.

For nested objects mapping you can use `ObjectMap` class:

```php
use Yiisoft\Hydrator\ArrayData;
use Yiisoft\Hydrator\Hydrator;
use Yiisoft\Hydrator\ObjectMap;

final class Message {
public string $subject = '';
public ?Body $body = null;
}

final class Body {
public string $text = '';
public string $html = '';
}

$hydrator = new Hydrator();

$data = [
'title' => 'Hello, World!',
'textBody' => 'Nice to meet you.',
'htmlBody' => '<h1>Nice to meet you.</h1>',
];
$map = [
'subject' => 'title',
'body' => new ObjectMap([
'text' => 'textBody',
'html' => 'htmlBody',
]),
Comment on lines +73 to +76
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about constructor, method calls, immutable objects etc.?

];
$message = $hydrator->create(Message::class, new ArrayData($data, $map));
```

## Strict mode

You can enable strict mode by passing `true` as a third argument of `ArrayData`:
Expand All @@ -54,7 +88,7 @@ use Yiisoft\Hydrator\ArrayData;

$hydrator = new Hydrator();

$map = ['title' => 'header', 'body' => 'text'],;
$map = ['title' => 'header', 'body' => 'text'];
$post = $hydrator->create(Post::class, new ArrayData($data, $map, true));
```

Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
failOnWarning="true"
stopOnFailure="false"
colors="true"
displayDetailsOnPhpunitDeprecations="true"
>
<php>
<ini name="error_reporting" value="-1"/>
Expand Down
45 changes: 35 additions & 10 deletions src/ArrayData.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,65 @@

use Yiisoft\Strings\StringHelper;

use function array_key_exists;
use function is_array;
use function is_string;
use function strlen;

/**
* Holds data to hydrate an object from and a map to use when populating an object.
*
* @psalm-type MapType=array<string,string|list<string>>
* @psalm-type MapType=array<string,string|list<string>|ObjectMap>
*/
final class ArrayData implements DataInterface
{
private readonly ObjectMap $objectMap;

/**
* @param array $data Data to hydrate object from.
* @param array $map Object property names mapped to keys in the data array that hydrator will use when hydrating
* an object.
* @param array|ObjectMap $map Object property names mapped to keys in the data array that hydrator will use when
* hydrating an object.
* @param bool $strict Whether to hydrate properties from the map only.
*
* @psalm-param MapType $map
* @psalm-param ObjectMap|MapType $map
*/
public function __construct(
private array $data = [],
private array $map = [],
private bool $strict = false,
private readonly array $data = [],
array|ObjectMap $map = [],
private readonly bool $strict = false,
) {
$this->objectMap = is_array($map) ? new ObjectMap($map) : $map;
}

public function getValue(string $name): Result
{
if ($this->strict && !array_key_exists($name, $this->map)) {
if ($this->strict && !$this->objectMap->exists($name)) {
return Result::fail();
}

return $this->getValueByPath($this->data, $this->map[$name] ?? $name);
$path = $this->objectMap->getPath($name) ?? $name;
if ($path instanceof ObjectMap) {
return $this->getValueByObjectMap($this->data, $path);
}

return $this->getValueByPath($this->data, $path);
}

/**
* Get an array given a map as resolved result.
*/
private function getValueByObjectMap(array $data, ObjectMap $objectMap): Result
{
$arrayData = new self($data, $objectMap);

$result = [];
foreach ($objectMap->getNames() as $name) {
$value = $arrayData->getValue($name);
if ($value->isResolved()) {
$result[$name] = $value->getValue();
}
}

return Result::success($result);
}

/**
Expand Down
47 changes: 47 additions & 0 deletions src/ObjectMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator;

use function array_key_exists;

/**
* Class provides mapping object property names to keys in the data array.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Class provides mapping object property names to keys in the data array.
* Class provides a mapping of object property names to keys in the data array.

*
* @psalm-import-type MapType from ArrayData
*/
final class ObjectMap
{
/**
* @param array $map Object property names mapped to keys in the data array that hydrator will use when hydrating
* an object.
* @psalm-param MapType $map
*/
public function __construct(
public readonly array $map
) {
}

/**
* @psalm-return string|list<string>|ObjectMap|null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need description.

*/
public function getPath(string $name): string|array|self|null
{
return $this->map[$name] ?? null;
}

/**
* @return string[]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need description.

* @psalm-return list<string>
*/
public function getNames(): array
{
return array_keys($this->map);
}

public function exists(string $name): bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need description.

{
return array_key_exists($name, $this->map);
}
}
11 changes: 11 additions & 0 deletions tests/ObjectMap/Nested.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\ObjectMap;

final class Nested
{
public string $var = '';
public ?Nested2 $nested2 = null;
}
11 changes: 11 additions & 0 deletions tests/ObjectMap/Nested2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\ObjectMap;

final class Nested2
{
public string $var1 = '';
public string $var2 = '';
}
103 changes: 103 additions & 0 deletions tests/ObjectMap/ObjectMapTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\ObjectMap;

use PHPUnit\Framework\TestCase;
use Yiisoft\Hydrator\ArrayData;
use Yiisoft\Hydrator\Hydrator;
use Yiisoft\Hydrator\ObjectMap;

final class ObjectMapTest extends TestCase
{
public function testBase(): void
{
$hydrator = new Hydrator();

$root = $hydrator->create(
Root::class,
new ArrayData(
['key' => 'test'],
['nested' => new ObjectMap(['var' => 'key'])],
),
);

$this->assertSame('test', $root->nested?->var);
$this->assertNull($root->nested->nested2);
}

public function testNested2(): void
{
$hydrator = new Hydrator();

$root = $hydrator->create(
Root::class,
new ArrayData(
['a' => 'A', 'b' => ['b1' => 'B1'], 'c' => 'C'],
[
'nested' => new ObjectMap([
'var' => 'a',
'nested2' => new ObjectMap([
'var1' => ['b', 'b1'],
'var2' => 'c',
]),
]),
],
),
);

$this->assertSame('A', $root->nested?->var);
$this->assertSame('B1', $root->nested?->nested2?->var1);
$this->assertSame('C', $root->nested?->nested2?->var2);
}

public function testWithSameKeyInData(): void
{
$hydrator = new Hydrator();

$root = $hydrator->create(
Root::class,
new ArrayData(
[
'var' => 'test',
'var1' => 'A',
'var2' => 'B',
],
[
'nested' => new ObjectMap([
'nested2' => new ObjectMap([
'var1' => 'var',
]),
]),
],
),
);

$this->assertSame('', $root->nested?->var);
$this->assertSame('test', $root->nested?->nested2?->var1);
$this->assertSame('', $root->nested?->nested2?->var2);
}

public function testWithoutMap(): void
{
$hydrator = new Hydrator();

$root = $hydrator->create(
Root::class,
new ArrayData(
[
'nested' => [
'nested2' => [
'var1' => 'A',
'var2' => 'B',
],
],
],
),
);

$this->assertSame('A', $root->nested?->nested2?->var1);
$this->assertSame('B', $root->nested?->nested2?->var2);
}
}
11 changes: 11 additions & 0 deletions tests/ObjectMap/Readme/Body.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\ObjectMap\Readme;

final class Body
{
public string $text = '';
public string $html = '';
}
11 changes: 11 additions & 0 deletions tests/ObjectMap/Readme/Message.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\ObjectMap\Readme;

final class Message
{
public string $subject = '';
public ?Body $body = null;
}
36 changes: 36 additions & 0 deletions tests/ObjectMap/Readme/ObjectMapReadmeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\ObjectMap\Readme;

use PHPUnit\Framework\TestCase;
use Yiisoft\Hydrator\ArrayData;
use Yiisoft\Hydrator\Hydrator;
use Yiisoft\Hydrator\ObjectMap;

final class ObjectMapReadmeTest extends TestCase
{
public function testBase(): void
{
$hydrator = new Hydrator();
$data = [
'title' => 'Hello, World!',
'textBody' => 'Nice to meet you.',
'htmlBody' => '<h1>Nice to meet you.</h1>',
];
$map = [
'subject' => 'title',
'body' => new ObjectMap([
'text' => 'textBody',
'html' => 'htmlBody',
]),
];

$message = $hydrator->create(Message::class, new ArrayData($data, $map));

$this->assertSame('Hello, World!', $message->subject);
$this->assertSame('Nice to meet you.', $message->body?->text);
$this->assertSame('<h1>Nice to meet you.</h1>', $message->body?->html);
}
}
10 changes: 10 additions & 0 deletions tests/ObjectMap/Root.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\ObjectMap;

final class Root
{
public ?Nested $nested = null;
}
Loading