Introduction

Janitor is a ready to use PSR7 middleware 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 (name `watchers`) and conditions to bypass the normal execution (named `excluders`). Default watchers and excluders allows you to cover a wide range of situations so you can drop Janitor in and start in no time, but if needed it's very easy to create your own conditions.

Once Janitor has determine 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).

Get started

1. Install

Use Composer and include Janitor in your project

composer require juliangut/janitor
require 'vendor/autoloader.php';

2. Watchers

Define as many watchers as you need to identify the triggers of maintenance mode on your page

$watchers = [
    new \Janitor\Watcher\File('/tmp/maintenance'),
    new \Janitor\Watcher\Environment('maintenance', 'ON'),
    new \Janitor\Watcher\Scheduled\Cron('0 0 * * 0', new \DateInterval('PT2H')),
];

3. Excluders

Add as many excluders as needed to determine conditions in which maintenance mode will be bypassed

$excluders = [
    new \Janitor\Excluder\IP('127.0.0.1'),
    new \Janitor\Excluder\Path(['/maintenance', '/admin']),
    new \Janitor\Excluder\BasicAuth(['admin' => 'password'])
];

4. Handler

Define your custom maintenance mode handler

$handler = function (\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, \Janitor\Watcher $watcher) {
    return $response->withCode(503);
}

5. Instantiate Janitor

Create Janitor instance with your defined watchers, excluders and custom handler

$janitor = new \Janitor\Janitor($watchers, $excluders, $handler);

6. Middleware use

Include Janitor as middleware in your project workflow as usual

// Example with Zend Expressive
$app->pipe($janitor);

Documentation

Watchers

Every possible condition that leads to maintenance mode activation is defined with a Janitor watcher. Define as many watchers as you need to cover all the possible scenarios that will trigger maintenance mode on your page.

There are two kinds of watchers, lets say a 'static' kind of watcher in which maintenance mode activation will be determied based on a condition check in real time, and 'scheduled' watchers in which maintenance mode will be determined by a given timeframe.

Watchers are checked in the order they are included so that when a watcher is determined to be active no other watcher will be checked. Set the more specific watchers first and the more general after.

Manual watcher

It's the simplest watcher possible. At creation time it is set to be active or not and so when the time to determine maintenance mode comes the value of the provider will be checked.

This watcher's best fit is in project with maintenance configuration variable that can be passed to the watcher.

$configuration = [
    // ...
    'maintenance' => true;
]

$manualWatcher = new \Janitor\Watcher\Manual($configuration['maintenance']);
$janitor->addWatcher($manualWatcher);

File watcher

This watcher will check for the existance of a file in the filesystem to determine if maintenance mode is active.

With this watcher a file can be created at the begining of an automatic maintenance processess and removed at the end of it so that the file can be watched for.

touch(sys_get_temp_dir() . '/maintenance');

$janitor->addWatcher(new \Janitor\Watcher\File(sys_get_temp_dir() . '/maintenance'));

Environment watcher

Similarly to file watcher environment watcher watches for the existance and value of an environment variable to determine maintenance mode.

An environment variable such as 'maintenance' can be set and destroyed while this watcher watches for it.

putenv('maintenance=On');

$janitor->addWatcher(new \Janitor\Watcher\Environment('maintenance', 'On'));

Scheduled Watchers

While normal 'static' watchers are OK for punctual situations or to be controlled by an external maintenance process, if periodical maintenance tasks are to be taken in the system then a scheduled watcher is a better fit.

With scheduled watchers a date/time (in the future) and a timeframe will be defined to set maintenance mode activation and extend it certain amount of time.

Fixed scheduled watcher

Quite similar to manual watcher, defines both start and end date/times for maintenance so that when the time comes and during the specified time window the page will be set on maintenance mode.

$janitor->addWatcher(new \Janitor\Watcher\Scheduled\Fixed(new \DateTime('2016/01/01 0:0:0'), new \DateTime('2016/01/01 1:0:0')));

Cron scheduled watcher

Cron watcher lets you define periodical maintenance timeframes with cron expression syntax.

