teapot/status-code
teapot/status-code gives Laravel and PHP apps a clean way to reference HTTP status codes. Replace magic numbers with readable constants and helpers in responses, middleware, and tests, keeping your API consistent and self-documenting, with a touch of teapot humor.
composer require teapot/status-code
Before writing any code, do three things:
composer.json. This package is built around PHP 8.1+ enums, so if any of your services are still on 8.0 this is a non-starter.src/) — it is the entire public API. Confirm the case naming convention (Status::NOT_FOUND vs Status::NotFound), the backing type (almost certainly int), and which helper methods exist (phrase(), reason(), isSuccess(), isClientError(), family() — names vary between packages). Thirty seconds here saves you a BadMethodCallException later.Response::HTTP_* constants, raw 200/404/422 literals, and abort(404). That list is your migration backlog and tells you where the package will actually pay off.First use case: a controller (or a base ApiController) returning a created resource.
use Teapot\StatusCode\Status; // verify exact namespace in src/
return response()->json($user, Status::CREATED->value);
If that one line reads better than 201 to you, the package earns its place. If it doesn't, stop here — Laravel's Illuminate\Http\Response::HTTP_CREATED already exists and adds zero dependencies.
Treat it as a type, not a constant table. The real win is signatures that can't be passed garbage:
function respond(array $payload, Status $status = Status::OK): JsonResponse
{
return response()->json($payload, $status->value);
}
Now respond($data, 999) is a TypeError at the call site rather than a broken response at runtime.
Custom exception carrying a status. This is the highest-leverage pattern in a Laravel app:
class ApiException extends \RuntimeException
{
public function __construct(
string $message,
public readonly Status $status = Status::BAD_REQUEST,
public readonly array $context = [],
) {
parent::__construct($message);
}
}
Then in bootstrap/app.php (Laravel 11+) or app/Exceptions/Handler.php:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(fn (ApiException $e) => response()->json([
'message' => $e->getMessage(),
], $e->status->value));
})
Every throw new ApiException('...', Status::CONFLICT) in your domain layer now renders correctly with no controller-level try/catch. Validate this with a Feature test per status you actually use.
Override failedValidation in a base FormRequest so 422s come from one place:
protected function failedValidation(Validator $validator)
{
throw new ValidationException($validator, response()->json([
'errors' => $validator->errors(),
], Status::UNPROCESSABLE_ENTITY->value));
}
Use it in abort() and middleware. abort(Status::FORBIDDEN->value) in a Gate::authorize fallback; a maintenance middleware returning Status::SERVICE_UNAVAILABLE->value with a Retry-After header.
Use it in assertions. $response->assertStatus(Status::CREATED->value) — self-documenting tests that fail loudly if someone "tidies" 201 into 200.
Bridge to OpenAPI/docs. One match or array map from Status cases to descriptions keeps your generated docs and your runtime in sync instead of maintaining two lists of numbers.
DB casts. For columns like last_response_status, an Eloquent enum cast to the package enum gives you $model->last_response_status->value on read and validation on write.
->value is mandatory at the Laravel boundary. response()->json($data, Status::OK) will not work — Symfony's Response wants an int. Do the unwrapping in one helper and never scatter ->value through your controllers.499 Client Closed Request, or internal-only codes). Check coverage before committing; if you need extras, wrap it in your own App\Http\Status enum or a static helper rather than forking.$status === 200 is always false. Compare $status->value === 200 or $status === Status::OK. This is the most likely silent bug in a migration.json_encode of a backed enum emits its scalar value, which is convenient for responses but surprising if you expected the case name — and pure (non-backed) enums throw on encode. Verify which you have.->phrase() is for logs, dev tools, and docs. Anything a customer reads goes through lang/.Response::HTTP_*. Pick one convention for the whole team and enforce it in code review, otherwise you have two canonical sources of truth. Realistically, keep the enum for new domain/signature code and leave existing constants alone."teapot/status-code": "^1.0") and read the changelog before upgrading, because you have no downstream ecosystem to signal breakage. The surface is tiny, so worst case you vendor the enum into your app in an afternoon.phrase() vs reason(), and on whether family helpers exist at all. If a helper you need is missing, add it as a method on a StatusHelper class in your app rather than requesting it upstream.3xx footgun — returning Status::FOUND from a JSON API instead of a documented Location header. The enum makes the number prettier, not the semantics correct.How can I help you explore Laravel packages today?