You are here
Home > !Laravel

Laravelの日本語レポジトリの作成

前回に作成したLaravelの日本語レポジトリ。今回はその作成の仕方を説明します。

コマンドの実行

まずは、以下のcomposerのコマンドを実行します。

composer create-project --prefer-dist laravel/laravel larajapan 5.3.*	

上で使用されているコマンドの引数は、

--prefer-dist laravel/laravel

https://packagist.org/packages/laravel/laravelからパッケージをダウンロードすることを指示します。

larajapan

パッケージのダウンロード先。その名前でディレクトリを作成します。このディレクトリ名は、先のコマンドラインで違う名前を指定可能であるし、実行完了してから改名も可能です。

5.3.*

パッケージのバージョンを指定。ここでは、laravelの5.3を使用します。マイナーバージョンを指定したいなら、5.3.30のように指定します。

実行すると、パッケージに含まれるファイル、さらにパッケージが依存するパッケージのファイルが多数ダウンロードされ少々時間がかかります。

最終的には、実行したディレクトリのもとにlarajapanのディレクトリが作成され、ダウンロードされたファイルが収納されます。

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

次に、ユーザー認証のためのファイル作成を以下の実行で行います。

php artisan make:auth	

この実行により、resources/viewsのディレクトリにおいて、すでにインストールされている以下のコントローラで使用されるbladeファイルが作成されます。

app/Http/Controllers
├── Auth/
│   ├── ForgotPasswordController.php(パスワードのリセットのリンク送信画面)
│   ├── LoginController.php(ログイン画面)
│   ├── RegisterController.php(会員登録画面)
│   └── ResetPasswordController.php(パスワードリセット画面)
├── Controller.php
└── HomeController.php(ログイン後のホーム画面)

最後に以下のコマンドを実行して、先のパスワードのリセットのリンク送信画面から発行されるEメールので使用されるHTMLのテンプレートを作成作成します。

php artisan vendor:publish

日本語化

さて、ここからが日本語化の作業です。

まず、config/app.phpの編集から。

                                                                                                                                                                                                                                                                                                                                                   
...
    'timezone' => 'UTC',                                                                                                                                                                                                                                                                                                               
                                                                                                                                                                                              
    'locale' => 'en',
...

                                                                                                                                                                                                                                                                                                                                                   
...
    'timezone' => 'Asia/Tokyo',                                                                                                                                                                                                                                                                                                               
                                                                                                                                                                                              
    'locale' => 'ja',
...

と変えて保存します。

timezone

これは、通常、プログラム内の日時設定のタイムゾーンとして使用されるもので、PHPの以下の関数で使用されます。

date_default_timezone_set()

ここで設定すれば、後はLaravelが面倒みてくれます。

日本時間の場合は、Aisa/Tokyoの設定だけで十分。

locale

resources/langで言語のファイルが以下のように存在します。これらは、Laravelのプロジェクトでバリデーションのエラーメッセージなどを定義しています。

resources/lang
└── en/
    ├── auth.php
    ├── pagination.php
    ├── passwords.php
    └── validation.php

デフォルトの設定では、英語のenのディレクトリしかありません。日本語の翻訳を作成するには、上で設定したjaと同じ名前のディレクトリをそこに作成します。以下の実行でディレクトリごとコピーしてください。

$ cp -pr en ja

バリデーションに関しては、見米氏のバリデーション(1)Validatorファサードのextend を参照してください。

次は、ユーザー認証画面などで使用されるブレードファイルの翻訳です。

私のLaravelの日本語レポジトリでは、以下は、すべて翻訳してあります。

resources/views
├── auth
│   ├── login.blade.php
│   ├── passwords
│   │   ├── email.blade.php
│   │   └── reset.blade.php
│   └── register.blade.php
├── errors
│   └── 503.blade.php
├── home.blade.php
├── layouts
│   └── app.blade.php
├── vendor
│   ├── notifications
│   │   ├── email.blade.php
│   │   └── email-plain.blade.php
│   └── pagination
│       ├── bootstrap-4.blade.php
│       ├── default.blade.php
│       ├── simple-bootstrap-4.blade.php
│       └── simple-default.blade.php
└── welcome.blade.php

翻訳とは関係ありませんが、app.blade.phpのレイアウトで参照されている、app.cssapp.jsには、url()を入れて、プロジェクトがインストールされるディレクトリが変わっても参照されるようにしてあります。

パスワードリセットで送信されるEメールの翻訳

ここまで来ても、残念ながら、パスワードを忘れたときに送信される、パスワードリセットを含むEメールの内容がまだ翻訳されていません。なぜなら、本文がハードコードされているからです。

これはちょっと頭をひねりましたが、多分以下が最小の変更で対応できると思います。

まず、

vendor/laravel/framework/src/Illuminate/AuthのディレクトリからResetPassword.phpCanResetPassword.phpのファイルを以下の場所にコピーします。

app/Auth
├── Notifications/
│   └── ResetPassword.php
└── Passwords/
    └── CanResetPassword.php

次に、以下のようにファイルを編集します。app/User.phpのファイルも変更必要です。

namespace App\Auth\Notifications;                                                                                                                                                             
                                                                                                                                                                                              
