DEV Community

Discussion on: Result<T, E> type in PHP

Collapse
 
mindplay profile image
Rasmus Schultz

Unfortunately, this pattern does not give you any type-safety in PHP - and therefore no real guarantees or safety, like you would get in Go or Rust.

You can work around that with generic type-annotations using something like PHPStan or another type-checker - but personally, I find the most explicit approach is a simply type union like Result|Error as you can see for example here:

github.com/mindplay-dk/timber/blob...

The explicit return type encourages the caller to use an instanceof type-check, which (in most IDEs and tools like PHPStan) will correctly narrow the local variable type in an if/else block.

This way, you avoid introducing a pseudo-generic abstract "result" type, which becomes a global dependency in your projects.

To really leverage this pattern, use a pair of case-dependent return types in your return type unions - for example, a createInvoice method might return Invoice|InvoiceError, such that you have two independent classes representing a successful invoice or providing details about why invoicing failed.

Bringing features from one language into another can sometimes make sense, but in this case, union types in PHP already work better than a generic result-type - they only really work well in languages that support generics, and in practice, a type union in PHP works much like the generic result-types in other languages anyway.

Collapse
 
ianflanagan profile image
Ian Flanagan • Edited

Type safety should be possible with phpstan. I haven't used this extensively yet, but it's working so far:

/**
 * @return Result<int, InternalError>
 */
public function called(): Result
{
    $rand = rand(0, 1);

    return match ($rand) {
        0 => Result::ok(1),
        1 => Result::fail(InternalError::NotFound),
    };
}
Enter fullscreen mode Exit fullscreen mode
/**
 * @template-covariant TValue
 * @template-covariant TError of BackedEnum
 */
final readonly class Result
{
    /** @var TValue|null */
    private mixed $value;

    /** @var TError|null */
    private ?BackedEnum $error;

    /**
     * @param TValue|null $value
     * @param TError|null $error
     */
    private function __construct(mixed $value, ?BackedEnum $error)
    {
        $this->value = $value;
        $this->error = $error;
    }

    /**
     * @template V
     * @param    V $value
     * @return   self<V, never>
     */
    public static function ok(mixed $value): self
    {
        /** @var Result<V, never> */
        return new self($value, null);
    }

    /**
     * @template E of BackedEnum
     * @param    E $error
     * @return   self<never, E>
     */
    public static function fail(BackedEnum $error): self
    {
        /** @var Result<never, E> */
        return new self(null, $error);
    }

    /**
     * @phpstan-assert-if-true  !null $this->value
     * @phpstan-assert-if-false !null $this->error
     */
    public function isOk(): bool
    {
        return $this->error === null;
    }

    /**
     * @phpstan-assert-if-true  !null $this->error
     * @phpstan-assert-if-false !null $this->value
     */
    public function isError(): bool
    {
        return $this->error !== null;
    }

    /**
     * @return TValue
     */
    public function value(): mixed
    {
        if (! $this->isOk()) {
            throw new LogicException('Tried to get value from error Result');
        }

        return $this->value;
    }

    /**
     * @return TError
     */
    public function error(): BackedEnum
    {
        if (! $this->isError()) {
            throw new LogicException('Tried to get error from ok Result');
        }

        return $this->error;
    }
}
Enter fullscreen mode Exit fullscreen mode

Unions must be the most performant solution, but if the project has more than one Error DTO/enum, then I'm attracted by the isOk() method removing the need to know the error type when checking. With unions, checking against the wrong error type gives a false-positive (although, likely caught by phpstan if you do anything meaningful with value).

I learned a lot from your comment so I really appreciate the time you put in.