From ce9165c2025d9062a34b72feae68ac0e398d8ee6 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Sat, 4 Apr 2026 19:17:19 +0100 Subject: [PATCH 1/4] docs: Add more explanations on what types to use when --- README.md | 61 +++++++++++++++++++ doc/UPGRADING.md | 31 ++++++++-- doc/USAGE.md | 153 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 README.md create mode 100644 doc/USAGE.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..0125c09c --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Horde Exception + +Exception handling library for the [Horde Project](https://www.horde.org/). + +Provides a set of exception base classes that integrate with the Horde +framework. All exceptions implement `HordeThrowable` (details support) and +`LogThrowable` (log-level tracking), and extend PHP's SPL exception hierarchy +for proper semantic categorisation. + +## Installation + +```bash +composer require horde/exception +``` + +## Quick Start + +```php +use Horde\Exception\HordeRuntimeException; +use Horde\Exception\HordeThrowable; + +// Throw a typed exception +throw new HordeRuntimeException('Connection failed', 0, $previous); + +// Catch any Horde exception via the interface +try { + // ... +} catch (HordeThrowable $e) { + echo $e->getMessage(); + echo $e->getDetails(); +} +``` + +## Choosing an Exception Base Class + +| Error type | Base class | +|---|---| +| Runtime / I/O failure | `HordeRuntimeException` | +| Invalid argument from caller | `HordeInvalidArgumentException` | +| Programmer logic error | `HordeLogicException` | +| Domain rule violation | `HordeDomainException` | +| Resource not found | `NotFound` | +| Permission denied | `PermissionDenied` | +| Generic / uncategorised | `HordeException` | + +Component exceptions should extend the SPL-based class that best fits +(e.g. `class CacheException extends HordeRuntimeException {}`). + +See [doc/USAGE.md](doc/USAGE.md) for the full use-case table, guidance on +`Wrapped` vs standard exceptions, and component exception patterns. + +## Documentation + +- **[doc/USAGE.md](doc/USAGE.md)** — Choosing the right base class, the + `Wrapped` pitfall, creating component exceptions +- **[doc/UPGRADING.md](doc/UPGRADING.md)** — Migrating from PSR-0 to PSR-4, + class mapping table, breaking changes + +## License + +LGPL-2.1-only. See [LICENSE](LICENSE) for details. diff --git a/doc/UPGRADING.md b/doc/UPGRADING.md index 349a59ff..c1e8bd0b 100644 --- a/doc/UPGRADING.md +++ b/doc/UPGRADING.md @@ -143,15 +143,36 @@ throw $e; ### Wrapped Exception +> **Note:** `Wrapped` is intended for converting `PEAR_Error` objects, not for +> general exception chaining. See [USAGE.md](USAGE.md) for guidance on choosing +> the right base class and why `Wrapped` is usually the wrong choice for +> component exceptions. + ```php -// Old -throw new Horde_Exception_Wrapped('Context', $previous); +// Converting PEAR_Error (the intended use) +throw new Horde_Exception_Wrapped($pearError); -// New -use Horde\Exception\Wrapped; -throw new Wrapped('Context', $previous); +// For exception chaining, use $previous (third arg) on any exception: +throw new HordeRuntimeException('Context', 0, $previousException); ``` +### Migrating Component Exceptions Off Wrapped + +Many component exceptions historically extend `Horde_Exception_Wrapped`. +The modern replacement is an SPL-based Horde exception: + +```php +// Old — component exception on Wrapped (loses $previous in lib/ path) +class Horde_MyComponent_Exception extends Horde_Exception_Wrapped {} + +// New — in src/, extend the matching SPL variant +use Horde\Exception\HordeRuntimeException; +class MyComponentException extends HordeRuntimeException {} +``` + +See [USAGE.md](USAGE.md) for the full use-case table and component exception +patterns. + ### Not Found Exception ```php diff --git a/doc/USAGE.md b/doc/USAGE.md new file mode 100644 index 00000000..38a1cc2f --- /dev/null +++ b/doc/USAGE.md @@ -0,0 +1,153 @@ +# Horde Exception — Usage Guide + +## Choosing the Right Exception Base Class + +Every Horde exception implements `HordeThrowable` and `LogThrowable`, giving you +`getDetails()`/`setDetails()` and `markAsLogged()`/`isLogged()` regardless of +which base you pick. The question is **which SPL base** fits your error. + +### Use-Case Table + +| Use case | Traditional (lib/) | Modern (src/) | +|---|---|---| +| Generic error, no specific category | `Horde_Exception` | `HordeException` | +| Runtime failure (I/O, process, network) | `Horde_Exception` | `HordeRuntimeException` | +| Caller passed an invalid argument | `Horde_Exception` | `HordeInvalidArgumentException` | +| Programmer error / impossible state | `Horde_Exception` | `HordeLogicException` | +| Value outside domain rules | `Horde_Exception` | `HordeDomainException` | +| Invalid method call / wrong state | `Horde_Exception` | `HordeBadMethodCallException` | +| Length constraint violated | `Horde_Exception` | `HordeLengthException` | +| Index out of bounds (data-dependent) | `Horde_Exception` | `HordeOutOfBoundsException` | +| Index out of range (programmer error) | `Horde_Exception` | `HordeOutOfRangeException` | +| Unexpected value from external source | `Horde_Exception` | `HordeUnexpectedValueException` | +| Resource not found (404-style) | `Horde_Exception_NotFound` | `NotFound` | +| Permission denied (403-style) | `Horde_Exception_PermissionDenied` | `PermissionDenied` | +| Wrapping a PHP error from `error_get_last()` | `Horde_Exception_LastError` | `LastError` | +| Converting a PEAR_Error | `Horde_Exception_Pear` | `Pear` (deprecated) | +| Wrapping a PEAR_Error with userinfo extraction | `Horde_Exception_Wrapped` | `Wrapped` (see below) | + +### The Rule of Thumb + +- **Component exceptions** (e.g. `SpellCheckException`, `CacheException`) + should extend the SPL-based class that matches the error category — usually + `HordeRuntimeException` for I/O-related failures. + +- **`HordeException`** is the catch-all. If none of the specific SPL classes + fit, use it — but prefer a specific one when one applies. + +- **Catch against `HordeThrowable`**, not concrete classes. This catches both + PSR-0 and PSR-4 exceptions. + +## What `Wrapped` Is For (and Why You Probably Don't Want It) + +### Purpose + +`Wrapped` exists for one specific use case from the Horde 3/4 era: converting a +**`PEAR_Error` object** into an exception. When you pass a `PEAR_Error` as the +first argument, `Wrapped` extracts: + +- The message via `getMessage()` +- The error code via `getCode()` +- Userinfo via `getUserinfo()` → stored in `$details` + +```php +// The intended use — wrapping a PEAR_Error: +$pearError = PEAR::raiseError('something failed', 42); +throw new Horde_Exception_Wrapped($pearError); +// message = "something failed", code = 42, details = userinfo +``` + +### Why It's Usually the Wrong Choice + +`Wrapped` was the only way to preserve exception chain context in the PEAR era. +Modern PHP has had `$previous` (the third constructor argument) since PHP 5.3. +Every SPL exception — and every Horde exception — already supports it: + +```php +throw new HordeRuntimeException('Context message', 0, $previousException); +``` + +Using `Wrapped` for exception chaining instead of `$previous` creates problems: + +1. **Legacy `Horde_Exception_Wrapped` silently drops `$previous`.** + Its constructor signature is `($message, $code)` — only two parameters. + If you write `new Horde_Exception_Wrapped($msg, $code, $previous)`, the + third argument is silently ignored by PHP. The chain is lost. + +2. **Passing an Exception as `$message` is a workaround, not an API.** + `Wrapped` detects `instanceof Exception` on the first argument and sets + `$previous` internally. This works but is confusing — callers expect the + first argument to be a string. + +3. **No SPL specificity.** `Wrapped` extends `HordeException` which extends + `Exception`. You lose the ability to distinguish runtime errors from logic + errors, invalid arguments from domain violations, etc. + +### What to Use Instead + +| Situation | Wrong | Right | +|---|---|---| +| Rethrowing with context | `new Wrapped($e)` | `new HordeRuntimeException('Context', 0, $e)` | +| Component exception chain | `new MyException($msg, $code, $e)` extending `Wrapped` | `new MyException($msg, $code, $e)` extending `HordeRuntimeException` | +| Converting PEAR_Error | — | `new Wrapped($pearError)` (this is the valid use) | + +### Migration from `Wrapped` + +If your component exception currently extends `Horde_Exception_Wrapped` or +`Horde\Exception\Wrapped`: + +```php +// Before — extends Wrapped, loses $previous on legacy path +class Horde_MyComponent_Exception extends Horde_Exception_Wrapped {} + +// After — component exception with proper chain support +// In src/: +use Horde\Exception\HordeRuntimeException; +class MyComponentException extends HordeRuntimeException {} + +// In lib/ (wrapper, kept for BC): +class Horde_MyComponent_Exception extends Horde_Exception_Wrapped {} +// Document in phpdoc: wontfix/BC, new code should use src/ exception +``` + +## Creating Component Exceptions + +A component exception should: + +1. Live in `src/Exception/` under the component namespace +2. Extend the SPL-based Horde exception that best fits +3. Add no logic unless the component genuinely needs it + +```php +spellCheck($text); +} catch (SpellCheckException $e) { + // handle spellcheck failure +} + +// Broad — catches any Horde exception +try { + $checker->spellCheck($text); +} catch (HordeThrowable $e) { + // handle any Horde error +} +``` From 4514decac98dd4adc9b3c109081f4fd719844f27 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Sat, 4 Apr 2026 20:04:07 +0100 Subject: [PATCH 2/4] style: php-cs-fixer --- compat/PEAR.php | 7 +++--- compat/PEAR/Error.php | 7 +++--- lib/Horde/Exception.php | 16 ++++++++------ lib/Horde/Exception/LastError.php | 11 +++++++--- lib/Horde/Exception/NotFound.php | 13 ++++++++---- lib/Horde/Exception/Pear.php | 23 ++++++++++++-------- lib/Horde/Exception/PermissionDenied.php | 13 ++++++++---- lib/Horde/Exception/Translation.php | 8 +++---- lib/Horde/Exception/Wrapped.php | 27 ++++++++++++++---------- src/HordeBadMethodCallException.php | 4 ++-- src/HordeDomainException.php | 4 ++-- src/HordeException.php | 7 +++++- src/HordeInvalidArgumentException.php | 4 ++-- src/HordeLengthException.php | 4 ++-- src/HordeLogicException.php | 4 ++-- src/HordeOutOfBoundsException.php | 4 ++-- src/HordeOutOfRangeException.php | 4 ++-- src/HordeRuntimeException.php | 4 ++-- src/HordeThrowable.php | 6 +++--- src/HordeUnexpectedValueException.php | 4 ++-- src/LastError.php | 7 +++++- src/LogThrowable.php | 7 +++--- src/NotFound.php | 7 +++++- src/PEAR.php | 11 ++++++---- src/Pear.php | 15 ++++++++----- src/PearError.php | 7 +++--- src/PermissionDenied.php | 7 +++++- src/PermissionDeniedThrowable.php | 10 ++++----- src/Translation.php | 7 ++++-- src/Wrapped.php | 21 +++++++++++------- test/ExceptionTest.php | 12 ++++++----- test/Stub/PearError.php | 1 + test/Unit/BuiltinExceptionTest.php | 6 +++--- test/Unit/ExceptionTest.php | 8 +++---- test/Unit/UnnamespacedExceptionTest.php | 7 +++--- test/UnnamespacedExceptionTest.php | 10 +++++---- 36 files changed, 193 insertions(+), 124 deletions(-) diff --git a/compat/PEAR.php b/compat/PEAR.php index 0460fc62..3efef9fe 100644 --- a/compat/PEAR.php +++ b/compat/PEAR.php @@ -1,4 +1,5 @@ + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -21,6 +22,4 @@ * * Extends Horde\Exception\PEAR which will be autoloaded by PSR-4. */ -class PEAR extends Horde\Exception\PEAR -{ -} +class PEAR extends Horde\Exception\PEAR {} diff --git a/compat/PEAR/Error.php b/compat/PEAR/Error.php index f7a244dd..17e476a6 100644 --- a/compat/PEAR/Error.php +++ b/compat/PEAR/Error.php @@ -1,4 +1,5 @@ + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -21,6 +22,4 @@ * * Extends Horde\Exception\PearError which will be autoloaded by PSR-4. */ -class PEAR_Error extends Horde\Exception\PearError -{ -} +class PEAR_Error extends Horde\Exception\PearError {} diff --git a/lib/Horde/Exception.php b/lib/Horde/Exception.php index 899dc94a..05e2636e 100644 --- a/lib/Horde/Exception.php +++ b/lib/Horde/Exception.php @@ -1,25 +1,29 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception */ use Horde\Exception\HordeException; + /** * Horde base exception class. * - * @author + * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2008-2017 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception */ -class Horde_Exception extends HordeException -{ -} +class Horde_Exception extends HordeException {} diff --git a/lib/Horde/Exception/LastError.php b/lib/Horde/Exception/LastError.php index 7b376e1b..2223cdd3 100644 --- a/lib/Horde/Exception/LastError.php +++ b/lib/Horde/Exception/LastError.php @@ -1,11 +1,14 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL * @package Exception @@ -15,7 +18,9 @@ * Horde exception class that accepts output of error_get_last() as $code and * mask itself as that error. * - * @author + * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2008-2017 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL diff --git a/lib/Horde/Exception/NotFound.php b/lib/Horde/Exception/NotFound.php index 760c0d6e..8cd9c024 100644 --- a/lib/Horde/Exception/NotFound.php +++ b/lib/Horde/Exception/NotFound.php @@ -1,11 +1,14 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL * @package Exception @@ -14,7 +17,9 @@ /** * Exception thrown if an object wasn't found. * - * @author + * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2010-2017 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL @@ -38,4 +43,4 @@ public function __construct($message = null, $code = null) } parent::__construct($message, $code ?? 0); } -} \ No newline at end of file +} diff --git a/lib/Horde/Exception/Pear.php b/lib/Horde/Exception/Pear.php index 58cec4eb..419a938b 100644 --- a/lib/Horde/Exception/Pear.php +++ b/lib/Horde/Exception/Pear.php @@ -1,11 +1,14 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL * @package Exception @@ -14,7 +17,9 @@ /** * Horde exception class that converts PEAR errors to exceptions. * - * @author + * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2008-2017 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL @@ -54,12 +59,12 @@ private function _getPearTrace(PEAR_Error $error) if (!empty($backtrace)) { $pear_error .= 'PEAR backtrace:' . "\n\n"; foreach ($backtrace as $frame) { - $pear_error .= - (isset($frame['class']) ? $frame['class'] : '') - . (isset($frame['type']) ? $frame['type'] : '') - . (isset($frame['function']) ? $frame['function'] : 'unkown') . ' ' - . (isset($frame['file']) ? $frame['file'] : 'unkown') . ':' - . (isset($frame['line']) ? $frame['line'] : 'unkown') . "\n"; + $pear_error + .= ($frame['class'] ?? '') + . ($frame['type'] ?? '') + . ($frame['function'] ?? 'unkown') . ' ' + . ($frame['file'] ?? 'unkown') . ':' + . ($frame['line'] ?? 'unkown') . "\n"; } } $userinfo = $error->getUserInfo(); diff --git a/lib/Horde/Exception/PermissionDenied.php b/lib/Horde/Exception/PermissionDenied.php index b356ba88..1b54aba9 100644 --- a/lib/Horde/Exception/PermissionDenied.php +++ b/lib/Horde/Exception/PermissionDenied.php @@ -1,11 +1,14 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL * @package Exception @@ -14,7 +17,9 @@ /** * Exception thrown if any access without sufficient permissions occured. * - * @author + * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2010-2017 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL @@ -38,4 +43,4 @@ public function __construct($message = null, $code = null) } parent::__construct($message, $code ?? 0); } -} \ No newline at end of file +} diff --git a/lib/Horde/Exception/Translation.php b/lib/Horde/Exception/Translation.php index f9f98f43..008e39fe 100644 --- a/lib/Horde/Exception/Translation.php +++ b/lib/Horde/Exception/Translation.php @@ -1,6 +1,7 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL * @package Exception @@ -15,7 +18,9 @@ * Horde exception class that can wrap and set its details from PEAR_Error, * Exception, and other objects with similar interfaces. * - * @author + * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2008-2017 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL @@ -33,22 +38,22 @@ class Horde_Exception_Wrapped extends Horde_Exception public function __construct($message = null, $code = 0) { $previous = null; - if (is_object($message) && - method_exists($message, 'getMessage')) { - if (empty($code) && - method_exists($message, 'getCode')) { - $code = (int)$message->getCode(); + if (is_object($message) + && method_exists($message, 'getMessage')) { + if (empty($code) + && method_exists($message, 'getCode')) { + $code = (int) $message->getCode(); } if ($message instanceof Exception) { $previous = $message; } - if (method_exists($message, 'getUserinfo') && - $details = $message->getUserinfo()) { + if (method_exists($message, 'getUserinfo') + && $details = $message->getUserinfo()) { $this->details = $details; } elseif (!empty($message->details)) { $this->details = $message->details; } - $message = (string)$message->getMessage(); + $message = (string) $message->getMessage(); } parent::__construct($message, $code, $previous); diff --git a/src/HordeBadMethodCallException.php b/src/HordeBadMethodCallException.php index 9602f9e4..b70da436 100644 --- a/src/HordeBadMethodCallException.php +++ b/src/HordeBadMethodCallException.php @@ -8,7 +8,7 @@ * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -30,7 +30,7 @@ * New namespaced code should NOT type hint against this exception but rather against HordeThrowable. * For domain-specific exceptions, inherit from BadMethodCallException and implement HordeThrowable directly. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/HordeDomainException.php b/src/HordeDomainException.php index f8cd9141..a8d52046 100644 --- a/src/HordeDomainException.php +++ b/src/HordeDomainException.php @@ -8,7 +8,7 @@ * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -30,7 +30,7 @@ * New namespaced code should NOT type hint against this exception but rather against HordeThrowable. * For domain-specific exceptions, inherit from DomainException and implement HordeThrowable directly. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/HordeException.php b/src/HordeException.php index 19478623..69fea54a 100644 --- a/src/HordeException.php +++ b/src/HordeException.php @@ -1,11 +1,14 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -26,6 +29,8 @@ * and possibly add functionality or context through interfaces and accompanying traits. * * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2008-2021 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/HordeInvalidArgumentException.php b/src/HordeInvalidArgumentException.php index 4eab3e50..21c445b6 100644 --- a/src/HordeInvalidArgumentException.php +++ b/src/HordeInvalidArgumentException.php @@ -8,7 +8,7 @@ * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -29,7 +29,7 @@ * New namespaced code should NOT type hint against this exception but rather against HordeThrowable. * For domain-specific exceptions, inherit from InvalidArgumentException and implement HordeThrowable directly. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/HordeLengthException.php b/src/HordeLengthException.php index 63863f48..50a43c12 100644 --- a/src/HordeLengthException.php +++ b/src/HordeLengthException.php @@ -8,7 +8,7 @@ * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -30,7 +30,7 @@ * New namespaced code should NOT type hint against this exception but rather against HordeThrowable. * For domain-specific exceptions, inherit from LengthException and implement HordeThrowable directly. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/HordeLogicException.php b/src/HordeLogicException.php index 45b66d98..1008c140 100644 --- a/src/HordeLogicException.php +++ b/src/HordeLogicException.php @@ -8,7 +8,7 @@ * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -30,7 +30,7 @@ * New namespaced code should NOT type hint against this exception but rather against HordeThrowable. * For domain-specific exceptions, inherit from LogicException and implement HordeThrowable directly. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/HordeOutOfBoundsException.php b/src/HordeOutOfBoundsException.php index ec49d0b5..7e4bea05 100644 --- a/src/HordeOutOfBoundsException.php +++ b/src/HordeOutOfBoundsException.php @@ -8,7 +8,7 @@ * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -29,7 +29,7 @@ * New namespaced code should NOT type hint against this exception but rather against HordeThrowable. * For domain-specific exceptions, inherit from OutOfBoundsException and implement HordeThrowable directly. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/HordeOutOfRangeException.php b/src/HordeOutOfRangeException.php index 8fd1a83b..cbe5c4ed 100644 --- a/src/HordeOutOfRangeException.php +++ b/src/HordeOutOfRangeException.php @@ -8,7 +8,7 @@ * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -30,7 +30,7 @@ * New namespaced code should NOT type hint against this exception but rather against HordeThrowable. * For domain-specific exceptions, inherit from OutOfRangeException and implement HordeThrowable directly. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/HordeRuntimeException.php b/src/HordeRuntimeException.php index 892c4377..1b6aadb0 100644 --- a/src/HordeRuntimeException.php +++ b/src/HordeRuntimeException.php @@ -8,7 +8,7 @@ * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -29,7 +29,7 @@ * New namespaced code should NOT type hint against this exception but rather against HordeThrowable. * For domain-specific exceptions, inherit from RuntimeException and implement HordeThrowable directly. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/HordeThrowable.php b/src/HordeThrowable.php index f25c5633..330509f4 100644 --- a/src/HordeThrowable.php +++ b/src/HordeThrowable.php @@ -2,12 +2,12 @@ declare(strict_types=1); /** - * Copyright 2008-2022 Horde LLC (http://www.horde.org/) + * Copyright 2008-2026 Horde LLC (http://www.horde.org/) * * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -20,7 +20,7 @@ /** * The root interface from which all Horde Exceptions must descend * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2022 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/HordeUnexpectedValueException.php b/src/HordeUnexpectedValueException.php index 5b850a44..20ae99a9 100644 --- a/src/HordeUnexpectedValueException.php +++ b/src/HordeUnexpectedValueException.php @@ -8,7 +8,7 @@ * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -30,7 +30,7 @@ * New namespaced code should NOT type hint against this exception but rather against HordeThrowable. * For domain-specific exceptions, inherit from UnexpectedValueException and implement HordeThrowable directly. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/LastError.php b/src/LastError.php index d6a0d46e..5dcc812e 100644 --- a/src/LastError.php +++ b/src/LastError.php @@ -1,11 +1,14 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -25,6 +28,8 @@ * An alternative implementation using a more well-defined static method should NOT use a custom constructor. * * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2008-2021 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL diff --git a/src/LogThrowable.php b/src/LogThrowable.php index 5ef710eb..126b8493 100644 --- a/src/LogThrowable.php +++ b/src/LogThrowable.php @@ -1,11 +1,12 @@ + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -16,7 +17,7 @@ /** * Horde extended exception interface. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2021 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/NotFound.php b/src/NotFound.php index 7f608a74..842b302f 100644 --- a/src/NotFound.php +++ b/src/NotFound.php @@ -1,11 +1,14 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -19,6 +22,8 @@ * Exception thrown if an object wasn't found. * * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2008-2021 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL diff --git a/src/PEAR.php b/src/PEAR.php index 5d298323..cf02086b 100644 --- a/src/PEAR.php +++ b/src/PEAR.php @@ -1,11 +1,12 @@ + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -13,6 +14,8 @@ namespace Horde\Exception; +use PEAR_Error; + /** * PEAR compatibility shim for legacy code. * @@ -20,11 +23,11 @@ * to create PEAR_Error instances. * * Original PEAR source: - * Copyright 1997-2010 The Authors + * Copyright 1997-2026 The Authors * Licensed under BSD-2-Clause * https://github.com/pear/PEAR * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2026 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 @@ -69,7 +72,7 @@ public static function raiseError( class_exists('PEAR_Error'); // Trigger PSR-0 autoload } - return new \PEAR_Error($message, $code, $mode, $options, $userinfo); + return new PEAR_Error($message, $code, $mode, $options, $userinfo); } /** diff --git a/src/Pear.php b/src/Pear.php index f7fb5091..91abde53 100644 --- a/src/Pear.php +++ b/src/Pear.php @@ -1,11 +1,14 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -25,6 +28,8 @@ * Instead, use a trait that actually unpacks a PEAR_Error into the regular exception constructor. * * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2008-2021 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL @@ -64,8 +69,8 @@ private function _getPearTrace(PEAR_Error $error) if (!empty($backtrace)) { $pear_error .= 'PEAR backtrace:' . "\n\n"; foreach ($backtrace as $frame) { - $pear_error .= - ($frame['class'] ?? '') + $pear_error + .= ($frame['class'] ?? '') . ($frame['type'] ?? '') . ($frame['function'] ?? 'unkown') . ' ' . ($frame['file'] ?? 'unkown') . ':' @@ -77,8 +82,8 @@ private function _getPearTrace(PEAR_Error $error) $pear_error .= "\n" . 'PEAR user info:' . "\n\n"; if (is_string($userinfo)) { $pear_error .= $userinfo; - // PEAR_Error is pretty legacy - // The phpdoc annotation cannot be fully trusted + // PEAR_Error is pretty legacy + // The phpdoc annotation cannot be fully trusted } else { $pear_error .= print_r($userinfo, true); } diff --git a/src/PearError.php b/src/PearError.php index 57357ca4..d46c7ade 100644 --- a/src/PearError.php +++ b/src/PearError.php @@ -1,11 +1,12 @@ + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -21,7 +22,7 @@ * compatibility with legacy code that still uses PEAR_Error. * * Original PEAR_Error source: - * Copyright 1997-2010 The Authors + * Copyright 1997-2026 The Authors * Licensed under BSD-2-Clause * https://github.com/pear/PEAR * @@ -31,7 +32,7 @@ * - No error_message_prefix * - Simplified backtrace (uses debug_backtrace directly) * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2026 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 diff --git a/src/PermissionDenied.php b/src/PermissionDenied.php index e787003e..84a305df 100644 --- a/src/PermissionDenied.php +++ b/src/PermissionDenied.php @@ -1,11 +1,14 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -23,6 +26,8 @@ * Consuming code should check against the PermissionDeniedThrowable, not this class * * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2008-2021 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL diff --git a/src/PermissionDeniedThrowable.php b/src/PermissionDeniedThrowable.php index 67270f79..2ac83dc4 100644 --- a/src/PermissionDeniedThrowable.php +++ b/src/PermissionDeniedThrowable.php @@ -2,12 +2,12 @@ declare(strict_types=1); /** - * Copyright 2008-2022 Horde LLC (http://www.horde.org/) + * Copyright 2008-2026 Horde LLC (http://www.horde.org/) * * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -20,12 +20,10 @@ * * Intentionally a noop / marker interface * - * @author Ralf Lang + * @author Ralf Lang * @category Horde * @copyright 2008-2022 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception */ -interface PermissionDeniedThrowable extends HordeThrowable -{ -} +interface PermissionDeniedThrowable extends HordeThrowable {} diff --git a/src/Translation.php b/src/Translation.php index 354abd8b..35f7cbec 100644 --- a/src/Translation.php +++ b/src/Translation.php @@ -1,11 +1,13 @@ + * @author Jan Schneider + * @author Ralf Lang * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -19,6 +21,7 @@ * Horde_Exception_Translation is the translation wrapper class for Horde_Exception. * * @author Jan Schneider + * @author Ralf Lang * @category Horde * @copyright 2010-2021 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL diff --git a/src/Wrapped.php b/src/Wrapped.php index ee85e314..70e9fbf9 100644 --- a/src/Wrapped.php +++ b/src/Wrapped.php @@ -1,11 +1,14 @@ + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 * @package Exception @@ -26,6 +29,8 @@ * Try using HordeException and pass previous exception as 3rd Argument. * * @author Jan Schneider + * @author Chuck Hagenbuch + * @author Michael Slusarz * @category Horde * @copyright 2008-2021 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL @@ -36,7 +41,7 @@ class Wrapped extends HordeException /** * Exception constructor. * - * @param string|Pear_Error|Throwable $message The exception message, + * @param string|PEAR_Error|Throwable $message The exception message, * a PEAR_Error object, or an Exception object. * The Exception case is deprecated * @param int $code A numeric error code. @@ -51,20 +56,20 @@ public function __construct($message = '', $code = 0, ?Throwable $previous = nul } if (is_object($message)) { if (method_exists($message, 'getMessage')) { - if (empty($code) && - method_exists($message, 'getCode')) { - $code = (int)$message->getCode(); + if (empty($code) + && method_exists($message, 'getCode')) { + $code = (int) $message->getCode(); } if ($message instanceof Throwable) { $previous = $message; } - if (method_exists($message, 'getUserinfo') && - $details = $message->getUserinfo()) { + if (method_exists($message, 'getUserinfo') + && $details = $message->getUserinfo()) { $this->details = $details; } elseif (!empty($message->details)) { $this->details = $message->details; } - $message = (string)$message->getMessage(); + $message = (string) $message->getMessage(); } elseif (method_exists($message, '__toString')) { // TODO: When PHP 8 is minimum, check for Stringable instead $message = (string) $message; diff --git a/test/ExceptionTest.php b/test/ExceptionTest.php index acbe8687..8ebc5b5d 100644 --- a/test/ExceptionTest.php +++ b/test/ExceptionTest.php @@ -1,6 +1,7 @@ - * @author Ralf Lang + * @author Ralf Lang * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 */ @@ -33,6 +34,7 @@ * @package Exception * @author Gunnar Wrobel * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class ExceptionTest extends TestCase { @@ -59,7 +61,7 @@ public function testMethodGetpreviousYieldsPreviousException() public function testMethodTostringYieldsExceptionDescription() { $e = new HordeException(); - $this->assertStringContainsString('Horde\Exception\HordeException', (string)$e); + $this->assertStringContainsString('Horde\Exception\HordeException', (string) $e); } /** @@ -69,8 +71,8 @@ public function testMethodTostringYieldsExceptionDescription() public function testMethodTostringContainsDescriptionOfPreviousException() { $e = new HordeException(null, null, new Exception('previous')); - $this->assertStringContainsString('Next Horde\Exception\HordeException', (string)$e); - $this->assertMatchesRegularExpression('/Exception: previous/', (string)$e); + $this->assertStringContainsString('Next Horde\Exception\HordeException', (string) $e); + $this->assertMatchesRegularExpression('/Exception: previous/', (string) $e); } // NotFound Exception Testing diff --git a/test/Stub/PearError.php b/test/Stub/PearError.php index fe49323d..dd58a88c 100644 --- a/test/Stub/PearError.php +++ b/test/Stub/PearError.php @@ -1,4 +1,5 @@ + * @author Ralf Lang * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 */ @@ -46,7 +46,7 @@ * * @category Horde * @package Exception - * @author Ralf Lang + * @author Ralf Lang * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 */ #[CoversClass(HordeRuntimeException::class)] @@ -164,7 +164,7 @@ public function testConstructionWithPreviousException(string $hordeClass, string public function testToStringContainsExceptionClass(string $hordeClass, string $builtinClass): void { $exception = new $hordeClass('Test message'); - $string = (string)$exception; + $string = (string) $exception; $this->assertStringContainsString($hordeClass, $string); $this->assertStringContainsString('Test message', $string); } diff --git a/test/Unit/ExceptionTest.php b/test/Unit/ExceptionTest.php index 20588454..6e4c55c7 100644 --- a/test/Unit/ExceptionTest.php +++ b/test/Unit/ExceptionTest.php @@ -11,7 +11,7 @@ * @category Horde * @package Exception * @author Gunnar Wrobel - * @author Ralf Lang + * @author Ralf Lang * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 */ @@ -68,7 +68,7 @@ public function testMethodGetpreviousYieldsPreviousException(): void public function testMethodTostringYieldsExceptionDescription(): void { $e = new HordeException(); - $this->assertStringContainsString('Horde\Exception\HordeException', (string)$e); + $this->assertStringContainsString('Horde\Exception\HordeException', (string) $e); } /** @@ -78,8 +78,8 @@ public function testMethodTostringYieldsExceptionDescription(): void public function testMethodTostringContainsDescriptionOfPreviousException(): void { $e = new HordeException('', 0, new Exception('previous')); - $this->assertStringContainsString('Next Horde\Exception\HordeException', (string)$e); - $this->assertMatchesRegularExpression('/Exception: previous/', (string)$e); + $this->assertStringContainsString('Next Horde\Exception\HordeException', (string) $e); + $this->assertMatchesRegularExpression('/Exception: previous/', (string) $e); } // NotFound Exception Testing diff --git a/test/Unit/UnnamespacedExceptionTest.php b/test/Unit/UnnamespacedExceptionTest.php index 3c56e8ba..e771f2d1 100644 --- a/test/Unit/UnnamespacedExceptionTest.php +++ b/test/Unit/UnnamespacedExceptionTest.php @@ -35,6 +35,7 @@ * @package Exception * @author Gunnar Wrobel * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class UnnamespacedExceptionTest extends TestCase { @@ -61,7 +62,7 @@ public function testMethodGetpreviousYieldsPreviousException(): void public function testMethodTostringYieldsExceptionDescription(): void { $e = new Horde_Exception(); - $this->assertMatchesRegularExpression('/(exception |^)\'?Horde_Exception\'? in/', (string)$e); + $this->assertMatchesRegularExpression('/(exception |^)\'?Horde_Exception\'? in/', (string) $e); } /** @@ -71,8 +72,8 @@ public function testMethodTostringYieldsExceptionDescription(): void public function testMethodTostringContainsDescriptionOfPreviousException(): void { $e = new Horde_Exception('', 0, new Exception('previous')); - $this->assertMatchesRegularExpression('/Next Horde_Exception/', (string)$e); - $this->assertMatchesRegularExpression('/Exception: previous/', (string)$e); + $this->assertMatchesRegularExpression('/Next Horde_Exception/', (string) $e); + $this->assertMatchesRegularExpression('/Exception: previous/', (string) $e); } // NotFound Exception Testing diff --git a/test/UnnamespacedExceptionTest.php b/test/UnnamespacedExceptionTest.php index 66fea24c..b59a67ce 100644 --- a/test/UnnamespacedExceptionTest.php +++ b/test/UnnamespacedExceptionTest.php @@ -1,6 +1,7 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class UnnamespacedExceptionTest extends TestCase { @@ -58,7 +60,7 @@ public function testMethodGetpreviousYieldsPreviousException() public function testMethodTostringYieldsExceptionDescription() { $e = new Horde_Exception(); - $this->assertMatchesRegularExpression('/(exception |^)\'?Horde_Exception\'? in/', (string)$e); + $this->assertMatchesRegularExpression('/(exception |^)\'?Horde_Exception\'? in/', (string) $e); } /** @@ -68,8 +70,8 @@ public function testMethodTostringYieldsExceptionDescription() public function testMethodTostringContainsDescriptionOfPreviousException() { $e = new Horde_Exception(null, null, new Exception('previous')); - $this->assertMatchesRegularExpression('/Next Horde_Exception/', (string)$e); - $this->assertMatchesRegularExpression('/Exception: previous/', (string)$e); + $this->assertMatchesRegularExpression('/Next Horde_Exception/', (string) $e); + $this->assertMatchesRegularExpression('/Exception: previous/', (string) $e); } // NotFound Exception Testing From 64fb1062e001cce8a052eaf769cbdd23b78bea1f Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Sat, 4 Apr 2026 20:43:28 +0100 Subject: [PATCH 3/4] f doc --- doc/UPGRADING.md | 2 +- doc/USAGE.md | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/UPGRADING.md b/doc/UPGRADING.md index c1e8bd0b..03b51596 100644 --- a/doc/UPGRADING.md +++ b/doc/UPGRADING.md @@ -8,7 +8,7 @@ Legacy PSR-0 `lib/` remain for backward compatibility with the old interface tho ## Requirements -- PHP 8.2 or later +- PHP 8.1 or later ## Key Changes diff --git a/doc/USAGE.md b/doc/USAGE.md index 38a1cc2f..041cb332 100644 --- a/doc/USAGE.md +++ b/doc/USAGE.md @@ -35,8 +35,12 @@ which base you pick. The question is **which SPL base** fits your error. - **`HordeException`** is the catch-all. If none of the specific SPL classes fit, use it — but prefer a specific one when one applies. -- **Catch against `HordeThrowable`**, not concrete classes. This catches both - PSR-0 and PSR-4 exceptions. +- Libraries should carefully choose: If they are fairly isolated from Horde ecosystem and have a high + chance of usage outside, NOT relying on the Horde exception tree might be attractive and reduce dependency creep. + +- **Broad Catch against `HordeThrowable`**: If you need a broad catch, catch against HordeThrowable. This catches both + PSR-0 and PSR-4 exceptions. This neatly separates framework internal concerns (we potentially know to handle) from outside failures + (which we would have caught somewhere more specific, closer to the root cause if we knew to handle them). ## What `Wrapped` Is For (and Why You Probably Don't Want It) From ffb899b974da1d587918126dd015ffff357ae8e4 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Sat, 4 Apr 2026 21:19:48 +0100 Subject: [PATCH 4/4] docs: Add meta keywords to .horde.yml --- .horde.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.horde.yml b/.horde.yml index 7e786142..d25eb5c3 100644 --- a/.horde.yml +++ b/.horde.yml @@ -52,3 +52,7 @@ autoload: classmap: - compat/ vendor: horde +keywords: + - framework + - spl + - pear_error