Skip to content

Commit 451e911

Browse files
authored
Merge pull request #164 from boesing/docs/initial-documentation
Documentation: add extended documentation
2 parents f459622 + 94fbded commit 451e911

File tree

8 files changed

+327
-15
lines changed

8 files changed

+327
-15
lines changed

README.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,139 @@ To use this library in your project, please install it via composer:
1616
```
1717
$ composer require boesing/typed-arrays
1818
```
19+
20+
## Usage
21+
22+
The main reason why this library was created was the fact, that *every* array in PHP is a hashmap.
23+
If you primarily work with APIs, you might have experienced that `json_encode` of an array type sometimes leads to annoying issues.
24+
25+
To get rid of `array` being passed through an application, the `OrderedListInterface` and the `MapInterface` became very handy.
26+
To also provide most if not any of the `array_*` functions to the developers, most of these array functions do have a method within `OrderedListInterface` or `MapInterface`.
27+
28+
### Common mistakes
29+
30+
Lets take some real-world use cases to better reflect the idea behind this library:
31+
32+
```php
33+
$listOfIntegers = [1, 2, 3, 4];
34+
35+
$myObject = new stdClass();
36+
$myObject->integers = $listOfIntegers;
37+
38+
echo json_encode($myObject) . PHP_EOL;
39+
40+
// Output of the code above will be: `{"integers":[1,2,3,4]}`
41+
// Now some refactoring has to be made since the requirement changed. The requirement now is that the integers list
42+
// must not contain odd values anymore. So `array_filter` to the rescue, right?
43+
44+
$listOfEvenIntegers = array_filter([1, 2, 3, 4], static fn (int $integer): int => $integer % 2 === 0);
45+
46+
$myObject = new stdClass();
47+
$myObject->integers = $listOfEvenIntegers;
48+
49+
echo json_encode($myObject) . PHP_EOL;
50+
51+
// Output of the refactored code above now became: `{"integers":{"1":2,"3":4}}`
52+
// So what now happened is a huge problem for highly type-sensitive API clients since we changed a list to a hashmap
53+
// Same happens with hashmaps which suddenly become empty.
54+
55+
$hashmap = [
56+
'foo' => 'bar',
57+
];
58+
$myObject = new stdClass();
59+
$myObject->map = $hashmap;
60+
61+
echo json_encode($myObject) . PHP_EOL;
62+
63+
// Output of the code above will be: `{"map":{"foo":"bar"}}`
64+
// So now some properties are being added, some are being removed, the definition of your API says
65+
// "the object will contain additional properties because heck I do not want to declare every property"
66+
// "so to make it easier, every property has a string value"
67+
// can be easily done with something like this in JSONSchema: `{"type": "object", "additional_properties": {"type": "string"}}`
68+
// Now, some string value might become `null` due to whatever reason, lets say it was a bug and thus the happy path always returned a string
69+
// The most logical way here is, due to our lazyness, to use something like `array_filter` to get rid of all our non-string values
70+
71+
$hashmap = [
72+
'foo' => null,
73+
];
74+
$myObject = new stdClass();
75+
$myObject->map = array_filter($hashmap);
76+
77+
echo json_encode($myObject) . PHP_EOL;
78+
79+
// Output of the refactored code above now became: `{"map":[]}`
80+
// So in case that every array value is being wiped due to the filtering, we suddenly have a type-change from
81+
// a hashmap to a list. This is ofc also problematic since we do not want to have a list here but an empty object like
82+
// so: `{"map":{}}`
83+
```
84+
85+
_(The above example can be verified on 3v4l.org - a PHP sandbox: https://3v4l.org/Gfogn#v8.1.6)_
86+
87+
### typed-arrays to the rescue
88+
89+
So with this library, one is a little bit more type-safe when it comes to array handling.
90+
However, the `MapInterface` actually will become `null` within a `json_encode` in case it is empty.
91+
92+
So lets take the above example in combination with our factories:
93+
94+
```php
95+
96+
use Boesing\TypedArrays\TypedArrayFactory;
97+
$factory = new TypedArrayFactory();
98+
99+
$listOfIntegers = $factory->createOrderedList([1, 2, 3, 4]);
100+
101+
$myObject = new stdClass();
102+
$myObject->integers = $listOfIntegers;
103+
104+
echo json_encode($myObject) . PHP_EOL;
105+
106+
// Output of the code above will be: `{"integers":[1,2,3,4]}`
107+
// Now some refactoring has to be made since the requirement changed. The requirement now is that the integers list
108+
// must not contain odd values anymore. So `array_filter` to the rescue, right?
109+
110+
$listOfEvenIntegers = $factory->createOrderedList([1, 2, 3, 4])->filter(static fn (int $integer): int => $integer % 2 === 0);
111+
112+
$myObject = new stdClass();
113+
$myObject->integers = $listOfEvenIntegers;
114+
115+
echo json_encode($myObject) . PHP_EOL;
116+
117+
// Output of the refactored code above now became: `{"integers":[2, 4]}`
118+
// Due to the internal handling of `array_filter`, the `OrderedListInterface` won't change its type.
119+
120+
// Even hashmaps can be filtered, the type stays the same but in case of an empty map, `null` is being passed to the JSON object
121+
$hashmap = $factory->createMap([
122+
'foo' => 'bar',
123+
]);
124+
$myObject = new stdClass();
125+
$myObject->map = $hashmap;
126+
127+
echo json_encode($myObject) . PHP_EOL;
128+
129+
// Output of the code above will be: `{"map":{"foo":"bar"}}`
130+
// So now some properties are being added, some are being removed, the definition of your API says
131+
// "the object will contain additional properties because heck I do not want to declare every property"
132+
// "so to make it easier, every property has a string value"
133+
// can be easily done with something like this in JSONSchema: `{"type": "object", "additional_properties": {"type": "string"}}`
134+
// Now, some string value might become `null` due to whatever reason, lets say it was a bug and thus the happy path always returned a string
135+
// The most logical way here is, due to our lazyness, to use something like `array_filter` to get rid of all our non-string values
136+
137+
$hashmap = $factory->createMap([
138+
'foo' => null,
139+
]);
140+
$myObject = new stdClass();
141+
$myObject->map = $hashmap->filter(static fn ($value) => $value !== null);
142+
143+
echo json_encode($myObject) . PHP_EOL;
144+
145+
// Output of the refactored code above now became: `{"map":null}`
146+
// So in case that every array value is being wiped due to the filtering, we suddenly have a type-change from
147+
// a hashmap to a list. This is ofc also problematic since we do not want to have a list here but an empty object like
148+
// so: `{"map":{}}`
149+
```
150+
151+
### Conclusion
152+
153+
When it comes to API responses, you might not want to rely on PHP array structure. Always prefer real objects with real
154+
properties and real property type-hints over `non-empty-array`.

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<exclude name="SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix"/>
2828
<exclude name="Generic.NamingConventions.ConstructorName.OldStyle"/>
2929
<exclude name="Generic.CodeAnalysis.UselessOverridingMethod.Found"/>
30+
<exclude name="Generic.Files.LineLength.TooLong"/>
3031
<!-- Can be removed with dropping support for PHP 7.3 -->
3132
<exclude name="SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingNativeTypeHint"/>
3233
<!-- Can be removed with dropping support for PHP 7.4 -->

src/ArrayInterface.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,36 @@
1717
interface ArrayInterface extends IteratorAggregate, Countable
1818
{
1919
/**
20+
* Verifies if an element is within the item storage.
21+
*
2022
* @psalm-param TValue $element
2123
*/
2224
public function contains($element): bool;
2325

2426
/**
27+
* Returns the very first item.
28+
* This method is an equivalent of the `reset` function - it just does not return `false` but throws an exception
29+
* in case there are no items stored.
30+
*
2531
* @psalm-return TValue
2632
* @throws OutOfBoundsException if there are no values available.
2733
*/
2834
public function first();
2935

3036
/**
37+
* Returns the very last item.
38+
* This method is an equivalent of the `end` function - it just does not return `false` but throws an exception
39+
* there are no items stored.
40+
*
3141
* @psalm-return TValue
3242
* @throws OutOfBoundsException if there are no values available.
3343
*/
3444
public function last();
3545

36-
public function isEmpty(): bool;
37-
3846
/**
39-
* @psalm-return array<TKey,TValue>
47+
* Returns `true` in case there are no items stored.
4048
*/
41-
public function toNativeArray(): array;
49+
public function isEmpty(): bool;
4250

4351
/**
4452
* Tests if all elements satisfy the given predicate.
@@ -55,6 +63,8 @@ public function allSatisfy(callable $callback): bool;
5563
public function exists(callable $callback): bool;
5664

5765
/**
66+
* Returns the amount of items.
67+
*
5868
* @return 0|positive-int
5969
*/
6070
public function count(): int;

src/Array_.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,6 @@ public function count(): int
7979
return count($this->data);
8080
}
8181

82-
public function toNativeArray(): array
83-
{
84-
return $this->data;
85-
}
86-
8782
/**
8883
* @psalm-return pure-callable(TValue $a,TValue $b):int
8984
*/

src/Map.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,4 +453,9 @@ public function keyExchange(callable $keyGenerator): MapInterface
453453

454454
return $exchanged;
455455
}
456+
457+
public function toNativeArray(): array
458+
{
459+
return $this->data;
460+
}
456461
}

0 commit comments

Comments
 (0)