カテゴリーアーカイブ PHP

著者:杉浦

PHPの抽象クラスとメソッドを定義するabstract

PHP: クラスの抽象化 – Manual
 abstractはPHPのクラス宣言時につけることでそのクラスを抽象クラスとして扱うことを示します。抽象クラスはインスタンスにできず、抽象メソッドを定義することが可能なクラスです。
 抽象という名が示す通り抽象クラスは子とも言える継承先、具体的なクラスを持つことが前提となっているクラスです。子のクラスの共通要素を抽出したクラスです。抽象クラスを用いることによって誤った実装を防ぎ、何を作るべきなのかをわかりやすくすることができます。
 抽象メソッドは実体を持たず、子に実装を任せるメソッド、インタフェース中のメソッドと同じ動きをします。これをもちいることによって次の様に、共通部分のほぼ固定の動作は実装、個々の要素は任せることを強制、というコードを記述できます。

abstract class AbstractClass
{
    // 拡張クラスにこのメソッドの定義を強制する
    abstract protected function getValue();
    abstract protected function prefixValue($prefix);

    // 共通メソッド
    public function printOut() {
        print $this->getValue() . "\n";
    }
    
    // プロパティ
    protected $hoge = 'hoge';
}

 taritと違って継承先で定義を新たにすることが容易であり、interfaceと違ってプロパティを持てます。ただしあくまでクラスであり、1つのクラスで継承できる元のクラスは1つだけ、抽象化のパターンにきっちり載せて使うべき機能です。

  • この記事いいね! (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)
takahashi 著者:takahashi

Laravel DuskでリモートのLaravelプロジェクトのテストに失敗するときの対処法

Laravelには公式のプラグインとして”Dusk”というテストモジュールがあります。

Artisan経由でコマンドでPHPUnitテストを実行できるのですが、このDuskを使ってリモートサーバーのLaravelプロジェクトをテストしようとすると、実際には正常に動作するのにエラーになってしまうことがあります。

> @ browser-test /var/lib/jenkins/workspace/hogeproject-auto-test
> php artisan dusk

Warning: TTY mode requires /dev/tty to be read/writable.
PHPUnit 7.5.0 by Sebastian Bergmann and contributors.

EEEEPHP Fatal error:  Uncaught Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable. in /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Container/Container.php:933
Stack trace:
#0 /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Container/Container.php(773): Illuminate\Container\Container->notInstantiable('Illuminate\\Cont...')
#1 /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Container/Container.php(646): Illuminate\Container\Container->build('Illuminate\\Cont...')
#2 /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Container/Container.php(601): Illuminate\Container\Container->resolve('Illuminate\\Cont...', Array)
#3 /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(734): Illuminate\Container\ in /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 933
PHP Fatal error:  Uncaught Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable. in /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Container/Container.php:933
Stack trace:
#0 /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Container/Container.php(773): Illuminate\Container\Container->notInstantiable('Illuminate\\Cont...')
#1 /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Container/Container.php(646): Illuminate\Container\Container->build('Illuminate\\Cont...')
#2 /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Container/Container.php(601): Illuminate\Container\Container->resolve('Illuminate\\Cont...', Array)
#3 /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(734): Illuminate\Container\ in /var/lib/jenkins/workspace/hogeproject-auto-test/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 933

npm ERR! Linux 4.14.70-72.55.amzn2.x86_64
npm ERR! argv "/usr/bin/node" "/usr/local/bin/npm" "run" "browser-test"
npm ERR! node v6.15.1
npm ERR! npm  v3.10.10
npm ERR! code ELIFECYCLE
npm ERR! @ browser-test: `php artisan dusk`
npm ERR! Exit status 255
npm ERR! 
npm ERR! Failed at the @ browser-test script 'php artisan dusk'.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the  package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     php artisan dusk
npm ERR! You can get information on how to open an issue for this project with:
npm ERR!     npm bugs 
npm ERR! Or if that isn't available, you can get their info via:
npm ERR!     npm owner ls 
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR!     /var/lib/jenkins/workspace/hogeproject-auto-test/npm-debug.log
Build step 'Execute shell' marked build as failure
'project' is not included in Backlog URL. Can't comment a pull request.
Finished: FAILURE

