カテゴリーアーカイブ Laravel

著者:杉浦

【Laravel】Laravel付きのPHPオンラインテストページImplode.io

Implode.io
 Implode.ioは簡易なPHPをテストする環境を持ったwebサービスです。Implode.ioにはコードを簡単に書き、共有できるような環境が整っています。Implode.ioの特長はフレームワークLaravelを使えることです。図の様にフレームワークを指定することで組み込まれているクラス、関数などが使えます。

 図のNewからセットを選ぶとモデルやテンプレートを試すこともできます。

 下図中の赤丸部分の画面上部にある検索ボックスから今まで大勢に作られてきたコードを探せます。

 これでLaravelのみに関わるテストでも、開発中のコードを触ったり余計な計算資源を食ったりせずに済みます。

  • この記事いいね! (0)
著者:杉浦

【Laravel】Bladeの制御構文の探し方

 LaravelのBladeドキュメントは丁寧でわかりやすいですが、わかりやすさのために切り捨てられている部分もあります。例えばBladeの制御構文がいくつか抜け落ちています。この記事では探し方を紹介します。探し方が分かればバージョンが変わっても平気です。
Bladeテンプレート 5.7 Laravel
 探すことをプログラムに任せる直接的な解決策があります。そういうプラグインのあるエディタを使用してそのプラグインを入れましょう。PhpStormならばBlade Supportがあり次の画像の様に、色付けと予測を行ってくれます。
 Blade Support – Plugins | JetBrains

 Bladeの元を探そうとしてもクラスやメソッドの呼び出しを辿って見つけることは難しいです。というのもLaravelはそのソースコード内で特定の名前のクラス、メソッドをまとめて呼び出したり、その場でPHPの構文を作ったりすることで根本のコードを小さく保っているためです。例えば次の様なコードが現れます。

    /**
     * Register the engine resolver instance.
     *
     * @return void
     */
    public function registerEngineResolver()
    {
        $this->app->singleton('view.engine.resolver', function () {
            $resolver = new EngineResolver;

            // Next, we will register the various view engines with the resolver so that the
            // environment will resolve the engines needed for various views based on the
            // extension of view file. We call a method for each of the view's engines.
            foreach (['file', 'php', 'blade'] as $engine) {
                $this->{'register'.ucfirst($engine).'Engine'}($resolver);
            }

            return $resolver;
        });
    }

 このためLaravelの中を探す時はそれっぽいクラス名、メソッド名を用いた正規表現によって隠された呼び出し元を探す方法が有効です。肝心のBlade構文を字句解析(大雑把いえば文字列を意味のある字句に区切る処理)している部分は次です。この部分はViewのProvider周りから呼び出されています。

// vendor/laravel/framework/src/Illuminate/View/Compilers/BladeCompiler.phpの内部
    /**
     * All of the available compiler functions.
     *
     * @var array
     */
    protected $compilers = [
        'Comments',
        'Extensions',
        'Statements',
        'Echos',
    ];
    
    
    /** 省略 **/
    
    /**
     * Parse the tokens from the template.
     *
     * @param  array  $token
     * @return string
     */
    protected function parseToken($token)
    {
        [$id, $content] = $token;

        if ($id == T_INLINE_HTML) {
/** ここから */
            foreach ($this->compilers as $type) {
                $content = $this->{"compile{$type}"}($content);
            }
/** ここまでが重要 */
        }

        return $content;
    }

 $this->{“compile{$type}”}($content);を満たすメソッドで字句解析をしています。これを満たすメソッドで@以下を解析しているのが次のcompileStatementsです。

    /**
     * Compile Blade statements that start with "@".
     *
     * @param  string  $value
     * @return string
     */
    protected function compileStatements($value)
    {
        return preg_replace_callback(
            '/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x', function ($match) {
                return $this->compileStatement($match);
            }, $value
        );
    }

    /**
     * Compile a single Blade @ statement.
     *
     * @param  array  $match
     * @return string
     */
    protected function compileStatement($match)
    {
        if (Str::contains($match[1], '@')) {
            $match[0] = isset($match[3]) ? $match[1].$match[3] : $match[1];
        } elseif (isset($this->customDirectives[$match[1]])) {
            $match[0] = $this->callCustomDirective($match[1], Arr::get($match, 3));
/** ここから*/
        } elseif (method_exists($this, $method = 'compile'.ucfirst($match[1]))) {
            $match[0] = $this->$method(Arr::get($match, 3));
        }
/** ここまで重要 ucfirstは最初の一文字が大文字なら小文字にする関数*/
        return isset($match[3]) ? $match[0] : $match[0].$match[2];
    }

 到達しました。BladeCompilerのインスタンスに引っ付いているcompileHogeなメソッドがあれば@hogeが動作するということです。後はBladeCompilerに生えているメソッド一覧を見て終わりです。


 多いです。使いこなせている人はいるのでしょうか。

  • この記事いいね! (0)
