You are here
Home > 未分類

メンテナンス画面の裏口

先日、開発完了のLaravelのプログラムをインストールするときに、私だけのためにメンテナンス画面に裏口があったらいいなと思いました。

Laravelでは、

php artisan down

このコマンドの実行で、すべてのアクセスを以下のようなメンテナンス画面にできます。そのモードに切り替えて、DBの変更とかなどのインストールの作業を誰にも邪魔されずにするのです。

Be right back. 2016-01-09 13-16-41

しかし、インストール後には、できる限りの動作テストを行いたいのですが、自分も含めて誰もアクセスできない状態は不都合です。せめて私や関係者だけ、つまりそれらのIPだけからは通常に見れるようにしたいのです。

私がフレームワークなしの時代に開発したプログラムでは、すべてのプログラムに共通で最初に読み込まれるPHPファイルに、以下のようなコードで、私だけが閲覧できるようにしていました。

$allow = array('xxx.xxx.xxx.xxx');

if (isset($_SERVER)) // ウェブのアクセスのみ
{
    if (isset($_SERVER["REMOTE_ADDR"]) && !in_array($_SERVER["REMOTE_ADDR"], $allow))
    {
        include_once "maintenance.php";
        exit;
    }
}

普段はこれらはコメントしておいて、メンテナンスのときにコメントを解除するのです。xxx.xxx.xxx.xxxは私の固定IPですが、そのIPだけが、メンテ画面を含むmaintenance.phpの実行をしないのです。

これと同じことをLaravelでできないでしょうか?

ちょっと調べたところ、以下のファイルにおいて、


class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * @var array
     */
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
    ];
...

vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php

このファイルがミドルウェアとして、メンテナンスのモードかどうかをチェックしているらしい。

ということで、そのファイルをコピーして、

app/Http/Middleware/CheckForMaintenanceMode.php

として、その中味を以下のように編集します。


namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Foundation\Application;
use Symfony\Component\HttpKernel\Exception\HttpException;

class CheckForMaintenanceMode
{
    /**
     * The application implementation.
     *
     * @var \Illuminate\Contracts\Foundation\Application
     */
    protected $app;
    protected $allow = ['xxx.xxx.xxx.xxx'];
    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function __construct(Application $app)
    {
        $this->app = $app;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {

        if ($this->app->isDownForMaintenance()) {
            if (!in_array($request->getClientIp(), $allow)) {
                throw new HttpException(503);
            }
        }

        return $next($request);
    }
}

基本的には、先のコードと同様なメカニズムです。

そして先のKernel.phpを

...
    protected $middleware = [
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
    ];
...

と編集する。簡単な変更ですが有用です。

ちなみに、メンテナンスの解除をするには、

php artisan up

これを実行するのみなのですが、ちょっとメカニズムに興味ありますね。

調べたところ、downでは、

storage/framework/down

という空のファイルを作成されます。そしてupの実行ではこのファイルが削除されます。意外とシンプルですね。

最後に、メンテナンスの画面は以下のファイルを編集します。

resources/views/errors/503.blade.php

publicのディレクトリを移動する

新規のLaravelのプロジェクトは、以下の実行で作成できます。

$ laravel new blog

/blogのディレクトリが作成され、そこには以下のようなディレクトリやファイルが作成されます。

app/
bootstrap/
config/
database/
public/
resources/
storage/
tests/
vendor/
artisan
composer.json
composer.lock
gulpfile.js
package.json
phpunit.xml
readme.md
server.php

/publicのディレクトリがウェブサイトのルートとなる仮定で、その他のディレクトリはユーザーからはアクセスできないという仮定なのですが、

しかし、現実は使用するサーバーの都合により、このpublicのディレクトリを違う場所に移動する必要があります。

例えば、

public

/usr/www/demo/webdocs

に、

その他のディレクトリやファイルは、ウェブがアクセスできない以下に

/usr/www/demo/blog

この変更に必要な設定は、Laravelではとても簡単です。

public/index.php、移動後では、/usr/www/demo/webdocs/index.phpを編集するだけです。

...
require __DIR__.'/../bootstrap/autoload.php';
...
$app = require_once __DIR__.'/../bootstrap/app.php';
..

...
require '/usr/www/demo/blog/bootstrap/autoload.php';
...
$app = '/usr/www/demo/blog/bootstrap/app.php';
..

と書き換えるだけです。

しかし、ここで1つ問題は、Laravel定義のpublic_path関数が返す値が、

/usr/www/demo/blog/public

となります。

これを修正するには、

...
require '/usr/www/demo/blog/bootstrap/autoload.php';
...
$app = '/usr/www/demo/blog/bootstrap/app.php';

$app->bind('path.public', function() {
    return __DIR__;
});
..

と、path.publicを設定する必要あります。
それにより、public_path関数が返す値は、

/usr/www/demo/blog/webdocs

となります。

Select2 セレクト複数選択

今回は人気のドロップダウンスクリプト、Select2を紹介します。

通常のドロップダウンリストは、

<div class="form-group{{ $errors->has('category') ? ' has-error' : '' }}">
    <label class="col-md-4 control-label">分類</label>

