Laravel使用時に困った点と対処方法

初めまして。レストランサービス開発Gのまつやんです。

ぐるなびを利用されている加盟店様が使われる、店舗ページ管理ツール「ぐるなび PRO for 飲食店」の開発・保守のほか社内用管理ツールのリプレイス等も行っております。

f:id:g-editor:20180628170828p:plain

今回は、社内用管理ツールのリプレイスでLaravelを使用して実装したときに困った点とその対処方法を書いてみます。

社内ツールのリプレイスでLaravelを導入

ぐるなびの社内用管理ツール(※1)のひとつ「エリア割付ツール」は、お店の緯度・経度を入力することで以下のようなことができます。
(※1)社外からのアクセスはできません

  • 周辺のスポット(例:上野動物園とか東京ドーム)などの情報を飲食店様に紐付け

  • 周辺駅の情報を取得(紹介している機能は一部です)

このツールが古くなり改修要望も上がってきておりましたので、リプレイスすることになりました。流行りのフレームワークかつ、チーム内の他の案件で使われていたという理由でLaravelを選択。チーム内で情報共有できるのは大きなメリットだと考えたのです。

いざ実装を進めていくも、困ってしまった点がいくつかありました。

1:キャッシュ制作ユーザと実行時のユーザが違うとキャッシュが削除できない

Laravelには独自のキャッシュ機構があり、デフォルトでは以下のディレクトリ配下にキャッシュが作成されていきます。

アプリケーションディレクトリ/bootstrap/cache/

Laravelではキャッシュを使うことで応答速度を上げています。その反面、内部キャッシュが残ってしまいデプロイしてもブラウザから最新の状態が確認できないという現象が起きます。

そこで、Laravelのコマンド artisan(アルチザンと読むらしい)を使うと、簡単にキャッシュがクリアできます。

php artisan cache:clear

しかし、ぐるなびの環境ではデプロイ時のユーザとApache実行ユーザが違うため、コマンドを実行しても権限エラーが起きてキャッシュが削除できません。

キャッシュを消せるようControllerを作成

どうすれば解決できそうかを検討しました。

A案:Apache実行時のユーザをデプロイ時のユーザと同じにする

この方法では、以下の懸念点がありました。

  • 用途ごとにユーザを用意・管理している意味が失われる
  • サーバ内にさまざまなツールが共存しているため他のツールにも影響が出る

B案:キャッシュ削除バッチを作成し実行を作成

次に検討したこちらの方法もデメリットがあります。

  • せっかく標準で用意されている機能と、同様の物を作るのが手間

  • 誤ったファイルを消してしまったら困りますよね

C案:Apache実行ユーザが、artisanコマンドでキャッシュを削除できるようにする

このC案を採用し、ユーザキャッシュを削除できるようなControllerを作成しました。もちろん、実行できるコマンドは絞っています。

<?php
namespace App\Http\Controllers;

use Response;

/**
 * デプロイ後等に、キャッシュクリア等を行うController
 */
