You are here
Home > Posts tagged "L5.4"

ユーザー認証のテスト(3) Laravel 5.4 ログアウト

前回のログインのテストに対して、今回はログアウトのテスト。

まず、以下を見てください。

   /** @test */
    public function logout()
    {
        // ユーザーを1つ作成
        $user = factory(User::class)->create();

        // 認証済み、つまりログイン済みしたことにする
        $this->actingAs($user);

        // 認証されていることを確認
        $this->assertTrue(Auth::check());

        // ログアウトを実行
        $response = $this->post('logout');

        // 認証されていない
        $this->assertFalse(Auth::check());

        // Welcomeページにリダイレクトすることを確認
        $response->assertRedirect('/');
    }

前回と違うのは、ログインの実行$response = $this->post(..)が今回のテストにはないことです。その代わりに、actingAs()を利用して、プログラムで認証済みにしてしまいます。

actingAs()のおかげで、コードが短くなりわかりやすくなっただけでなく、その使用では以下のようにセッション値を入れたり、リダイレクトすることも可能です。認証された後の、つまりパスワード保護された中の機能のテストではいつも必要なことなので、将来重宝しそうです。

..
 $response = $this->actingAs($user)
                   ->withSession(['foo' => 'bar'])
                   ->get('/');
...

さて、テストとは関係ないですが、ログアウト後はどうして、/loginでなく/にリダイレクトされるのでしょう?また、その変更は可能なのでしょうか?

リダイレクト先は、残念ながら、以下のlaravelのパッケージの中のファイルでハードコードされています。

vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticateUsers.php

...
   /**
     * Log the user out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function logout(Request $request)
    {
        $this->guard()->logout();

        $request->session()->invalidate();

        return redirect('/');
    }
...

上のファイルは編集するべきではありません。しかし、ログアウト後のリダイレクト先を変更したいなら、

app/Http/Controllers/Auth/LoginController.php

に、上のlogoutのコードを追加して、リダイレクト先を編集すればよいです。親のメソッドが上書きされて変更されることになります。use Illuminate\Http\Request;の行の追加も忘れないように。以下に全コードを掲載します。

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    /**
     * Log the user out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function logout(Request $request)
    {
        $this->guard()->logout();

        $request->session()->invalidate();

        return redirect('/');
    }
}

最後に今回のテストのコードは以下から利用可能です。

https://github.com/lotsofbytes/larajapan/blob/l54-test/tests/Feature/LoginTest.php

ユーザー認証のテスト(2) Laravel 5.4 ログイン

前回は、

  • ログイン画面にアクセスできる
  • ログインしないで、ホームページにアクセスするとログイン画面にリダイレクトされる

のテストを作成しました。

今回は、同じくログイン画面において以下のテストを作成します。

  • 登録されているユーザーのEメールとパスワードでログインできる
  • 間違ったパスワードでログインした場合、ログイン失敗してエラーが出力される

モデルファクトリ

さて、前回と違って今回は、ログインのテストゆえにテストのデータベースにユーザーのレコードが必要となります。もちろん、手動でレコードを作成しておいてテストも可能です。しかし、テストが複雑になって様々なケースのテストが増えてくると非常に面倒です。

そこで登場するのは、モデルファクトリ。テストのために、必要なデータを必要な数だけ自動生成してDBレコードを作成してくれます。ファクトリの設定は、LaravelのEloquentのモデルを指定するのでとても簡単。以下ではUserのモデルをもとにしています。Fakerのパッケージを利用しているので各項目での詳細はそちらを参照してください。

use Faker\Generator as Faker;

/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\User::class, function (Faker $faker) {
    static $password;

    return [
        'email'          => $faker->unique()->safeEmail, // 複数レコード作成時には重複しないEメールを生成
        'password'       => $password ?: $password = bcrypt('test1234'), // 複数レコード作成時には同じパスワードを生成
        'name'           => $faker->name,
        'remember_token' => str_random(100), //ランダム値
    ];
});

上の設定では、複数のレコード作成時には、emailは重複しないように、パスワードはstatic変数を利用してどのレコードも同じパスワードを作成するようなっています。bcryptは、LaravelのヘルパーでHash::makeと同じです。

日本語のデータを生成したいなら、config/app.phpで、

..
    /*
    |--------------------------------------------------------------------------
    | Application Locale Configuration
    |--------------------------------------------------------------------------
    |
    | The application locale determines the default locale that will be used
    | by the translation service provider. You are free to set this value
    | to any of the locales which will be supported by the application.
    |
    */

    'locale' => 'ja',
    'faker_locale' => 'ja_JP',
..

