NovinkaZverejnili sme 50+ Laravel balíkov ako open source
Custom AI apps, agents and automation — Roundly ConsultingRoundly
Všetky balíky
Package Toolkit for Laravel

Výnimky a retry-after

Všetko, čo toolkit vyhadzuje, dedí z jedného základu, takže akékoľvek zlyhanie toolkitu zachytíte na jednom mieste — alebo konkrétnu podtriedu:

  • PackageToolkitException — abstraktný základ pre všetko, čo toolkit vyhadzuje (RuntimeException).
  • InvalidConfigurationException — konfiguračná hodnota chýba, má nesprávny typ, je mimo rozsahu, nie je prípadom enumu, nie je platnou položkou zoznamu, nie je implementáciou kontraktu, nie je mapou reťazcov alebo nie je triedou modelu; vyhadzuje ju aj DatabaseDriver::current() pre driver, ktorý enum nemodeluje. Trieda je final.
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
}

Správy

Každé zlyhanie má pomenovanú továrenskú metódu a pevnú správu, takže logy sa dajú ľahko prehľadávať. Každá správa o chybnej hodnote má rovnaký tvar — Configuration value [key] must be …, [given] given. (pri položke zoznamu must contain …) — kde given je chybná hodnota, pri štyroch továrenských metódach pre položky (notAStringItem, notAValidListItem, notAnIntegerItem, itemOutOfRange) chybná položka, tak, ako bola zapísaná ('' pre prázdnu), int, float či bool ako PHP literál (500, 1.5, true) a čokoľvek iné ako jeho typ (array, null):

Továrenská metódaSpráva
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.
notAFloat($key, $value)Configuration value [{key}] must be a decimal number, [{given}] given.
outOfRange($key, $min, $max, $value)Configuration value [{key}] must be between {min} and {max}, [{given}] given.
notAList($key, $value)Configuration value [{key}] must be a list of strings (an array or a comma-separated string), [{given}] given.
notAStringItem($key, $item)Configuration value [{key}] must contain only string items, [{item}] given.
notAValidListItem($key, $item)Configuration value [{key}] must contain only valid items, [{item}] given.
notAnIntegerList($key, $value)Configuration value [{key}] must be a list of integers (an array or a comma-separated string), [{given}] given.
notAnIntegerItem($key, $item)Configuration value [{key}] must contain only integer items, [{item}] given.
itemOutOfRange($key, $min, $max, $item)Configuration value [{key}] must contain only items between {min} and {max}, [{item}] given.
notAnIntegerOrOff($key, $offWords, $value)Configuration value [{key}] must be an integer or one of [{off words}], [{given}] given.
notAStringMap($key, $value)Configuration value [{key}] must be a map of names to non-empty strings, [{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}].

Pri jedinej hranici znie outOfRange() „must be at least {min}“ alebo „must be at most {max}“ namiesto „between“. Používajú ju integer(), float() a optionalInteger(); itemOutOfRange() znie rovnako pre integerList(). notAnIntegerOrOff() vypíše vypínacie slová, ktoré volanie prijalo, a optionalInteger() s offWords: [] namiesto nej vyhodí notAnInteger(). notAStringMap() pri položke bez názvu uvedie chybný názov; chybná hodnota pod platným názvom je notAString() s kľúčom {key}.{name}.

Hodnotu zabalenú do SensitiveParameterValue z PHP opíše správa typom toho, čo obaľuje ([int] given., [string] given.), nikdy jej obsahom — čítače tajných hodnôt takto odovzdávajú každú hodnotu. Každý parameter s hodnotou ($value, $item) má #[SensitiveParameter], takže hodnota sa ukáže jedine v správe, nikdy v argumentoch rámca stack trace. Pozrite časť Tajné hodnoty v konfigurácii a fasádach.

Rovnaké správy pre vašu vlastnú výnimku

Továrenské metódy sú v traite DescribesConfigurationErrors (RoundlyConsulting\PackageToolkit\Exceptions\Concerns) a každá vracia static. Obsahuje všetky továrenské metódy z tabuľky vyššie okrem unsupportedDatabaseDriver(). InvalidConfigurationException ho používa a zostáva final — slovník správ balík zdieľa cez trait, nie cez dedičnosť. Použite ho vo vlastnej konfiguračnej výnimke vášho balíka, aby kontrola, ktorú čítače nepokrývajú, vyhodila rovnakú správu, no ako vašu výnimku:

use RoundlyConsulting\PackageToolkit\Exceptions\Concerns\DescribesConfigurationErrors;
use RoundlyConsulting\PackageToolkit\Support\Config;
use RuntimeException;

final class CommentsConfigurationException extends RuntimeException
{
    use DescribesConfigurationErrors;
}

// A check the readers don't cover, in the toolkit's message shape, typed as your exception:
throw CommentsConfigurationException::outOfRange('comments.per_page', 1, 100, $value);
// Configuration value [comments.per_page] must be between 1 and 100, [500] given.

// Pair it with Config::using(), and the readers throw the same class:
Config::using(CommentsConfigurationException::class)->integer('comments.per_page', 20, min: 1, max: 100);
  • Konštruktor triedy musí prijať správu ako prvý argument. Každá podtrieda Exception to spĺňa.
  • Správy sú bajt po bajte zhodné so správami toolkitu — každú z nich fixuje golden test, pre InvalidConfigurationException aj pre výnimku balíka, ktorá trait používa.
  • Každý parameter s hodnotou zostáva #[SensitiveParameter] a SensitiveParameterValue sa stále opíše len typom.
  • Privátne pomocné metódy traitu majú dlhé názvy (describeConfigurationValue(), …), takže sa nemôžu zraziť s metódou describe(), ktorú už vaša trieda má. Metóda deklarovaná vo vašej triede má prednosť pred rovnomennou metódou traitu, takže vlastnou notAnImplementation() nahradíte tú zdieľanú.

Retry-after

HasRetryAfter je kontrakt pre výnimky (alebo iné signály), ktoré nesú informáciu o čakaní pred ďalším pokusom. Hostiteľ tak zlyhanie rate limitu prevedie na hlavičku Retry-After bez väzby na konkrétnu základnú výnimku. Implementuje ho trait ProvidesRetryAfter: withRetryAfter() uloží oneskorenie v sekundách, orezané na nulu a viac, a vráti výnimku; retryAfterSeconds() ho prečíta, predvolene 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());
}

Jeden handler v hostiteľskej aplikácii

Ak chcete na všetky takéto výnimky odpovedať rovnako, zaregistrujte jeden handler v bootstrap/app.php hostiteľskej aplikácie cez $exceptions->map() alebo $exceptions->respond(). Nepoužívajte $exceptions->render(fn (HasRetryAfter $e) …): Laravel spustí najprv map(), potom vlastnú metódu render() výnimky a až po nej callbacky render(). Mnohé výnimky balíkov majú render() implementovanú samy, a keď vráti odpoveď, callback render() sa už nikdy nespustí.

map() zmení výnimku na ThrottleRequestsException z Laravelu, ktorá sa vykreslí ako 429 s hlavičkami, ktoré odovzdáte. Beží ešte pred vlastnou render() výnimky, takže pokryje každú HasRetryAfter:

use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use RoundlyConsulting\PackageToolkit\Contracts\HasRetryAfter;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->map(fn (HasRetryAfter $e) => new ThrottleRequestsException(
        $e->getMessage(),
        $e,
        ['Retry-After' => (string) $e->retryAfterSeconds()],
    ));
})

Mapovanie platí aj pri reportovaní výnimky. ThrottleRequestsException Laravel nereportuje, takže namapované výnimky sa do vašich logov už nedostanú — pri „príliš veľa pokusov“ zvyčajne presne to, čo chcete. respond() naopak ponechá odpoveď, ktorú výnimka vykreslila, a upraví ju; beží pri každej vykreslenej odpovedi vrátane tej z vlastnej render() výnimky:

use Illuminate\Http\Request;
use RoundlyConsulting\PackageToolkit\Contracts\HasRetryAfter;
use Symfony\Component\HttpFoundation\Response;

$exceptions->respond(function (Response $response, Throwable $e, Request $request): Response {
    if ($e instanceof HasRetryAfter) {
        $response->setStatusCode(429);
        $response->headers->set('Retry-After', (string) $e->retryAfterSeconds());
    }

    return $response;
});

Prejavte lásku k open source

Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.

Ďalšie spôsoby podpory vrátane kryptomien

Odoslaním daru súhlasíte s našimi podmienkami prijímania darov.

Chcete to zabudovať do svojho produktu?

Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.