著者:杉浦

【Laravel】Laravel5.6以降のヘルパ関数abortは結構自由

 LaravelにはHTTP例外を投げるヘルパ関数abortが用意されています。

abort関数は、例外ハンドラによりレンダーされるであろう、HTTP例外を投げます。

abort(403);

例外のレスポンステキストと、カスタムヘッダを指定することもできます。

abort(403, 'Unauthorized.', $headers);

ヘルパ 5.7 Laravel#method-abort

 abort(403);と一言記述するのみで権限エラーである403のエラーページにクライアントを飛ばせます。この使い方が最も主で伝統的な使い方です。しかしもう少し複雑なHTTPレスポンスを返したい場合もあります。
 使い方が書かれている公式の翻訳ドキュメントにおいて、abort関数はステータスコードとメッセージとヘッダを指定するだけの簡易な関数に見えますが、実際はもっと自由です。ヘルパ関数abortは/vendor/laravel/framework/src/Illuminate/Foundation/helpers.phpにあるabort関数を呼び出しています。その呼び出される部分が次です。

// Laravel5.7版のコード
if (! function_exists('abort')) {
    /**
     * Throw an HttpException with the given data.
     *
     * @param  \Symfony\Component\HttpFoundation\Response|\Illuminate\Contracts\Support\Responsable|int     $code
     * @param  string  $message
     * @param  array   $headers
     * @return void
     *
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
     */
    function abort($code, $message = '', array $headers = [])
    {
        if ($code instanceof Response) {
            throw new HttpResponseException($code);
        } elseif ($code instanceof Responsable) {
            throw new HttpResponseException($code->toResponse(request()));
        }

        app()->abort($code, $message, $headers);
    }
}

 注目する部分はPHPDocの$codeで指定されている型と関数が始まった直後のif文部分です。これは$codeにはステータスコードになるintのみでなく\Symfony\Component\HttpFoundation\Responseを継承するか\Illuminate\Contracts\Support\Responsableを実装したクラスのインスタンスを引数にすれば、HTTPレスポンスとして適した形で例外の結果をクライアントに返します。
 PHPのinstanceofは完全にイコールのインスタンスのみでなく継承元、実装元の場合でも真になります。
PHP: 型演算子 – Manual

 このinstanceofの性質からabort関数は例えばこのように使えます。

abort(resnponse()->json("I'm a teapot", 418));

 ExceptionクラスやHandlerクラスを気にすることなく例外の結果をJSON型のHTTPレスポンスとしてクライアントに投げられます。
 LaravelにはインタフェースIlluminate\Contracts\Support\Jsonableがあり、これを実装したクラスはJSON化が担保され、至る所でJSONとして扱えます。JsonableはModelやCollectionを始めとして多くのもので実装されています。このため次の様なこともできます。

// User.php
class User extends Model implements Responsable
{
    public function isOne()
    {
        return $this->getKey() === 1;
    }
    public function toResponse($request)
    {
        return response()->json($this);
    }
}


