You are here
Home > Author: khino

ユーザー認証のテスト(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)

Laravel Shiftでバージョンを更新

Laravelのメジャーのバージョンは、以下のように、ほぼ年に2回のペースでリリースされています。

頻繁に更新されることは、Laravelがいつもアクティブである証拠でとても良いのですが、追い付いていくのが大変なのが現状です。

もちろん便利な機能が登場してきたり、今まで複雑だったことが簡単にできるようなったりとかプラスの面が多いのですが、過去には、バージョン間でフレームワークのディレクトリのレイアウトが変わったり、関数名が変わったりと、ほとんど書き直しを強制される結果となることもあります。

さらに、お客さんのプロジェクトとなれば、バージョンの更新ではUIは何も変わらないので、どうコストを正当化するのかも頭が痛いことです。しかし、バージョンアップしないとLaravelのサポートが期限切れとなるし、便利な新しい機能も使えないし。。。

こういうときに、見つけたのが、バージョン更新サービスの、Laravel Shift

早速、このサービスを利用して、私のLaravel5.3の日本語のリポジトリを5.4に更新してみましょう。

残念ながら英語のサービスですが、すでにgithubbitbucketのリポジトリを使用しているならことは簡単です。

  1. ホームページで「Get Started Now」をクリックすると、以下のようなポップアップが登場します。

  2. Gitのレポジトリにログインします。


     

  3. Laravel Shiftがgithubの私のレポジトリにアクセスすることを許可します。

     
     

  4. Welcome画面が表示されます。そこの右上の「New Shift」をクリックして次の画面へ進みます。

     
     

  5. 更新先のバージョンを選択します。ここでは5.3から5.4なので、「Laravel 5.4」にマウスを持っていき、「Purchase」をクリックします。

     

  6. 更新元のレポジトリとブランチ名を入力します。

  7. 支払い画面です。「Purchase Shift」をクリックします。

  8. クレジットカード情報を入れます。

     

  9. カード決済が完了すると、作業が開始されます。プロジェクトのサイズにもよりますが、完了するとステータスのアイコンが変わり終了です。

     

これで、Laravel Shiftのサイトでの作業は終わりです。領収書のEメールも送信されます。

一方、gifthub.comの方では、プルリクエストが作成され、shift-のプレフィックスのブランチが作成されています。

プルリクエストの内容は以下で閲覧できます。

https://github.com/lotsofbytes/larajapan/pull/2

このブランチでは、コミットは6つ存在し、先のプルリクエストでそれらに関して説明が記されています。この説明は十分理解する必要あり。

作成されたブランチがそのまま使用できるのではなく、以下の作業が必要と書かれています。

  • 作成されたブランチをgitでチェックアウトしてください。
  • プルリクエストのすべてのコメントをレビューしてください。追加の変更を行う必要があるかもしれません。
  • Laravel 5.4の更新のために依存部分を更新してください。
  • composer updateを実行してください。失敗するなら–no-scriptsを付けて再度実行してください
  • 徹底的にテストしてください/li>

以上をこなして実行してみると、動作しましたね!

もちろん今回はとてもシンプルなプロジェクトでの更新なので、調整も最低限度と思いますが、大きなプロジェクトではShiftのブランチで簡単にはいかないと思います。しかし、LaravelのサイトでのUpgrade Guideを読んで、現在のプロジェクトを手動で更新、または新バージョンをまっさらでインストールして、そこにプログラムを1つ1つ移行していくなどの作業よりは、Shiftを使うとスタート時点でかなり違うかなと感じです。今回は11ドルとコストも低いし。

私のクライアントのプロジェクトでは、5.2 ⇒ 5.3 ⇒ 5.4 ⇒ 5.5と3回Larvel Shiftの作業が必要となるわけですが、試してみようと思います。

もっとティンカー(tinker)を使おう!

今年最後の投稿です。まだまだ投稿のネタはたくさんあります。来年も楽しみに!

tinkerに関しては、以前に紹介していますが、私には現在もなくてはならないコマンドラインツール。

このツール、実は機能が豊富にあるということ最近気づきました。ますます好きになりました。

私がtinkerを利用する一番の理由は、Laravelの知らない、あるいは正確なシンタックスを覚えていないメソッドをテストしたいときです。dd()入れてブラウザで実行して結果をチェックするのは面倒、unit testを書くには大袈裟なとき、tinkerが一番です。