と、faker_localeを設定してください。

テストでレコードの自動作成

ユニットテストでは、毎回のテストにおいてテストDBを空にしてモデルファクトリで新しいデータを埋めます。これを実行するために、Laravelで2つの異なるメカニズムが用意されています。

  • DataMigrations:Laravelのmigrationのメカニズムを使用して、毎回テストごとにDBテーブルを1から作成する
  • DatabaseTransactions:DBのトランザクションの機能を利用して、毎回テストごとにDBテーブルを空にする

違いは重要で、前者はテストDBにテーブルが何も存在しなくてもOKですが、後者では空のDBテーブルが必要です。

前者は、新規のプロジェクトで最初からLaravelのmigrationを使用してDBを管理しているならお薦めですが、既存のプログラムをLaravelに移行したプロジェクト(私の場合はこれが多い)では、違うメカニズムでDBを管理しているため前者が使用できないので後者となります。

後者では、本DBと同じDB構造とするため、以下のようにmysqldumpなどを使用してコピーする必要あります。

$ mysqludmp -u root larajapan -p -d > larajapan_test.sql
$ mysql -u root larajapan_test -p < larajapan_test.sql

ここでは、すでにmigrationの設定がなされているうえに、DatabaseMigrationsのトレイトをユニットテストの中で宣言します。

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
//use Illuminate\Foundation\Testing\DatabaseTransactions;

use App\User;

class LoginTest extends TestCase
{
    use DatabaseMigrations;
..

ログインテスト

準備整ったところで、テストしてみましょう。まずは、ログイン成功のテスト。

先ほど設定したファクトリを使用して、最初に1つだけユーザーのレコードを作成します。その後、作成したのと同じパスワードを使用してログインを実行。実行後は、認証されていることと、ホームページにリダイレクトされることを確認します。

    /** @test */
    public function valid_user_can_login()
    {
        // ユーザーを1つ作成
        $user = factory(User::class)->create([
            'password'  => bcrypt('test1111')
        ]);

        // まだ、認証されていない
        $this->assertFalse(Auth::check());

        // ログインを実行
        $response = $this->post('login', [
            'email'    => $user->email,
            'password' => 'test1111'
        ]);

        // 認証されている
        $this->assertTrue(Auth::check());

        // ログイン後にホームページにリダイレクトされるのを確認
        $response->assertRedirect('home');
    }

次はログイン失敗のテスト。ここでもファクトリで1つだけレコードを作成しますが、わざと作成したのとは違うパスワードを使用してログインして、エラーメッセージが表示されることを確認します。