// 適当な別ファイル
$a = User::inRandomOrder();
$a->isOne() ?: abort($a);

 Modelの場合、プロパティがJSON化されます。abortに渡されたUserインスタンスのtoResponseメソッドが動作し、response()->json($this)によりJSON化されたUserインスタンスがHTTPレスポンスとして返されます。
 この変更はLaravel5.6時に追加されました。
allow giving a response to abort · laravel/framework@4e29889

  • この記事いいね! (0)
著者:杉浦

PHPStormでartisanコマンドの予測入力をする

 PHPフレームワークLaravelにはartisanコマンドがあります。artisanはアプリケーション開発全体で役に立つ、数多くのコマンドを提供しています。例えば必須ないしあった方がよいコードを持ったモデル、テスト、ミドルウェアの作成、データベースのまとまった操作、Laravelが今持つことになるルーティングリストの表示です。もちろん他にも様々なものがあります。
 artisanの一覧を見ることはphp artisan listからできますが、いちいち確認するのは面倒です。PHPStormのコマンドラインツールにはSymfonyのコマンドライン用クラス\Symfony\Component\Console\Command\Commandを元に作成されたコマンドのコマンドライン入力とサポートするプラグインCommand Line Tool Supportがあります。LaravelのartisanコマンドはSymfonyのCommandクラスから継承されて作られています。次の図はCommandクラスの継承先のリストの一部です。各aritsanコマンドの名が冠されたクラスが並んでいます。
 
 プラグインは設定->プラグインのマーケットプレイスから検索できます。インストールできたなら設定->ツール->コマンド・ライン・ツールのサポートを選び、追加からTool Based on Symfony Consoleを選択、php実行ファイルとartisanファイルのパスを設定して完成です。


 これでartisanコマンドの予測ができるようになりました。artisanを介することでnamespaceや必須メソッド、継承元などをいちいち記述せずとも各基本クラスのひな型を作ることができ、データベースの操作もまとめてでき、開発がはかどります。

  • この記事いいね! (0)
著者:杉浦

ダミー画像表示サイトlorempixelの紹介

lorempixel – placeholder images for every case
 lorempixelはurl指定するとダミーの画像を表示してくれるサイトです。urlの指定の仕方は次の画像の通り。urlを指定すればするほどオンリーワンな画像が表示されます。例はhttpプロトコルですがhttpsでもアクセスできます。

 例えば、https://lorempixel.com/400/200/animals/5/msgならば次の様に、横幅400px、縦幅200px、カテゴリanimalsの5番目の画像の左下にmsgの文字を入れた画像が返ってきます。

 webページを制作した時の画像確認においてカテゴリ分けが特に便利で仮のそれらしい画像をいくつも作る手間が省かれます。
 PHPのダミーデータ作成ライブラリfzaninotto/Faker: Faker is a PHP library that generates fake data for youにはlorempixelが組み込まれています

// Image generation provided by LoremPixel (http://lorempixel.com/)
imageUrl($width = 640, $height = 480) // 'http://lorempixel.com/640/480/'
imageUrl($width, $height, 'cats')     // 'http://lorempixel.com/800/600/cats/'
imageUrl($width, $height, 'cats', true, 'Faker') // 'http://lorempixel.com/800/400/cats/Faker'
imageUrl($width, $height, 'cats', true, 'Faker', true) // 'http://lorempixel.com/grey/800/400/cats/Faker/' Monochrome image

 Fakerが組み込まれているLaravelならばダミーデータ作成機能ファクトリに次の様にランダムURL生成を定義できます。

use Faker\Generator as Faker;

