Skip to content

Commit f48ad1f

Browse files
committed
improve TryBox
1 parent 213d997 commit f48ad1f

File tree

2 files changed

+68
-99
lines changed

2 files changed

+68
-99
lines changed

src/TryBox.php

Lines changed: 28 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -10,148 +10,87 @@
1010
* Will catch exceptions and return them as a value.
1111
*
1212
* @template T
13+
* @template E
1314
*/
1415
class TryBox
1516
{
1617
/**
1718
* @template U
1819
* @param U $value
19-
* @return TryBox<U>
20+
* @return TryBox<U, null>
2021
*/
2122
public static function of($value): TryBox
2223
{
23-
return new self($value);
24+
/** @var TryBox<U, null> $box */
25+
$box = new self($value, null);
26+
27+
return $box;
2428
}
2529

2630
/**
2731
* @param T $value
2832
*/
29-
public function __construct(private mixed $value)
30-
{
33+
public function __construct(
34+
private $value,
35+
private ?Throwable $error = null
36+
) {
3137
}
3238

3339
/**
3440
* Apply a transformation function to the value.
3541
*
3642
* @template U
3743
* @param callable(T): U $callback
38-
* @return TryBox<Throwable>|TryBox<U>
44+
* @return TryBox<U, null>|TryBox<T, Throwable>
3945
*/
4046
public function map(callable $callback): TryBox
4147
{
42-
return $this->value instanceof Throwable
43-
? $this
44-
: $this->transform($callback);
45-
}
46-
47-
/**
48-
* Apply a transformation function to the value.
49-
* Will catch exceptions and return them as a value.
50-
*
51-
* @template U
52-
* @param callable(T): U $callback
53-
* @return TryBox<U>|TryBox<Throwable>
54-
*/
55-
private function transform(callable $callback): TryBox
56-
{
57-
try {
58-
return new self($callback($this->value));
59-
} catch (Throwable $e) {
60-
return new self($e);
48+
if ($this->error !== null) {
49+
return $this;
6150
}
62-
}
6351

64-
/**
65-
* Apply a transformation function to the box itself
66-
*
67-
* @template U
68-
* @param callable(self<T>): TryBox<U> $callback
69-
* @return TryBox<U>|TryBox<Throwable>
70-
*/
71-
public function mod(callable $callback): TryBox
72-
{
73-
try {
74-
return $callback($this);
75-
} catch (Throwable $e) {
76-
return new self($e);
77-
}
52+
return $this->try($callback);
7853
}
7954

8055
/**
8156
* Unbox the value, which might be anything including a throwable.
8257
*
83-
* @return T
58+
* @return Throwable|T
8459
*/
8560
public function value()
8661
{
87-
return $this->value;
62+
return $this->error ?? $this->value;
8863
}
8964

90-
9165
/**
9266
* @return T
9367
*
9468
* @throws Throwable
9569
*/
96-
public function rip(): mixed
70+
public function rip()
9771
{
98-
if ($this->value instanceof Throwable) {
99-
throw $this->value;
72+
if ($this->error !== null) {
73+
throw $this->error;
10074
}
10175

102-
assert(($this->value instanceof Throwable) === false);
103-
10476
return $this->value;
10577
}
10678

10779
/**
108-
* Assert that the value is equal to the expected value.
109-
*
110-
* @param T $expected
111-
* @return TryBox<T>|TryBox<Throwable>
80+
* @template U
81+
* @param callable(T):U $callback
82+
* @return TryBox<U, null>|TryBox<T, Throwable>
11283
*/
113-
public function assert(mixed $expected): TryBox
84+
private function try(callable $callback): TryBox
11485
{
11586
try {
116-
return $this->performAssertion($expected);
87+
/** @var TryBox<U, null> $result */
88+
$result = new self($callback($this->value), null);
11789
} catch (Throwable $e) {
118-
return new self($e);
119-
}
120-
}
121-
122-
/**
123-
* Run an assertion against the value.
124-
* Example of a simple strict equality check: ->assert(5)
125-
* Example of a callback check: ->assert(fn($x) => $x > 5)
126-
*
127-
* @template U
128-
* @param U|callable(T):bool $check
129-
* @return TryBox<T>|TryBox<Throwable>
130-
*/
131-
public function performAssertion(mixed $check, string $message = ''): TryBox
132-
{
133-
$isClosure = is_callable($check);
134-
135-
$pass = $isClosure
136-
? $check($this->value)
137-
: $this->value === $check;
138-
139-
if (! $pass) {
140-
$report = $isClosure
141-
? 'Value did not pass the callback check.'
142-
: sprintf(
143-
'Failed asserting that two values are the same. Expected %s, got %s.',
144-
var_export($check, true),
145-
var_export($this->value, true)
146-
);
147-
148-
if ($message !== '') {
149-
$report = $message . ' | ' . $report;
150-
}
151-
152-
throw new LogicException($report);
90+
/** @var TryBox<T, Throwable> $result */
91+
$result = new self($this->value, $e);
15392
}
15493

155-
return $this;
94+
return $result;
15695
}
15796
}

tests/Unit/TryBoxTest.php

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,52 @@
33
namespace Tests\Unit;
44

55
use Epic64\PhpBox\TryBox;
6+
use RuntimeException;
67
use Throwable;
78

8-
test('TryBox can catch exceptions', function () {
9+
test('TryBox returns exception as value', function () {
910
$result = TryBox::of(5)
10-
->map(fn(int $value) => $value / 1)
11-
->map(fn(int $value) => $value * 2)
11+
->map(fn($value) => throw new RuntimeException('boo'))
12+
->map(fn($value) => $value * 2)
13+
->map(fn($value) => $value + 1)
1214
->value();
1315

14-
expect($result)->toBeInstanceOf(Throwable::class);
16+
expect($result)
17+
->toBeInstanceOf(RuntimeException::class)
18+
->and($result->getMessage())->toBe('boo'); // @phpstan-ignore method.nonObject
1519
});
1620

17-
test('TryBox result can be used when nothing is thrown', function () {
18-
$result = TryBox::of(5)
19-
->map(fn(int $value) => $value * 2)
20-
->map(fn(int $value) => $value + 1)
21-
->rip();
21+
test('TryBox value can be used when nothing is thrown', function () {
22+
$result = TryBox::of(1)
23+
->map(fn($value) => $value + 1)
24+
->map(fn($value) => $value + 1)
25+
->value();
26+
27+
// manual narrowing of $result from int|Throwable to int
28+
if ($result instanceof Throwable) {
29+
throw $result;
30+
}
31+
32+
$result = $result + 1;
33+
34+
expect($result)->toBe(4);
35+
});
36+
37+
test('Using rip() on the TryBox will narrow the type but risk throwing an exception', function () {
38+
$result = TryBox::of(1)
39+
->map(fn($value) => $value + 1)
40+
->map(fn($value) => $value + 1)
41+
->rip(); // Can throw exception but the type is narrowed to int
42+
43+
$result = $result + 1;
44+
45+
expect($result)->toBe(4);
46+
});
47+
48+
test('Using rip() on TryBox with error will cause exception', function () {
49+
$box = TryBox::of(1)
50+
->map(fn($value) => throw new RuntimeException('boo'))
51+
->map(fn($value) => $value + 1);
2252

23-
expect($result * 2)->toBe(11);
53+
expect(fn() => $box->rip())->toThrow(RuntimeException::class, 'boo');
2454
});

0 commit comments

Comments
 (0)