diff --git a/proposed/error-result-meta.md b/proposed/error-result-meta.md new file mode 100644 index 000000000..78af93bbf --- /dev/null +++ b/proposed/error-result-meta.md @@ -0,0 +1,106 @@ +# Error & Result Handling Meta Document + +## 1. Summary + +This PSR proposes standard interfaces for representing operation results and errors in a type-safe, composable manner. It defines: + +- A `ResultInterface` representing the outcome of an operation (success or failure and the actual result value) +- An `ErrorInterface` representing detailed error information +- Standard patterns for chaining, transforming, and inspecting results + +This actually enables libraries to return predictabe, structured outcomes without exceptions while maintaining interoperability. + +Note: Error messages follow PSR-3 placeholder semantics. This enables deferred formatting, localization, and structured validation output without imposing a formatter or translator. + +Note #2: `ResultFactoryInterface` is OPTIONAL. Libraries and apps MAY use direct construction or static named constructors (like Result::success()), if they control the implementation. The factory interface exists only to enable interoperable creation when implementations are abstracted. + +Note #3: This PSR implements the well-established Either monad (from functional programming), commonly used in Haskell, Scala, Rust, and Kotlin. The interface provides map(fmap), mapError(leftMap), then(flatMap/bind), and fold(either)—all standard Either operations. + +## 2. Why Bother? + +### Current Problems + +1. Libraries use mixed approaches (exceptions, error codes, null returns) +2. PHP's type system cant distinguish between valid returns and errors +3. Simple `false` or `null` returns don't carry error details +4. Each framework implements its own result/error objects +5. Not all failures are exceptional; some are expected business cases + +### Benefits + +- Chain operations without try-catch nesting +- Preserve error context across bondaries +- Libraries can share error semantics +- Avoids exception overhead for expected failures +- Distinguishes between technical errors and business rule violations +- _PHPStan/PHPCS/Psalm can verify error handling_ + +## 3. Scope + +### 3.1 Goals + +- Define standard interfaces for operation results +- Provide base implementation for common use cases +- Enable interoperability between error-aware libraries +- Support both synchronous and asynchronous patterns +- Integrate with existing PSRs (PSR-3 logging, PSR-14 events) +- Be compatible with PHP 7.4+ type systems + +### 3.2 Non-Goals + +- **Not** replacing exceptions for truly exceptional conditions +- **Not** prescribing logging or monitoring implementation +- **Not** definng transport/serialization formats +- **Not** handling global error/exception handlers +- **Not** replacing HTTP status codes in PSR-7/15/18 + +## 4. Approaches + +### 4.1 Chosen Approach: Tagged Union with Monadic Methods + +**Why this approach:** + +- PHP's type system supports it via `isSuccess()`/`isFailure()` discrimination +- Provides both imperative `if` and functional `map()` access patterns +- Familiar to developers from modern languges (Rust, Kotlin, Swift...) +- Maintains PHP's pragmatic balance between OOP and functional patterns +- Can be extended for async/await patterns when Fibers mature + +## 5. Backward Compatibility + +For gradual adoption, libraries MAY: + +1. Add new methods returning `ResultInterface` alongside old methods +2. Provide adapters from exceptions to results + +## 6. Error Classification + +Implementations MAY extend error types: + +- `ValidationError` (business rule violations) +- `NotFoundError` (missing resources) +- `ConflictError` (state conflicts) +- `AuthenticationError` (auth failures) +- `AuthorizationError` (permission issues) +- `InfrastructureError` (system failures) + +## 7. Typing and Generics + +Since PHP does not currently support generics at the language level: + +- This PSR uses phpdoc-based generics (@template) to express the relationship between success values and error values, following established practice in the PHP ecosystem. +- Implementations are expected to enforce the invariant that a Result contains either a success value or an error, but never both. +- Consumers SHOULD rely on `isSuccess()` / `isFailure()` (or equivalent terminal operations such as `fold()`) before accessing the contained value. +- This PSR does not require nor encourage implementors to create separate result classes for each possible value or error type. + +## 8. Namespaces + +- `ErrorInterface` is defined in the `Psr\Error` namespace to allow error objects to be reused independently of `ResultInterface`. +- Errors in this PSR are plain value objects that may be created, transformed, transported, logged, or rendered without necessarily being wrapped in a `Result`. +- `ResultInterface` composes an error but does not own the error abstraction. Keeping these concerns in separate namespaces avoids coupling error representation to a single control-flow mechanism and preserves flexibility for future use-cases. + +## 9. People + +- **PHP-FIG team** +- **Proposer:** Yousha Aleayoub - [blog](https://yousha.blog.ir) +- **Discussion Group:** https://groups.google.com/g/php-fig/c/OpEuvGERM5A diff --git a/proposed/error-result.md b/proposed/error-result.md new file mode 100644 index 000000000..b33f8f94c --- /dev/null +++ b/proposed/error-result.md @@ -0,0 +1,262 @@ +# DRAFT: Error and Result Handling + +## 1. Overview + +This document describes standard interfaces for representing operation results in PHP applications. + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119). + +## 2. Definitions + +- **Result**: The outcome of an operation that may succeed or fail. +- **Success Result**: A result containing a succesful value. +- **Failure Result**: A result containing error information. +- **Error**: Structured information about why an operation failed. + +## 3. Interfaces + +### 3.1 `ErrorInterface` + +```php + + */ + public function getContext(): array; + + /** + * Returns the underlying/previous error. + */ + public function getPrevious(): ?ErrorInterface; + + /** + * Creates a new instance with additional context. + * + * @param array $context + */ + public function withContext(array $context): self; +} +``` + +### 3.2 `ResultInterface` + +```php + + */ + public function map(callable $transform): ResultInterface; + + /** + * Applies a transformation to the error. + * + * Called only if the result is a failure. + * Success values are propagated unchanged. + * + * @template TNewError of ErrorInterface + * @param callable(TError): TNewError $transform + * A callable that receives the error and returns a new error. + * @return ResultInterface + */ + public function mapError(callable $transform): ResultInterface; + + /** + * Chains another operation that returns a Result. + * + * Called only if the result is successful. + * The returned Result is flattened (no nesting). + * + * @template TNew + * @param callable(TValue): ResultInterface $operation + * A callable that receives the success value and returns a Result. + * @return ResultInterface + */ + public function then(callable $operation): ResultInterface; + + /** + * Resolves the result into a single value. + * + * Exactly one callback is called. + * This terminates the Result pipeline. + * + * @template TReturn + * @param callable(TValue): TReturn $onSuccess + * @param callable(TError): TReturn $onFailure + * @return TReturn + */ + public function fold(callable $onSuccess, callable $onFailure): mixed; + + /** + * Returns the success value or a default if failed. + * + * Does not expose the error. + * + * @param TValue $default + * The value to return if the result is a failure. + * @return TValue + */ + public function getValueOr(mixed $default): mixed; +} +``` + +### 3.3 `ResultFactoryInterface` + +```php + + */ + public function success(mixed $value): ResultInterface; + + /** + * Create a failed result. + * + * @template E of ErrorInterface + * @param E $error + * @return ResultInterface + */ + public function failure(ErrorInterface $error): ResultInterface; + + /** + * Create result from a callable that may throw. + * + * @template T + * @param callable(): T $operation + * @param callable(\Throwable): ErrorInterface $errorMapper + * @return ResultInterface + */ + public function try(callable $operation, callable $errorMapper): ResultInterface; +} +``` + +## 4. Usage Examples + +### 4.1 Basic Usage + +```php +$result = $userRepository->findById($id); + +if ($result->isSuccess()) { + $user = $result->getValue(); + echo "Found: " . $user->getName(); +} else { + $error = $result->getError(); + logError($error->getCode(), $error->getContext()); +} +``` + +### 4.2 Functional paradigm + +```php +$email = $userRepository->findById($id) + ->map(fn($user) => $user->getEmail()) + ->getValueOr('default@example.com'); +``` + +### 4.3 Chaining Operations + +```php +$result = $validator->validate($input) + ->then(fn($v) => $repository->save($v)) + ->then(fn($e) => $notifier->notifyCreated($e)) + ->mapError(fn($err) => new PublicError($err->getMessage())); + +// At the end... +if ($result->isSuccess()) { + $finalEntity = $result->getValue(); // From notifier. +} else { + $publicError = $result->getError(); // Already transformed to PublicError. +} +```