Skip to content

Commit abf6b63

Browse files
committed
Replace Tree options array with typesafe DTO (fixes #25)
1 parent 0b49c5a commit abf6b63

File tree

4 files changed

+38
-109
lines changed

4 files changed

+38
-109
lines changed

Tests.txt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,6 @@ Node (BlueM\Tree\Node)
3939
[x] When typecasted to string, the string representation of the node’s ID is returned
4040

4141
Tree (BlueM\Tree)
42-
[x] Constructor args: An exception is thrown if a non scalar value should be used as root id
43-
[x] Constructor args: An exception is thrown if a non string value should be used as id field name
44-
[x] Constructor args: An exception is thrown if a non string value should be used as parent id field name
45-
[x] Constructor args: An exception is thrown if a non object should be used as serializer
46-
[x] Constructor args: The serializer can be set to an object implementing Serializerinterface
4742
[x] Constructor args: Root nodes’ parent ID can be defined as null
4843
[x] Constructor args: The root node’s ID can be defined as null while there is a node with ID 0
4944
[x] Constructor args: Name of fields for id and parent id in the input data can be changed

src/Tree.php

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use BlueM\Tree\Exception\MissingNodeInvalidParentException;
77
use BlueM\Tree\Exception\SelfReferenceInvalidParentException;
88
use BlueM\Tree\Node;
9+
use BlueM\Tree\Options;
910
use BlueM\Tree\Serializer\FlatTreeJsonSerializer;
1011
use BlueM\Tree\Serializer\TreeJsonSerializerInterface;
1112

@@ -47,53 +48,22 @@ class Tree implements \JsonSerializable, \Stringable
4748

4849
/**
4950
* @param iterable<iterable<string, mixed>> $data The data for the tree (iterable)
50-
* @param array<string, mixed> $options 0 or more of the following keys, all of which are optional: "rootId" (ID of
51-
* the root node, default: 0), "id" (name of the ID field / array key, default:
52-
* "id"), "parent" (name of the parent ID field / array key, default: "parent"),
53-
* "jsonSerializer" (instance of \BlueM\Tree\Serializer\TreeJsonSerializerInterface),
54-
* "buildWarningCallback" (a callable which is called when detecting data
55-
* inconsistencies such as an invalid parent)
5651
*
5752
* @throws InvalidParentException
5853
* @throws \InvalidArgumentException
5954
*/
60-
public function __construct(iterable $data = [], array $options = [])
55+
public function __construct(iterable $data = [], Options $options = new Options())
6156
{
62-
$options = array_change_key_case($options);
57+
$this->rootId = $options->rootId;
58+
$this->idKey = $options->idFieldName;
59+
$this->parentKey = $options->parentIdFieldName;
6360

64-
if (array_key_exists('rootid', $options)) {
65-
if (!\is_scalar($options['rootid']) && null !== $options['rootid']) {
66-
throw new \InvalidArgumentException('Option “rootid” must be scalar or null');
67-
}
68-
$this->rootId = $options['rootid'];
69-
}
70-
71-
if (!empty($options['id'])) {
72-
if (!\is_string($options['id'])) {
73-
throw new \InvalidArgumentException('Option “id” must be a string');
74-
}
75-
$this->idKey = $options['id'];
61+
if ($options->jsonSerializer) {
62+
$this->jsonSerializer = $options->jsonSerializer;
7663
}
7764

78-
if (!empty($options['parent'])) {
79-
if (!\is_string($options['parent'])) {
80-
throw new \InvalidArgumentException('Option “parent” must be a string');
81-
}
82-
$this->parentKey = $options['parent'];
83-
}
84-
85-
if (!empty($options['jsonserializer'])) {
86-
if (!is_object($options['jsonserializer'])) {
87-
throw new \InvalidArgumentException('Option “jsonSerializer” must be an object');
88-
}
89-
$this->setJsonSerializer($options['jsonserializer']);
90-
}
91-
92-
if (!empty($options['buildwarningcallback'])) {
93-
if (!is_callable($options['buildwarningcallback'])) {
94-
throw new \InvalidArgumentException('Option “buildWarningCallback” must be a callable');
95-
}
96-
$this->buildWarningCallback = $options['buildwarningcallback'];
65+
if ($options->buildWarningCallback) {
66+
$this->buildWarningCallback = $options->buildWarningCallback;
9767
} else {
9868
$this->buildWarningCallback = $this->buildWarningHandler(...);
9969
}

src/Tree/Options.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace BlueM\Tree;
4+
5+
use BlueM\Tree\Serializer\TreeJsonSerializerInterface;
6+
7+
final readonly class Options
8+
{
9+
public function __construct(
10+
public string|int|float|null $rootId = 0,
11+
public string $idFieldName = 'id',
12+
public string $parentIdFieldName = 'parent',
13+
public ?TreeJsonSerializerInterface $jsonSerializer = null,
14+
public mixed $buildWarningCallback = null,
15+
) {
16+
if ($buildWarningCallback
17+
&& !is_callable($buildWarningCallback)) {
18+
throw new \InvalidArgumentException('$buildWarningCallback must be a callable');
19+
}
20+
}
21+
}

tests/TreeTest.php

Lines changed: 8 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,67 +6,14 @@
66
use BlueM\Tree\Exception\InvalidParentException;
77
use BlueM\Tree\Exception\MissingNodeInvalidParentException;
88
use BlueM\Tree\Node;
9-
use BlueM\Tree\Serializer\HierarchicalTreeJsonSerializer;
9+
use BlueM\Tree\Options;
1010
use PHPUnit\Framework\Attributes\Test;
1111
use PHPUnit\Framework\Attributes\TestDox;
1212
use PHPUnit\Framework\Attributes\Ticket;
1313
use PHPUnit\Framework\TestCase;
1414

1515
class TreeTest extends TestCase
1616
{
17-
#[Test]
18-
#[TestDox('Constructor args: An exception is thrown if a non scalar value should be used as root id')]
19-
public function constructorArgsNonScalarRootId(): void
20-
{
21-
$this->expectException(\InvalidArgumentException::class);
22-
$this->expectExceptionMessage('Option “rootid” must be scalar or null');
23-
24-
new Tree([], ['rootId' => []]);
25-
}
26-
27-
#[Test]
28-
#[TestDox('Constructor args: An exception is thrown if a non string value should be used as id field name')]
29-
public function constructorArgsNonStringIDFieldName(): void
30-
{
31-
$this->expectException(\InvalidArgumentException::class);
32-
$this->expectExceptionMessage('Option “id” must be a string');
33-
34-
new Tree([], ['id' => 123]);
35-
}
36-
37-
#[Test]
38-
#[TestDox('Constructor args: An exception is thrown if a non string value should be used as parent id field name')]
39-
public function constructorArgsNonStringParentIDFieldName(): void
40-
{
41-
$this->expectException(\InvalidArgumentException::class);
42-
$this->expectExceptionMessage('Option “parent” must be a string');
43-
44-
new Tree([], ['parent' => $this]);
45-
}
46-
47-
#[Test]
48-
#[TestDox('Constructor args: An exception is thrown if a non object should be used as serializer')]
49-
public function constructorArgsNonObjectSerializer(): void
50-
{
51-
$this->expectException(\InvalidArgumentException::class);
52-
$this->expectExceptionMessage('Option “jsonSerializer” must be an object');
53-
54-
new Tree([], ['jsonSerializer' => 'not an object']);
55-
}
56-
57-
#[Test]
58-
#[TestDox('Constructor args: The serializer can be set to an object implementing Serializerinterface')]
59-
public function constructorArgsValidSerializer(): void
60-
{
61-
$serializer = new HierarchicalTreeJsonSerializer();
62-
63-
$subject = new Tree([], ['jsonSerializer' => $serializer]);
64-
65-
$serializerProperty = new \ReflectionProperty($subject, 'jsonSerializer');
66-
67-
static::assertSame($serializer, $serializerProperty->getValue($subject));
68-
}
69-
7017
#[Test]
7118
#[TestDox('Constructor args: Root nodes’ parent ID can be defined as null')]
7219
public function constructorArgsNullAsRootNodeParentId(): void
@@ -78,7 +25,7 @@ public function constructorArgsNullAsRootNodeParentId(): void
7825
['id' => 4, 'parent' => null, 'name' => 'Root'],
7926
];
8027

81-
$tree = new Tree($data, ['rootId' => null]);
28+
$tree = new Tree($data, new Options(rootId: null));
8229

8330
$nodes = $tree->getNodes();
8431
static::assertCount(4, $nodes);
@@ -100,7 +47,7 @@ public function constructorArgsNullAsRootNodeIdPlusNodeWithId0(): void
10047
['id' => 3, 'parent' => 0, 'name' => 'Grandchild'],
10148
];
10249

103-
$tree = new Tree($data, ['rootId' => null]);
50+
$tree = new Tree($data, new Options(rootId: null));
10451

10552
$nodes = $tree->getNodes();
10653
static::assertCount(3, $nodes);
@@ -118,7 +65,7 @@ public function constructorArgsChangeFieldNames(): void
11865
{
11966
$data = self::dummyDataWithStringKeys('id_node', 'id_parent');
12067

121-
$tree = new Tree($data, ['rootId' => '', 'id' => 'id_node', 'parent' => 'id_parent']);
68+
$tree = new Tree($data, new Options(rootId: '', idFieldName: 'id_node', parentIdFieldName: 'id_parent'));
12269

12370
$nodes = $tree->getRootNodes();
12471

@@ -223,9 +170,7 @@ public function aCustomBuildWarningCallbackCanBeSpecifiedWhichIsCalledWithNodeAn
223170
['id' => 1, 'parent' => 0],
224171
['id' => 2, 'parent' => ''],
225172
],
226-
[
227-
'buildwarningcallback' => $buildwarningcallback,
228-
]
173+
new Options(buildWarningCallback: $buildwarningcallback),
229174
);
230175

231176
static::assertSame(1, $invocationCount);
@@ -284,7 +229,7 @@ public function nodeGetByIntId(): void
284229
public function nodeGetByStringId(): void
285230
{
286231
$data = self::dummyDataWithStringKeys();
287-
$tree = new Tree($data, ['rootId' => '']);
232+
$tree = new Tree($data, new Options(rootId: ''));
288233
$node = $tree->getNodeById('library');
289234
static::assertEquals('library', $node->getId());
290235
}
@@ -378,16 +323,14 @@ public function anExceptionIsThrownWhenAnInvalidParentIdIsReferenced(): void
378323
public function anExceptionIsThrownIfTheBuildWarningCallbackOptionIsNotACallable(): void
379324
{
380325
$this->expectException(\InvalidArgumentException::class);
381-
$this->expectExceptionMessage('Option “buildWarningCallback must be a callable');
326+
$this->expectExceptionMessage('$buildWarningCallback must be a callable');
382327

383328
new Tree(
384329
[
385330
['id' => 1, 'parent' => 0],
386331
['id' => 2, 'parent' => ''],
387332
],
388-
[
389-
'buildwarningcallback' => 'Must be a callable',
390-
]
333+
new Options(buildWarningCallback: 'Will cause exception'),
391334
);
392335
}
393336

0 commit comments

Comments
 (0)