Skip to content

Commit

Permalink
Neverthrow v2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
sethsandaru committed Nov 5, 2023
1 parent 5db0a25 commit 73fbb1c
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 89 deletions.
140 changes: 89 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# NeverThrow - PHP Version
# ShipSaaS NeverThrow for PHP

[![codecov](https://codecov.io/gh/shipsaas/never-throw/branch/main/graph/badge.svg?token=P1E3WP84MG)](https://codecov.io/gh/shipsaas/never-throw)
[![Build & Test (PHP 8.1, 8.2)](https://github.com/shipsaas/never-throw/actions/workflows/build.yml/badge.svg)](https://github.com/shipsaas/never-throw/actions/workflows/build.yml)

Inspired by [NeverThrow](https://github.com/supermacro/neverthrow) so here we are with a simple implementation of NeverThrow from PHP.
Provide a strictly typed way to deal with Success & Error Outcomes for your PHP applications.

See more [notes of NeverThrow](https://github.com/supermacro/neverthrow#a-note-on-the-package-name) on the whys.
Inspired by [NeverThrow](https://github.com/supermacro/neverthrow) so here we are with a simple implementation of NeverThrow from PHP.

No throw = happier life & application.
> Throwing and catching is very similar to using goto statements - in other words; it makes reasoning about your programs harder. Secondly, by using throw you make the assumption that the caller of your function is implementing catch. This is a known source of errors. Example: One dev throws and another dev uses the function without prior knowledge that the function will throw. Thus, and edge case has been left unhandled and now you have unhappy users, bosses, cats, etc.
>
> With all that said, there are definitely good use cases for throwing in your program. But much less than you might think.
Use the simple `ok` and `error` result for your application code with a strictly-typed way in PHP.
TL;DR: no throw = happier life & application. Fewer errors on production & increase the DRY & reusable things in your PHP apps.

## Supports
PHP 8.1+
PHP 8.2+ since v2.0.0

## Installation

Expand All @@ -24,10 +26,6 @@ composer require shipsaas/never-throw

`NeverThrow\\Result` class and ready to be extended anytime.

### Static
- `Result::ok(..)`: create an `ok` result.
- `Result::error(..)`: create an `error` result.

### Non-static
- `isOk()`
- `isError()`
Expand All @@ -38,32 +36,43 @@ composer require shipsaas/never-throw

### Create your response classes

Basically, A simple class to hold `ok` or `error` data.

For example, I'll create 2 new classes - 1 ok and 1 error.
We'll create two new classes: Success & Error. And feel free to put any of your data

```php
class BookShipperOkResult
use NeverThrow\SuccessResult;
use NeverThrow\ErrorResult;

// Success
class BookShipperOkResult extends SuccessResult
{
public function __construct(
public string $bookingId
) {}
}

class BookShipperErrorResult
enum BookingErrors {
case NO_SHIPPER_AVAILABLE;
case OVERWEIGHT_PACKAGE;
case INVALID_ADDRESS;
}

// Error
class BookShipperErrorResult extends ErrorResult
{
// note even more strict, you can use `enum` - php8.1
public function __construct(
public string $outcome
public BookingErrors $outcome
) {}
}
```

### Create your new Result class
### Create your dedicated Result class

The reason why you need to create your own Result class:
Last step before using NeverThrow. Creating a dedicated Result class helps us to:

- Cast the correct types for the error & success.
- Define types for the error & success result class
- => IDE-friendly
- => Happier reader/caller
- A centralized place that reference to the Success & Error result classes.

#### First way
```php
Expand All @@ -83,36 +92,32 @@ class BookShipperResult extends Result
}
```

#### Second way

```php
use NeverThrow\Result;

/**
* @method BookShipperOkResult getOkResult()
* @method BookShipperErrorResult getErrorResult()
*/
class BookShipperResult extends Result
{
}
```

### Set Error / OK
### Returns the Result in your business logic

```php
public function createBooking(User $user, BookingOrder $order): BookShipperResult
{
if (!$user->hasEnoughtBalance()) {
return BookShipperResult::error(new BookShipperErrorResult('INSUFFICIENT_BALANCE'));
$hasAnyShipperAvailable = $this->shipperService->hasAnyShipperAvailable();
if (!$hasAnyShipperAvailable) {
return new BookShipperResult(
new BookShipperErrorResult(
BookingErrors::NO_SHIPPER_AVAILABLE
)
);
}

if (!$order->isValid()) {
return BookShipperResult::error(new BookShipperErrorResult('ORDER_IS_INVALID'));
$isOverweight = !$this->weightService->isValid($order);
if ($isOverweight) {
return new BookShipperResult(
new BookShipperErrorResult(
BookingErrors::OVERWEIGHT_PACKAGE
)
);
}

$bookingId = $this->book($user, $order);

return BookShipperResult::ok(new BookShipperOkResult($bookingId));
return new BookShipperResult(new BookShipperOkResult($bookingId));
}
```

Expand All @@ -122,10 +127,12 @@ public function createBooking(User $user, BookingOrder $order): BookShipperResul
$bookingResult = $this->service->createBooking($user, $order);

if ($bookingResult->isError()) {
$errorResult = $bookingResult->getErrorResult();

// handle error
return showError(match ($bookingResult->getErrorResult()->outcome) {
'INSUFFICIENT_BALANCE' => 'Not enough balance to make the booking',
'ORDER_IS_INVALID' => 'The order is invalid (probably expired)',
return showError(match ($errorResult->outcome) {
BookingErrors::NO_SHIPPER_AVAILABLE => 'No shipper available at the moment. Please wait',
BookingErrors::OVERWEIGHT_PACKAGE => 'Your package is overweight',
});
}

Expand All @@ -134,20 +141,51 @@ return showBooking($bookingResult->getOkResult()->bookingId);

## Conclusion

As you can see, there is no:
As you can see with the above code, there are:

- try/catch everywhere, try/catch abuse
- no try/catch, 0 try/catch abuse
- no exception, never
- explicit return types & information.

Only ok/err response. It would bring the development truly awesome and no pain. There won't be any error thrown in a middle of a big flow.
It would bring the development truly awesome and no pain.

With strictly typed return types, developers can know what is going on with other services/libraries.
Thus makes the reusable better.
And we don't have to wrap the try/catch and uglify our code.

Don't abuse Exceptions, they should be only used for the unexpected situations (and Errors !== Exceptions, fact).

### Additional

```php
function transfer(): Transaction
{
if (!$hasEnoughBalance) {
thrown new InsufficientBalanceError();
}

if (!$invalidRecipient) {
throw new InvalidRecipientError();
}

if (!$invalidMoney) {
throw new InvalidTransferMoneyError();
}

$transaction = $this->transferService->transfer(...);
if (!$transaction) {
throw new TransferFailedError();
}

return $transaction;
}
```

With strictly-typed return types, developers can know what is going on with other's services/libraries, it makes the
reusable skyrocketing. And they don't have to wrap the try/catch while using the stuff.
Most of this function is actually about the things that can go wrong, but our types only inform us of the successful path. That means 4/5ths of the function's output is **untyped**!

Don't abuse Exceptions, they should be used for the unexpected things. And error !== exception.
The above "exceptions" or "errors" aren't really exceptions or errors at all. They are outcomes. They are predictable, reasonable parts of our system. My heuristic is, if they are something a good product manager would care about, they are not exceptions and you shouldn't throw them!

## Contributor
Seth Phat
Exceptions are unpredictable things we cannot reasonably plan for, that the system should not attempt recovery from, and we should not route to the user.

## License
MIT License
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"name": "shipsaas/never-throw",
"type": "library",
"version": "1.0.1",
"version": "2.0.0",
"description": "Response-first over Throw. PHP NeverThrow library sample implementation",
"keywords": [
"php",
"library",
"response-first",
"neverthrow"
"neverthrow",
"no exception"
],
"authors": [
{
Expand All @@ -19,7 +20,7 @@
],
"license": "MIT",
"require": {
"php": "^8.1|^8.2"
"php": "^8.2"
},
"autoload": {
"psr-4": {
Expand Down
11 changes: 11 additions & 0 deletions src/ErrorResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace NeverThrow;

abstract class ErrorResult implements ResultInterface
{
public final function isOk(): false
{
return false;
}
}
45 changes: 24 additions & 21 deletions src/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,17 @@

namespace NeverThrow;

use LogicException;

abstract class Result
{
private bool $isOk;
protected readonly mixed $okResult;
protected readonly mixed $errorResult;

public static function ok(mixed $okResult): static
{
$result = new static();
$result->isOk = true;
$result->okResult = $okResult;

return $result;
}
private SuccessResult|ErrorResult $result;

public static function error(mixed $errorResult): static
public function __construct(SuccessResult|ErrorResult $result)
{
$result = new static();
$result->isOk = false;
$result->errorResult = $errorResult;

return $result;
$this->isOk = $result->isOk();
$this->result = $result;
}

public function isOk(): bool
Expand All @@ -36,13 +25,27 @@ public function isError(): bool
return !$this->isOk;
}

public function getOkResult(): mixed
/**
* @throws LogicException
*/
public function getOkResult(): SuccessResult
{
return $this->okResult ?? null;
if ($this->isError()) {
throw new LogicException('Result is not OK');
}

return $this->result;
}

public function getErrorResult(): mixed
/**
* @throws LogicException
*/
public function getErrorResult(): ErrorResult
{
return $this->errorResult ?? null;
if ($this->isOk()) {
throw new LogicException('Result is not ERROR');
}

return $this->result;
}
}
8 changes: 8 additions & 0 deletions src/ResultInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace NeverThrow;

interface ResultInterface
{
public function isOk(): bool;
}
11 changes: 11 additions & 0 deletions src/SuccessResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace NeverThrow;

abstract class SuccessResult implements ResultInterface
{
public final function isOk(): true
{
return true;
}
}
Loading

0 comments on commit 73fbb1c

Please sign in to comment.