// あらかじめ使用する予定のカテゴリを定義
$category = ['abstract', 'animals', 'business', 'cats', 'city', 'food', 'nightlife', 'fashion', 'people', 'nature', 'sports', 'technics', 'transport'];
/* @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\Models\Post::class, function (Faker $faker) use ($category) {
    $word = $faker->word; // 画像に記述する文字列をランダム生成
    return [
        'img_url' =>
            $faker->imageUrl(640, 480, $faker->randomElement($category), false) // https://lorempixel.com/640/480/{$categoryのどれか}/
            . $faker->numberBetween(1, 10) . '/' . $word, // 末尾に、1~10のいずれかと書き込む$wordを追加
    ];
});
  • この記事いいね! (0)
著者:杉浦

duskによるテストの開始時、終了時にLaravel内で定義してある関数を用いる

 Laravel内で共通のことですがクラス中で用いる初期化関数__construct()が上手く働かないことがあります。これに陥る原因としてLaravel内でクラスを呼び出した時、まだインスタンス化されていないクラスに関わる関数を呼び出すというものがあります。Laravelの組み込み関数や定義したEloquentによる関数、プロパティは便利ですが__construct()内では使えません。duskも同様です。
 Laravel内で用いられているクラスはこれを解決するために、前処理が終わった後、本処理が始まる前に呼び出される関数をboot,setupなどといった名前で定義しています。__construct()の次にboot()が走り、boot()の次にコントローラなどの本処理が走るといった具合です。duskではsetUp()という名前で定義されています。このsetUp()関数内でテストに使うデータを定義すると幾分かすんなりとテストを記述できます。例えば次です。

    /**
     * @inheritDoc
     */
    protected function setUp()
    {
        parent::setUp();
        // テストに用いる編集対象の管理者を指定、インスタンス化
        $admin_key = static::$admin_key ?? Admins::inRandomOrder()->first()->getKey();
        $this->admin = Admins::findOrFail($admin_key);
    }

 あらかじめ特定の管理者を対象にするならばその管理者を、そうでないならばランダムに選んだ管理者を対象にして様々なテストに用います。
 開始に対応するように終了時にフックされる関数もあります。dusk内ではtearDownとされています。あるテストが終わるたびにtearDown関数が呼ばれているため、ブラウザテストの完全終了を待たずともあるテストケースの失敗とそれを解決するための情報を得る記述をすることが出来ます。tests/DuskTestCase.phpを継承することで各テストケースがブラウザ上で走るため、tests/DuskTestCase内に次の様に記述するのが良いでしょう。DuskTestCaseより深く潜るとvendorの領域に入ってしまいます。

    /**
     * @inheritDoc
     */
    protected function tearDown()
    {
        parent::tearDown();
        if ($this->hasFailed()) {
            dump('failed in ' . static::class . '::' . $this->getName() . '. test case instance is');
            $test_class_data = (new \Illuminate\Support\Collection($this))->filter(function ($value, $key) {
                return !strpos($key, 'PHPUnit');// 2、3000行程になるPHPUnit自体の情報を削除
            });
            dump($test_class_data);
        }
    }
  • この記事いいね! (0)
著者:杉浦

dusk中でwebdriverを呼び出し多彩な操作をする

 Laravelのブラウザテストツールduskは以下の様なコードでdriverメソッドからwebdriverを呼び出すことによってブラウザを動かします。このコードはLaravelのデフォルトに近い形でGoogle Chromeを呼び出しています。

    /**
     * Create the RemoteWebDriver instance.
     * @return \Facebook\WebDriver\Remote\RemoteWebDriver
     */
    protected function driver()
    {
        $options = (new ChromeOptions())->addArguments([
            '--disable-gpu',
            '--headless',
        ]);

        return RemoteWebDriver::create(
            'http://localhost:9515',
            DesiredCapabilities::chrome()->setCapability(ChromeOptions::CAPABILITY, $options),
            300 * 1000,
            300 * 1000
        );
    }

 通常duskはLaravel\Dusk\Browserクラスからbrowserを操作します。Browserクラスは堅牢であり、クラス中に生やされたプロパティによるブラウザの状態の把握は便利です。Browserクラスは便利ですが、操作に難があります。本当に手打ちの操作を再現するのみならば問題がない程度には操作メソッドが備わってますが、これは手打ちの再現であり手打ちでは膨大な時間がかかる入力をテストする際に面倒が起きます。typeメソッドによる長文入力はこれに見事に引っかかります。
 BrowserクラスはRemoteWebDriverクラスをラッピングしてブラウザ操作を行っています。Browserクラスの下に入っているRemoteWebDriverクラスを直接操作することでより多彩な操作ができます。呼び出し方はいたって簡単次のコードの通り

