Skip to content

Commit 42402ef

Browse files
Add flatten tranformer, fix #4
1 parent 4153c74 commit 42402ef

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
namespace Camillebaronnet\ETL\Tests\Transformer;
4+
5+
use Camillebaronnet\ETL\Transformer\Flatten;
6+
use PHPUnit\Framework\TestCase;
7+
8+
final class FlattenTest extends TestCase
9+
{
10+
/**
11+
* @var Flatten
12+
*/
13+
private $flatten;
14+
15+
/**
16+
* @var array
17+
*/
18+
private $complexObject = [
19+
'name' => 'Bar',
20+
'address' => [
21+
'street' => '1 road',
22+
'zip' => 'xxx',
23+
],
24+
'cards' => [
25+
['name' => 'lo'],
26+
['name' => 'lol'],
27+
['name' => 'oll'],
28+
],
29+
'sub-tree' => [
30+
'sub-key' => 'some value',
31+
're-sub-key' => 'another value',
32+
],
33+
];
34+
35+
36+
protected function setUp()
37+
{
38+
$this->flatten = new Flatten();
39+
}
40+
41+
public function testCanFlattenObjectWithDefaultContext()
42+
{
43+
$result = $this->flatten->__invoke($this->complexObject);
44+
45+
$this->assertEquals($result, [
46+
'name' => 'Bar',
47+
'address.street' => '1 road',
48+
'address.zip' => 'xxx',
49+
'cards.0.name' => 'lo',
50+
'cards.1.name' => 'lol',
51+
'cards.2.name' => 'oll',
52+
'sub-tree.sub-key' => 'some value',
53+
'sub-tree.re-sub-key' => 'another value',
54+
]);
55+
}
56+
57+
public function testCanFlattenOnlySomeSegments()
58+
{
59+
$result = $this->flatten->__invoke($this->complexObject, [
60+
'only' => ['address', 'sub-tree'],
61+
]);
62+
63+
$this->assertEquals($result, [
64+
'name' => 'Bar',
65+
'address.street' => '1 road',
66+
'address.zip' => 'xxx',
67+
'cards' => [
68+
['name' => 'lo'],
69+
['name' => 'lol'],
70+
['name' => 'oll'],
71+
],
72+
'sub-tree.sub-key' => 'some value',
73+
'sub-tree.re-sub-key' => 'another value',
74+
]);
75+
}
76+
77+
public function testCanFlattenAllSegmentsExceptOne()
78+
{
79+
$result = $this->flatten->__invoke($this->complexObject, [
80+
'ignore' => ['address'],
81+
]);
82+
83+
$this->assertEquals($result, [
84+
'name' => 'Bar',
85+
'address' => [
86+
'street' => '1 road',
87+
'zip' => 'xxx',
88+
],
89+
'cards.0.name' => 'lo',
90+
'cards.1.name' => 'lol',
91+
'cards.2.name' => 'oll',
92+
'sub-tree.sub-key' => 'some value',
93+
'sub-tree.re-sub-key' => 'another value',
94+
]);
95+
}
96+
97+
public function testCanSpecifyACustomRootKey()
98+
{
99+
$result = $this->flatten->__invoke($this->complexObject, [
100+
'rootKey' => '__',
101+
]);
102+
103+
$this->assertEquals($result, [
104+
'__name' => 'Bar',
105+
'__address.street' => '1 road',
106+
'__address.zip' => 'xxx',
107+
'__cards.0.name' => 'lo',
108+
'__cards.1.name' => 'lol',
109+
'__cards.2.name' => 'oll',
110+
'__sub-tree.sub-key' => 'some value',
111+
'__sub-tree.re-sub-key' => 'another value',
112+
]);
113+
}
114+
115+
public function testCanSpecifyACustomGlue()
116+
{
117+
$result = $this->flatten->__invoke($this->complexObject, [
118+
'glue' => '_',
119+
]);
120+
121+
$this->assertEquals($result, [
122+
'name' => 'Bar',
123+
'address_street' => '1 road',
124+
'address_zip' => 'xxx',
125+
'cards_0_name' => 'lo',
126+
'cards_1_name' => 'lol',
127+
'cards_2_name' => 'oll',
128+
'sub-tree_sub-key' => 'some value',
129+
'sub-tree_re-sub-key' => 'another value',
130+
]);
131+
}
132+
}

src/Transformer/Flatten.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace Camillebaronnet\ETL\Transformer;
4+
5+
class Flatten implements TransformInterface
6+
{
7+
/**
8+
* Default context.
9+
*/
10+
public const DEFAULT_CONTEXT = [
11+
'rootKey' => '',
12+
'glue' => '.',
13+
'ignore' => null,
14+
'only' => null,
15+
];
16+
17+
/**
18+
* The saved context, hydrated by the __invoke.
19+
*
20+
* @var array
21+
*/
22+
protected $context = [];
23+
24+
/**
25+
* The class entry point.
26+
*
27+
* @param array $data
28+
* @param array $context
29+
* @return array
30+
*/
31+
public function __invoke(array $data, array $context = []): array
32+
{
33+
34+
$context = $this->context = array_merge(static::DEFAULT_CONTEXT, $context);
35+
36+
$flattened = [];
37+
$this->flatten(
38+
$data,
39+
$flattened,
40+
$context['glue'],
41+
$context['rootKey']
42+
);
43+
44+
return $flattened;
45+
}
46+
47+
/**
48+
* Flatten recursively the data.
49+
*
50+
* @param array $input
51+
* @param array $result
52+
* @param string $glue
53+
* @param string $parentKey
54+
* @param bool $escapeFormulas
55+
*/
56+
private function flatten(array $input, array &$result, string $glue, string $parentKey = '')
57+
{
58+
59+
foreach ($input as $key => $value) {
60+
$only = $this->context['only']
61+
? $this->stringStartBy($parentKey.$key, $this->context['only'])
62+
: true;
63+
$ignore = $this->context['ignore']
64+
? !$this->stringStartBy($parentKey.$key, $this->context['ignore'])
65+
: true;
66+
67+
if (is_array($value) && $only && $ignore) {
68+
$this->flatten($value, $result, $glue, $parentKey.$key.$glue);
69+
} else {
70+
$result[$parentKey.$key] = $value;
71+
}
72+
}
73+
}
74+
75+
/**
76+
* Check if the needle can be found on the haystack.
77+
*
78+
* @param $needle
79+
* @param array $haystack
80+
* @return bool
81+
*/
82+
private function stringStartBy($needle, array $haystack): bool
83+
{
84+
foreach ($haystack as $input) {
85+
if ($input === substr($needle, 0, strlen($input))) {
86+
return true;
87+
}
88+
}
89+
90+
return false;
91+
}
92+
}

0 commit comments

Comments
 (0)