use Illuminate\Notifications\Notification;                                                                                                                                                    
use Illuminate\Notifications\Messages\MailMessage;                                                                                                                                            
                                                                                                                                                                                              
class ResetPassword extends Notification                                                                                                                                                      
{                  
...
    /**                                                                                                                                                                                       
     * Build the mail representation of the notification.                                                                                                                                     
     *                                                                                                                                                                                        
     * @param  mixed  $notifiable                                                                                                                                                             
     * @return \Illuminate\Notifications\Messages\MailMessage                                                                                                                                 
     */                                                                                                                                                                                       
    public function toMail($notifiable)                                                                                                                                                       
    {                                                                                                                                                                                         
        return (new MailMessage)                                                                                                                                                              
            ->subject('パスワードリセット')                                                                                                                                                   
            ->greeting('パスワードリセット')                                                                                                                                                  
            ->line('パスワードリセットリンクの送信のリクエストがありました。')                                                                                                                
            ->action('リセットパスワード', url('password/reset', $this->token))                                                                                                               
            ->line('リクエストされていなかったら、無視してください。');                                                                                                                       
    }     
}
namespace App\Auth\Passwords;                                                                                                                                                                 
                                                                                                                                                                                              
use App\Auth\Notifications\ResetPassword as ResetPasswordNotification;                                                                                                                        
                                                                                                                                                                                              
trait CanResetPassword                                                                                                                                                                        
{                                                                                                                                                                                             
    /**                                                                                                                                                                                       
     * Get the e-mail address where password reset links are sent.                                                                                                                            
     *                                                                                                                                                                                        
     * @return string                                                                                                                                                                         
     */                                                                                                                                                                                       
    public function getEmailForPasswordReset()                                                                                                                                                
    {                                                                                                                                                                                         
        return $this->email;                                                                                                                                                                  
    }                                                                                                                                                                                         
                                                                                                                                                                                              
    /**                                                                                                                                                                                       
     * Send the password reset notification.                                                                                                                                                  
     *                                                                                                                                                                                        
     * @param  string  $token                                                                                                                                                                 
     * @return void                                                                                                                                                                           
     */                                                                                                                                                                                       
    public function sendPasswordResetNotification($token)                                                                                                                                     
    {                                                                                                                                                                                         
        $this->notify(new ResetPasswordNotification($token));                                                                                                                                 
    }                                                                                                                                                                                         
}                                                                                                                                                                                             
         
                                                                                                                                                                                              
namespace App;                                                                                                                                                                                
                                                                                                                                                                                              
use Illuminate\Notifications\Notifiable;                                                                                                                                                      
use Illuminate\Foundation\Auth\User as Authenticatable;                                                                                                                                       
use App\Auth\Passwords\CanResetPassword;                                                                                                                                                      
                                                                                                                                                                                              
class User extends Authenticatable                                                                                                                                                            
{                                                                                                                                                                                             
    use Notifiable;                                                                                                                                                                           
    use CanResetPassword;                                                                                                                                                                     
...                         

認証のroutesの設定

日本語化とは関係ないですが、私がわかりやすいと思ったことです。

オリジナルのroutes.phpは、

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', 'HomeController@index');

とシンプルですが、Auth::routes()で認証のrouteが隠されてしまった不透明。

ということで、私のLaravelの日本語レポジトリでは、以下のように編集しました。

// 以下は、Auth::routes()の中身を移したもの。将来において変更が可能なように                                                                                                                   
                                                                                                                                                                                              
// Authentication Routes...                                                                                                                                                                   
Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');                                                                                                                     
Route::post('login', 'Auth\LoginController@login');                                                                                                                                           
Route::post('logout', 'Auth\LoginController@logout')->name('logout');                                                                                                                         
                                                                                                                                                                                              
// Registration Routes...                                                                                                                                                                     
Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');                                                                                                     
Route::post('register', 'Auth\RegisterController@register');                                                                                                                                  
                                                                                                                                                                                              
// Password Reset Routes...                                                                                                                                                                   
Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm');                                                                                                            
Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');                                                                                                            
Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm');                                                                                                           
Route::post('password/reset', 'Auth\ResetPasswordController@reset');                                                                                                                          
                                                                                                                                                                                              
Route::get('/home', 'HomeController@index');  

最後に

Laravelの最新バージョンは今週に5.3から5.4となりました。今回説明した(前回に作成したLaravelの日本語レポジトリも)はバージョン5.3をベースにしたものです。しかし、バージョン5.4での日本語レポジトリ作成もいくつかファイルは違いますが、基本的には、最初のcomposerの実行で、5.3.*の代わりに、5.4.*と指定すれば、同じような手順で作成できます。

Laravelの日本語レポジトリ

Laravelにおいて新規のプロジェクト作成はとても簡単。コマンドラインでいくつかのコマンドを実行をちょちょいとすれば完了。しかし、インストールされるのは英語のプロジェクト。テンプレートやメッセージの翻訳をいちいちしなければ日本語のプロジェクトにはならない。

ここのプロセスを簡単にと、Laravelバージョン5.3をもとに、開発者のために日本語化したレポジトリを作成してみました。

このレポジトリには、

