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

Merged
merged 15 commits into from
Oct 7, 2024
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',
]),
];
$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
57 changes: 57 additions & 0 deletions src/ObjectMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator;

use function array_key_exists;

/**
* 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
) {
}

/**
* Returns a path for a given property name or null if mapping dosen't exist.
*
* @psalm-return string|list<string>|ObjectMap|null
*/
public function getPath(string $name): string|array|self|null
{
return $this->map[$name] ?? null;
}

/**
* Returns a list of property names for which mapping is set.
*
* @return string[] List of property names.
* @psalm-return list<string>
*/
public function getNames(): array
{
return array_keys($this->map);
}

/**
* Checks if a given property name exists in the mapping array.
*
* @param string $name The property name.
* @return bool Whether the property name exists in the mapping array.
*/
public function exists(string $name): bool
{
return array_key_exists($name, $this->map);
}
}
13 changes: 13 additions & 0 deletions tests/ObjectMap/Car.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\ObjectMap;

final class Car
{
public function __construct(
public ?Engine $engine = null,
) {
}
}
15 changes: 15 additions & 0 deletions tests/ObjectMap/Engine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\ObjectMap;

final class Engine
{
public string $version = '';

public function __construct(
public string $name,
) {
}
}
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 = '';
}
Loading
Loading