    <div class="col-md-6">
        {!! Form::select('category', ['犬', '猫', '猿'], null, ['class' => 'form-control']) !!}
    </div>
</div>

とテンプレートで指定すると、

select1

と表示。

これに、

<div class="form-group{{ $errors->has('category') ? ' has-error' : '' }}">
    <label class="col-md-4 control-label">分類</label>

    <div class="col-md-6">
        {!! Form::select('category[]', ['犬', '猫', '猿'], null, ['class' => 'form-control', 'multiple' => 'multiple']) !!}
    </div>
</div>

'multiple' => 'multiple'を追加すると、

multiple1

と複数選択に変わります。

しかし、この複数選択にSelect2を使用すれば、

multiple2

のように、ドロップダウンから複数選択して、最初の行に選択した値を表示してくれます。また、そこでXをクリックすることで削除もできます。

必要な設定は、

まず、

https://github.com/select2/select2/archive/master.zip

よりダウンロードして解凍してから、レイアウトのテンプレートを以下のように編集します。

...
<head>
...
<link href="{{ url('assets/css/select2/select2.min.css') }}" rel="stylesheet" type="text/css">
...
</head>
<body>
...

<script src="{{ url('assets/js/select2/select2.min.js') }}"></script>
<script src="{{ url('assets/js/select2/ja.js') }}"></script>

<script type="text/javascript">
      $(document).ready(function() {
          $(".js-multiple").select2({ width: 'resolve' });
      });
</script>
...

そして、画面のテンプレートを、

<div class="form-group{{ $errors->has('category') ? ' has-error' : '' }}">
    <label class="col-md-4 control-label">分類</label>

    <div class="col-md-6">
        {!! Form::select('category[]', ['犬', '猫', '猿'], null, ['class' => 'form-control js-multiple', 'multiple' => 'multiple']) !!}
    </div>
</div>

と編集します。

Select2には他にもいろいろな機能があります。

https://select2.github.io/examples.html

namespaceを指定してroutesすっきり

以前、マルチ認証の説明で以下のような、routes.phpを掲載しました(ユーザー認証(10)Laravel 5.2 マルチ認証)。

   Route::group(['middleware' => 'guest:users'], function()
    {
        Route::get('login', 'user\AuthController@getLogin');
        Route::post('login', 'user\AuthController@postLogin');
        Route::get('signup', 'user\SignupController@getSignup');
        Route::post('signup', 'user\SignupController@postSignup');
        Route::get('password/email', 'user\PasswordController@getEmail');
        Route::post('password/email', 'user\PasswordController@postEmail');
        Route::get('password/reset/{token}', 'user\PasswordController@getReset');
        Route::post('password/reset', 'user\PasswordController@postReset');
    });

ここ、user\AuthControllerとか、user\SignupControllerとか、namespaceのuser\がいつも繰り返されていて、面倒だなあと思いませんでしたか?

賢くなるものです。最近、ここnamespaceを使用して、user\を削除することが可能なこと見つけました。

