You are here
Home > Author: khino

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']の実行では、ララジャパンの値が返ってきます。

Laravel 5.3 タイムスタンプのDB項目名の指定

Laravel 5.3に更新して、Eloquentのモデルの設定において嬉しいこと発見しました。

LaravelのEloquentでは、指定のDBテーブルにおいて、作成日時と編集日時に、規定のcreated_atupdated_atの項目名が使用されているなら、いちいち、

use Users;
use Carbon\Carbon;

$user = new Users;
...
$user->created_at = $user->upddated_at = Carbon::now();

$user->save();

というようなことを、DBレコードの追加や編集の際に、書かなくとも自動で作成日時と編集日時に値を入れてくれます。

私のケースでは、既存のプロジェクトのDBにおいて、作成日時と編集日時には、date_createddate_modifiedと違う名前を使用していて、今まで「いちいち」コードで指定していました。

しかし、Laravel 5.3では、以下のようにモデルの定数(const)を指定することで、項目名を指定できるようになりました。


namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Company extends Model
{
	protected $table = 'company';
	protected $primaryKey = 'company_id';
	public $timestamps = true; //デフォルトではtrueなので、指定する必要はない

	const CREATED_AT = 'date_created';
	const UPDATED_AT = 'date_modified';
..

これは大変便利です。

もう1つ便利なことで最近見つけたことは、最初の例で使用したCarbonは、Laravelをインストールしたら一緒にインストールされるパッケージですが、使い勝手あります。

例えば、DBに記録した日時から現在までの「経過日数」を計算するには、

$days_past = (new Carbon($user->updated_at))->diff(Carbon::now())->days;

簡単でわかりやすいですね。これをphpでやろうとすると、文字列から秒数に変換してなどと大変です。

最後に、日時の使用で忘れてならないのは、必ずアプリの設定ファイルで、タイムゾーンを設定すること。

..
    'timezone' => env('APP_TIMEZONE', 'Asia/Tokyo'),
..

ログイン成功のイベント

ユーザー認証(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が入っているので、それを非暗号化して再度セッションを作成します。これにより、再度ログインをすることなしに、ログインした状態をキープすることが可能となるのです。

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']]);

});

Laravel 5.3 AWS S3にファイルをアップロード

アップロードしたファイルの保存のメソッドがLaravelで5.3で少し変わりました。ここでそれらの情報更新とともに、AmazonのストレージサービスS3にファイルをアップロードする仕方を紹介します。

まず、準備から、

パッケージの追加と設定

コマンドラインで以下の実行が必要です。


composer require league/flysystem-aws-s3-v3 ~1.0

これにより、

config/filesystems.php

の設定ファイルが作成されます。


return [
    'default' => 'local',
    'cloud' => 's3',

    'disks' => [

        'local' => [
            'driver' => 'local',
            'root' => storage_path('app'),
        ],

        'public' => [
            'driver' => 'local',
            'root' => storage_path('app/public'),
            'visibility' => 'public',
        ],

        's3' => [
            'driver' => 's3',
            'key' => 'AWSのキー',
            'secret' => '秘密のキー',
            'region' => '地域のコード', // 日本なら、ap-northeast-1
            'bucket' => 'バケット名'
        ],
    ],
];
    

localは、使用しているサーバーのストレージのことです。

rootは、Laravelをインストールしたディレクトリのサブディレクトリ、storage/appの場所となります。

publicは、ウェブユーザーにアップロードしたファイルをパブリックに紹介する場所です。

以下の実行で、public/storageが、storage/app/publicにリンクされます


php artisan storage:link

例えば、アップロードされたファイルは、

storage/app/public/mario.jpg

に保存され、

http://localhost/public/storage/mario.jp

で閲覧できるということです。

s3のkey, secret, region, bucketの指定は必須です。これらは、Amazonのウェブサービスのコンソールで取得できます。

これで設定終わりです。

ファイルのアップロードのプログラム

簡単なファイルのアップロードのプログラムを書いてみます。

まず、routeの設定から、


    Route::get('upload', 'UploadController@create');
    Route::post('upload', 'UploadController@store');

これで、

http://localhost/upload

にアクセス可能です。

次にコントローラ、

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use Storage;

class UploadController extends Controller
{
    public function create()
    {
        return view('upload');
    }

    public function store(Request $request)
    {
        $filename = $request->file('image')->getClientOriginalName(); //アップロードしたファイル名を取得

        $path = $request->file('image')->storeAs('public', $filename);

        return back()->with('filename' => $filename);
    }
}

storeAs('public', $filename);

この最初のパラメータは、ファイルを保存するディレクトリ名です。先のconfig/filesystems.phpの設定で、storage/appがルートのディレクトリゆえに、上のコードではstorage/app/publicにファイルが保存されることになります。

ファイル名がmario.jpgであれば、

storage/app/public/mario.jpg

と保存されます。

ファイルをアップロードするフォームのブレードは、


@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Media Upload</div>
                <div class="panel-body">
                    <form class="form-horizontal" role="form" method="POST" action="{{ url('upload') }}" enctype="multipart/form-data">
                        {{ csrf_field() }}

                        <div class="form-group">
                            <label for="image" class="col-md-4 control-label">File</label>

                            <div class="col-md-6">
                                <input id="image" type="file" name="image">
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                <button type="submit" class="btn btn-primary">
                                    Upload
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-body">
                @if (session('filename'))
                    <h4>Local</h4>
                    <img src="{!! asset('storage/'.session('filename')) !!}">
                @endif
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

先の例で、ファイル名が、mario.jpgならば、

asset('storage/'.session('filename'))

は、

http://localhost/public/storage/mario.jpg

のようになるわけです。

ファイルをアップロードした後の画面はこんな感じです。
my-application

S3に画像をアップロード

さて、サーバーにアップした画像を、今度はS3にアップするのですが、これはconfig/filesystems.phpの設定が済んでいれば、本当に簡単です。

コントローラのstoreメソッドにたったの2行追加するだけです。


   public function store(Request $request)
    {
        $filename = $request->file('image')->getClientOriginalName();

        $path = $request->file('image')->storeAs('public', $filename);

        $contents = Storage::get('public/'.$filename); //ファイルを読み取る
        Storage::disk('s3')->put($filename, $contents, 'public'); // S3にアップ
 
        return back()->with(['filename' => $filename]);
    }

>put($filename, $contents, 'public')

ここのpublicに注意してください。これがないと一般には公開されません。

以下のAWSのコンソールの赤箱の部分がそれにより追加されます。

s3-management-console

先の画面にS3から直接画像を表示したいなら、ブレードに以下の変更を。

    <div class="panel-body">
    @if (session('filename'))
       <h4>Local</h4>
       <img src="{!! asset('storage/'.session('filename')) !!}">
       <h4>S3</h4>
       <img src="{!! Storage::disk('s3')->url(session('filename')) !!}">
    @endif
    </div>

S3の以下のURLが生成されます。

https://s3-us-west-2.amazonaws.com/demo53/mario.jpg

us-west-2は、設定に使用した地域コードです。日本ならap-northeast-1となります。

Top