例えば、以下pluckの関数のパラメータ、どっちが配列のインデックスになるのだっけ? all()のコールは必要だっけ? ・・というときに。

$ php artisan tinker
Psy Shell v0.8.11 (PHP 7.0.23 — cli) by Justin Hileman
>>> use App\User;
>>> User::all()->pluck('id', 'name');
=> Illuminate\Support\Collection {#705
     all: [
       "kenji" => 1,
       "test" => 2,
     ],
   }
>>> User::all()->pluck('name', 'id');
=> Illuminate\Support\Collection {#703
     all: [
       1 => "kenji",
       2 => "test",
     ],
   }
>>> User::pluck('name', 'id');
=> Illuminate\Support\Collection {#705
     all: [
       1 => "kenji",
       2 => "test",
     ],
   }
>>> 

と、いちいちマニュアルをチェックすることなしに、まずは実行して試す。

実行した結果だけでなく、プログラム自体の表示も可能です。

>>> show User
  >  9| class User extends Authenticatable
    10| {
    11|     use Notifiable;
    12|         use CanResetPassword;
    13| 
    14|     /**
    15|      * The attributes that are mass assignable.
    16|      *
    17|      * @var array
    18|      */
    19|     protected $fillable = [
    20|         'name', 'email', 'password',
    21|     ];
    22| 
    23|     /**
    24|      * The attributes that should be hidden for arrays.
    25|      *
    26|      * @var array
    27|      */
    28|     protected $hidden = [
    29|         'password', 'remember_token',
    30|     ];
    31| }

クラス全体だけでなく、指定のメソッドのコード部分も。実際は以下の画像のように、綺麗な色付きで表示されるので読みやすいです。

bashのhistoryのように、今まで実行した履歴も閲覧可能です。

>>> hist
0: use App\User;
1: User::all()->pluck('id', 'name');
2: User::all()->pluck('name', 'id');
3: User::pluck('name', 'id');

histの結果は、とても長くなるので、一部だけを閲覧することも可能です。

>>> hist --show 1..3
0: use App\User;
1: User::all()->pluck('id', 'name');
2: User::all()->pluck('name', 'id');
3: User::pluck('name', 'id');

さらに、履歴を--replayで再実行可能です。

>>> hist --show 2 --replay
Replaying 1 line of history
-->   User::all()->pluck('name', 'id');
=> Illuminate\Support\Collection {#708
     all: [
       1 => "kenji",
       2 => "test",
     ],
   }

histには、他にも–head、–tail、–grepのオプションがあります。

さらに、tinkerは、php関数のマニュアルも出力することも可能です。

まず、設定として、マニュアルをダウンロードします。ファイルは11MBのサイズです。

mkdir ~/.local/share/psysh
cd ~/.local/share/psysh
wget http://psysh.org/manual/ja/php_manual.sqlite

関数のマニュアルを表示するには、docを利用します。例えば、

>>> doc array_shift

とタイプすれば、

と日本語で表示してくれます。また、Laravelの関数では、クラスのパス名を指定すれば、

>>> doc \Illuminate\Http\Request::input
public function input($key = null, $default = null)

Description:
  Retrieve an input item from the request.

Param:
  string             $key     
  string|array|null  $default 

Return:
  string|array 

と説明が見れます。

最後に、Laravelのtinkerは、以下のPsySHのラッパーです。
https://github.com/bobthecow/psysh
こんな便利なツールを開発してくれて、ありがとう!

PSR2変換ツール

前回で説明したPHPのコーディングスタイルの基準PSR2、これをすべて覚えて手動で実行しようというのは到底無理なこと、そこで登場するのが、php-cs-fixer、自動でそれを行ってくれるツールです。

インストールはいたって簡単。

今回は、phanphpunitとは違い、プロジェクトが使用しているLaravelやPHPのバージョンに合わせる必要はないので、プロジェクトごとではなくグローバルに、つまり自分のホームディレクトリでのインストールとします。

$ composer global require friendsofphp/php-cs-fixer

これを実行すると、いくつかの必要なパッケージがインストールされます。

そして、~/.composer/vendor/bin/php-cs-fixerにファイル(実際はシムリンク)が作成されます。

どこでもこれが実行できるように、.bahsrcに以下の設定を入れておくことを勧めます。