/** @var Browser $browser */
$web_driver = $browser->driver;

 この$web_driver変数からRemoteWebDriverクラスのメソッドを呼びだすことによってさまざまなことが出来ます。できることの一覧はFacebook\WebDriver\Remote\RemoteWebDriver | php-webdriver APIやvendor/facebook/webdriver/lib/Remote/RemoteWebDriver.phpの中身にあります
 特に便利なメソッドはexecuteScriptとtakeScreenshotです。executeScriptは次の様にjavascriptを実行できます。長文入力の様な手打ちでやっていられない、手打ちの必要が無い操作はこれで肩代わりするによって実行時間の短縮が図れます。

$browser->driver->executeScript('document.querySelector("[name=\"' . $field . '\"]").value = "' . $value . '";');

 takeScreenshotはその名の通りスクリーンショットを取得するメソッドです。Laravelのヘルパ関数によるpath指定を行うことでLaravelプロジェクト内部に任意のタイミングで取得したスクリーンショットを保存できます。

  • この記事いいね! (0)
著者:杉浦

LaravelのCRUDテストとfactory

 CRUDの内updateとdeleteは既存データを加工します。この機能をテストするならば、必然データを壊します。この壊すデータの範囲に重要なものが紛れている場合、問題が起こります。私は今日テスト環境の画面確認用管理者アカウントを壊しました。
 update,deleteをテストするために重要なデータをテスト対象とした場合、テスト中止ないし再起動とする方法があります。この方法にはテスト対象とした範囲に重要なデータしか存在しない場合テストを行えないという欠点があります。もう一つ新たにデータを作成し、それに対してupdate,deleteをテストする方法があります。この手法はコード量が増えがちです。Laravelはfactoryという仕組みを用いてコード量の節約、責任の分担をしています。
データベースのテスト 5.6 Laravel#ファクトリの記述
 ファクトリはダミーデータを作成する仕組みでデータベースにテスト用初期データを入れる、データベースのテストに用いるといった使い方が主ですが、更新、削除のテストでもよく働いてくれます。factoryは次の様な記述によって与えるデータを定義します。

/** /database/factories/UserFactory.php */
use Faker\Generator as Faker;

$factory->define(App\User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
        'remember_token' => str_random(10),
    ];
});

 これを定義した場合、次の様にダミーデータをワンライナーで呼び出せます。createはデータベースへの書き込みを実行してインスタンスのCollectionを返し、makeはインスタンスのCollecitonを返します。

// factory(Model::class, 個数)
$user = factory(User::class, 1)->create()->first();
$new_user_data = factory(User::class, 1)->make()->first();

 createは削除と更新用の壊していいデータ、makeは更新用の入力値として便利に使えます。

  • この記事いいね! (0)
著者:杉浦

dusk中でブラウザに依らないassertを実行

 ブラウザテスト(Laravel Dusk) 5.6 Laravel#使用可能なアサート
 Laravelの自動ブラウザテスト機能duskでは多くのassertが使用可能です。多くのassertが使用可能ですがいずれもブラウザ上で何かを確認するためのassertです。duskは単一責任の原則を守りきれいに作られています。時にはブラウザ上の操作の結果データベース上の値がどうなったかを確認するケースの様にブラウザを介さないassertを実行したい場合があります。このためにduskで使用するコード中にduskを飛び越してduskの使っているPHPUnitにアクセスするコードを埋め込みます。
 duskの使用可能なアサートはだいたい次の様なコードで作られています。

