Skip to content

Commit 9dab397

Browse files
committed
Merge branch 'graph'
2 parents d9a0ff3 + a4a517b commit 9dab397

File tree

2 files changed

+270
-0
lines changed

2 files changed

+270
-0
lines changed

src/Support/Graph.php

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Lkrms\Support;
4+
5+
use ArrayAccess;
6+
use LogicException;
7+
use ReturnTypeWillChange;
8+
9+
/**
10+
* Surfaces the properties and elements of arbitrarily nested objects and arrays
11+
* via a unified array interface
12+
*
13+
* Useful when working with data that has the same structure whether it's given
14+
* as an object or as an array.
15+
*
16+
* @implements ArrayAccess<array-key,int|float|string|bool|object|mixed[]|static>
17+
*/
18+
final class Graph implements ArrayAccess
19+
{
20+
/**
21+
* @var object|mixed[]
22+
*/
23+
private $Graph;
24+
25+
/**
26+
* @var class-string|null
27+
*/
28+
private $DefaultClass;
29+
30+
/**
31+
* @var mixed[]
32+
*/
33+
private $Args;
34+
35+
/**
36+
* @var bool
37+
*/
38+
private $IsObject = true;
39+
40+
/**
41+
* Creates a new Graph object
42+
*
43+
* @param object|mixed[]|null $graph
44+
* @param class-string|null $defaultClass If `null`, arrays are added to the
45+
* graph as needed to accommodate values assigned by key.
46+
* @param mixed ...$args Passed to the constructor of `$defaultClass`.
47+
*/
48+
public function __construct(
49+
&$graph = null,
50+
?string $defaultClass = null,
51+
...$args
52+
) {
53+
$this->DefaultClass = $defaultClass;
54+
$this->Args = $args;
55+
56+
if ($graph === null) {
57+
$graph = $this->createInnerGraph();
58+
}
59+
60+
if (is_object($graph)) {
61+
$this->Graph = $graph;
62+
return;
63+
}
64+
65+
if (!is_array($graph)) {
66+
throw new LogicException('$graph must be an object, an array, or null');
67+
}
68+
69+
$this->Graph = &$graph;
70+
$this->IsObject = false;
71+
}
72+
73+
/**
74+
* Creates a new Graph object
75+
*
76+
* Syntactic sugar for `new Graph()`.
77+
*
78+
* @param object|mixed[]|null $graph
79+
* @param class-string|null $defaultClass If `null`, arrays are added to the
80+
* graph as needed to accommodate values assigned by key.
81+
* @param mixed ...$args Passed to the constructor of `$defaultClass`.
82+
*/
83+
public static function with(
84+
&$graph = null,
85+
?string $defaultClass = null,
86+
...$args
87+
): self {
88+
return new self($graph, $defaultClass, ...$args);
89+
}
90+
91+
/**
92+
* Get the graph's underlying object or array
93+
*
94+
* @return object|mixed[]
95+
*/
96+
public function getInnerGraph()
97+
{
98+
return $this->Graph;
99+
}
100+
101+
public function offsetExists($offset): bool
102+
{
103+
if ($this->IsObject) {
104+
return property_exists($this->Graph, $offset);
105+
}
106+
107+
return array_key_exists($offset, $this->Graph);
108+
}
109+
110+
#[ReturnTypeWillChange]
111+
public function offsetGet($offset)
112+
{
113+
// If there is nothing at the requested offset, create a new object or
114+
// array, add it to the graph and return a new `Graph` for it
115+
if (!$this->offsetExists($offset)) {
116+
$value = $this->createInnerGraph();
117+
118+
if ($this->IsObject) {
119+
$this->Graph->$offset = $value;
120+
} else {
121+
$this->Graph[$offset] = $value;
122+
}
123+
} else {
124+
$value =
125+
$this->IsObject
126+
? $this->Graph->$offset
127+
: $this->Graph[$offset];
128+
129+
if (!is_object($value) && !is_array($value)) {
130+
return $value;
131+
}
132+
}
133+
134+
if ($this->IsObject) {
135+
return new self(
136+
$this->Graph->$offset,
137+
$this->DefaultClass,
138+
...$this->Args,
139+
);
140+
}
141+
142+
return new self(
143+
$this->Graph[$offset],
144+
$this->DefaultClass,
145+
...$this->Args,
146+
);
147+
}
148+
149+
public function offsetSet($offset, $value): void
150+
{
151+
if ($offset === null) {
152+
if ($this->IsObject) {
153+
throw new LogicException('Offset required');
154+
}
155+
$this->Graph[] = $value;
156+
return;
157+
}
158+
159+
if ($this->IsObject) {
160+
$this->Graph->$offset = $value;
161+
return;
162+
}
163+
164+
$this->Graph[$offset] = $value;
165+
}
166+
167+
public function offsetUnset($offset): void
168+
{
169+
if ($this->IsObject) {
170+
unset($this->Graph->$offset);
171+
return;
172+
}
173+
174+
unset($this->Graph[$offset]);
175+
}
176+
177+
/**
178+
* @return object|mixed[]
179+
*/
180+
private function createInnerGraph()
181+
{
182+
if ($this->DefaultClass === null) {
183+
return [];
184+
}
185+
186+
return new $this->DefaultClass(...$this->Args);
187+
}
188+
}