  • デフォルトのユーザー認証の機能:会員登録、パスワードリセット、会員ログイン
  • 日本語に翻訳されたデフォルトのテンプレートとEメールメッセージ
  • 日本語に翻訳されたデフォルトの入力エラーメッセージ
  • デバッグのためのDebugbarツール
  • ウェブ解析ツールGoogle Analyticsのトラッキングスクリプト

以上を含みます。

さらに、今回は、実際動作するデモとして以下に用意しました。
https://larajapan.lotsofbytes.com/larajapan

さて、このレポジトリのインストールは以下の手順で簡単にできます。

レポジトリのインストール

SSHを利用しているなら、以下をコマンドラインで実行してレポジトリをインストールします。

$ git clone git@github.com:lotsofbytes/larajapan.git

あるいは、Httpsを使用するなら、以下を実行します。

$ git clone https://github.com/lotsofbytes/larajapan.git

それから、ファイルのパーミッションを与えるべく以下を実行。

$ chmod -R a+w storage

インストール後は、以下を実行してください。

$ composer install

.envの編集

.env.example をコピーして、.env を作成し編集して以下のように設定します。*****の部分を適切な値に変更してください。

APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_DATABASE=*****
DB_USERNAME=*****
DB_PASSWORD=*****
MAIL_DRIVER=sendmail

その後、以下を実行して.env内のAPP_KEYを更新します。

$ php artisan key:generate

APP_DEBUG=trueこれによりDebugbarが画面下方に表示されます。Debugbarに関しては、Debugbarで楽々デバッグも読んでください。

また、
ANALYTICS=UA-XXXXXXのようにサイトのためにGoogleから取得したコードを設定すれば、Google Analyticsでウェブでのユーザーの動向が追跡できます。

DBを作成

.envで指定したDBを作成。

$ echo 'CREATE DATABASE larajapan CHARACTER SET utf8' | mysql -u root -p
php artisan migrate

ウェブサーバーの立ち上げ

最後に以下を実行して、ウェブサーバーを立ち上げると、

$ php artisan serve

以下のアドレスでブラウザーからアクセスできます。

http://localhost:8000

以上です。

上の設定は私の開発環境Fedora LinuxとAmazon Linux OSで、5.6のバージョンのPHPで、DBにはMysqlあるいはMaria DBを使用して動作確認しています。しかし、皆さんの環境ではいろいろ異なることがあると思います。問題や指摘があれば、ご連絡ください。

パブリックキーを使用してsftp (2) sftponlyの環境で

前回では、Laravel Collective Remoteを利用して、sftpでファイルをアップロード・ダウンロードする話をしました。それだけで事はほとんど足りるのですが、1つ困ったことがありました。

sftpを使用するのは、たいてい自社のサーバーとではなく、他社とのサーバーとです。注文データを取得してくるのも、こちらのデータをアップロードするのも。御存知のように、sftpはsshと同じプロトコルであり、sshが使用できるならsftpも使用可能。しかし、先のような状況だと、セキュリティのために、sftpは使用できるけれど、sshは使用できないようにサーバーが設定されています。また、sftpでは、勝手に他のディレクトリへ行ったりできないように、閲覧できるディレクトリを特定して、jailします。

このような制限された設定となると、例えば以下のように、lsコマンドの実行が不可能となり、ファイルのリストさえ取ってくることできなくなります。

Psy Shell v0.7.2 (PHP 5.6.26 — cli) by Justin Hileman
>>> use SSH;
=> null
>>> SSH::into('acme')->run(['ls data']);
[foo@acme] (acme) This service allows sftp connections only.
..

かと言って、

>>> SSH::into('acme')->list('data');
PHP Fatal error:  Call to undefined method Collective\Remote\Connection::list()

のように、listのコマンドがあるわけでもありません。

いろいろ、探ってみると、

Laravel Collective Remoteは、

https://github.com/LaravelCollective/remote/blob/5.3/src/SecLibGateway.php

では、

以下のphpseclibというパッケージを使用しています。

https://github.com/phpseclib/phpseclib/blob/master/phpseclib/Net/SFTP.php

これらを参考にすると、

>>>SSH::into('acme')->getGateway()->getConnection()->nlist('data');
=> [
     "base-invoice.csv",
     "base-product.csv",
     ".",
   ]
..

というように、nlistを実行できます。

他にも、chdirとも実行できるようです。オープンソースのおかげでこういう問題自分で解決できます。

パブリックキーを使用してsftp

中規模のECを営む私のお客さんのところでは、自社製品を持ち出荷するゆえに、自社のウェブで販売するのみではなく、他社でのウェブサイトでも製品が売られています。となると、そこからも注文データが来ます。

その発注データは、ウェブサービスを使用したAPIを使用して取得、というようなものではなく、彼らが生成した注文データをCSVファイルとして指定のサーバーに置かれ、それを毎日sftpでダウンロードして、システム内に取り込みます。

また、逆に自社サイトで販売した注文情報を、出荷や解析の目的で他のサーバーにsftpでアップロードというケースもあります。

ここで重要なのは、パブリックキーを使用したsftpのコミュニケーションが必要なことです。

そこで登場するのが、以前会員編集フォーム紹介した、Laravel Collectiveです。

Laravel Collectiveのインストール

最初に、以下をコマンドラインで実行します。

$ composer require "laravelcollective/remote":"^5.3.0"

この実行で、必要なライブラリがインストールされ、composer.jsonが更新されます。

次に、config/app.phpのファイルをエディタで開き、以下を追加します。