If you have periodical maintenance tasks, all occurring on the same moment every week, month, year, ... then this watcher is your best option. Define that same moment with a cron expression and a time interval and your page will be down for maintenance during your periodical processes.

$janitor->addWatcher(new \Janitor\Watcher\Scheduled\Cron('0 0 1 * *', new \DateInterval('PT2H')));

Excluders

Once maintenance mode is determined to be active there is still a way to bypass it thanks to certain conditions defined by Janitor Excluders

Excluders define conditions that if met will allow access to the page even if maintenance mode is active. Useful to grant you or your team access to the page after maintennace tasks have completed and before opening access again to check everything went smoothly.

As with watchers excluders are tested in the order they are included and when one is set to exclude mainenance mode the verification chain stops.

On the contraty of what happened with watchers, set general excluders first and the more specific after them.

IP excluder

Set allowed IPs to be excluded from maintenance mode so that requests coming from those IPs will not be affected by maintenance mode and will access the page as normal.

$janitor->addExcluder(new \Janitor\Excluder\IP('127.0.0.1'));

Path excluder

In the same way as IP excluder, path excluder will allow access to certain defined URL paths. Most normally you'll want to grant access to administration area or even maintenance information page even during maintenance processes are running.

$janitor->addExcluder(new \Janitor\Excluder\Path(['/maintenance', '/private']));

BasicAuth excluder

Inspects Authorization request header to allow access only to certain users accessing with Basic Authorization.

$janitor->addExcluder(new \Janitor\Excluder\BasicAuth(['root' => 'secret']));

Header excluder

Allows access only to those clients accessing with a specific request header's value. Not the most secure way to handle access actually.

$janitor->addExcluder(new \Janitor\Excluder\Header(['X-My-Header', 'access']));

Handler

When maintenance mode is active then a handler will be triggered to cope with the response, Janitor allows you to define this handler providing your own callable. In case you don't provide a handler Janitor will use a basic handler that will prepare response object with a nicely formatted 503 based on request's Accept header.

$janitor->setHandler(function (\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, \Janitor\Watcher $watcher) {
    $body = new \Zend\Diactoros\Stream('php://temp', 'r+');
    $body->write(sprintf('Maintenance period because of "%s"!', get_class($watcher)));

    return $response->withCode(503)->withBody($body);
});

Bundled Handlers

Render

Sets basic response with 503 HTTP status header and compose a simple maintenance page which content depends on request Accept header.

This is the default handler in case you didn't define any.

$janitor->setHandler(new \Janitor\Handler\Render);
Redirect

In the case you want to get users redirected to another page this handler sets HTTP 302 code redirection for you.

This handler fits like a charm with a path excluder for the maintenance page.

$janitor->setHandler(new \Janitor\Handler\Redirect('/maintenance'));

Active Watcher

When maintenance mode is active but at the same time an excluder is applied and thus user accesses the page a new Attribute is added to the request object with currently active watcher. You can use this request attribute to inform accessing user that the page is actually under maintenance even though he/she can view it. Attribute default name is active_watcher

$janitor->setAttributeName('activated_watcher');
// Down the execution path
$activatedWatcher = $request->getAttribute($janitor->getAttributeName());

Service

Aside of aiding you with maintenance mode activation you can use Janitor as a service in your project so that you can access future maintenance periods if using scheduled watchers. Helpful to keep your users informed in an hypotetical maintenance timetable page.

foreach ($janitor->getScheduledTimes(10) as $time) {
    echo sprintf('Maintenance from %s to %s', $time->getStart()->format('d/M/Y H:i:s'), $time->getEnd()->format('d/M/Y H:i:s'));
}

Usage examples

Usage on Janitor with several PSR7 middleware based frameworks is insanely easy, take a look at this examples.

Slim3

use Janitor\Janitor;
use Slim\App;

$watchers = [];
$excluders = [];

$app = new App();

// Add middleware (using default Render handler)
$app->add(new Janitor($watchers, $excluders));

$app->run();

Zend Expressive

use Janitor\Handler\Redirect;
use Janitor\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();

Symfony's HttpFoundation

If using Symfony's HttpFoundation you can still add Janitor to your toolbelt, just as easy as using Symfony's PSR HTTP message bridge

use Janitor\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();