tests/unit/Support/GraphTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Lkrms\Tests\Support;
4+
5+
use Lkrms\Support\Graph;
6+
use Lkrms\Tests\TestCase;
7+
use LogicException;
8+
use stdClass;
9+
10+
final class GraphTest extends TestCase
11+
{
12+
public function testWithObject(): void
13+
{
14+
$graph = new stdClass();
15+
$normaliser = new Graph($graph, stdClass::class);
16+
17+
$normaliser['foo'] = 'bar';
18+
$this->assertSame('bar', $graph->foo);
19+
20+
$normaliser['q']['qux']['quux'] = 'foobar';
21+
$this->assertSame('foobar', $graph->q->qux->quux);
22+
23+
unset($normaliser['q']['qux']);
24+
$this->assertSame(false, isset($normaliser['q']['qux']['quux']));
25+
$this->assertSame(false, isset($normaliser['q']['qux']));
26+
27+
$normaliser['arr'] = ['alpha', 'bravo', 'charlie'];
28+
$normaliser['arr'][3] = 'delta';
29+
$this->assertSame(false, is_array($normaliser['arr']));
30+
$this->assertSame(true, is_array($graph->arr));
31+
$this->assertSame(['alpha', 'bravo', 'charlie', 'delta'], $graph->arr);
32+
33+
$normaliser['obj']['a'] = 'alpha';
34+
$normaliser['obj']['b'] = 'bravo';
35+
$this->assertSame(false, is_array($normaliser['obj']));
36+
$this->assertSame(false, is_array($graph->obj));
37+
$this->assertSame(['a' => 'alpha', 'b' => 'bravo'], (array) $graph->obj);
38+
39+
$normaliser['obj'][0] = 'charlie';
40+
$normaliser['obj'][1] = 'delta';
41+
$this->assertSame(false, is_array($normaliser['obj']));
42+
$this->assertSame(false, is_array($graph->obj));
43+
$this->assertSame(['a' => 'alpha', 'b' => 'bravo', 'charlie', 'delta'], (array) $graph->obj);
44+
45+
$this->expectException(LogicException::class);
46+
$normaliser['obj'][] = 'echo';
47+
}
48+
49+
public function testWithArray(): void
50+
{
51+
$graph = [];
52+
$normaliser = new Graph($graph);
53+
54+
$normaliser['foo'] = 'bar';
55+
$this->assertSame('bar', $graph['foo']);
56+
57+
$normaliser['q']['qux']['quux'] = 'foobar';
58+
$this->assertSame('foobar', $graph['q']['qux']['quux']);
59+
60+
unset($normaliser['q']['qux']);
61+
$this->assertSame(false, isset($normaliser['q']['qux']['quux']));
62+
$this->assertSame(false, isset($normaliser['q']['qux']));
63+
64+
$normaliser['arr'] = ['alpha', 'bravo', 'charlie'];
65+
$normaliser['arr'][] = 'delta';
66+
$this->assertSame(false, is_array($normaliser['arr']));
67+
$this->assertSame(true, is_array($graph['arr']));
68+
$this->assertSame(['alpha', 'bravo', 'charlie', 'delta'], $graph['arr']);
69+
70+
$normaliser['obj']['a'] = 'alpha';
71+
$normaliser['obj']['b'] = 'bravo';
72+
$this->assertSame(false, is_array($normaliser['obj']));
73+
$this->assertSame(true, is_array($graph['obj']));
74+
$this->assertSame(['a' => 'alpha', 'b' => 'bravo'], $graph['obj']);
75+
76+
$normaliser['obj'][] = 'charlie';
77+
$normaliser['obj'][] = 'delta';
78+
$this->assertSame(false, is_array($normaliser['obj']));
79+
$this->assertSame(true, is_array($graph['obj']));
80+
$this->assertSame(['a' => 'alpha', 'b' => 'bravo', 'charlie', 'delta'], $graph['obj']);
81+
}
82+
}

0 commit comments

Comments
 (0)