1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Cakephp4にMaintenance plugin for CakePHPを実装

Posted at

Cakephp4(4.4.14)でメンテナンス画面を表示させるときにイロイロ修正した部分のメモです

composer

まずcomposerにてMaintenance Plugin for CakePHPを対象のCakephpに導入します

composer require fusic/maintenance

Application.php

useでインポートします

src/Application.php
namespace App;
// ...
//add
use Maintenance\Middleware\MaintenanceMiddleware;
use Cake\Http\Exception\ForbiddenException;
// ...

middlewareにMaintenanceMiddlewareを追加します

src/Application.php
    public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
    {
        $middlewareQueue
            ->add(new ErrorHandlerMiddleware(Configure::read('Error')))

            // ...

            ->add(new MaintenanceMiddleware([
                'enabled' => isMaintenance, // メンテナンスモードを有効にするかどうか
                'exception' => new ForbiddenException(), // 例外を指定
                'allowIp' => ['127.0.0.1'], // メンテナンスモードを無効化する IP アドレスリスト
            ]));
        return $middlewareQueue;
    }

fusic/maintenanceのGithubでは、「tmp/maintenance」の存在で有効無効を判別する方針みたいですが、今回私はisMaintenanceはconst.phpで定数を設定し、メンテナンスモードの切り替えをconst.phpで行う手法にしました。直接にtrue falseを指定しても大丈夫です。

(参考)CakePHP4で定数を定義する方法(外部サイト)▶https://417.run/pg/php/cakephp4/cakephp4-const/

config/const.php
define('isMaintenance',true);
// ...
bootstrap.php
// ...
try {
    Configure::config('default', new PhpConfig());
    Configure::load('app', 'default', false);
    Configure::load('const', 'default');//ここでconst.phpを読み込み
} catch (\Exception $e) {
    exit($e->getMessage() . "\n");
}
// ...

メンテナンスページの作成

メンテナンス時に表示する画面テンプレートを作成します

src/Template/Error/maintenance.php
<p>maintenance page. </p>

MaintenanceMiddlewareの編集

最後の肝になります
私の環境では通常のインストール手順ではエラーが多発しましたので、MaintenanceMiddleware.phpを修正しました。
面倒くさいのでそのまま貼り付けますので参考にしてください。

vendor/cakephp/cakephp/fusic/maintenance/src/Middleware/MaintenanceMiddleware.php
<?php
namespace Maintenance\Middleware;

use Cake\Core\InstanceConfigTrait;
use Cake\Utility\Inflector;
use Cake\View\ViewBuilder;
use Cake\Network\Request;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Cake\Http\Response;

class MaintenanceMiddleware implements MiddlewareInterface
{
    use InstanceConfigTrait;

    /**
     * Default config.
     *
     * @var array
     */
    protected $_defaultConfig = [
        'allowIp' => [],

        'className' => 'Cake\View\View',
        'templatePath' => 'Error',

        'statusFilePath' => TMP,
        'statusFileName' => 'maintenance',
        'statusCode' => 503,

        'ctpFileName' => 'maintenance',
//        'ctpExtension' => '.ctp',

        'contentType' => 'text/html',

        'useXForwardedFor' => false,
    ];


    public function __construct($config = [])
    {
        $this->setConfig($config);
    }

    public function __invoke($request, $response, $next)
    {
        $isActive = $this->isMaintenance($request);

        if ($isActive === false) {
            $response = $next($request, $response);
        } else {
            $response = $this->execute($response);
        }

        return $response;
    }
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $isActive = $this->isMaintenance($request);

        if ($isActive === false) {
            return $handler->handle($request);
        } else {
            return $this->execute($handler, $request);
        }
    }
    private function execute(RequestHandlerInterface $handler, ServerRequestInterface $request): ResponseInterface
    {        
        $contentType = $this->getConfig('contentType');
        $statusCode = $this->getConfig('statusCode');
        $templateName = $this->getConfig('ctpFileName');
        $templatePath = $this->getConfig('templatePath');
        
        $builder = new ViewBuilder();
            
        $view = $builder
            ->setClassName('Cake\View\View')
            ->setTemplatePath(Inflector::camelize($templatePath))
            ->disableAutoLayout()
            ->build([], $request);
        
        $bodyString = $view->render($templateName);
        
        $response = new Response();
        $response = $response->withType($contentType)
            ->withStatus($statusCode)
            ->withStringBody($bodyString);
        return $response;
    }

    /**
     * @return bool
     * @author gorogoroyasu
     */
    // private function checkStatusFile()
    // {
    //     $path = $this->getConfig('statusFilePath');
    //     if (is_string($path)) {
    //         $path = [$path];
    //     }
    //     foreach($path as $p) {
    //         $fullPath = $p . $this->getConfig('statusFileName');
    //         $ret = file_exists($fullPath);
    //         if ($ret === true) {
    //             return true;
    //         }
    //     }
        
    //     return false;
    // }


    private function isMaintenance($request)
    {
        $ret = $this->getConfig('enabled');
        if ($ret === false) {
            return false;
        }
        $ret = $this->isAllowIp($request);
        if ($ret === true) {
            return false;
        }

        return true;
    }

    private function getMyIpAddress($request)
    {
        $params = $request->getServerParams();

        // X-Forwarded-Forはカンマ区切り。一番近いReverse proxyで付与されたIPを末尾から取得する。
        if ($this->getConfig('useXForwardedFor') && isset($params['HTTP_X_FORWARDED_FOR'])) {
            $ips = explode(',', $params['HTTP_X_FORWARDED_FOR']);
            return trim(array_pop($ips));
        } else {
            return isset($params['REMOTE_ADDR']) ? $params['REMOTE_ADDR'] : null;
        }
    }

    private function isAllowIp($request)
    {
        
        $myIpAddress = $this->getMyIpAddress($request);
        if (is_null($myIpAddress)) {
            return false;
        }

        $ipAddressList = $this->getConfig('allowIp');
        if (empty($ipAddressList)) {
            return false;
        }

        foreach ($ipAddressList as $allowIP) {
            // サブネットマスクが指定されていない場合 /32 を追加
            if (strpos($allowIP, '/') == 0) {
                $allowIP .= '/32';
            }
            // IPアドレスの書式チェック
            if (!preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/([1-9]|1[0-9]|2[0-9]|3[0-2])$/', $allowIP)) {
                // 書式が不正
                continue;
            }
            list($ip, $maskBit) = explode("/", $allowIP);
            $ipLong = ip2long($ip) >> (32 - $maskBit);

            $selfIpLong = ip2long($myIpAddress) >> (32 - $maskBit);
            if ($selfIpLong === $ipLong) {
                return true;
            }
        }

        return false;
    }
}

とりあえず無事にメンテナンス画面が表示されたので良かったです。
定数isMaintenanceでメンテナンス中か判断できるので、切り戻し防止にレイアウト追加も出来るので良かったです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?