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:
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.
/**
* @template-covariant TValue
* @template-covariant TError of BackedEnum
*/finalreadonlyclassResult{/** @var TValue|null */privatemixed$value;/** @var TError|null */private?BackedEnum$error;/**
* @param TValue|null $value
* @param TError|null $error
*/privatefunction__construct(mixed$value,?BackedEnum$error){$this->value=$value;$this->error=$error;}/**
* @template V
* @param V $value
* @return self<V, never>
*/publicstaticfunctionok(mixed$value):self{/** @var Result<V, never> */returnnewself($value,null);}/**
* @template E of BackedEnum
* @param E $error
* @return self<never, E>
*/publicstaticfunctionfail(BackedEnum$error):self{/** @var Result<never, E> */returnnewself(null,$error);}/**
* @phpstan-assert-if-true !null $this->value
* @phpstan-assert-if-false !null $this->error
*/publicfunctionisOk():bool{return$this->error===null;}/**
* @phpstan-assert-if-true !null $this->error
* @phpstan-assert-if-false !null $this->value
*/publicfunctionisError():bool{return$this->error!==null;}/**
* @return TValue
*/publicfunctionvalue():mixed{if(!$this->isOk()){thrownewLogicException('Tried to get value from error Result');}return$this->value;}/**
* @return TError
*/publicfunctionerror():BackedEnum{if(!$this->isError()){thrownewLogicException('Tried to get error from ok Result');}return$this->error;}}
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.
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
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|Erroras you can see for example here:github.com/mindplay-dk/timber/blob...
The explicit return type encourages the caller to use an
instanceoftype-check, which (in most IDEs and tools like PHPStan) will correctly narrow the local variable type in anif/elseblock.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
createInvoicemethod might returnInvoice|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.
Type safety should be possible with phpstan. I haven't used this extensively yet, but it's working so far:
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 withvalue).I learned a lot from your comment so I really appreciate the time you put in.