...
export PATH="$PATH:$HOME/.composer/vendor/bin"
...

実行も簡単。例えば、変換対象のファイルを、test.phpとすると、

$ php-cs-fixer fix test.php
Loaded config default.
Using cache file ".php_cs.cache".
   1) test.php

Fixed all files in 0.193 seconds, 10.000 MB memory used

test.phpが、デフォルトのルールで、PSR1とPSR2に基づいて変換されます。

以下は、前回の例のファイルを崩したものです。

<?php

namespace Vendor\Package;
use FooInterface;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;
class Foo extends Bar implements FooInterface {

    protected $a, $b, $c;

    public function sampleMethod( $a,$b = null )
    {
        if ( $a === $b )
		{
            bar();
        }
		elseif ($a > $b)
		{
            $foo->bar($arg1);
        }
		else
		{
            BazClass::bar($arg2,$arg3);
        }
    }
}
?>

これをphp-cs-fixerで修正すると、前回のように

<?php

namespace Vendor\Package;

use FooInterface;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class Foo extends Bar implements FooInterface
{
    protected $a;
    protected $b;
    protected $c;

    public function sampleMethod($a, $b = null)
    {
        if ($a === $b) {
            bar();
        } elseif ($a > $b) {
            $foo->bar($arg1);
        } else {
            BazClass::bar($arg2, $arg3);
        }
    }
}

となります。

php-cs-fixerの実行では、.php_cs.cacheのファイルが作成されます。この中には、使用されたJSON設定が収められています。バージョン管理には要らないファイルなので、.gitignoreなどに含むようにしてください。

さて、php-cs-fixerを使用してLaravelのプロジェクト全体をPSR2準拠に変換します。Laravelのプロジェクトにはいろいろなディレクトリがあるために、変換必要なファイルだけを指定する必要があります。そのためには以下のような設定ファイル.php_csが必要です。

<?php

$finder = PhpCsFixer\Finder::create()
    ->exclude('bootstrap/cache')
    ->exclude('storage')
    ->exclude('vendor')
    ->in(__DIR__)
    ->name('*.php')
    ->notName('*.blade.php')
    ->ignoreDotFiles(true)
    ->ignoreVCS(true)
;

return PhpCsFixer\Config::create()
    ->setRules(array(
        '@PSR2' => true,
    ))
    ->setFinder($finder)
;

設定ファイル自体がphpのプログラムです。そこで見られるように、bootstrap/cache, storage, vendorのディレクトリ以外のphpファイルはすべてPSR2準拠に変換してくれます。しかし、bladeのファイルはphpファイルでないゆえに変換しません。

このファイルを、Laravelプロジェクトのルートディレクトリに置きます、そして変換は、そこで以下の実行だけです。

$ php-cs-fixer fix

変換前に、gitのブランチをこのために作成しておいてから実行してください。例えば、新ブランチをpsr2と命名して、変換後に差分をチェックしたいなら、

$ git diff master -w

と実行すれば、タブからスペースへの変更以外の変更をチェックできます。

最後に、このツールの作者は、有名なphpフレームワーク、symfonyのフレームワークの作者でもあります。Laravelのライブラリの一部はsymfonyのを利用しています。

PSR2

PHP5.6からPHP7.0へ移行したところで、クライアントのプロジェクトもLaravel5.2からLaravel5.5へのアップグレードが必要と思う最近。

というのは、Laravel5.5は、LTS(Long Term Support)のバージョンであり、長期サポートということで2年間続くからです。Laravelは、現在ほぼ1年に2回にメジャーなバージョンアップをします。あるバージョンでは前バージョンと異なる部分が大きいこともあり、現実的には変更や検証の作業が大変で毎回毎回はとても。ということで、Laravel5.5に移行したら当分はバージョンアップはマイナーだけで済むのではないかという考えです。

しかし、この検討中に、私らが開発で使用しているコーディングスタイルがLaravelのそれとは違うことに気づきました。一番大きい違いは、私らはインデントにタブ文字を使用していますが、現在のLaravelは、タブの代わりに空白文字(スペース)の使用です。なんと、Laravel5.1からすでにそうなっていたらしい。

調べてみると、Laravelは、PSR2というコーディングスタイルを現在採用しています。PSRは、PHP Standard Recommendationの略で、PHPのお勧めの標準という意味。標準にもいろいろなレベルがあって、コーディングスタイルのスタンダードだけでなく、ローディング、ログ、キャッシュなどいろいろなスタンダードがあります。