   Route::group(['middleware' => 'guest:users', 'namespace' => 'user'], function()
    {
        Route::get('login', 'AuthController@getLogin');
        Route::post('login', 'AuthController@postLogin');
        Route::get('signup', 'SignupController@getSignup');
        Route::post('signup', 'SignupController@postSignup');
        Route::get('password/email', 'PasswordController@getEmail');
        Route::post('password/email', 'PasswordController@postEmail');
        Route::get('password/reset/{token}', 'PasswordController@getReset');
        Route::post('password/reset', 'PasswordController@postReset');
    });

すっきりしましたね。

また、php artisan route:listの実行でrouteをチェックしても、変更の前後ではまったく変わりません。

親子関係のテーブルでのクエリーの作成(クエリビルダー編)

いつもの例を使いますと、商品productと商品画像product_imageの親子関係、つまり、1対多の関係があるとして、これに対して検索画面を作成するとします。

検索画面では、商品名だけでなく、商品画像のMIMEも検索項目として、検索できるようにします。つまり、親のテーブルの項目(商品名)でなく、子のテーブルの項目(MIME)も指定可能とします。

この場合、すぐに思いつくのは、以下のようにjoinして、その検索結果を表示です。

//検索値
$input[] = [
    'name' => '商品名',
    'mime' => 'image/gif'
];

$products = DB::table('product')
    ->join('product_image', 'product.product_id', '=', 'product_image.product_id')
    ->where('product.name', $input['name'])
    ->where('product_image.mime', $input['mime'])
    ->get();

しかし、これでは検索結果の各行は、商品画像のレコードとなってしまいます。1商品に対して、複数のGIF画像があるときは、複数分商品が表示されます。今回は、該当する商品のみを表示したいです。

となると、


$products = DB::table('product')
    ->join('product_image', 'product.product_id', '=', 'product_image.product_id')
    ->where('product.name', $input['name'])
    ->where('product_image.mime', $input['mime'])
    ->groupBy('product.product_id')
    ->get();

あるいは、joinでなくwhereInを使用して、


$products = DB::table('product')
    ->where('product.name', $input['name'])
    ->whereIn('product.product_id', function($query) use($input) {
        $query->from('product_image')->select('product_id')->where('mime', $input['mime']);
    })
    ->get();

$query->tableでなく、$query->fromというところがちょいとややこしいですね。しかし、親のレコードだけを引き出すという点では、joinを使用するよりわかりやすいです。

実効すると、このSQL文は以下のようになります。

select * from `product` where `name` = '商品名' and `product`.`product_id` in (select `product_id` from `product_image` where `mime` = 'image/gif')

ちょっとしたパフォーマンスの改善

Laravelは非常にたくさんのファイルを起動時に読み込んでいるので、パフォーマンスの改善は以前から感心があります。最近、管理画面だけでrouteの数が300近いプログラムをインストールするにあたり、重たくなることを予想して、簡単にできる範囲でLaravelでのパフォーマンスの改善を調査してみました。

1.設定ファイルのキャッシュ作成

php artisan config:cache

この実行は、configのディレクトリにあるファイルすべてを合わせて1つのキャッシュファイルにします。これにより設定データのローディング時間を速めようということです。

ファイルは、

bootstrap/cache/config.php

として作成されます。

この使用において、注意は3点あります。

注意点1:プログラムにおいてenv()の関数を使用してはいけない

例えば、

.envのファイルにおいて、

APP_ENV=local

と設定していて、

$path = storage_path().'/'.env('APP_ENV').'/logs/test.log';

のように、現在の環境変数の値によりログファイルを置くディレクトリを変えるとすると、

$pathの値は、

/var/www/test/storage/local/logs/test.log

のようになります。

しかし、php artisan config:cacheを実行すると、env()の値は空となり、

/var/www/test/storage/logs/test.log

となってしまい、意図した場所とは違うことになってしまいます。

それゆえに、先のプログラムは以下と変更することが必要です。

$path = storage_path().'/'.config('app.env').'/logs/test.log';

もちろん、config/app.phpなどの設定ファイルの中でのenv()の使用は問題ないです。

注意点2:ダイナミックに値を設定しているconfigに気をつける

設定ファイルの値は、

config(['some_setting' => 'some_value']);

のようにダイナミックに値の設定が可能です。例えば、DBからの値をAppServiceProvider::boot()で読み込んでおいて、DBに再度アクセスすることなくプログラムのあちらこちらで使用しようというときなどに便利です。

ところが、php artisan config:cacheの実行時に、AppServiceProvider::boot()が実行されるので、それらの設定もキャッシュファイルに保存されてしまいます。これで、ダイナミックに変わる値が固定されては困ります。

しかし、ダイナミックに上書きされるので問題はなさそうです。しかし、プログラムの他の部分では問題があるかもしれません。使用には気をつけてください。

注意点3:.envやconfigのファイルを編集したら、必ずキャッシュを再作成

最後に忘れてはならないのは、.envconfig/*.phpのファイルを編集したら、必ずphp artisan config:cacheの実行が必要なことです。それなくしては、せっかくの変更も反映されません。

2.routeのキャッシュ作成

php artisan route:cache

こちらは、config:cacheと違い、この実行で作成されるファイル、

bootstrap/cache/routes.php

には、routes.phpファイルを読み込んでLaravelがマップしたデータ構造をbase64_encodeして、serializeした形で収めます。それゆえに、この読み込みとマップの作業を一気に短縮します。とくに、routeの数が大きいプロジェクトではパフォーマンスの改善に期待できる仕組みです。

それからconfig:cacheと同様に、routes.phpファイルを編集したら、必ずphp artisan route:cacheの実行が必要です。

3.共有クラスファイルの最適化

これは、環境変数のAPP_ENVがproductionの値のときにだけに有効なものです。また、設定ファイルに依存するので、設定ファイルをキャッシュするなら、php artisan config:cacheを実行してから以下を実行してください。

php artisan optimize

この実行で、共有されるクラスファイルを1つのファイルにまとめ、読み込み時間を短縮するのが目的です。以下のファイルを作成します。

bootstrap/cache/compiled.php

このファイルには、デフォルトでは、Laravel関連のファイルが含まれますが、必要なら以下の設定ファイルに追加が可能です。

bootstrap/cache/compile.php

改善した?

さて、実際にこれらの最適化でパフォーマンスはどれくらい変わるのでしょうか?

私の厳密ではないテストでは、DB操作を伴わない計測では10~30%の違いがありました。configよりもrouteやoptimizeの方が実際のパフォーマンスにより影響ありました。多分、設定ファイルが小さく数少ないためがその違いと思います。しかし、現実では、DBクエリーやCSS、JS、画像などのダウンロードがはるかに時間がかかるので、まだ私のプロジェクトのスケールではちょっとした改善というところです。スケールがより大きくなるとかなりの差となるかもしれません。

Top