    /** @test */
    public function invalid_user_cannot_login()
    {
    // ユーザーを1つ作成
        $user = factory(User::class)->create([
            'password'  => bcrypt('test1111')
        ]);

        // まだ、認証されていないことを確認
        $this->assertFalse(Auth::check());

        // 異なるパスワードでログインを実行
        $response = $this->post('login', [
            'email'    => $user->email,
            'password' => 'test2222'
        ]);

        // 認証失敗で、認証されていないことを確認
        $this->assertFalse(Auth::check());

        // セッションにエラーを含むことを確認
        $response->assertSessionHasErrors(['email']);

        // エラメッセージを確認
        $this->assertEquals('メールアドレスあるいはパスワードが一致しません',
            session('errors')->first('email'));

$responseに使用されるassertの関数は、phpunitからのassertTrueと違い、Laravelで宣言されているものです。

参照としては、本サイトの以下

https://laravel.com/docs/5.4/http-tests#available-assertions

ですが、そこには掲載されていない関数もあるので、以下もチェックしてください。

https://laravel.com/api/5.4/Illuminate/Foundation/Testing/TestResponse.html

最後に、テストを実行してみましょう。

$ vendor/bin/phpunit --filter LoginTest
PHPUnit 5.7.22 by Sebastian Bergmann and contributors.

....                                                                4 / 4 (100%)

Time: 1.09 seconds, Memory: 16.00MB

OK (4 tests, 12 assertions)

エラーもなく成功ですね!

今回のコードは以下から利用可能です。

https://github.com/lotsofbytes/larajapan/blob/l54-test/tests/Feature/LoginTest.php

ユーザー認証のテスト(1) Laravel 5.4

Laravelでのユーザー認証は私のブログの中では最も人気のあるトピックです。今回は、私のLaravelの日本語のレポジトリ(Laravel 5.4)のコードをもとに、ユーザー認証のテストに取り組んでいきます。

テストの種類

テストと言っても、いくつか種類があり、Laravel5.4からはtestsのディレクトリ構造も変わり、

tests
├── CreatesApplication.php
├── Feature
│   └── ExampleTest.php
├── TestCase.php
└── Unit
    └── ExampleTest.php

のようにFeatureUnitの2つのサブディレクトリができました。

Unitには、一般的にはユニットテストと呼ばれるもので、大方は画面の表示を伴わないModelのメソッドに対するテストを作成します。一方、Featureには、機能テストやアクセプタンステストとも言われ、複数のクラスの複数のメソッドが関わる主にコントローラの機能を検証するためで、あたかもユーザーがテストするようなテストを作成します。

私の今までの開発では、比較的作成しやすいUnitテストが主で、Featureテストは皆無に近く、人間のテスターがその仕事を行っています。人間をFeatureテストに置き換える予定はないですが、Laravelが提供するテストの環境がFeatureテストを作成しやすくなってきているので、この機会に習得しようということです。

Laravel5.4では、Featureテストのために、2つのテストのフレームワークが提供されています。

ここでは、まず、追加のパッケージのインストールも要らない、高速なHTTPテストを作成していきます。

準備

github.comにおいて以下のブランチを用意したので利用してください。

https://github.com/lotsofbytes/larajapan/tree/l54-test

インストールは以下を参照してください。

Laravelの日本語のレポジトリ(Laravel 5.4)

設定したら、必ず以下を実行してブランチを変えてください。

$ git checkout l54-test

また、そこでは、phpunit.xmlを編集も必要です。以下を参考にしてください。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="bootstrap/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>

        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="APP_URL" value="http://localhost"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="DB_HOST" value="localhost"/>
        <env name="DB_DATABASE" value="larajapan_test"/>
        <env name="DB_USERNAME" value="test"/>
        <env name="DB_PASSWORD" value="password"/>
        <env name="MAIL_DRIVER" value="log" />
        <env name="QUEUE_DRIVER" value="sync"/>
    </php>
</phpunit>

テストのDBは.envで設定しているものと違うDBが必要なことに注意してください。

最初のテスト

もとからあるtests/Feature/ExampleTest.phpのファイルを、LoginTest.phpと改名して以下のように編集します。

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class LoginTest extends TestCase
{
    /** @test */
    public function user_can_view_login()
    {
        $response = $this->get('login');

        $response->assertStatus(200);
    }

    /** @test */
    public function unauthenticated_user_cannot_view_home()
    {
        $this->get('home')
        	->assertRedirect('login');
    }
}

最初のテストuser_can_view_loginは、ログイン画面が閲覧できるかどうかのテストです。
返ってくるHTTPのステータスのコードが200なら、成功ということです。これが404(ページが見つかりません)とかだと何かがおかしいということになります。

次のテストunauthenticated_user_cannot_view_homeは、認証が必要なホームページに、認証もなしにアクセスしてみます。もちろん、アクセスできないでログイン画面にリダイレクトされるはずです。

ここ、メソッドが連結されていることに気づきましたか?

最初のテストの例のように、以下のよう2つの文に分けても書くこともできます。

$response = $this->get('home');
$response->assertRedirect('login');

これらのテストの実行ですが、Laravelのインストールにより、すでにvendorのディレクトリにphpunitのパッケージもインストールされています。

ということで、

$ vendor/bin/phpunit
PHPUnit 5.7.23 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)
Time: 171 ms, Memory: 10.00MB

OK (3 tests, 4 assertions)

と実行してテストは皆成功となります。3 testsとあるのは、app/tests/Unit/ExampleTestがあるからです。
以下のようにフィルタを書ければ、LoginTestの中のテストだけや、user_can_view_loginの1つテストだけの実行も可能です。

$ vendor/bin/phpunit --filter=LoginTest
HPUnit 5.7.23 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 157 ms, Memory: 10.00MB

OK (2 tests, 3 assertions)

$ vendor/bin/phpunit --filter=user_can_view_login
PHPUnit 5.7.23 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 111 ms, Memory: 10.00MB

OK (1 test, 1 assertion)

Route::match

プログラマというのは、その性質上、いかにプログラムの行数を少なくして、やりたいことをクリーンに明確に表現できるかに時間を費やしたりします。そして、重複の表現はすぐに気づき、忌み嫌い、どうしたらそれをなくすことができることを日夜考えます。

私もそのひとりで、例えば、昔以下のようなコードありました、「どうにかならんかな?」と気になっていました。

..
// 会員登録     
Route::get ('member/signup',  'MemberController@signup')->name('member.signup');                                                                                                                                                                                     
Route::post('member/signup',  'MemberController@postSignup')->name('member.signup');  
..

見ての通り、どちらもメソッド(get, post)は違いますが、URLは同じmember/signupとなります。もちろん、Route::resourceを使うことも可能ですが、URLは、createでなくsignupとしたいし、必要な処理も登録だけです。

上のコードのphp artisan route:listでの出力はこんな感じです。

+--------+----------+-------------------+---------------+---------------------------------------------------+--------------+
| Domain | Method   | URI               | Name          | Action                                            | Middleware   |
+--------+----------+-------------------+---------------+---------------------------------------------------+--------------+
|        | GET|HEAD | member/signup     | member.signup | App\Http\Controllers\MemberController@signup      | web,guest    |
|        | POST     | member/signup     | member.signup | App\Http\Controllers\MemberController@postSignup  | web,guest    |
+--------+----------+-------------------+---------------+---------------------------------------------------+--------------+

2行も必要でしょうかね?

こう思っているのは私だけではなかったようです。LaravelのマニュアルにRoute::matchを見つけました。

先の2行のルートの指定を以下のように1行にすることができました。

..
// 会員登録     
Route::match(['get', 'post'], 'member/signup',  'MemberController@signup')->name('member.signup');                                                                                                                                                                                     
..

php artisan route:listでの出力はこうなりました。

+--------+---------------+-------------------+---------------+---------------------------------------------------+--------------+
| Domain | Method        | URI               | Name          | Action                                            | Middleware   |
+--------+---------------+-------------------+---------------+---------------------------------------------------+--------------+
|        | GET|POST|HEAD | member/signup     | member.signup | App\Http\Controllers\MemberController@signup      | web,guest    |
+--------+---------------+-------------------+---------------+---------------------------------------------------+--------------+

素晴らしい!と思うとともに、さてどうやって1つのメソッドsignupでGETとPOSTの2つに対応したらよいものか?

解決方法は、

namespace App\Http\Controllers;
                                                                                                                                                                                
use Illuminate\Http\Request                                                                                                                                                                                                                                                                                                                                                        
use App\Models\Member;                                                                                                                                                                                            
                                                                                                                                                                                                                   
class MemberController extends Controller {
...
	/**
	 * Signup
	 *
	 * @param  \Illuminate\Http\Request $request
	 * @return \Illuminate\Http\Response
	 */
	public function signup(Request $request)
	{
		if ($request->method() == 'POST')
		{
			return $this->postSignup($request);
		}

		return view('user/member_add');
	}