  'providers' => [
    // ...
    Collective\Remote\RemoteServiceProvider::class,
    // ...
  ],

  'aliases' => [
    // ...
    'SSH' => Collective\Remote\RemoteFacade::class,
    // ...
  ],

設定

SSHのパッケージがインストールされたところで、次は設定です。Laravel用に開発されたパッケージでは、たいていはパッケージ独自の設定ファイルを、config/ディレクトリに作成します。

以下を、コマンドラインで実行してください。

$ php artisan vendor:publish --provider="Collective\Remote\RemoteServiceProvider"

この実行により、config/remote.phpの設定ファイルが作成されます。

エディターで、そのファイルを開き、acmeエントリーを追加します。acmeでなくても、名前はなんでもよいです。

  'default' => 'production',

  'connections' => [
        'production' => [
            'host'      => '',
            'username'  => '',
            'password'  => '',
            'key'       => '',
            'keytext'   => '',
            'keyphrase' => '',
            'agent'     => '',
            'timeout'   => 10,
        ],

        'acme' => [
            'host'      => env('ACME_HOST'), // sftp先
            'username'  => env('ACME_USERNAME'), // ログイン名
            'password'  => '', // 
            'key'       => base_path(env('ACME_KEY')), // パブリックキーファイルのパス名',
            'keytext'   => '',
            'keyphrase' => '', 
            'agent'     => '',
            'timeout'   => 10,
        ],
    ],

見ての通り、このファイルにはデフォルトとして、すでにproductionがありますが、それには触らず、ここではacmeの配列を新設しました。開発サイトでのテストも考えて、いろいろな情報をここで設定できるのは便利です。

今回は、バージョン管理にプライベートの値が入らないように、.envで指定するようにしました。特に、大きい値のパブリックキーを含むファイルを、keyで指定できるのも便利です。このファイルは、.gitignoreで必ずバージョン管理から排除が必要ですね。

keyphraseは、パブリックキーを生成したときに指定したパスワードを入れますが、パスワード指定なしで作成することが多いです。それでも十分セキュアであるのでここでは空とします。

.envでは、以下のような変数を追加します。

..
ACME_HOST=foo.google.com
ACME_USERNAME=foo
ACME_KEY=config/keys/test.pem
..

以上でテスト可能となります。もし、sftp先でsftpのみで設定されていないなら、

$ php artisan tinker

を実行して、接続テストができます。以下では、sshでacmeに接続して、lsコマンドを実行しました。

Psy Shell v0.7.2 (PHP 5.6.25 — cli) by Justin Hileman
>>> use SSH;
=> null
>>> SSH::into('acme')->run(['ls']);
[foo@acme] (acme) data.csv
=> null
>>> 

sftpでファイルのダウンロード・アップロード

ここまで来たら、あとは簡単です。プログラムの中で以下を実行するだけです。

ファイルのダウンロードは、

SSH::info('acme')->get('data.csv', storage_path('data.csv'));

ファイルのアップロードは、

SSH::info('acme')->put(storage_path('data.csv'), 'data.csv');

あたかも、コマンドラインで実行するような軽さですね。

グローバル変数をダイナミックに管理

Laravelのバージョンが5になってから、グローバルスコープを持つ変数の設定は、.envconfig/のディレクトリの設定ファイルで、実用的しかも綺麗にまとまりました。変数の使用も、config('app.url')と、プログラムのでどこでも簡単に取得できます。

また、DBの設定などのデフォルト設定ファイル以外にも、独自の設定ファイルも作成できます。

例えば、

config/local.phpというファイルを作成して、


return [
        'convert' => '/usr/bin/convert', // 画像変換のプログラムのパス名
        'items_per_page' => 50,          // 1ページのアイテム数
        'max_upload_size' => 20,         // 最大アップロードの画像サイズ(MB)
        'company' => env('LOCAL_COMPANY', 'Lots of Bytes'), // 会社名
];

とすれば、config('local.company')と参照できます。

しかし、これらのグローバル変数設定の問題は、プログラムとして設定ファイルあるいは.envでハードコードしなければならないことです。

グローバル変数をDBに値を保存して、管理画面で以下のように値を編集できて、プログラムの各所で使用できるようにするには、どうしたらよいのでしょうか?

my-application

幸いにも、config()の関数は、取得だけでなく書き込みも可能なのです。


config(['global.admin_email' => 'abc@gmail.com']);

というように実行すれば、config('global.admin')abc@gmail.comを返すことができます。

global.とプリフィックスしたのは、単に他の変数と区別やグループ分けがしたいがためです。

ということは、DBからデータを読んでプログラムの実行時の最初に、configして入れしまえば良いわけです。

このための適切な場所は、プログラムの初期に実行される、app/Providers/AppServiceProvider.phpの中です。


namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use DB;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
	$setting = DB::table('setting')->pluck('value', 'key');

	config(['setting' => $setting]);        
    }
...
}

DBテーブルsettingの中身は、以下とすると、

