Skip to content

Commit 50fab6b

Browse files
committed
improve TryBox
1 parent f48ad1f commit 50fab6b

File tree

4 files changed

+72
-42
lines changed

4 files changed

+72
-42
lines changed

README.md

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# Welcome to PhpBox
22

3-
PhpBox contains exactly one class: `Box`.
4-
5-
You can find it here: [src/Box.php](src/Box.php)
3+
PhpBox contains two classes:
4+
- `Box` [src/Box.php](src/Box.php)
5+
- `TryBox` [src/TryBox.php](src/TryBox.php)
66

77
# Installation
8-
Currently, there is no composer package.
9-
10-
Simply copy the `Box.php` file to your project and adjust its namespace if needed.
8+
A composer package is in the works, but I recommend just copying the files into your project for now.
119

1210
# Requirements
1311
The only hard requirement is PHP 8.0 or higher.
@@ -23,25 +21,20 @@ You put any value into a box.
2321

2422
Then you can chain map(), dump() and assert() calls on it.
2523

26-
To get the value out of the box, call unbox() or get().
24+
To get the value out of the box, call value() or get().
2725

28-
## Note
29-
In an earlier version:
30-
- map() was called pipe()
31-
- get() was called pull()
32-
33-
## Examples
26+
## Box Examples
3427

3528
```php
3629
$value = Box::of(5)
3730
->map(fn($value) => $value + 1)
3831
->map(fn($value) => $value * 2)
39-
->unbox();
32+
->value();
4033

4134
echo $value; // 12
4235
```
4336

44-
Use get() to combine map() and unbox() in one call:
37+
Use get() to combine map() and value() in one call:
4538
```php
4639
$value = Box::of(5)
4740
->map(fn($value) => $value + 1)
@@ -57,7 +50,7 @@ $isEven = fn($value) => $value % 2 === 0;
5750
$value = Box::of(5)
5851
->map(fn($it) => $it + 1)->assert(6)
5952
->map(fn($it) => $it * 2)->assert($isEven)->dump()
60-
->unbox();
53+
->value();
6154

6255
echo $value; // 12
6356
```
@@ -96,14 +89,74 @@ function assertEmail(Box $box): Box
9689
->assert(fn(string $x) => filter_var($x, FILTER_VALIDATE_EMAIL), 'Not an email');
9790
}
9891

99-
$validEmail = Box::of('')->flatMap(assertEmail(...))->unbox();
92+
$validEmail = Box::of('')->flatMap(assertEmail(...))->value();
10093
// throws LogicException: "Too short"
10194

10295
$user = Box::of('john@example.org')
10396
->mod(fn($box) => assertEmail($box))
10497
->get(fn($email) => $userRepository->create(['email' => $email]));
10598
```
10699

100+
## TryBox Examples
101+
102+
TryBox works just like Box, except that it catches any and all errors instead of blowing up directly.
103+
Its value() method has a more complicated return type, which is either T or Throwable.
104+
- advantage: you can run a chain without risk of blowing up (subsequent map() calls will be skipped),
105+
and then decide what to do with the error at the end.
106+
- disadvantage: you have to handle the error case even if you're sure it will never happen.
107+
108+
Let's define a realistic function that sometimes throws an error (like a database call or an API request). In this
109+
case it either returns whatever was passed in or throws a RuntimeException at random.
110+
```php
111+
/**
112+
* @template T
113+
* @param T $value
114+
* @return T
115+
*
116+
* @throws RuntimeException
117+
*/
118+
function roulette($value)
119+
{
120+
if (random_int(0, 1) === 1) {
121+
throw new RuntimeException('boo');
122+
}
123+
124+
return $value;
125+
}
126+
```
127+
128+
Let's use this dangerous function in a TryBox chain:
129+
```php
130+
$result = TryBox::of(5)
131+
->map(fn($value) => roulette($value)) // TryBox stores the exception
132+
->map(fn($value) => $value * 2)
133+
->map(fn($value) => $value + 1)
134+
->value();
135+
136+
// type signature is int|Throwable, so we have to handle the error case
137+
if ($result instanceof Throwable) {
138+
echo $result->getMessage(); // boo
139+
return;
140+
}
141+
142+
// if we reach this line, $result is guaranteed to be an int
143+
$calc = $result + 1;
144+
echo $calc;
145+
```
146+
147+
If you are sure the error case will never happen, or simply don't care, you can use rip() instead of value():
148+
`rip()` will either throw the stored error (if any) or return the value of type T.
149+
```php
150+
$result = TryBox::of(5)
151+
->map(fn($value) => roulette($value))
152+
->map(fn($value) => $value * 2)
153+
->map(fn($value) => $value + 1)
154+
->rip(); // The original runtime exception will be thrown here, if present
155+
156+
// if we reach this line, $result is guaranteed to be an int
157+
$calc = $result + 1;
158+
```
159+
107160
# Type Safety
108161
Thanks to meticulously crafted PHPDoc annotations, this class is type safe if you use PHPStan for static analysis.
109162

src/Box.php

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,6 @@ public function assert(mixed $check, string $message = ''): Box
118118
return $this;
119119
}
120120

121-
/**
122-
* Run an assertion against the value and return it.
123-
*
124-
* @template U
125-
* @param U|callable(T):bool $check
126-
* @return T
127-
*/
128-
public function assertGet(mixed $check): mixed
129-
{
130-
$this->assert($check);
131-
132-
return $this->value();
133-
}
134-
135121
/**
136122
* Dump the value to the console.
137123
*

src/TryBox.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
/**
99
* A container that allows chaining transformations and assertions on a value.
10-
* Will catch exceptions and return them as a value.
10+
* value() will return T|Throwable (must be narrowed manually with error handling).
11+
* rip() will return T, but throws an exception if there is an error.
1112
*
1213
* @template T
1314
* @template E

tests/Unit/BoxTest.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,6 @@
6060
expect($result->number)->toBe(5);
6161
});
6262

63-
test('assertGet returns the value when the assertion passes', function () {
64-
$result = Box::of(5)->assertGet(5);
65-
66-
expect($result)->toBe(5);
67-
});
68-
69-
test('assertGet throws an exception when the assertion fails', function () {
70-
expect(fn() => Box::of(5)->assertGet(6))->toThrow(LogicException::class);
71-
});
72-
7363
test('flatMap allows us to replace the box itself', function () {
7464
$result = Box::of(5)
7565
->mod(fn(Box $x) => $x->assert(5)->map(fn($x) => $x + 1))

0 commit comments

Comments
 (0)