Skip to content

Commit

Permalink
Add nested mapping support via new ObjectMap class (#97)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Makarov <[email protected]>
  • Loading branch information
vjik and samdark authored Oct 7, 2024
1 parent 27de991 commit 69ab3fe
Show file tree
Hide file tree
Showing 14 changed files with 397 additions and 12 deletions.
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

0 comments on commit 69ab3fe

Please sign in to comment.