恥ずかしながら自分はいろいろ調べても原因がわからなかったので、社内のメンバーに相談したところ、どうやらサイトへのアクセス時にタイムアウトが発生が発生しているのでは、とのことだったため、タイムアウト時間を延ばして解消するか確認してみました。

Laravel Duskのタイムアウト時間を延ばすには、
プロジェクトのルート/tests/DuskTestCase.php

RemoteWebDriver::create
を次の例のように書き換えます。

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

(参考: OctoberCMSでLaravel Dusk + Seleniumを使ってE2Eテストをする – Qiita)

これで実行したところ、無事Duskのテストが通過するようになりました!

リモート環境に接続する際は、
FQDNをIPアドレスに変換し、リモートサーバーに接続し、リクエストを投げ、応答を待つ…
といった手順を踏む必要があるのですが、Duskのタイムアウトデフォルト値が1分となっており、上記の手順を踏んだ場合にこの時間を超過してしまった結果テスト失敗となっていたようです。

同じような症状でお悩みの方は確認してみてください。

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

PHPの単体テストを行いやすくするコンストラクター・インジェクション

 コンストラクター・インジェクションはコンストラクタで依存性をインジェクション(注入)する方法のことです。何の気なしに次の様なコードを書いた場合、そのコードには依存関係が発生します。

public static function createNewMember(Request $request)
{
    $new_member = new Members();
    // $new_memberをどうこうするコード
}

 このコードはMembersの実装に依存しています。Membersの中身が変更された場合、createNewMemberが壊れる場合があります。壊れた場合、createNewMember内部に問題が無いにもかかわらずテストが失敗し、createNewMemberを探る羽目になります。そのためにnew Members()と呼ぶのでなく手製のテスト用オブジェクトをcreateNewMemberに渡せるようにします。この渡す手法の一つがコンストラクター・インジェクションです。
 コンストラクター・インジェクションの方法は次の通りです。

class MemberController
{
    public function __construct(Members $member = null)
    {
        $this->member = $member ?? new Members();
    }

    public function createNewMember(Request $request)
    {
        $new_member = $this->member;
        // $new_memberをどうこうするコード
    }
}

 コンストラクタで引数としてMembersオブジェクトを渡すか、新たにインスタンスを作るかして、それを自身のプロパティに保持します。こうすることで依存性のあったコードのクラスインスタンス化した際にテスト用オブジェクトを渡してテストを行うことが出来ます。このテストはMembersクラスに依存していない為Membersクラスの変更によってテスト中のcreateNewMemberが壊れることはありません。

  • この記事いいね! (0)
村上 著者:村上

【PHP】文字列からHTMLタグを取り除く「strip_tags」関数

特殊文字を HTMLエンティティに変換する「htmlspecialchars」関数はたまに使いますが、こちらは実はあまり使ったことがなかったので、自分用のメモとしてまとめ。
HTMLタグ や PHPタグ を取り除いてくれる関数「strip_tags」です。

PHPマニュアルはこちららから。

PHP: strip_tags – Manual
http://php.net/manual/ja/function.strip-tags.php

 

使い方…といってもとても簡単で、下記のように、ただ削除したいHTMLタグを含む文字列を引数に入れるだけです。

$result = strip_tags([変換したい文字列]);

上記を実行すると、HTMLタグ等が削除された文字列が、変数 $result に代入されます。

また、第二引数で、削除から除外するタグを指定することもできます。