+------------+---------------------+---------------------+------------------+-------------------+
| setting_id | created_at          | updated_at          | key              | value             |
+------------+---------------------+---------------------+------------------+-------------------+
|          1 | 2016-12-03 01:09:51 | 2016-12-03 01:09:51 | email_admin      | admin@gmail.com   | 
|          2 | 2016-12-03 01:09:51 | 2016-12-03 01:09:51 | email_from       | from@gmail.com    | 
|          3 | 2016-12-03 01:09:51 | 2016-12-03 01:09:51 | site_name        | ララジャパン       | 
+------------+---------------------+---------------------+------------------+-------------------+

例えば、config('setting')['site_name']の実行では、ララジャパンの値が返ってきます。

ログイン成功のイベント

ユーザー認証(11)Laravel 5.2 ログインの記録で、ログイン成功後の処理に関して説明しました。

しかし、前回のログインのRemember Meのポストのための調査で、この「Remember Me」がオンになっているときは、先のログイン成功後の処理は、最初のログインのときだけしか実行しないことを見つけました。

つまり、最悪のケース、5年間ログイン成功後の処理は実行されません。同じブラウザを使用してもIPアドレスが変わることがあるし、記録としても不十分となり不都合です。

さて、どう解決したらよいでしょうか?

これにはLaravelのイベントのメカニズムを利用します。イベントの仕組みは、イベントとリスナーの2つのクラスの定義から構成され、イベントのクラスのオブジェクトをプログラムの中でコール(ファイヤー)することでリスナーのオブジェクトのアクションが実行されます。

例えば、ログインが成功したときに希望するアクション(ログインの記録)を実行したいときは、ログイン成功のイベントとそのリスナーを作成します。

ありがたいことに、ユーザー認証に関してのイベントは、すでにIlluminiate\Auth\Eventsで定義されており、Illuminiate\Auth\SessionGuardのクラス内のメソッドで要所要所でファイヤーされています。

イベントの種類としては、以下が揃っています。

Attempting ログインを試みたとき
Authenticated ログイン成功後のセッションにアクセスするとき
Failed ログインが失敗したとき
Lockout ログイン連続失敗でロックアウトされたとき
Login ログインが成功したとき
Logout ログアウトしたとき
Registered 会員登録完了したとき

今回は、Loginのイベントが使えそうです。

コードをチェックしたところ、Remember Meがオンのときも、このイベントをファイヤーします。

最初にログイン成功したときだけでなく、デフォルトでは2時間アイドル後にセッションが切れるときや、ブラウザを閉じて再度オープンするときなど、再度ログインが必要なときにRemember Meの情報を使用して自動ログインするときにも、このイベントがファイヤーされます。

ということで、イベントのクラスはすでにあるので、このイベントに対応するリスナークラスを作成して登録すれば作業終了です。

ログイン成功のイベントLoginに対して、リスナーを作成してみましょう。逆になりますが、まず登録から始めます。

登録は、

namespace App\Providers;

use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        'Illuminate\Auth\Events\Login' => [
            'App\Listeners\LoginListener',
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

        //
    }
}

$listenの変数に、イベントに対応するクラスを指定するだけです。

次にリスナーのクラスを作成しますが、先の登録が完了していれば、便利なことに以下をコマンドラインで実行するだけで作成してくれます。

$ php artisan event:generate

もちろん、以下でも作成可能です。

$ php artisan make:listener LoginListner --event=Illuminate\Auth\Events\Login

作成された、リスナーを以下のように編集します。


namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Http\Request;

use App\UserLog;

class LoginListener
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    /**
     * Handle the event.
     *
     * @param  Illuminate\Auth\Events\Login  $event
     * @return void
     */
    public function handle(Login $event)
    {
        $user = $event->user;

        $ip = $this->request->ip();
        $agent = $this->request->header('User-Agent');

        UserLog::crate([
            'user_id'     => $user->id,
            'ip'          => $ip,
            'hostname'    => gethostbyaddr($ip),
            'agent'       => $agent
        ]);
    }
}

これで、ユーザーがログインをしたときや、セッションが期限切れで失って自動ログインされるときに、ログインの記録のレコードを作成してくれます。

ログインのRemember Me

Laravel 5.3になって、認証の部分が変りました。それに関していっぱい紹介したいことありますが、それは将来でのポストとして、今回は、「ログインのRemember Me」に関して学んだことを紹介します。

以下の画面のようにログインには、

my-application

「Remember Me」あるいは「次から入力を省略」、ログインのときにオンとすると次回から毎回毎回ログインする必要ないよという便利な機能。巷ではどこでも見かけます。

Laravelでも、認証のパッケージのインストール、

$ php artisan make:auth

を実行すると、デフォルトでついてくる機能です。

さて、この「Remember Me」でどうやってログインを要らなくするのか、そのメカニズムを探ってみましょう。