use PHPUnit\Framework\Assert as PHPUnit;
/* 省略 */
    /**
     * Assert that the given text appears within the given selector.
     *
     * @param  string  $selector
     * @param  string  $text
     * @return $this
     */
    public function assertSeeIn($selector, $text)
    {
        $fullSelector = $this->resolver->format($selector);

        $element = $this->resolver->findOrFail($selector);

        PHPUnit::assertTrue(
            Str::contains($element->getText(), $text),
            "Did not see expected text [{$text}] within element [{$fullSelector}]."
        );

        return $this;
    }

 与えられたセレクタに従った要素中に与えられたtextが存在しないならばエラーというassertTrueを呼び出しています。このコード同様にPHPUnit\Framework\Assertからより素朴なassert関数を呼び出すことによってより自由度の高いテストをすることが出来ます。もちろんエラーが起きた際、ログやコンソールにエラーが起きたコード中の場所とエラーメッセージが表示されます。

    /**
     * 契約取消しテスト
     * @param Browser $browser
     * @throws \Facebook\WebDriver\Exception\TimeOutException
     */
    private function cancelContractTest(Browser $browser)
    {
        $browser->//省略
        $member = Members::find($this->member_key);
        PHPUnit::assertTrue(is_null($member->contract), "exist expected null contract.");
        $this->assertSeeMemberType($browser, $member);
    }

 最近書いたコードではこのようなものを書きました。これは、契約取消し操作をしたならばmemberはcontractを持っていないはず、というテストでした。
 duskは見事にSeleniumとPHPUnitを繋げて分かりやすく覆った便利なテスト機能です。duskはブラウザ操作とテストケースの抽象化のために幾重ものコードになっていますが、個々のソースはわかりやすく結局はPHPUnitでSeleniumの操作に対してassertするという言葉でまとめられる機能です。以前紹介した個々のテストケース実行コマンドもduskの説明書にないがPHPUnitの説明書にはある機能です。ヘルプには大して記述はありませんがPHPUnitの動作のかなりの部分が簡単にduskで使えます。

追記:
 テストケースを記述するクラスが継承するべきTestCaseクラス自身がPHPUnit\Framework\Assertを継承していました。useせずとも

$this->assertTrue(is_null($member->contract), "exist expected null contract.");

で十分でした。

  • この記事いいね! (0)
著者:杉浦

Laravelの自動ブラウザテスト機能duskで任意のテストケースのみを実行する

 ブラウザテスト(Laravel Dusk) 5.6 Laravel
 Laravelには自動でブラウザテストを行ってくれるduskという機能があります。それなりの手間がかかり、何度も行わなければならないブラウザ上における手作業を代行してくれる機能です。ブラウザテストなしに複雑な機能を変更の都度テストするのはとてもとても辛いです。
 そのように便利なブラウザテスト機能を行うduskですが、様々な機能を実装するにつれテストが肥大化し総実行時間も長くなります。私的な環境ですが軽い検索機能付きページのテストは1ページ2分ほどで終わります。これに機能の大小、ページ数が加わり総実行時間は10分、20分となります。とてもやっていられないため結果が欲しい一部のテストのみを実行する必要に駆られます。
 duskはPHPUnitの機能の大部分を引き継ぎブラウザテスト用にラッピングされたテストツールです。そしてPHPUnit同様のコマンドラインオプションが備わっており次の様なフィルターパターンによって、任意のクラス、任意のメソッドのテストを実行できます
 

例 3.2: フィルターパターンの例

  • --filter 'TestNamespace\\TestCaseClass::testMethod'

  • --filter 'TestNamespace\\TestCaseClass'

  • --filter TestNamespace

  • --filter TestCaseClass

  • --filter testMethod

  • --filter '/::testMethod .*"my named data"/'

  • --filter '/::testMethod .*#5$/'

  • --filter '/::testMethod .*#(5|6|7)$/'

 PHPUnit マニュアル – 第3章 コマンドラインのテストランナーから引用
 これにより何分も待たずに変更の度にテストをかけること、あるテストが通る様なコードを書くことが楽になります。

  • この記事いいね! (0)