$result = strip_tags([変換したい文字列], '

<b>');

上記を実行すると、<p>タグ<b>タグ を除いて、HTMLタグやPHPタグを削除してくれます。
なので、文字列の装飾に関するタグは残したいけど、それ以外のタグは削除したい!というときに使えますね。

使い方としては以上です。

なお、HTMLタグは無効にしたいけど、タグが削除されては困る!というときには、htmlspecialchars を使いましょう。
こちらの関数は、<p>文字列</p>&lt;p&gt;文字列&lt;/p&gt; に変換し、HTMLタグを無効にしてくれます。
状況に応じて使い分けてください。
※なお、本当なら &lt; と &gt; の最初のアンパサンドは小文字なのですが、表示の都合上、大文字で記載しています。

 

以上、HTMLタグを削除してくれる「strip_tags」関数についてでした。
まとめるほどでもなかったですが…使用頻度の低い関数はあっという間に忘れてしまうので、いつかの私のためにまとめておきます。

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

PhpStormの何でも屋コマンドAlt+Enter

インテンション・アクション – 公式ヘルプ | PhpStorm
 自分が使っているIDEのPhpStormにはAlt+Enterというとりあえず使おうという意味合いのショートカットキーがあります。Alt+Enterコマンドはカーソルを合わせた場所に合わせた何でも屋の様なコマンドです。操作名はインテンション・アクション、公式の説明はPhpStormの提案を実行する、ととてもふんわりしています。インテンション・アクションはふんわりとした分様々なことを行ってくれます。
 Alt+Enterから実際よく使う動作としてスペルミス、インポート、PHPDoc補完があります。波線が現れたらとりあえずAlt+Enterしてみるぐらいでちょうどいい塩梅で、PhpStormがかくあるべしというコードを教えてくれます。



 PhpStormという名前ですが縁があるくらいの他言語、例えばSQLのコマンドでも同じように有効です。識別子で修飾はスキーマ名やテーブル名を修飾してくれます。

 

  • この記事いいね! (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)
著者:杉浦

Laravelでクエリのロギングを簡単な記述で行う

 ORM(データベースの中身をオブジェクトにマッピングする仕組み)は便利であり、Laravelに入っているEloquent ORMはとてもとても便利で開発を簡単にしてくれます。しかしながらORMを介して記述をしていると実際に発行されているSQL命令がコードから読み取りにくいものになります。グローバルスコープにより暗黙的に条件を追加する、User::create($request->all())だけでINSERTを済ませる、といったことをするならばなおさらです。デバッグのためには実際に発行されているクエリをロギングしたいものです。
 LaravelのDB接続クラスには次のlistenメソッドが備わっており、これを用いることでクエリの発行の度にlistenイベントを発生させます。
 データベース:利用開始 5.6 Laravel#クエリイベントのリッスン

    /**
     * Register a database query listener with the connection.
     * データベースクエリリスナを接続に登録する。
     *
     * @param  \Closure  $callback
     * @return void
     */
    public function listen(Closure $callback)
    {
        if (isset($this-&gt;events)) {
            $this-&gt;events-&gt;listen(Events\QueryExecuted::class, $callback);
        }
    }

 このlistenメソッドに渡す関数にクエリをロギングする関数にし、その記述を上記リンクにある様にアプリケーションサービスの初期処理で呼び出されるapp\Providers\AppServiceProviderのbootメソッドに行います。

class AppServiceProvider extends ServiceProvider
{
    /**
     * アプリケーションサービスの初期処理
     * @return void
     */
    public function boot()
    {
        DB::listen(function (QueryExecuted $query) {
            Log::debug(str_replace_array('?', $query-&gt;bindings, $query-&gt;sql));// クエリロギングコード
        });
    }
}

 ここからは、コードの細部の説明です。ロギングを行う関数部はLog::debug(ログに残すメッセージ)です。debugレベルのログを/config/logging.phpの設定に従って出力します。デフォルトでは/storage/logs/laravel.logファイルに追記されます。
 ログに残すメッセージはstr_replace_array(‘?’, $query->bindings, $query->sql)です。$query->bindings、$query->sql、str_replace_array(‘?’, $query->bindings, $query->sql)の中身はそれぞれ次の画像の通りです。

 それぞれバインドした値、値を入れる前のクエリ、バインドした値を当てはめたクエリです。str_replace_arrayはLaravelのヘルパ関数の一つです。
 ヘルパ 5.6 Laravel#str_replace_array()
 str_replace_array関数は配列を使い、文字列を指定値へ順番に置き換えます。

str_replace_array('?', $query-&gt;bindings, $query-&gt;sql)

 このコードは値を入れる前のクエリの中の’?’をバインドした値に順番に置き換えるものということです。
 このコードの注意点としてログをDBに吐き出す設定にしておくと無限ループが起きることです。ログをDBに保存すると、そのDBへの保存をログする動作が走り、またログのログを……と続きます。

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

Laravelのルーティングを司るRouteServiceProvider

 Laravelのチュートリアルにおいてルーティングは/route/web.phpに記述する様に指示されます。実際このweb.phpとRouteファサード、ミドルウェアを用いるだけで大抵のやりたいルーティングは実現できます。しかしながらあくまで大抵であり、より深いことを行おうとした場合、もっと元の部分からルーティングを扱う必要があります。またルーティングパターンがあまりに多い時も元をさわってルーティングを記述するファイルを増やすことが可読性に貢献します。
 /app/Providers/RouteServiceProviderは/route/web.phpを呼び出し、ルーティングのマップを定義するコードです。ソースコードには次の様にあります。

    /**
     * Define the routes for the application.
     * @return void
     */
    public function map()
    {
        $this->mapApiRoutes();

        $this->mapWebRoutes();
    }

    /**
     * Define the "web" routes for the application.
     * These routes all receive session state, CSRF protection, etc.
     * @return void
     */
    protected function mapWebRoutes()
    {
        Route::middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/web.php'));
    }

 コメントとコードの通り各ルーティングのマップ呼び出し、webのルーティングを定義、というメソッドらです。例えばルーティングファイルを増やしたいならば次の様に行えます。

    /**
     * Define the routes for the application.
     * @return void
     */
    public function map()
    {
        $this->mapApiRoutes();

        $this->mapWebRoutes();

        $this->mapAppRoutes();
    }

    /**
     * Define the "web" routes for the application.
     * These routes all receive session state, CSRF protection, etc.
     * @return void
     */
    protected function mapWebRoutes()
    {
        Route::middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/web.php'));
    }
    /**
     * アプリ用ルーティングの定義
     * @return void
     */
    protected function mapAppRoutes()
    {
        Route::prefix('app')
             ->middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/app.php'));
    }

 もうほとんどコピペです。他にもモデルを定義する事によりコントローラ上の引数で楽をできます。

    // RouteServiceProvider内部
    /**
     * Define your route model bindings, pattern filters, etc.
     * @return void
     */
    public function boot()
    {
        parent::boot();
        Route::model('member_rank', MemberRankMaster::class);// 追記部
    }
          // route/web.php内部
          Route::resource('member_rank', 'Admin\MemberRankController');

 上のコードの様にモデルクラスとルーティング名の対応の定義とそのルーティング名によるコントローラの呼び出しを記述することによって

    /**
     * member_rank/{id}/editで呼び出されるメソッド
     * Show the form for editing the specified resource.
     * @param  MemberRankMaster          $rank
     * @return \Illuminate\Http\Response
     */
    public function edit(MemberRankMaster $rank)
    {
        return view('admin.pages.member_rank.edit')
            ->with(compact('rank'));
    }

 このように引数でいきなりMemberRankMaster::findOrFail($id)で得られる様なEloquentオブジェクトが使えます。わかりやすい上にシンプルな記述で済みますね。

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

PHPDocの内容を継承する@inheritdoc

Inheritance — phpDocumentor
 @inheritdocは継承元(implements元やextends元やtrait元)のPHPDocの説明を継承するタグです。@inheritdocを使うと次の様に継承元のPHPDocの説明文を受け継げます。
 
 元の振る舞いと継承後の振る舞いに大した違いが無い場合、同じ説明を記述することは面倒です。@inheritdocを使うことで何度も同じ記述を行わず済み、楽が出来ます。
 @inheritdocは継承元の説明をインラインに埋め込むコメントとして扱えます。冒頭の説明文の部分に関しては次の様にけっこう自由がききます。

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