まず、「Remember Me」をクリックしてログイン成功すると、以下のように2つのクッキーを作成します。

  • laravel_session

    通常のセッションのためのクッキー。期限は、

    ..
        'lifetime' => 120,
        'expire_on_close' => true,
    ..
        'cookie' => 'laravel_session',
    ..
    

    lifetimeあるいはexpires_on_closeの値で決定されます。

    デフォルトでは、前者が2時間(120分)、後者は、fale。後者をtrueとするとブラウザをクローズしたときに期限切れとなります。ちなみに、このクッキーの名前は、cookieの値で変更可能です。

  • remember_web_59ba36addc2b2f9401580f014c7f58ea4e30989d

    こちらは、「Remember Me」のためのクッキーで、Laravelではログインした日から5年間と期限はハードコードされています。つまり、同じブラウザを使用し続けるなら、ログオフしない限り5年間ログインなしでアクセス可能です。

このクッキーが使用されるシナリオは簡単にはこうです。

ログイン後、2時間アイドルが続いたらあるいはブラウザを閉じたら、現在のセッションは無効になります。

セッションが無効となると、ユーザーの認証が不可能となります。なぜなら、セッションの中に含まれていたユーザーのIDの取得が不可能となるからです。そうなるとログインの画面に遷移して、再度ログインしなければなりません。

しかし、「Remember Me」をオンとしてログインしていたなら、この時点で、まだ期限までたっぷり時間がある「Remember Me」のクッキーを見に行きます。

このクッキーの中には、暗号化されたユーザーIDが入っているので、それを非暗号化して再度セッションを作成します。これにより、再度ログインをすることなしに、ログインした状態をキープすることが可能となるのです。

バリデーション (9) 配列のDistinctと暗黙の拡張

配列のDistinctバリデーション

前回の例題とした商品オプションに、オプション名に同じ値が入ることを禁じるバリデーションを追加します。

distinct

これには、配列プレースホルダー .* ともに laravel 5.2 で追加された distinct バリデーションが利用できます。よく考えられていますね。

$rules = [
  'option_id'     => 'required',
  'option_name.*' => 'required_with:option_id.*|distinct',
  'unit_price.*'  => 'required_with:option_id.*|integer|min:0',
  'inventory.*'   => 'required_with:option_id.*|integer|min:0',
];

 

空の入力に対するdistinct?

ところで、ここまで「オプション名」は必須入力であることを前提としてきましたが、商品オプションが1つだけなら「オプション名」は空のまま「販売単価」と「在庫数」だけ入力できるようにしたい、という要望が現場から上がってくるかもしれません。

もう少し仕様追求すると、商品オプションが2つ以上であっても、先頭(とは限らないかもしれません)の1つは空でよいかもしれません。

このために空の入力が1個まで可というバリデーションルールを追加するとロジックが複雑化します。required_with を外したとき distinct が空文字列もあわせて重複チェックしてくれれば話が簡単ですね。こんなふうに・・・

distinct_with_blank

3つの入力欄のうち2つを空にした場合のリクエストは次のようになります。