参照:http://www.php-fig.org/psr

さて、Laravelが使用しているPSR2は、

http://www.php-fig.org/psr/psr-2/

で詳しく説明されていますが、

要は、複数のプログラマが同じプロジェクトで開発を行うときに、コードの読みやすさのためのコーディングの規則ということです。

取り決めの例としては、

  • インデントには、タブではなく4文字のスペース文字を使用する。
  • namespaceの宣言の後には空行を1行入れる。useの宣言のブロックの後にも空行1行を入れる。
  • クラスの定義の括弧は、
    class Foo {

    }
    でなく、

    Class Foo
    {

    }
    である。

  • 条件文(if, switch)やループ文(for, foreach)の括弧は、
    if (true)
    {

    }
    else
    {

    }
    でなく、

    if (true) {

    } else {

    }
    である。

  • PHPの定数、true, false, nullは必ず小文字

などなど、変数名に関しての規則も含み結構たくさんあります。

PSR2準拠のサンプルとして以下が表示されています。

<?php
namespace Vendor\Package;

use FooInterface;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class Foo extends Bar implements FooInterface
{
    public function sampleMethod($a, $b = null)
    {
        if ($a === $b) {
            bar();
        } elseif ($a > $b) {
            $foo->bar($arg1);
        } else {
            BazClass::bar($arg2, $arg3);
        }
    }

    final public static function bar()
    {
        // method body
    }
}

私は、昔からタブ派でなので、ここに来てスペース(空白)派になることには抵抗あります。また、bitbucketやgithubのサイトでコードが表示されるときにタブだと上下で揃わなくなったりとマイナスな面にも気づいていました。しかし、sublimeのようなエディターは特別に指定しなくても既存のインデントに合わせてくれるし、コーディングにおいてはタブからスペースへの変換はそう問題ないだろうと、PSRの標準に移行することに決めました。

決めたところで、問題は、現行のプログラムをどうすべてPSR2のコーディングスタイルに変換するか。次回の課題です。

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行になったけれど、コントローラはちょっとわかりにくいかな、とも思いますね。

turbolinksで画面の表示をスピードアップ (2) <script>

前回に続いて、turbolinksの話。

今回は、turbolinksをもとで、ページに含まれる<script>がどう実行されるか説明します。

前回と同様に以下の2つのファイルを用意します。

<html lang="ja">                                                                                                                                                                                                   
  <head>                                                                                                                                                                                                           
    <meta charset="utf-8">                                                                                                                                                                                         
    <title>Turbolinks</title>                                                                                                                                                                                      
    <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.0.3/turbolinks.js"></script>                                                                                                                  
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>                                                                                                                      
    <script>                                                                                                                                                                                                       
    $(document).on('turbolinks:load', function() {                                                                                                                                                                 
      $("#button").click(function(){                                                                                                                                                                               
        $("#box").animate({left: '250px'});                                                                                                                                                                        
      });                                                                                                                                                                                                          
    });                                                                                                                                                                                                            
    </script>                                                                                                                                                                                                      
  </head>                                                                                                                                                                                                          
  <body>                                                                                                                                                                                                           
    <h1>Turbolinks</h1>                                                                                                                                                                                            
    <ul>                                                                                                                                                                                                           
      <li><a href="page1.html">ページ 1</a></li>                                                                                                                                                                   
    </ul>                                                                                                                                                                                                          
                                                                                                                                                                                                                   
    <button id="button">箱を動かす</button>                                                                                                                                                                        
    <div id="box" style="background:#98bf21;height:100px;width:100px;position:absolute;"></div>                                                                                                                    
  </body>                                                                                                                                                                                                          
</html>                                                                                                                                                                                                            

CDNでturbolinksとjqueryを読み込んでいるところ注意してください。

