Download the PHP package freshwaveonline/janitor without Composer
On this page you can find all versions of the php package freshwaveonline/janitor. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download freshwaveonline/janitor
More information about freshwaveonline/janitor
Files in freshwaveonline/janitor
Package janitor
Short Description Janitor cleans up after your application: informative, white-label error pages for Laravel with message numbers, request IDs, retry countdowns, Livewire pop-ups and optional Filament branding.
License MIT
Homepage https://github.com/freshwaveonline/janitor
Informations about the package janitor
Janitor
Informative, white-label error pages for Laravel.
Laravel's default error pages show a status code and one line of text. That is enough for a developer and almost useless for the person who hit the error.
This package replaces them with pages that actually explain the situation: what happened, why, what to do next, a button that does it, and — when something went wrong on your side — a short code the visitor can quote so you can find that exact error in your logs.
Why
- The visitor learns something. Every status code has a message, a reason, an explanation and a list of things to try — in their language, not HTTP's.
- Support gets a handle. The message number is a hash of where the error
happened, so the same bug always produces the same code. Two tickets quoting
ERR-3F9A2Care the same bug. It is written to your logs too. - It works when everything else is broken. All CSS, JavaScript and icons are inlined. No CDN, no Vite build, no external request — an error page that depends on the asset pipeline fails exactly when you need it.
- Nothing is required. Livewire and Filament are used when present and ignored when not.
Installation
Requirements: PHP 8.2+ and Laravel 12 or 13.
Laravel 13 requires PHP 8.3+, so PHP 8.2 pairs with Laravel 12 only. Laravel 11 is not supported.
To pin the major explicitly:
The package registers itself through Laravel's auto-discovery and starts rendering immediately.
Installing from a private fork or a mirror
If you run this from a repository of your own rather than from Packagist, point Composer at it directly. In the **consuming application's** `composer.json`: Composer needs credentials for the private repository. Either a GitHub token with `repo` scope: or SSH, by using `[email protected]:freshwaveonline/janitor.git` as the `url` and relying on your existing SSH key. On a deploy server, use a machine user or a deploy key rather than a personal token. To track the branch rather than a release, require it directly — `dev-main` is aliased to `2.0.x-dev`:For local development
To work on the package and the application side by side, use a path repository. Composer symlinks the directory, so edits are picked up without reinstalling:
Releasing a version
Composer resolves versions from git tags, so cutting a release is a tag:
That runs the release workflow, which validates composer.json with
composer validate --strict, runs the suite against both supported Laravel
majors, and publishes a GitHub release with generated notes only if all of that
passes. The Packagist GitHub App picks the tag up from there, and the
update changelog workflow writes the release notes back into CHANGELOG.md.
Configuring
To tune the package, publish the config:
or publish assets individually:
Quick start
Four environment variables cover most projects:
Then look at every page without breaking anything:
The preview lists all eighteen states and accepts ?theme=dark, ?details=1,
?retry=120, ?modal=1 and ?position=top-right.
What you get
Message numbers
The number is derived from a normalised app/Http/Controllers/OrderController.php:88
fingerprint — never from the message, the timestamp or the user. That means:
- The same failure gives the same number on every server and after every deploy (absolute paths and release directories are stripped first).
- A different line gives a different number.
- It appears on the page, in the
X-Message-Numberresponse header, and in your log context, sogrep ERR-3F9A2C storage/logsfinds the exception.
OriginStrategy::Application is the default and usually the right one: a
QueryException thrown deep inside Illuminate still fingerprints to the line in
your code that caused it.
Set
JANITOR_SALTin production. Without it, someone who can trigger errors could confirm a guessed file path by comparing hashes. Keep the salt stable — changing it changes every number you have ever quoted.
Request IDs
Read from X-Request-Id, X-Correlation-Id, X-Amzn-Trace-Id,
X-Cloud-Trace-Context, CF-Ray or W3C traceparent — AWS, Google Cloud and
traceparent formats are unwrapped to the bare trace id. When nothing upstream
supplied one, the package generates it at the start of the request, so the id
on the page matches the id in every log line for that request.
Header values are length-capped and character-validated before they reach the page; anything unusual is replaced by a generated id rather than echoed back.
Retry-After
For 429s, maintenance mode and any exception carrying a Retry-After or
X-RateLimit-Reset header, the page shows when to come back:
When to try again — You can try again at 14:32. (04:12)
The countdown ticks live and the retry button is disabled until it reaches zero.
Waits longer than retry_after.max_seconds (a day by default) show no countdown
at all, because "try again in three days" is not something a visitor can act on.
Call-to-action buttons
Configured per status code, from built-ins (home, back, reload, retry,
login, support, status_page) or inline definitions:
An action that cannot resolve removes itself: no support address configured means
no support button, no login route means no sign-in button. Exactly one button
always carries the primary emphasis.
The 500 page
On a 500 the page can show the exception, formatted and copyable:
Auto shows it when app.debug is on or when the current environment is in
the list. That is the useful case: on staging (debug=false, env=staging)
testers get a full stack trace and a Copy report button that puts the message
number, request id, URL, exception and trace on the clipboard in one click — while
production shows nothing.
replace_debug_page is off by default, so locally you keep Ignition. Turn it on
to design against these pages.
The copied report contains only what is deliberately put in it — never request bodies, headers, session data or environment variables.
Livewire
An error during a Livewire round-trip normally drops Livewire's iframe overlay over your page. Instead:
Modal renders the same information as a pop-up and leaves the page — and
whatever the user had typed — completely intact. ModalPosition has nine values:
TopLeft, TopCenter, TopRight, MiddleLeft, Center, MiddleRight,
BottomLeft, BottomCenter, BottomRight. Center gets a backdrop; the rest
behave as toasts.
The handler is injected automatically when Livewire is installed. To place it
yourself, set inject_assets to false and add @janitorScripts to your
layout.
Filament
If Filament is installed, an error inside a panel inherits the panel's brand name, logo, primary colour, home URL and login URL:
Everything you set explicitly in this package's config wins over Filament. Every call into Filament is guarded, so a Filament upgrade that renames a method turns into "no branding inherited", never into a 500 on your error page.
Colours, light and dark
You configure one colour. The package derives the hover state, the active state, the soft background, the border, the focus ring and the readable text colour on top of it, for both schemes:
light and dark exist for brand colours that work on white and vanish on
near-black. When they are empty, primary is used for both.
With auto_contrast on, a primary colour that fails WCAG contrast against its own
surface is nudged lighter or darker until it passes — a pale yellow brand colour
still produces a readable button instead of an invisible one.
Everything else is neutral grey. That is deliberate: the pages stay white-label until you give them a colour.
Translations
English and Dutch ship with the package. Copy resolution falls back from the status code to the family to the default, so you only translate what differs:
Each status has title, message, reason, explanation and suggestions,
with :status, :brand, :message_number and :support_email placeholders.
API responses
Requests that expect JSON get the same information in a machine-readable shape:
Customising the views
Every blade file lands in resources/views/vendor/janitor/. Laravel's
namespaced view finder checks that directory first, so a published file wins
immediately — no config, no registration. Delete a file to fall back to the
packaged version.
There are three levels of override, and you pick the smallest one that does the job:
One partial. Change the meta chips and keep everything else:
The whole page. Replace error.blade.php and lay the page out yourself:
One status code. Add errors/{code}.blade.php under the published
directory and only that status gets the bespoke page:
Every view receives the same $error object
(FreshwaveOnline\Janitor\Data\ErrorContext):
| Property | What it holds |
|---|---|
$error->statusCode |
404 |
$error->title / ->message |
translated headline and lead |
$error->reason / ->explanation |
the "what happened" block |
$error->suggestions |
list<string> |
$error->actions() |
list<ErrorAction> — label, url, icon, style, behaviour |
$error->messageNumber / ->requestId |
the quotable codes |
$error->retryAt / ->retryInSeconds() |
the retry moment |
$error->details |
ExceptionDetails or null |
$error->branding |
Branding — name, logo, homeUrl, supportEmail, … |
$error->palette |
resolved colour tokens for both schemes |
$error->copyReport() |
the plain-text support report |
The error inside your own layout
The card is then rendered into your layout's content section instead of as a
standalone document.
Extending
Every moving part is bound by contract, so you replace one piece from your own
AppServiceProvider and the rest of the package keeps working around it.
| Contract | Default | Replace it when |
|---|---|---|
MessageNumberGenerator |
MessageNumber |
you already have an incident-code scheme |
RequestIdResolver |
RequestId |
your APM assigns the id |
RetryAfterResolver |
RetryAfter |
the moment comes from a deploy window or queue depth |
BrandingResolver |
ConfigBranding |
branding is per tenant, not per app |
ActionResolver |
ActionFactory |
the buttons depend on runtime state |
ErrorContextBuilder |
ErrorContextFactory |
the copy lives in a CMS, not in lang files |
ErrorRenderer |
ErrorPageRenderer |
you want different take-over rules |
The multi-tenant case, which is the one that comes up most:
The concrete classes stay bound under their own names, so you can decorate the default instead of replacing it wholesale:
ErrorContextFactory and ErrorPageRenderer are non-final with protected
methods throughout, so subclassing one method is often enough:
Custom icons
Icons are SVG path data, so they are registered rather than configured:
Registering an existing name replaces that bundled Heroicon everywhere. Any
registered name can then be used from the actions config.
Keeping Laravel's behaviour
resources/views/errors/404.blade.php in your application wins — the package
steps aside for any error view you wrote yourself. Turn that off with
views.prefer_application_views.
To disable the package for specific codes or exceptions:
Validation and authentication exceptions are excluded by default because Laravel
redirects for them — hijacking those would break every form and every login
redirect in your application. The one exception: when your app has no login
route, handle_missing_login_route renders a proper 401 page instead of letting
Laravel throw a RouteNotFoundException and turn it into a 500.
Abort messages
That message is shown to the visitor, because you wrote it. Framework-generated
messages are not: 404 and every 5xx are excluded by default, so a
ModelNotFoundException cannot leak your model class names onto the page.
Testing
PHPStan runs at level: max with no baseline, and the package's own config
directory is analysed alongside src. Larastan and
phpstan/phpstan-deprecation-rules are registered by
phpstan/extension-installer, so they must not also be listed under
includes: in phpstan.neon.dist — PHPStan aborts with "These files are
included multiple times" before analysing anything.
Contributing
Pull requests are welcome. Please run composer check before opening one — it
runs the same four things CI does: composer validate --strict, Pint, PHPStan
and Pest.
CI runs the suite against every supported pairing, on both the lowest and the highest resolvable dependencies:
| PHP | Laravel 12 | Laravel 13 |
|---|---|---|
| 8.2 | ✅ | — |
| 8.3 | ✅ | ✅ |
| 8.4 | ✅ | ✅ |
| 8.5 | — | ✅ |
CI never rewrites your code. If Pint reports a violation, the check fails and
asks you to run composer format locally.
Security
If you discover a security issue, email [email protected] rather than opening a public issue.
Credits
- Vincent van den Boogaard
- Icons from Heroicons (MIT), inlined.
License
The MIT License (MIT). See LICENSE.md.
All versions of janitor with dependencies
illuminate/console Version ^12.0|^13.0
illuminate/contracts Version ^12.0|^13.0
illuminate/http Version ^12.0|^13.0
illuminate/support Version ^12.0|^13.0
illuminate/view Version ^12.0|^13.0
nesbot/carbon Version ^3.8.4
symfony/http-kernel Version ^7.2|^8.0