Array
(
    [option_name] => Array
        (
            [11] => 
            [12] => 
            [13] => オレンジ
        )

option_name のキー 11 と 12 は同じ値(空文字列)なのだから重複チェックにかかりそうなものです。

しかしこのバリデーションはそんな都合よく機能しません。

 

distinct バリデーションは、option_name 全体ではなく、配列の値それぞれに働きます。キー 11 の値は配列内で重複してるか? キー 12 の値は・・・と。

そして、バリデーションの原則は値が入力された項目にのみ働きます。このケースでは、そもそもキー 11 と 12 は調査されることなく、キー 13 の値が配列内で重複してるかだけが調べられ、重複なしと判定されるのです。

そう、リクエストが空でも働くのは、required 系バリデーションだけでした。

 

暗黙の拡張 implicitRules

ここまでくると次の展開が読めましたか?

この課題は解決には、distinctrequired と同様に 空リクエストに対してもバリデーションが働く 必要があり、これを実現するのが 暗黙の拡張 ルールです。

この暗黙の拡張を「暗黙の必須」と読んでしまうと理解を間違えます。空リクエストに対するバリデーション、それ以上でも以下でもありません。

暗黙の拡張ルールを作るには、Validator::extendImplicit() メソッドを使います。通常の拡張ルールとして登録されると同時に、暗黙の拡張ルールの名前を収めた配列 $implicitRules にルール名が追加されます。

Validator::extendImplicit('foo', function($attribute, $value, $parameters, $validator) {
    return $value == 'foo';
});

ということは、Validator を継承して作成した customValidaor クラスでは、直接 $implicitRules にルール名を追加することで暗黙の拡張を作ることができます。

以下は、空文字列を含んだ重複判定ができる distinct_with_blank バリデーションの作成例です。

class CustomValidator extends \Illuminate\Validation\Validator {

    public function __construct($translator, $data, $rules, $messages = [])
    {
        parent::__construct($translator, $data, $rules, $messages);

        // 暗黙の拡張に追加
        $this->implicitRules[] = 'DistinctWithBlank';
    }

    /**
     * 空文字列も含む重複判定 distinct_with_blank
     * @param  string $attribute
     * @param  string $value
     * @param  array  $parameters
     * @return true
     */
    public function validateDistinctWithBlank($attribute, $value, $parameters)
    {
        // 標準distinctを呼ぶだけ
        return parent::validateDistinct($attribute, $value, $parameters);
    }

これを用いて option_name.* から required_with を外すと、最初のルール定義は次のようになります。無事に、オプション名の空は1つだけ許可されるようになりました。

$rules = [
  'option_id'     => 'required',
  'option_name.*' => 'distinct_with_blank',
  'unit_price.*'  => 'required_with:option_id.*|integer|min:0',
  'inventory.*'   => 'required_with:option_id.*|integer|min:0',
];

 

この他にも、DB上の重複をチェックする unique に対し、空入力でも機能する unique_with_blank も状況によっては(DB定義がNULL OKではないなど)ニーズがあるかもしれませんね。

バリデーション (8) 配列をバリデーションする

laravel 5.2 で、バリデーションルールに配列を表す .* というプレースホルダーが使えるようになりました。

例えば、次のような商品オプションのバリデーションを考えます。

option_name

入力行の追加UIやドラッグ&ドロップによるソートはjQueryなどで実装することにします(laravel から離れるので解説は省きます)

この場合、項目数がいくつになるかわからないので、エレメントの属性名を name="option_name[{{$id}}]" などとし、配列を返すように作りますよね。

リクエストは次のようなものになるでしょう。配列のキーは編集データに依存しますので不定です(ここでは仮に 11, 12, 13 としました)

最低必要なバリデーションとしては、「保存」が少なくとも1つチェックされることと、「保存」がチェックされた行は必須入力となること、そして「販売単価」と「在庫数」の数値判定です。

苦労していた配列のバリデーションですが、5.2 からは次のようなルール定義が可能になったのです。

$rules = [
  'option_id'     => 'required',
  'option_name.*' => 'required_with:option_id.*',
  'unit_price.*'  => 'required_with:option_id.*|integer|min:0',
  'inventory.*'   => 'required_with:option_id.*|integer|min:0',
];

$messages = [
  'option_id.required' => '保存するレコードを少なくとも1つ選択してください',
];

「オプション名」、「販売単価」、「在庫数」のルール定義には属性名に .* を加えることで、配列の値それぞれにバリデーションが適用されるようになります。

「保存」がチェックされた行だけ入力が必要なので、required_with の引数も option_id.* となります。一方で「保存」の必須チェックは配列全体の required です。

配列部分のエラーは array_dot() による配列ドット記法で返ってきますので、Bladeテンプレートのエラーメッセージ表示は次のような書き方になります。

{{ $errors->first("unit_price.$id") }}

 

laravel 4 や 5.1 以前での配列バリデーション

後出しジャンケンでの自慢話となってしまいますが、私たちのプロジェクトでは lavavel 4 のころから配列に対するバリデーションを、5.2 と同じルールの書き方で処理してきました。

もともと laravel のバリデーションでは、リクエストは配列のまま処理されるでのはなく、array_dot によるドット記法の一次元データに変換されて内部処理されていました。

例えば次のように、配列のキーが固定されてる入力フォームの場合ならば、

<input type="text" name="name[last]" value="{{ old('name.last') }}">
<input type="text" name="name[first]" value="{{ old('name.first') }}">
Array
(
     [name] => Array
        (
            [last] => 山田
            [first] => 太郎
        )
)

以下のようにドット記法でルールを定義することで、配列データもバリデーションできたのです。

$rules = [
  'name.last'  => 'required',
  'name.first' => 'required',
];

 

最初に、「5.2 で .* というプレースホルダーに対応した」と書き、配列バリデーションそのものに対応したと書かなかった意味がここにあります。
このプレースホルダーの変換を自分で対応すれば、5.1 以前や 4 でも同様の処理ができるわけです。

定義するルール:

$rules = [
  'option_id'     => 'required',
  'option_name.*' => 'required_with:option_id.*',
  'unit_price.*'  => 'required_with:option_id.*|integer|min:0',
  'inventory.*'   => 'required_with:option_id.*|integer|min:0',
];

変換後のルール:

$rules = [
  'option_id'      => 'required',
  'option_name.11' => 'required_with:option_id.11',
  'option_name.12' => 'required_with:option_id.12',
  'option_name.13' => 'required_with:option_id.13',
  'unit_price.11'  => 'required_with:option_id.11|integer|min:0',
  'unit_price.12'  => 'required_with:option_id.12|integer|min:0',
  'unit_price.13'  => 'required_with:option_id.13|integer|min:0',
  'inventory.11'   => 'required_with:option_id.11|integer|min:0',
  'inventory.12'   => 'required_with:option_id.12|integer|min:0',
  'inventory.13'   => 'required_with:option_id.13|integer|min:0',
];

この変換は次のような関数で対応できます。ベースコントローラかトレイトに入れて、バリデーション直前にルールの変換を挿入してみてください。

/**
 * 配列バリデーションルールの変換
 * @param \Illuminate\Http\Request $request
 * @param array                    $rules
 * @param array                    $messages
 */
public static function setArrayRules($request, &$rules, &$messages)
{
    foreach($rules as $field => $rule)
    {
        // field文字列は foo.* か?
        if (!preg_match('/^(.+)\.\\*$/', $field, $m)) continue;

        // fooはリクエストに存在して配列か?
        $name = $m[1];
        if (!($req = $request->get($name)) || !is_array($req)) continue;

        foreach(array_keys($req) as $i)
        {
            // rulesに foo.$i を複写
            $rules["$name.$i"] = str_replace('*', $i, $rule);

            // messagesにfoo.$i.barがあれば複写
            foreach($messages as $key => $message)
            {
                if (!preg_match("/^$name\.\\*\.(.+)$/", $key, $m)) continue;
                $messages["$name.$i.{$m[1]}"] = $message;
            }
        }
        unset($rules[$field]);
    }
}

 

p.s.

上で紹介した setArrayRules() では、次のような配列の記述には対応してません。もし必要とするならばご自身で考えてみてくださいね。

Bladeソース:

<input type="text" name="option[{{$id}}][name]" value="old("opton.$id.name")">

バリデーションルール:

 'option.*.name' => 'required_with:option_id.*',

Laravel 5.3 resourceでの名前付きrouteの変更

以前に、Route::resourceの便利さを紹介しました。

routesを使いこなす(1)resourceを使う

routesを使いこなす(2)resourceを使いこなす

また、名前付きrouteがもたらす便宜さも紹介しました。

routesを使いこなす(4)routeを名付ける

しかし、Laravel 5.3のバージョンアップで「ちょっと、それはないよ」みたいな問題が出てきました。

Route::resourceでは、名前付きrouteが自動で作成されます。

例えば、Laravel 5.2では、routes.phpにこう設定すると、

Route::group(['prefix' => 'admin', 'middleware' => 'web', 'namespace' => 'Admin'], function () {

    Route::resource('product', 'ProductController');

});

以下のように名前付きroutesがprefixをもとに設定されます。「Name」の列です。

+--------+-----------+------------------------------+-----------------------+---------------------------------------------------------------+------------+
| Domain | Method    | URI                          | Name                  | Action                                                        | Middleware |
+--------+-----------+------------------------------+-----------------------+---------------------------------------------------------------+------------+
|        | POST      | admin/product                | admin.product.store   | App\Http\Controllers\Admin\ProductController@store            | web,web    |
|        | GET|HEAD  | admin/product                | admin.product.index   | App\Http\Controllers\Admin\ProductController@index            | web,web    |
|        | GET|HEAD  | admin/product/create         | admin.product.create  | App\Http\Controllers\Admin\ProductController@create           | web,web    |
|        | DELETE    | admin/product/{product}      | admin.product.destroy | App\Http\Controllers\Admin\ProductController@destroy          | web,web    |
|        | PUT|PATCH | admin/product/{product}      | admin.product.update  | App\Http\Controllers\Admin\ProductController@update           | web,web    |
|        | GET|HEAD  | admin/product/{product}      | admin.product.show    | App\Http\Controllers\Admin\ProductController@show             | web,web    |
|        | GET|HEAD  | admin/product/{product}/edit | admin.product.edit    | App\Http\Controllers\Admin\ProductController@edit             | web,web    |
+--------+-----------+------------------------------+-----------------------+---------------------------------------------------------------+------------+

しかし、おなじ、routes.phpをLaravel 5.3に持っていくと(もちろん、web.phpと改名して、app/Httpからroutes/のディレクトリに移して)、

+--------+-----------+------------------------------+-----------------+---------------------------------------------------------------+------------+
| Domain | Method    | URI                          | Name            | Action                                                        | Middleware |
+--------+-----------+------------------------------+-----------------+---------------------------------------------------------------+------------+
|        | POST      | admin/product                | product.store   | App\Http\Controllers\Admin\ProductController@store            | web,web    |
|        | GET|HEAD  | admin/product                | product.index   | App\Http\Controllers\Admin\ProductController@index            | web,web    |
|        | GET|HEAD  | admin/product/create         | product.create  | App\Http\Controllers\Admin\ProductController@create           | web,web    |
|        | DELETE    | admin/product/{product}      | product.destroy | App\Http\Controllers\Admin\ProductController@destroy          | web,web    |
|        | PUT|PATCH | admin/product/{product}      | product.update  | App\Http\Controllers\Admin\ProductController@update           | web,web    |
|        | GET|HEAD  | admin/product/{product}      | product.show    | App\Http\Controllers\Admin\ProductController@show             | web,web    |
|        | GET|HEAD  | admin/product/{product}/edit | product.edit    | App\Http\Controllers\Admin\ProductController@edit             | web,web    |
+--------+-----------+------------------------------+-----------------+---------------------------------------------------------------+------------+

なんと!
admin.product.store ⇒ product.store とadmin.が皆消えてしまいました。

ドキュメントを読むと、どうもこのprefixの設定は、resourceの名前付きrouteに反映される意図はなかったとのことで、5.3で修正という訳です。

以前と同様な、名前付きのrouteとするためには、asを使用して、以下のように設定します。

Route::group(['prefix' => 'admin', 'as' => 'admin.', 'middleware' => 'web', 'namespace' => 'Admin'], function () {

    Route::resource('product', 'ProductController');

});

注意:adminでなく、admin.です。最後にドットが必要。

あるいは、ひとつひとつを命名するというオプションもあります。しかし、これはちょっと。。

Route::group(['prefix' => 'admin', 'middleware' => 'web', 'namespace' => 'Admin'], function () {

    Route::resource('product', 'ProductController', ['names' => ['index' => 'admin.product.index']]);

});
Top