html lang="ja">                                                                                                                                                                                                   
  <head>                                                                                                                                                                                                           
    <meta charset="utf-8">                                                                                                                                                                                         
    <title>ページ 1</title>                                                                                                                                                                                        
    <script>                                                                                                                                                                                                       
    $(document).ready(function() {                                                                                                                                                                                 
      $("#button2").click(function(){                                                                                                                                                                              
        $("#box").animate({left: '10px'});                                                                                                                                                                         
      });                                                                                                                                                                                                          
    });                                                                                                                                                                                                            
    </script>                                                                                                                                                                                                      
                                                                                                                                                                                                                   
  </head>                                                                                                                                                                                                          
  <body>                                                                                                                                                                                                           
    <h1>ページ 1</h1>                                                                                                                                                                                              
    <p><a href="index.html">戻る</a></p>                                                                                                                                                                           
                                                                                                                                                                                                                   
    <button id="button">箱を動かす</button> <button id="button2">箱を戻す</button>                                                                                                                                 
    <div id="box" style="background:#98bf21;height:100px;width:100px;position:absolute;"></div>                                                                                                                    
  </body>                                                                                                                                                                                                          
</html>  

最初のページで、「箱を動かす」のボタンをクリックすると、箱が右へ移動します。

「ページ1」をクリックして、ページ1に行くと今度も同じく「箱を動かす」のボタンをクリックすると、箱が右へ移動します。このjqueryのコードがページ1(page1.html)になくても。

以下にアップしたので、そこで体験できます。

https://larajapan.lotsofbytes.com/turbolinks/2/index.html

ここで大事なのは、通常なら、

$(document).ready(function() {
..
});

とするところ、

$(document).on('turbolinks:load', function() { 
..
});

とします。window.onloadやjQueryのreadyは、index.htmlがロードしたときにのみ実行されますが、ページ1のリンクのクリックでページ1をajaxで読み込むときには実行されません。ゆえに、ページ1では「箱を動かす」ボタンをクリックしても箱は移動しません。

一方、turbolinks:loadを使用すると、リンク先のページにjavascriptのコードがなくとも、index.htmlのコードを実行します。

さらに、turbolinksは、リンク先のファイル(ここではpage1.html)に、現在のファイル(ここではindex.html)に存在しない、<script>のコードがあるなら、それもロードしてくれます。それゆえに、「箱を動かす」のボタンをクリックすると、箱が右へ移動するし、「箱を戻す」をクリックすると、箱は左に移動します。

最後に、今回の例はturbolinksの機能の説明のために、このようなjavascriptのコードとなること理解してください。実際にはpage1.htmlがブックマークされていて、次回のアクセスがそこから始まるなら現在のpage1.htmlのコードでは困ります。つまり、page1.html自体が更新されても同様な動作となる必要があります。

turbolinksで画面の表示をスピードアップ (1)

Laravelのフレームワークのおかげで、自分で作成した古いフレームワークもどきや、CodeIgniterの「もうサポートしません」(注1)フレームワークを脱出できて、以前よりしっかりした開発の領域に入ってきたと感じているこの頃。そして、ファサード、ネームスペース、クロージャ、トレイトなどを活用して、とてもモダン。しかし、最近人気が出てきたJavascriptのフレームワーク、Angular, React, Vuejsを使用したシングルページアプリ(SPA)がとても気になります。

Laravelは基本的にサーバーサイドで、Angularなどはクライアントサイドなので、共存は可能と言えばそうなのだけれど、せっかく時間かけてマスターしたLaravelのプログラムを書き直すとか、今度はどのJavascriptフレームワークをマスターすればよいのとか、ajaxばかりでプログラム複雑になるのでは、とか、過去には、CodeIgniterで苦い目にあったし、最近やっと大きいプロジェクトを3年かけてLaravelに書き直したばかりなので、ちょっとポジティブにはなれません。

そんな中で、知り合ったのが題名のturbolinks。このテクノロジーの謳い文句は、

Javascriptのフレームワークを使用して複雑にすることなしに、シングルページアプリ(SPA)のパフォーマンスが得られる!

なんか「何もしなくても痩せる!」というダイエットサプリメントのような感じですが、今の私にピッタリ。とりあえず、紹介しましょう。

まず、メカニズムですが、そう難しくはありません。ページ内に同じサイト内のリンク、つまり<a href= ..>があると、turbolinksはajaxでそのページを取ってきてその中の<body>のデータを現在のと取り換えます。<head>は同じなので、結果的には、

のように、タブのところに表示される「ページロード中」のぐるぐるがなくなります。さらに、<title>やURLも正しく変わります。

インストールはいたって簡単。

以下の2つのファイルを用意してください。最初のファイルだけに、turbolinks.jsが入っていることに注意してください。

