uam/maintenance-bundle
UAM Maintenance Bundle adds a lightweight maintenance mode to Laravel/PHP apps, with configurable access rules and friendly downtime messaging so teams can safely handle deployments, upgrades, and planned outages.
First, a compatibility check. Despite the "maintenance" name, uam/maintenance-bundle is a Symfony bundle, not a Laravel package. The Bundle suffix, the uam vendor namespace, and the DependencyInjection/ layout are Symfony conventions. If you're in a Laravel codebase, this is not a drop-in — you'd be fighting the framework boundary (no bundles.php, different container, different event dispatcher). Confirm your target framework before spending time on it.
Assuming a Symfony app:
Read the repo before installing. With 1 star and 0 dependents, there is no community safety net. Open, in this order:
README.md — the only source of truth for config keys and the toggle mechanism.composer.json — check require for the Symfony version constraint (symfony/framework-bundle, symfony/http-kernel). A stale constraint here is the most likely reason it won't boot on a modern Symfony (6/7).src/ — look for a *Extension.php, a *Bundle.php, a Command/ directory, and any EventSubscriber/EventListener. Those four files tell you the entire design.DependencyInjection/Configuration.php — the real config schema, more reliable than the README.Install and register.
composer require uam/maintenance-bundle
With Symfony Flex the bundle may auto-register; otherwise add it to config/bundles.php:
Uam\MaintenanceBundle\UamMaintenanceBundle::class => ['all' => true],
Find the toggle. Maintenance bundles almost always expose one of: a console command (bin/console uam:maintenance:enable|disable), a config flag (uam_maintenance.enabled: true), or a lock/sentinel file on disk. Run bin/console list and grep for maintenance — that's your fastest discovery step.
First use case — verify the 503 path end to end. Enable maintenance mode, hit a route in a browser, and confirm you get an actual HTTP 503 (not 200, not 500) with a rendered page. Check curl -I https://your-app.test/ for the status code. If it 200s, the listener isn't registered or is being shadowed. If it 500s, the template or a service dependency is missing. This one check validates install, registration, listener wiring, and template resolution at once.
Environment-scoped toggling. Don't wire maintenance mode to a single global boolean. Most teams end up with a config that resolves per-environment, so staging can be put into maintenance without touching prod:
# config/packages/uam_maintenance.yaml
uam_maintenance:
enabled: '%env(bool:MAINTENANCE_MODE)%'
Then a deploy step sets MAINTENANCE_MODE=1 in the environment file. This is far more reliable than a filesystem sentinel when you run multiple app servers — a local lock file on server A does nothing for server B.
Deploy-window choreography. The standard pattern is a wrapper around your deploy, not a manual button:
bin/console uam:maintenance:enable # or set env + cache:clear
php bin/console doctrine:migrations:migrate --no-interaction
bin/console cache:clear
bin/console uam:maintenance:disable
Treat enable/disable as idempotent so a failed deploy doesn't leave you stuck in maintenance. Wrap the disable in a trap/finally so a crash mid-migration still restores service.
Whitelisting for health checks and office IPs. The single most important integration detail: your load balancer health check and your uptime monitor will start paging you the moment maintenance mode goes on. Look for an IP allowlist and a path allowlist in the config and put /health, /healthz, and your internal ranges in it. If the bundle doesn't support path exclusions, you'll need a small kernel.request listener with a higher priority that returns early for those paths.
Custom maintenance page. The default template is almost certainly a bare placeholder. Override it per Symfony convention — the bundle's extension typically declares a template path, so check config/packages/uam_maintenance.yaml for a template: key and point it at templates/maintenance/index.html.twig. Keep it self-contained: no database calls, no auth, no service dependencies. A maintenance page that itself depends on the thing being maintained is a classic own-goal.
Set Retry-After. If the bundle doesn't already send it, add Retry-After: 3600 to the 503 response. Search engines and well-behaved clients honor it; without it you get crawl churn and repeated client retries during the window.
Compose with your own listener if the bundle is too thin. For anything beyond a static page — showing a countdown, notifying admins, logging who enabled it — attach to the same event the bundle uses. Find the priority in the bundle's service definition (tags: [{ name: kernel.event_listener, event: kernel.request, priority: N }]) and register yours slightly earlier so your logic runs first.
composer.json (don't use ^), commit composer.lock, and read every line of src/ before production. It's small enough that a full read is a 15-minute job — do it. If it's abandoned, forking into your own src/ namespace is cheaper than carrying an unmaintained dependency.composer.json constraints before composer require so you don't get a dependency resolution error and waste a cycle. If it targets Symfony 4/5 and you're on 7, expect ContainerBuilder and event-dispatcher-signature changes.bin/console cache:clear (or cache:warmup) is mandatory after changing config-driven maintenance flags. Editing a YAML file and expecting an immediate effect on a cached prod container is the single most common "it doesn't work" report.kernel.request fires for sub-requests too. If a fragment controller gets the maintenance page injected mid-page, you'll render a maintenance blob inside an otherwise-working layout. Confirm the listener checks $event->isMainRequest() (Symfony 6+) or isMasterRequest() (5 and below) — this is a frequent omission in small bundles.bin/console commands, which means you can't run the disable command to get out of maintenance mode. Test the escape hatch before you need it: enable, then confirm bin/console uam:maintenance:disable still runs.config/bundles.php? (2) bin/console debug:event-dispatcher kernel.request — is the listener listed? (3) bin/console debug:container --tag=kernel.event_listener — is the service registered and is its priority sane? (4) bin/console debug:config uam_maintenance — did your YAML actually merge into the tree? That sequence finds the problem in under two minutes.php artisan down --render="errors::503" --retry=60 --secret="..." and php artisan up. It ships with the framework, supports bypass secrets and Retry-After, and has zero dependency risk. There is no reason to reach for a Symfony bundle in a Laravel app.How can I help you explore Laravel packages today?