Effortless maintenance management for PSR7.
Janitor is a ready to use PSR7 package that provides you with an easy configurable and extensible way to handle maintenance mode on your project, because maintenance handling goes beyond responding to the user with an HTTP 503 code and a simple message.
Set several conditions that will be checked to determine if the maintenance handler should be triggered. This conditions are of two kinds, 'activation' conditions (named watchers
) and conditions to bypass the normal execution (named excluders
).
Already builtin watchers and excluders allows you to cover a wide range of situations so you can drop Janitor in and start in no time, but at the same time it's very easy to create your own conditions if needed by implementing the corresponding interface.
Once Janitor has determined maintenance mode is active it let you use your handler to get a response ready for the user or you can let Janitor handle it all by itself (a nicely formatted 503 response).
Learn more in Janitor's page
Best way to install is using Composer:
composer require juliangut/janitor
Then require the autoload file:
require_once './vendor/autoload.php';
use Janitor\Excluder\IP as IPExcluder;
use Janitor\Excluder\Path as PathExcluder;
use Janitor\Runner as Janitor;
use Janitor\Watcher\WatcherInterface;
use Janitor\Watcher\File as FileWatcher;
use Janitor\Watcher\Cron as CronWatcher;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
$watchers = [
new FileWatcher('/tmp/maintenance'),
new CronWatcher('0 0 * * 0', new \DateInterval('PT2H')),
];
$excluders = [
new IPExcluder('127.0.0.1'),
new PathExcluder(['/maintenance', '/^\/admin/']),
];
$handler = function (ServerRequestInterface $request, ResponseInterface $response, WatcherInterface $watcher) {
$response->getBody()->write('This page is in maintenance mode!');
return $response;
}
$activeWatcherAttributeName = 'maintenance_watcher'; // Default is 'active_watcher'
$janitor = new Janitor($watchers, $excluders, $handler, $activeWatcherAttributeName);
$response = $janitor(
ServerRequestFactory::fromGlobals(),
new Response('php://temp'),
function ($request, $response) use ($activeHandlerAttributeName) {
$activeHandler = $request->getAttribute($activeHandlerAttributeName);
// ...
}
);
In case a watcher is active at any given point (and so maintenance mode does) it will be attached as an attribute to the request object so it can be retrieved during execution.
Watchers serve different means to activate maintenance mode by verifying conditions.
Manual
Just set it to be active. Useful to be used with a configuration parameter.File
Checks the existence of the provided file(s).Environment
Checks if an environment variable is set to a value.
$manualWatcher = new \Janitor\Watcher\Manual(true);
// Always active
$manualWatcher->isActive();
$fileWatcher = new \Janitor\Watcher\File('/tmp/maintenance');
// Active if /tmp/maintenance file exists
$fileWatcher->isActive();
$envWatcher = new \Janitor\Watcher\Environment('maintenance', 'ON');
// Active if 'maintenance' environment variable value is 'ON'
$envWatcher->isActive();
Scheduled watchers are a special type of watchers that identify a point in time in the future for a maintenance period.
Fixed
Hard set start and/or end times for a scheduled maintenance period.Cron
Set maintenance periods using cron expression syntax.
$fixedWatcher = new \Janitor\Watcher\Fixed('2026/01/01 00:00:00', '2026/01/01 01:00:00');
// Active only 1st January 2026 at midnight for exactly 1 hour
$fixedWatcher->isActive();
$cronWatcher = new \Janitor\Watcher\Cron('0 0 1 * *', new \DateInterval('PT2H'));
// Active the first day of each month at midnight during 2 hours
$cronWatcher->isActive();
From a scheduled watcher you can get a list of upcoming maintenance periods
$cronWatcher = new \Janitor\Watcher\Cron('0 0 1 * *', new \DateInterval('PT2H'));
// Array of ['start' => \DateTime, 'end' => \DateTime] of next maintenance periods
$scheduledPeriods = $cronWatcher->getScheduledTimes(10);
Watchers are checked in the order they are added, once a watcher is active the rest won't be checked.
If you perform maintenance tasks periodically (maybe on the same day of every week) you may want to use either Cron
watcher to identify the date and the time period needed, or File
watcher to watch for a file in your system and set your maintenance process to touch
and rm
that file as part of the maintenance process.
Cron watcher uses Michael Dowling's cron-expression.
Excluders set conditions to bypass maintenance mode in order to allow certain persons through or certain pages to be accessed.
IP
Verifies user's IP to allow access.Path
Excludes certain URL paths from maintenance.BasicAuth
Excludes based on request Authorization header.Header
Excludes based on request Header value.
$ipExcluder = new \Janitor\Excluder\IP('127.0.0.1');
// Users accessing from IP 127.0.0.1 are excluded
$ipExcluder->isExcluded($request);
$pathExcluder = new \Janitor\Excluder\Path('/maintenance');
// Users accessing 'http://yourdomain.com/maintenance' are excluded
$pathExcluder->isExcluded($request);
$pathExcluderRegex = new \Janitor\Excluder\Path('/^\/admin/');
// Can also be a regex
$pathExcluderRegex->isExcluded($request);
$basicAuthExcluder = new \Janitor\Excluder\BasicAuth(['root' => 'secret']);
// Users accessing with basic authorization 'root' user are excluded
$basicAuthExcluder->isExcluded($request);
$headerExcluder = new \Janitor\Excluder\Header('X-Custom-Header', 'custom');
// Users accessing with 'X-Custom-Header' header's value 'custom' are excluded
$headerExcluder->isExcluded($request);
$headerExcluderRegex = new \Janitor\Excluder\Header('X-Custom-Header', '/^custom/');
// Again a regex can be used
$headerExcluderRegex->isExcluded($request);
When adding excluders consider they are checked in the same order they are included so that when an excluder condition is met the rest of the excluders won't be tested. Add more general excluders first and then more focused ones.
Tipically you'll want to exclude your team's IPs and certain pages such as maintenance or administration zone.
In order to handle maintenance mode any callable can be provided to setHandler
method given it follows this signature:
function (\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, \Janitor\Watcher\WatcherInterface $watcher);
Two really basic handlers are suplied by default to cope with maintenance mode.
Render
Sets response with 503 code and add basic formatted maintenance output based on request's Accept header.Redirect
Prepares response to be a 302 redirection to a configured URL (typically maintenance page).
Of the two Render
will be automatically created and used in case none is provided.
If scheduled watchers are being used they open the option to show a list of future maintenance periods, for example on a page dedicated to inform users about future maintenance actions.
use Janitor\Runner as Janitor;
use Janitor\Watcher\Cron;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
$watchers = [new Cron('0 0 1 * *', new \DateInterval('PT2H'));];
$janitor = new Janitor($watchers);
$response = $janitor(
ServerRequestFactory::fromGlobals(),
new Response('php://temp'),
function ($request, $response) use ($janitor) {
// Array of ['start' => \DateTime, 'end' => \DateTime]
$scheduledPeriods = $janitor->getScheduledTimes();
}
);
use Janitor\Runner as Janitor;
use Slim\App;
$watchers = [];
$excluders = [];
$app = new App();
// Add middleware (using default Render handler)
$app->add(new Janitor($watchers, $excluders));
$app->run();
use Janitor\Handler\Redirect;
use Janitor\Runner as Janitor;
use Zend\Expresive\AppFactory;
$watchers = [];
$excluders = [];
$handler = new Redirect('/maintenance');
$app = AppFactory::create();
// Add middleware
$app->pipe(new Janitor($watchers, $excluders, $handler));
$app->run();
If using Symfony's HttpFoundation you can still add Janitor to your tool belt by using Symfony's PSR HTTP message bridge
An example using Silex
use Janitor\Runner as Janitor;
use Silex\Application;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Component\HttpFoundation\Request;
use Zend\Diactoros\Response;
$janitor = new Janitor();
$app = new Application;
$app->before(function (Request $request, Application $app) use ($janitor) {
$response = $janitor(
(new DiactorosFactory)->createRequest($request),
new Response('php://temp'),
function ($request, $response) {
return $response;
}
);
return (new HttpFoundationFactory)->createResponse($response);
});
$app->run();
Found a bug or have a feature request? Please open a new issue. Have a look at existing issues before.
See file CONTRIBUTING.md
See file LICENSE included with the source code for a copy of the license terms.