class ArtisanController extends Controller
{
    public function index(\Illuminate\Http\Request $request)
    {
        // cache:clear
        $artisan = base_path() . DIRECTORY_SEPARATOR . 'artisan cache:clear';
        $command = "php {$artisan}";
        
        exec("{$command} > /dev/null");
        
        // config:cache
        $artisan = base_path() . DIRECTORY_SEPARATOR . 'artisan config:cache';
        $command = "php {$artisan}";
        
        exec("{$command} > /dev/null");
        
        // route:cache
        $artisan = base_path() . DIRECTORY_SEPARATOR . 'artisan route:cache';
        $command = "php {$artisan}";
        
        exec("{$command} > /dev/null");
        
        // view:clear
        $artisan = base_path() . DIRECTORY_SEPARATOR . 'artisan view:clear';
        $command = "php {$artisan}";
        
        exec("{$command} > /dev/null");

Jenkinsデプロイ時にキャッシュ削除ビルドを実行する(このControllerを呼び出す)ことで、キャッシュの削除漏れを防止しています。

2:デフォルトでは環境ごとの設定ファイル切り替えと管理がしづらい

Laravelには環境ごとの設定ファイル.envがあり、実行時に自動的に読み込まれます。そのため、複数の環境で開発をするには設定ファイル切り替えとその管理が必要になってきます。

ぐるなびの環境は、開発環境・テスト環境・本番環境と分かれています。そのため各々の環境に反映するたびに、.envの切り替えが発生していました。

あわせて、環境ごとに設定ファイルを分けて管理する必要もあります。環境が変わるとAPIやDBの接続先も変わるので、同じ設定ファイルを使っていると間違った箇所を編集してしまう可能性があったのです。

環境ごとの.envを用意して、デプロイ時に各環境の.envを反映

そこで検討したのが以下の方法です。

A案:Git管理外で.envを各環境用に用意して、各サーバーに配置しておく

ただし、以下の懸念がありました。

  • チーム内の.envのバージョンを統一できない

サーバー上に置いた定義ファイル(.env)を、各個人が取得して開発すればバージョン統一できるはずですが、往々にして取得し忘れる人はいるものです。

さらに、平行開発時に同一の定義ファイル(.env)を修正すると、新規追加した内容が上書きされて動かなくなるケース、別の内容に置き換わって不具合が発生するケースが考えられます。

B案:Git管理内で、環境ごとに.envを用意

A案は前述の通り、採用できません。B案を採用し、下記のように管理しました。

環境 ファイル名
開発環境 .env
テスト環境 .env.TEST
本番環境 .env.PROD

「.envに開発環境の設定を書いたら、本番で開発環境用の設定になってしまうじゃん」と思った方、その通りです。

このままでは開発環境用の設定がそのまま本番環境に反映され、障害の原因となります。そこで各環境へのデプロイ時に、.env.TESTや.env.PRODを.envに上書きするようにして対処しています。

例. 本番環境へデプロイする時
cp .env.PROD .env

デフォルトを開発環境にしている理由はデプロイ環境にあります。テスト環境と本番環境は、Jenkinsでデプロイをしており、そのデプロイビルドコマンドに.env.各環境のファイルを.envへ上書きしています。

しかし開発環境ではJenkinsによるデプロイ環境を用意していません。 そのため、.envの上書きの手間を省略するようデフォルトの設定ファイルを開発環境にしています。

3:ログとキャッシュの出力ディレクトリが同じになっている

Laravelで出力するログやキャッシュの場所は、デフォルトのままだと以下のようになります。

アプリケーションディレクトリ/storage/logs/

しかしぐるなびの開発環境では、アプリケーションのデプロイ時のユーザとApacheの実行ユーザが違うため、このままだとログが書き込めない事態に……。

Applicationファイルを書き換えてログの出力先を変える

ということで、ふたつの解決案を検討しました。

A案:Apacheの実行ユーザでも書き込める権限を付与する

ただし、以下の懸念がありました。

  • すでに稼働中のプログラムが配置されているアプリケーションディレクトリに対して、権限を操作するのはそもそも危険
  • アプリケーションディレクトリ配下にログディレクトリがあると、次回デプロイ時にログが消去される可能性もある
  • ログを見ようとしただけなのに、誤ってソースコードファイルに触れていたなんてことにもなりかねません

B案:ログの出力先を変更する

B案の、ログの出力先変更を採用しました。

appディレクトリ直下のApplication.phpを下記の要領で修正します。もし存在しない場合は、新たに作成してください。その際にはフレームワークのApplicationクラスを継承するのを忘れずに。

<?php
namespace App;

class Application extends \Illuminate\Foundation\Application
{
    private $cache_path = '出力先ディレクトリ';

    public function publicPath()
    {
        return dirname($this->basePath()) . '/docroot';
    }
    
    public function storagePath()
    {
        return $this->cache_path . 'storage';
    }
    
    public function getCachedCompilePath()
    {
        return $this->cache_path . 'bootstrap/cache/compiled.php';
    }
    
    public function getCachedServicesPath()
    {
        return $this->cache_path . 'bootstrap/cache/services.php';
    }
    
    public function getCachedConfigPath()
    {
        return $this->cache_path . 'bootstrap/cache/config.php';
    }
    
    public function getCachedRoutesPath()
    {
        return $this->cache_path . 'bootstrap/cache/routes.php';
    }
}

storagePath()の戻り値のパス/logs/配下にログが、getCachedXxx()の戻り値のパスに各キャッシュが出力されます。

4:「重い処理」をLaravelでどう実行するか?

リプレイスしたツールの中には、「入力された位置情報から周辺のスポット情報や駅情報を取得して、Excelファイル(最大5000行)を作成してメール送信を実行する」という重い機能がありました。

Webアプリケーション内で時間のかかる処理は避けたいところ。レスポンスが返ってこなかったり、タイムアウトでエラーページが表示されてしまったらユーザのストレスになります。

こういったときに有効なのが「キュー」です。

キューとは
Laravelのキューを使うと、データベーステーブルに登録したジョブを任意のタイミングで実行できます。時間のかかる処理を非同期で実行できるので、
  • データを抽出してメールを送信する処理(今回のケース)
  • 大量データを集計する処理
  • 多量の複数テーブルを参照・更新する処理
のような重たい処理でも、ユーザへのレスポンスを速くできます。

artisanコマンドでバックエンド処理を呼び出す

せっかくのLaravelなのでキューを使ってみたかったのですが、リリース日やリソースの関係で断念。

バックエンド処理(コンソール)を呼び出す方法を採用しました。

処理を呼び出す側の書き方

ここでもartisanコマンドを使います。artisanを使わずに直接PHPを実行しようとすると、Laravel内の各種機能(作成したModel等も)が使えないため注意が必要です。

   nohup php アプリケーションディレクトリ/artisan 作成したコンソール名 引数 > /dev/null &

このスクリプトで以下のコンソール(PHPコード)を呼び出します。

$dl_no = $request->input('dl_no');
        
$format = $request->input('format');
$mail_address = $request->input('mail_address');
$artisan = base_path() . DIRECTORY_SEPARATOR . 'artisan downloadareas';
$argument = "{$dl_no} {$format} {$mail_address}";
$command = "php {$artisan} {$argument}";
        
exec("nohup {$command} > /dev/null &");

処理を呼び出された側の書き方

artisanでコンソールを作成します。

 php artisan make:console コンソール名

こちらのコードでは、データ(引数)の受け方がLaravel独特のバックエンド処理をしています。

$signature にはコンソール名{引数1の名前}{引数2の名前}が入り、引数は$this->argument(引数の名称) で取得しています。

class DownloadAreas extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'downloadareas {dl_no} {format} {mail_address}'; 

    public function handle()
    {
        logger()->info(__CLASS__ . ':start', $this->argument());
        
        $dl_no = $this->argument('dl_no'); 

その後は通常のPHP処理と変わりません。

結び

Laravelで、困った点とその対処方法をまとめてみました。実際にLaravelを使用していて似たような箇所で困っている方の手助けになれば幸いです。


お知らせ
ぐるなびでは一緒に働く仲間を募集しています。


‘まつやん’

SIerで約5年、Web業界で10年(ぐるなび含め)。 好物は、おにぎり。 趣味は、パイプ煙草・ダーツ・バーめぐり(肝臓の数値的に禁酒中)な変な人w