<html lang="ja">                                                                                                                                                                                                   
  <head>                                                                                                                                                                                                           
    <meta charset="utf-8">                                                                                                                                                                                         
    <title>Turbolinks</title>                                                                                                                                                                                      
    <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.0.3/turbolinks.js"></script>                                                                                                                  
  </head>                                                                                                                                                                                                          
  <body>                                                                                                                                                                                                           
    <h1>Turbolinks</h1>                                                                                                                                                                                            
    <ul>                                                                                                                                                                                                           
      <li><a href="page1.html">ページ 1</a></li>                                                                                                                                                                       
    </ul>                                                                                                                                                                                                          
  </body>                                                                                                                                                                                                          
</html>                                                                                                                                                                                                            
<html lang="ja">                                                                                                                                                                                                   
  <head>                                                                                                                                                                                                           
    <meta charset="utf-8">                                                                                                                                                                                         
    <title>ページ 1</title>                                                                                                                                                                                        
  </head>                                                                                                                                                                                                          
  <body>                                                                                                                                                                                                           
    <h1>ページ 1</h1>                                                                                                                                                                                              
    <p><a href="index.html">戻る</a></p>                                                                                                                                                                               
  </body>                                                                                                                                                                                                          
</html>                                                                                                                                                                                                            

これらのファイルをサーバーにアップして、「ページ1」や「戻る」リンクをクリックしてください。タブの部分において、もうロード中のぐるぐるは見えませんね。「すっ」とページが変わる感じです。また、URLやタイトルも正しく更新されています。

しかし、ページ1においてブラウザの更新ボタンを押して画面を更新した後に、「戻る」のリンクをクリックするときは、ロード中のぐるぐるが見えます。これはpage1.htmlには、turbolinksがないからです。

以下のデモでも体験できます。

https://larajapan.lotsofbytes.com/turbolinks/index.html

ブラウザのインスペクトツールでも、ajaxが使用されていることがわかります。


index.htmlのturbolinksをコールしているscriptの行を削除して違いも見てください。

CDNではなく、turbolinksのファイルが欲しいなら、

$ wget https://github.com/turbolinks/turbolinks/archive/master.zip

で取得可能です。unzipしてから、dist/turbolinks.jsのファイルが取り出せます。

知っておくこととして、

ページの特定のリンクにおいてturbolinksを無効にしたいなら、data-turbolinks="false"を入れてください。

..
  <body>                                                                                                                                                                                                           
    <h1>Turbolinks</h1>                                                                                                                                                                                            
    <ul>                                                                                                                                                                                                           
      <li><a href="page1.html" data-turbolinks="false">ページ 1</li>                                                                                                                                                                       
    </ul>                                                                                                                                                                                                          
  </body>   
..

クリック先のページを読み直したいときとかに必要です。私の経験では、ページを変更してセッションに値を入れるときには、これが必要でした。期待した動作にならないとか問題があるときに試してみること必要です。それから、hrefのリンクのクリックはGETのアクションとなりますが、POSTのアクションには、turbolinksは関与しないので、フォームの投稿はスピードアップはしません。通常の画面の更新となります。

実際のプロジェクトで使用となると、他にも知る必要なことがいくつか(たくさんではない)あります。例えば、Google Analyticsは正しく反映されるのか、とか、Wordpressでも使用できるかとか。知識を整理して、将来により情報を共有します。

注1

CodeIgniterは私が初めて使用したPHP言語のフレームワークです。どこかのサイトで紹介されていて簡単そうなので使い始めました。日本語のデータ処理に問題があったので使える部分はほとんどコントローラの部分だけでしたが、いくつかのお客さんのプロジェクトの開発に使いました。当時は一番人気のフレームワークでしたが、その絶頂期の2013年にCodeIgniterを開発した会社がリソースがないことを理由に開発を辞める宣言をしました。そこで開発が止まり1年後にはカナダの専門学校がメンテナーとなり現在はそこでオープンソースとして管理されています。

ということで、CodeIgniterはまったく消え去ったわけではありませんが、Laravelを使い始めて振り返ると、CodeIgniter自体のフレームワークはその時点ですでに古く、現在のPHP言語のネームスペースに基づくcomposerのパッケージを基本として作成されたものでもなく、より複雑になるウェブアプリの開発にはそのままでは不可能です。

Top