NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Package Toolkit for Laravel

Exceptions & retry-after

Everything the toolkit throws extends one base, so you catch any toolkit failure in one place — or the specific subclass:

  • PackageToolkitException — the abstract base for everything the toolkit throws (a RuntimeException).
  • InvalidConfigurationException — a config value is missing, of the wrong type, out of range, not one of an enum’s cases, or not a model class; also thrown by DatabaseDriver::current() for a driver the enum does not model.
use RoundlyConsulting\PackageToolkit\Exceptions\InvalidConfigurationException;
use RoundlyConsulting\PackageToolkit\Exceptions\PackageToolkitException;
use RoundlyConsulting\PackageToolkit\Support\Config;
use RoundlyConsulting\PackageToolkit\Support\ModelResolver;

try {
    $perPage = Config::integer('comments.per_page', 20, min: 1, max: 100);
} catch (InvalidConfigurationException $e) {
    // Configuration value [comments.per_page] must be between 1 and 100, [500] given.
}

try {
    $class = ModelResolver::for('comments.models.comment', Comment::class);
} catch (PackageToolkitException $e) {
    // any toolkit failure
}

Messages

Each failure has a named factory and a fixed message, so logs stay greppable. Every “wrong value” message has one shape — Configuration value [key] must be …, [given] given. — where given is the offending value as written ('' when empty), an int, float or bool as its PHP literal (500, 1.5, true), and anything else by its type (array, null):

FactoryMessage
missing($key)Configuration value [{key}] is required but missing.
notAString($key, $value)Configuration value [{key}] must be a non-empty string, [{given}] given.
notAnInteger($key, $value)Configuration value [{key}] must be an integer, [{given}] given.
outOfRange($key, $min, $max, $value)Configuration value [{key}] must be between {min} and {max}, [{given}] given.
notABoolean($key, $value)Configuration value [{key}] must be a boolean (true/false, 1/0, on/off or yes/no), [{given}] given.
notAValidEnum($key, $enum, $value)Configuration value [{key}] must be one of [{values}], [{given}] given.
notOneOf($key, $allowed, $value)Configuration value [{key}] must be one of [{allowed}], [{given}] given.
notAKeyType($key, $value)Configuration value [{key}] must be one of [bigint, uuid, ulid] (case-insensitive), [{given}] given.
notAnImplementation($key, $contract, $value)Configuration value [{key}] must be a class-string of [{contract}], [{given}] given.
notAModel($key, $value, $base)Configuration value [{key}] must be a class-string of [{base}], [{given}] given.
unsupportedDatabaseDriver($driver)Unsupported database driver [{driver}].

With only one bound, outOfRange() reads “must be at least {min}” or “must be at most {max}” instead of “between”.

Retry-after hints

HasRetryAfter is a contract for exceptions (or other signals) that carry a retry-after hint, so a host can translate a rate-limit failure into a Retry-After header without coupling to any particular base exception. The ProvidesRetryAfter trait implements it: withRetryAfter() stores the delay in seconds, clamped to zero or more, and returns the exception; retryAfterSeconds() reads it back and defaults to 0:

use RoundlyConsulting\PackageToolkit\Concerns\ProvidesRetryAfter;
use RoundlyConsulting\PackageToolkit\Contracts\HasRetryAfter;
use RuntimeException;

final class TooManyRequestsException extends RuntimeException implements HasRetryAfter
{
    use ProvidesRetryAfter;
}

$e = (new TooManyRequestsException('Rate limit reached.'))->withRetryAfter(30);
$e->retryAfterSeconds();                                                 // 30

(new TooManyRequestsException)->retryAfterSeconds();                     // 0 — the default
(new TooManyRequestsException)->withRetryAfter(-5)->retryAfterSeconds(); // 0 — clamped

throw $e;
use RoundlyConsulting\PackageToolkit\Contracts\HasRetryAfter;

// Host side — no knowledge of the package's exception classes needed:
try {
    // … call into the package
} catch (HasRetryAfter $e) {
    return response('Too Many Requests', 429)
        ->header('Retry-After', (string) $e->retryAfterSeconds());
}

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.