	/**
	 * Save Signup
	 *
	 * @param  \Illuminate\Http\Request $request
	 * @return \Illuminate\Http\Response
	 */
	public function postSignup(Request $request)
	{
...
}

のように、signupメソッドにおいて、POSTで呼ばれたかを判断して、postSignupをコールします。

しかし、routesは1行になったけれど、コントローラはちょっとわかりにくいかな、とも思いますね。

phanは楽しい!(2) laravel編

前回紹介した楽しい開発ツールphanを、今回はLaravelのプロジェクトに適用してみます。さて、どれだけ楽しくなるか!

まず、Laravel 5.4の私の日本語開発用のレポジトリをインストールします。

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

次に、phanのパッケージをインストールします(注1)

$ cd larajapan
$ composer require --dev phan/phan

インストールできたところで、phanの設定をします。

.phanのディレクトリを作成して、config.phpのファイルを作成します。


return [
    // 解析のためにクラスやメソッドの情報を取得するディレクトリ。
    // これらと、以下の exclude_analysis_directory_list で指定した
    // ディレクトリとの差分が解析チェックの対象となる
    'directory_list' => [
        'app',
        'routes',
        'vendor'
    ],

    // 解析チェックの対象から除外するディレクトリ。
    'exclude_analysis_directory_list' => [
       'vendor'
    ],
];

設定ファイルが用意できたところで、最初のphanの実行をしてみましょう。

$ vendor/bin/phan -p 
app/Auth/Notifications/ResetPassword.php:51 PhanTypeMismatchArgument Argument 2 (parameters) is string but \route() takes array defined at vendor/laravel/framework/src/Illuminate/Foundation/helpers.php:707
app/Auth/Passwords/CanResetPassword.php:16 PhanUndeclaredProperty Reference to undeclared property \App\Auth\Passwords\CanResetPassword->email
app/Auth/Passwords/CanResetPassword.php:27 PhanUndeclaredMethod Call to undeclared method \App\Auth\Passwords\CanResetPassword::notify
app/Providers/RouteServiceProvider.php:54 PhanUndeclaredMethod Call to undeclared method \Illuminate\Routing\Route::namespace
app/Providers/RouteServiceProvider.php:68 PhanUndeclaredMethod Call to undeclared method \Illuminate\Routing\Route::namespace
routes/api.php:16 PhanUndeclaredClassMethod Call to method middleware from undeclared class \Route
routes/channels.php:14 PhanUndeclaredClassMethod Call to method channel from undeclared class \Broadcast
routes/console.php:16 PhanUndeclaredClassMethod Call to method command from undeclared class \Artisan
routes/console.php:17 PhanUndeclaredVariable Variable $this is undeclared
routes/web.php:14 PhanUndeclaredClassMethod Call to method get from undeclared class \Route
routes/web.php:22 PhanUndeclaredClassMethod Call to method get from undeclared class \Route
routes/web.php:23 PhanUndeclaredClassMethod Call to method post from undeclared class \Route
routes/web.php:24 PhanUndeclaredClassMethod Call to method post from undeclared class \Route
routes/web.php:27 PhanUndeclaredClassMethod Call to method get from undeclared class \Route
routes/web.php:28 PhanUndeclaredClassMethod Call to method post from undeclared class \Route
routes/web.php:31 PhanUndeclaredClassMethod Call to method get from undeclared class \Route
routes/web.php:32 PhanUndeclaredClassMethod Call to method post from undeclared class \Route
routes/web.php:33 PhanUndeclaredClassMethod Call to method get from undeclared class \Route
routes/web.php:34 PhanUndeclaredClassMethod Call to method post from undeclared class \Route
routes/web.php:36 PhanUndeclaredClassMethod Call to method get from undeclared class \Route

たくさんエラー出てきました。

その中でもPhanUndeclaredClassMethodが多い。これは宣言されていないクラスのメソッドをコールしているエラーです。

例えば、一番多いエラーとなっているRouteのクラスは、Laravel特有のFacade(ファサード)です。ネームスペースnamespaceがあるクラスと違い、phanにとっては解析が難しいようです。

調査したところ、IDEヘルパーの開発ツールを使用して、stabのファイルを作成することで回避できそうです。Laravelのstabのファイルの作成には、まず以下を実行してide-helperのパッケージをインストールします。

$ composer require --dev barryvdh/laravel-ide-helper

次に、config/app.phpを編集して以下を追加、

Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class

このパッケージは、debugbarの開発者が作成したものです。本来は、phpStormなどのIDEのエディターでのヘルパーが目的(それゆえにlaravel-ide-helper)ですが、ファサードのクラスのメソッドを宣言してくれるために、phanでも役に立つようです。

..
    'providers' => [  
..
        /*                                                                                                                                                                                                         
        * Package Service Providers...                                                                                                                                                                            
        */                                                                                                                                                                                                        
        Laravel\Tinker\TinkerServiceProvider::class,                                                                                                                                                               
        Barryvdh\Debugbar\ServiceProvider::class,
        Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,

..

そして、以下を実行。

$ php artisan ide-helper:generate

_ide_helper.phpのファイルが実行したディレクトリに作成されます。これを、.phan/stabsのディレクトリを作成して移します。

$ mkdir .phan/stabs
$ mv _ide_helper.php .phan/stabs

さらに、このファイルを利用するために、.phan/config.phpも編集が必要です。


return [
    // 解析のためにクラスやメソッドの情報を取得するディレクトリ。
    // これらと、以下の exclude_analysis_directory_list で指定した
    // ディレクトリとの差分が解析チェックの対象となる
    'directory_list' => [
     '.phan/stabs',
        'app',
        'routes',
        'vendor'
    ],

    // 解析チェックの対象から除外するディレクトリ。
    'exclude_analysis_directory_list' => [
       '.phan',
       'vendor'
    ],
];

これで実行準備完了です。再度実行してみましょう。

$ vendor/bin/phan -p
app/Auth/Notifications/ResetPassword.php:51 PhanTypeMismatchArgument Argument 2 (parameters) is string but \route() takes array defined at vendor/laravel/framework/src/Illuminate/Foundation/helpers.php:707
app/Auth/Passwords/CanResetPassword.php:16 PhanUndeclaredProperty Reference to undeclared property \App\Auth\Passwords\CanResetPassword->email
app/Auth/Passwords/CanResetPassword.php:27 PhanUndeclaredMethod Call to undeclared method \App\Auth\Passwords\CanResetPassword::notify
app/Providers/RouteServiceProvider.php:54 PhanUndeclaredStaticMethod Static call to undeclared method \Illuminate\Support\Facades\Route::middleware
app/Providers/RouteServiceProvider.php:68 PhanUndeclaredStaticMethod Static call to undeclared method \Illuminate\Support\Facades\Route::prefix
routes/api.php:16 PhanUndeclaredStaticMethod Static call to undeclared method \Route::middleware
routes/channels.php:14 PhanUndeclaredStaticMethod Static call to undeclared method \Broadcast::channel
routes/console.php:17 PhanUndeclaredVariable Variable $this is undeclared

PhanUndeclaredClassMethodのエラーが少なくなりました。しかし、残り未宣言のエラーの解決は現段階では無理そうです。実際宣言されていにも関わらず、その情報をphanに伝えることが自動のツールでは不可能だからです。将来に期待しましょう。

とりあえず、ここでは-iオプションで、この未宣言のエラーを無視することにします。

$ vendor/bin/phan -p -i
app/Auth/Notifications/ResetPassword.php:51 PhanTypeMismatchArgument Argument 2 (parameters) is string but \route() takes array defined at vendor/laravel

-iで、エラーは1つだけとなりました。

このエラーは、以下のroute()の2番目の引数が配列である必要があるということで、

..
   public function toMail($notifiable)                                                                                                                                                                            
    {                                                                                                                                                                                                              
        return (new MailMessage)                                                                                                                                                                                   
            ->subject('パスワードリセット')                                                                                                                                                                        
            ->greeting('パスワードリセット')                                                                                                                                                                       
            ->line('パスワードリセットリンクの送信のリクエストがありました。')                                                                                                                                     
            ->action('リセットパスワード', url(config('app.url').route('password.reset', [$this->token], false)))                                                                                                    
            ->line('リクエストされていなかったら、無視してください。');                                                                                                                                            
    }     
..

と編集して解決できました。

ということで、インストールは”そう楽しくなさそうな”ツールですが、もし大きなプログラムがあるなら一度でも試してください。私のプロジェクトではいくつか重要なバグをみつけてくれてそれだけでも十分な貢献です。これからも、検証のツールとしてunit testとともに重要なものになりそうです。

ひとつ注意することは、この実行には空きメモリが2GB以上必要です。残念ながら私のテスト環境のawsのsmall(メモリ2GB)のインスタンスでは実行不可能でした。仮想の開発環境(メモリ4GB)で実行可能でした。

注1

composerで、phpdocumentor/reflection-docblockのバージョンのコンフリクトが出ました。すでにインストールされているそのパッケージのバージョンは4.1.1ですが、phan/phanが必要とするものはそれよりも昔のバージョン。ということで、

..
   "require": {
        "php": ">=5.6.4",
        "barryvdh/laravel-debugbar": "~2.4",
        "laravel/framework": "5.4.*",
        "laravel/tinker": "~1.0",
        "phpdocumentor/reflection-docblock": "4.1.1 as 3.3.0"
    },
..

4.1.1 as 3.3.0として4.1.1のバージョンを3.3.0のバージョンとして扱ってもらうことで解決できました。

phanは楽しい!(1)

php7に更新したら使ってみたいと思っていたツールがありました。

このphpの静的解析ツールツールの名前は、Phan。ファンと呼びます。多分、楽しいという英語のfunにひっかけて。Githubでは、以下で公開されています。

https://github.com/phan/phan

さて、これがどうして重要なツールかというと、

phpはもともと開発の敷居を落とすためにCやJavaなどと違ってデータタイプを宣言しなくてよいプログラム言語、しかしプログラムが大きくなり複雑になってくると逆にその緩さが多くの間違いの原因となります。

例えば、

function double($i)
{
  return $i*2;
}

$x = '文字列';

echo double($x);

これを実行するとphpではエラーなしに、0を返します。関数doubleは数字を引数に期待しているはずなのに、phpは数字でなく文字列を渡しても問題なく処理してしまいます。もちろん、ここでの実行は意図的ですが、もし間違いで文字列を渡してしまったら、そのコードがもし他のコードの奥に隠れていたら、原因がわからないバグになりかねません。

phanは、ここの間違いをphpのプログラムを実行することなし(それゆえに静的解析)に見つけてくれる重要な開発検証ツールなのです。

まずは、phanのインストールからなのですが、今回はphanの紹介ということで、Laravelなしの環境でのインストールです。次回には、より複雑なLaravelプログラムの環境でのphanの実行を説明します。

さて、簡単にインストールできるよ、と行きたいのですが、実際は少々ややこしく、astのモジュールとphanのパッケージの2ステップのインストールとなります。

1.astのモジュールのインストール

ast(abstract syntax tree、抽象シンタックスツリー)は、phanに不可欠なphpプログラムの解析モジュールで、特定のプログラムでなくマシン全体でのグローバルのインストールとなります。そして、多分たいていの環境ではデフォルトでインストールされていないと思います。

まずは、そのモジュールが存在するかどうかのチェックとして、

$ php -m

の実行で、astがでて来なかったら、以下を実行してください。

$ pecl install ast

その後、/etc/php.iniを編集して、

extension=ast.so

を追加します。

残念ながらここでくじけたら、もうphanは使えません。くじけたくないなら、以下の開発者のgithubを見てください。

https://github.com/nikic/php-ast

話それますが、このastの開発者、いろいろ面白いツール作成しています。プログラムのコンパイルなどの理論に興味のある方は是非以下も。

https://github.com/nikic

2.phanのインストール

次は以下を実行して、パッケージをインストールします。

$ composer require --dev phan/phan

--devは、開発だけに使用という意味です。

これで、現在のディレクトリは、

.
├── composer.json
├── composer.lock
├── test.php
└── vendor
    ├── autoload.php
    ├── bin
    ├── composer
    ├── felixfbecker
    ├── netresearch
    ├── nikic
    ├── phan
    ├── phpdocumentor
    ├── psr
    ├── sabre
    ├── symfony
    └── webmozart

となります。test.phpは先ほどの例のプログラム。

3.phanの実行

実行は簡単です。ターゲットのファイルを指定するのみです。

$ ./vendor/bin/phan test.php

これを実行すると、何も出力ありません!

何がおかしいのでしょう?

phanは、エラーを出すには関数の引数のデータタイプが必要なのです。phpではそのためにDocblock(ドクブロック)をコメントの中に入れる必要あります。test.phpを編集して、@param float $iを関数定義の直上のコメントに入れます。

/**                                                                                                                                                                                                                
 * @param float $i                                                                                                                                                                                                   
 * @return float                                                                                                                                                                                                    
 */                                                                                                                                                                                                                
function double($i)                                                                                                                                                                                                
{                                                                                                                                                                                                                  
  return $i*2;                                                                                                                                                                                                     
}                                                                                                                                                                                                                  
                                                                                                                                                                                                                   
$x = '文字列';                                                                                                                                                                                                     
                                                                                                                                                                                                                   
echo double($x); 

再度、phanを実行すると、

test.php:14 PhanTypeMismatchArgument Argument 1 (i) is string but \double() takes float defined at test.php:7

とエラーを出してくれます。もちろん、$x = 1.5と数字ならエラーは出ません。

簡単なphanの実演でしたが、このツールを活用するにはDocblockの作成がまず必要ということです。Docblockの作成はちょっと面倒な作業かもしれません。しかし、phpはバージョン5から、関数で型宣言ができ、sublimeなどの最近のエディターはそれを探知して、自動的にエディタでドクブロックも作成してくれます。また、それをもとにAPIドキュメントも作成してくれるツールもあります。ドクブロックは必須となっている昨今です。しかし、最終的には、1つの間違いでも減らしたいかどうかなのです。
 
 
最後に、このツールは、つい最近までは、phan/phanではなくetsy/phanという名前でした。
 

 
このetsy(エッチーあるいはイッチーと発音)というのはetsy.comハンドクラフトの人が自分の作品を販売できるサイトです。基本的にはハンドメードのものしか販売ダメという方針で人気があり、2014年末で登録会員(売る方でなく買う方)がなんと5400万人という。

そして凄いことに、この大きなスケールのサイトは、なんとphpで動いています。さらにでかいFacebookもPHPという話だからそう驚く必要はないですけれど。そして、それゆえに、今回のようなphpの開発のためのプログラムツールを公開してくれています。

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

前回に作成したLaravelの日本語のリポジトリ(Laravel 5.4)。今回はその作成の仕方を説明します。ほとんどは、Laravel 5.3のときと同じですが、いくつか違いがあります。

コマンドの実行

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

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

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

--prefer-dist laravel/laravel

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

larajapan

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

5.4.*

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

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

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

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

*5.3と違いgulpfile.jsの代わりにwebpack.mix.jsがあります。asset(sassやjs)のコンパイルは、gulpでなくwebpackを使用したlaravel-mixに変わったからです。

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

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 を参照してください。

今回は、新しく追加されたバリデーションの以下のエントリーもあります。

after_or_equal
before_or_equal

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

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

resources/views
├── auth
│   ├── login.blade.php
│   ├── passwords
│   │   ├── email.blade.php
│   │   └── reset.blade.php
│   └── register.blade.php
├── errors
│   └── 404.blade.php
├── home.blade.php
├── layouts
│   └── app.blade.php
├── vendor
│   ├── mail
│   │   ├── html
│   │   │   ├── button.blade.php
│   │   │   ├── footer.blade.php
│   │   │   ├── header.blade.php
│   │   │   ├── layout.blade.php
│   │   │   ├── message.blade.php
│   │   │   ├── panel.blade.php
│   │   │   ├── promotion
│   │   │   │   └── button.blade.php
│   │   │   ├── promotion.blade.php
│   │   │   ├── subcopy.blade.php
│   │   │   ├── table.blade.php
│   │   │   └── themes
│   │   │       └── default.css
│   │   └── markdown
│   │       ├── button.blade.php
│   │       ├── footer.blade.php
│   │       ├── header.blade.php
│   │       ├── layout.blade.php
│   │       ├── message.blade.php
│   │       ├── panel.blade.php
│   │       ├── promotion
│   │       │   └── button.blade.php
│   │       ├── promotion.blade.php
│   │       ├── subcopy.blade.php
│   │       └── table.blade.php
│   ├── notifications
│   │   └── email.blade.php
│   └── pagination
│       ├── bootstrap-4.blade.php
│       ├── default.blade.php
│       ├── simple-bootstrap-4.blade.php
│       └── simple-default.blade.php
└── welcome.blade.php

5.3と違い、errorsのフォルダーは作成されていませんでしたので追加しました。また、vendor/mailのフォルダーは新規のもので、HTMLだけでなくマークダウンも対応しています。

パスワードリセットで送信される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(config('app.url').route('password.reset', $this->token, false)))
            ->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')->name('password.request');
Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
Route::post('password/reset', 'Auth\ResetPasswordController@reset');                                                                                                                
                                                                                                                                                                                              
Route::get('/home', 'HomeController@index');  

その他

インストールにおいて経験した問題として、

php artisan migrate	

を実行したときに、以下のエラーとなりました。

[Illuminate\Database\QueryException]
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table users add unique users_email_unique(email))

[PDOException]
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes

これは、使用しているデータベースがMariaDBで10.2.2のバージョンより古いか、あるいはMySQLで5.7.7より古いときに起こるエラーです。Laravelは5.4より、絵文字対応のutf8mb4のエンコーディングがデフォルトとなりました。

古いバージョンを使用する場合(大半がそうでは?)は、以下の変更が必要となります。日本語のレポにはこの変更が含まれています。


namespace App\Providers;                                                                                                                                                                                           
                                                                                                                                                                                                                   
use Illuminate\Support\ServiceProvider;                                                                                                                                                                            
use Illuminate\Support\Facades\Schema;                                                                                                                                                                             
                                                                                                                                                                                                                   
class AppServiceProvider extends ServiceProvider                                                                                                                                                                   
{                                                                                                                                                                                                                  
    /**                                                                                                                                                                                                            
     * Bootstrap any application services.                                                                                                                                                                         
     *                                                                                                                                                                                                             
     * @return void                                                                                                                                                                                                
     */                                                                                                                                                                                                            
    public function boot()                                                                                                                                                                                         
    {                                                                                                                                                                                                              
        Schema::defaultStringLength(191);                                                                                                                                                                          
    }                                                                                                                                                                                                              
                                                                                                                                                                                                                   
