juststeveking/http-status-code
PHP package that turns HTTP status codes into a typed enum, with helpers for messages, code categories and Laravel validation. Replace magic numbers with descriptive cases for clearer, safer API and controller responses—no more guessing what 418 means.
Install
composer require juststeveking/http-status-code
Constrain it to ^3.0 in composer.json — that is the backed-enum line and requires PHP 8.1+. Older tags (1.x, 2.x) predate the enum and are covered by stale blog posts; match the tag to your runtime, not the other way round.
There is no service provider, no facade, no config file and no publish step. Laravel will autoload the single enum via Composer and nothing else happens at boot. If you find yourself running php artisan vendor:publish, you've misread the package.
Where to look first
README.md — the entire public surface fits on a screen.src/Enums/HttpStatusCode.php — this file is the package. Skim the case names before coding, because they follow IANA reason phrasing: UNPROCESSABLE_ENTITY, METHOD_NOT_ALLOWED, IM_A_TEAPOT. They will not match the short aliases you may have memorised from other libraries.composer.json — confirm the PHP constraint before you assume it installs on a legacy branch.Your first day-one use case
Strip magic numbers out of a controller action:
use JustSteveKing\HttpStatusCode\Enums\HttpStatusCode;
return response()->json($user, HttpStatusCode::CREATED->value);
return response()->json(['message' => 'Not found'], HttpStatusCode::NOT_FOUND->value);
That's the value: an autocompletable, greppable vocabulary so the next reviewer doesn't have to remember what 422 means. Note the ->value — it is mandatory on response()->json()'s int $status parameter and forgetting it is the standard first-hour mistake.
Translate domain results in the controller, not in the domain. Let your action return a domain status, then match it onto HTTP semantics at the boundary:
$result = $this->registerUser->execute($request->validated());
return match ($result->status) {
RegistrationStatus::Created => response()->json($result->user, HttpStatusCode::CREATED->value),
RegistrationStatus::Duplicate => response()->json(['message' => 'Exists'], HttpStatusCode::CONFLICT->value),
default => response()->json(null, HttpStatusCode::NO_CONTENT->value),
};
This ages well because the mapping is exhaustively visible in one place, and PHPStan will nag you when a new domain case appears.
Central exception handler with a status map. Instead of chained ifs, keep a lookup array. Laravel 10: app/Exceptions/Handler.php. Laravel 11: bootstrap/app.php via withExceptions. Either way:
protected array $statusMap = [
UserNotFoundException::class => HttpStatusCode::NOT_FOUND,
ValidationException::class => HttpStatusCode::UNPROCESSABLE_ENTITY,
InsufficientFundsException::class => HttpStatusCode::PAYMENT_REQUIRED,
];
Self-describing exceptions — the highest-leverage integration. Give each domain exception a status(): HttpStatusCode method. The enum becomes the contract, the handler becomes generic (one match for the whole app), and you can unit-test the mapping without booting the framework:
final class UserNotFoundException extends RuntimeException
{
public function status(): HttpStatusCode
{
return HttpStatusCode::NOT_FOUND;
}
}
A single response helper per layer. If your API uses an envelope, wrap the enum once so it appears in exactly one file and is trivial to audit:
final class ApiResponse
{
public static function ok(mixed $data = null): JsonResponse
{
return response()->json(['data' => $data], HttpStatusCode::OK->value);
}
}
Dynamic codes from config, DB columns or webhooks. Use HttpStatusCode::tryFrom($code) and branch on null. Never from() — it throws ValueError and converts a bad input into a 500:
$status = HttpStatusCode::tryFrom((int) $payload['status'])
?? HttpStatusCode::INTERNAL_SERVER_ERROR;
Testing. Put the enum on the expected side so failures read like English:
$response->assertStatus(HttpStatusCode::CREATED->value);
For large suites, register a TestResponse macro (assertHttpStatus(HttpStatusCode $code)) in a service provider and keep call sites uniform.
Do not replace framework machinery. abort(404), ValidationException, and Response::HTTP_* already emit correct codes. Use this package for responses you construct; pick either this enum or Response::HTTP_* and enforce it project-wide.
response()->json($data, HttpStatusCode::OK) type-errors on the int $status parameter. Always ->value. Run Larastan/PHPStan at a level that catches HttpStatusCode passed where int is declared.=== comparisons fail silently. HttpStatusCode::OK === 200 is always false. Compare ->value to ->value, or better, compare case to case.tryFrom() returns ?self. A webhook forwarding "status": 999 or "abc" yields null. Decide explicitly — reject the payload or fall back — rather than letting it fall through.json_encode(HttpStatusCode::OK) yields 200 in PHP 8.1+. Usually convenient; occasionally a surprise when an enum leaks into a log context or a test fixture.Retry-After concept, wrap the package enum in your own class. Don't fork the package for it.lang/ files and use the enum only for the numeric code. Treat ->name as a developer-facing identifier.Response::HTTP_* and Guzzle has its own constants. UNPROCESSABLE_ENTITY vs HTTP_UNPROCESSABLE_ENTITY — let autocompletion pick it, don't type from memory.config/http-status-code.php, no container binding, no way to change messages. Don't hunt for one.3.0.2 is a patch from a first-time contributor described only as "minor improvements", with no public API change — bumping 3.0.1 → 3.0.2 needs no code edits or test changes. Constrain to ^3.0 and treat major bumps as the only reviewable upgrade events. The surface area is ~10 lines of frozen spec, so risk is genuinely low; if it ever becomes inconvenient, inline the enum under MIT. Bus-factor is a fair concern for a constants list, and Response::HTTP_* is a defensible substitute that you could migrate to in an afternoon.dump() the enum case rather than the raw int — the case name instantly tells you whether you picked the wrong constant or whether middleware overwrote the response downstream.How can I help you explore Laravel packages today?