Skip to content

Commit 9a36762

Browse files
committed
add more examples
1 parent 5e12ecf commit 9a36762

File tree

3 files changed

+58
-15
lines changed

3 files changed

+58
-15
lines changed

README.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,24 +84,28 @@ $user = Box::of($inputEmail)
8484
->get(fn($it) => $userRepository->create(['email' => $it]));
8585
```
8686

87-
Or make it shorter by using higher abstraction levels:
88-
87+
Using flatMap, you can compose presets of operations on boxes:
8988
```php
90-
// potentially defined elsewhere
91-
function isValidEmail(mixed $email): bool
89+
/** @throws LogicException */
90+
function assertEmail(Box $box): Box
9291
{
93-
return is_string($email)
94-
&& strlen($email) > 0
95-
&& strlen($email) < 256
96-
&& filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
92+
return $box
93+
->assert(fn($x) => is_string($x), 'Not a string')
94+
->assert(fn(string $x) => strlen($x) > 0, 'Too short')
95+
->assert(fn(string $x) => strlen($x) < 256, 'Too long')
96+
->assert(fn(string $x) => filter_var($x, FILTER_VALIDATE_EMAIL), 'Not an email');
9797
}
9898

99-
$validEmail = Box::of('john.doe@example.org')->assertGet(isValidEmail(...));
100-
// $validEmail === 'john.doe@example.org'
99+
$validEmail = Box::of('')->flatMap(assertEmail(...))->unbox();
100+
// throws LogicException: "Value is too short"
101101

102-
$validEmail2 = Box::of('asdf')->assertGet(isValidEmail(...));
103-
// throws LogicException
102+
$user = Box::of('john@example.org')
103+
->flatMap(fn($box) => assertEmail($box))
104+
->get(fn($email) => $userRepository->create(['email' => $email]));
104105
```
106+
Note, in this example we still have 4 separate assertions and error messages.
107+
Using flatMap is the key here. Unlike map(), which transforms the value inside the Box,
108+
flatMap() transforms the Box itself. This allows us to compose behavior in a more functional way.
105109

106110
# Type Safety
107111
Thanks to meticulously crafted PHPDoc annotations, this class is type safe if you use PHPStan for static analysis.

src/Box.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ public function map(callable $callback): Box
4545
return new self($callback($this->value));
4646
}
4747

48+
/**
49+
* Apply a transformation function to the box itself
50+
*
51+
* This method will always return a new instance of Box, even for objects.
52+
*
53+
* @template U
54+
* @param callable(self<T>): Box<U> $callback
55+
* @return Box<U>
56+
*/
57+
public function flatMap(callable $callback): Box
58+
{
59+
return $callback($this);
60+
}
61+
4862
/**
4963
* Unwrap the value and apply a final transformation on it.
5064
* Can be used instead of `unbox` to terminate the sequence.
@@ -77,7 +91,7 @@ public function unbox()
7791
* @param U|callable(T):bool $check
7892
* @return Box<T>
7993
*/
80-
public function assert(mixed $check): Box
94+
public function assert(mixed $check, string $message = ''): Box
8195
{
8296
$isClosure = is_callable($check);
8397

@@ -86,15 +100,19 @@ public function assert(mixed $check): Box
86100
: $this->value === $check;
87101

88102
if (! $pass) {
89-
$message = $isClosure
103+
$report = $isClosure
90104
? 'Value did not pass the callback check.'
91105
: sprintf(
92106
'Failed asserting that two values are the same. Expected %s, got %s.',
93107
var_export($check, true),
94108
var_export($this->value, true)
95109
);
96110

97-
throw new LogicException($message);
111+
if ($message !== '') {
112+
$report = $message . ' | ' . $report;
113+
}
114+
115+
throw new LogicException($report);
98116
}
99117

100118
return $this;

tests/Unit/BoxTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,27 @@
7070
expect(fn() => Box::of(5)->assertGet(6))->toThrow(LogicException::class);
7171
});
7272

73+
test('flatMap allows us to replace the box itself', function () {
74+
$result = Box::of(5)
75+
->flatMap(fn(Box $x) => $x->assert(5)->map(fn($x) => $x + 1))
76+
->unbox();
77+
78+
expect($result)->toBe(6);
79+
});
80+
81+
test('use flatMap to compose actions', function () {
82+
$isValidEmail = function (Box $box) {
83+
return $box
84+
->assert(fn(mixed $x) => is_string($x), 'Not a string')
85+
->assert(fn(string $x) => strlen($x) > 0, 'Too short')
86+
->assert(fn(string $x) => strlen($x) < 256, 'Too long')
87+
->assert(fn(string $x) => filter_var($x, FILTER_VALIDATE_EMAIL), 'Not an email');
88+
};
89+
90+
expect(fn() => Box::of('asdf')->flatMap($isValidEmail)->unbox())
91+
->toThrow(LogicException::class, 'Not an email | Value did not pass the callback check.');
92+
});
93+
7394
// This test is not here to "lock in" desired behavior. It is here to document a pitfall.
7495
test('performing a mutation on an object using map() will produce side effects', function () {
7596
$object = (object)['number' => 5];

0 commit comments

Comments
 (0)