    /**                                                                                                                                                                                                            
     * Register any application services.                                                                                                                                                                          
     *                                                                                                                                                                                                             
     * @return void                                                                                                                                                                                                
     */                                                                                                                                                                                                            
    public function register()                                                                                                                                                                                     
    {                                                                                                                                                                                                              
        //                                                                                                                                                                                                         
    }                                                                                                                                                                                                              
}   

Laravelの日本語のレポジトリ(Laravel 5.4)

Laravelの日本語のリポジトリをLaravel5.4に更新しました。もうLaravel5.5がリリースされていますが、Laravel5.5と違ってLaravel5.4は、php7でなくphp5.6のバージョンで動作する最後のバージョンなゆえに重要です。前回同様に、以下インストールの手順です。

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

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

このレポジトリには、

  • デフォルトのユーザー認証の機能:会員登録、パスワードリセット、会員ログイン
  • 日本語に翻訳されたデフォルトのテンプレートと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

実行後は、larajapanのディレクトリが作成されますが、お好きな名前に改名してもらってOKです。

その後、

cd larajapan

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

$ chmod -R a+w bootstrap/cache
$ chmod -R a+w storage

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

$ composer install

.envの編集

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

APP_NAME=私のララベル
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で、7.0のバージョンのPHPで、DBにはMysqlあるいはMaria DBを使用して動作確認しています。しかし、皆さんの環境ではいろいろ異なることがあると思います。問題や指摘があれば、ご連絡ください。

Top