You are here
Home > 入力の空白文字のトリム(連載)

入力の空白文字のトリム(3)配列の入力値

今回は、「入力の空白文字のトリム」の最後です。

以下のようなフォームでは、入力の変数に配列を使用することできます。


<form>
...
<div>あなたの趣味を以下に記入してください:</div>
<ul>
  <li><input type="text" name="hobby[]" value=""></li>
  <li><input type="text" name="hobby[]" value=""></li>
...
</form>

これらがプログラムには、

hobby[0] = "映画鑑賞";
hobby[1] = " 読書";
hobby[2] = "テニス ";
..

という形で入ってきます。

しかし、前回のミドルウェアの定義では、このような配列には対応できませんので、以下のように書き直しました。

namespace App\Http\Middleware;

use Closure;

class TrimInput {

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $input = $request->all();

        $request->merge(self::trim($input));

        return $next($request);
    }

    public static function trim($value)
    {
        if (is_array($value))
        {
            $value = array_map(['self', 'trim'], $value);
        }
        elseif (is_string($value))
        {
            $value = preg_replace('/(^\s+)|(\s+$)/u', '', $value);
        }

        return $value;
    }
}

一見複雑そうに見えますが、それほどでもありません。まず、回帰を使用するためにトリムの関数を分けました。クラスメソッドとしています。他でも使用するなら自身のライブラリのメソッドとしてもよいです。そのトリムのメソッドでは、パラメが配列のときには回帰を使用して配列の個々の値をトリムします。

これに伴い、ユニットテストも書き換えました。

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

class SignupTest extends TestCase
{
    use DatabaseTransactions;

    /**
     * @dataProvider providerTrimpInput
     */
    public function testTrimpInput2($data, $expected)
    {
        $input = [
            'email'                 => 'success@gmail.com',
            'password'              => 'testtest',
            'password_confirmation' => 'testtest',
            'last_name'             => $data['last_name'],
            'first_name'            => '太郎',
            'hobby'                 => $data['hobby']
        ];

        $this->visit('/signup')
            ->submitForm('保存', $input)
            ->see('会員登録完了');

        $member = \App\Member::where('email', 'success@gmail.com')->first();

        $this->assertEquals($expected['last_name'], $member->last_name);
        $this->assertEquals($expected['hobby'], unserialize($member->hobbies));
    }

    public function providerTrimpInput()
    {
        return [
            [ //半角スペース
                ['last_name' => ' 山田 ', 'hobby' => [' 映画鑑賞 ', ' 読書 ', '']],
                ['last_name' => '山田',   'hobby' => ['映画鑑賞', '読書', '']],
            ],
            [ //全角スペース
                ['last_name' => ' 山田 ', 'hobby' => [' 映画鑑賞 ', ' 読書 ', '']],
                ['last_name' => '山田',     'hobby' => ['映画鑑賞', '読書', '']],
            ],

        ];
    }
}

前回のテストでは、

      $this->visit('/signup')
           ->type('success@gmail.com', 'email')
   ...
      ->press('保存')
           ->see('会員登録完了')

のように、入力をシミュレートするのに、type()の関数を使用しましたが、今回のテストでは、配列の入力は残念ながらtype()では対応できません。そこで、type()press()をまとめた、submitForm()を使っています。

データプロバイダーの方も、last_namehobbyのおいてそれぞれ値を変えることができるように、インデックスをつけました。

さて、これで前回の全角スペースとともに配列値でのトリムも可能となりました。空白文字をトリムするミドルウェアの完成です。

実行できるコードはこのリンクから、コード。タグは、2015-10-19となっています。

git fetch && git checkout -b 2015-10-19

入力の空白文字をトリム(2)全角スペース

前回の「空白文字の入力をトリムする」の続きです。

まず、動作確認を簡単にするために、ユニットテストを作成しましょう。

入力画面のユニットテストをもとに、いろいろなデータをテストできるように、データプロバイダーを使用します。


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

class SignupTest extends TestCase
{
    use DatabaseTransactions;

    /**
     * @dataProvider providerTrimpInput
     */
    public function testTrimpInput($last_name, $expected)
    {
        $this->visit('/signup')
            ->type('success@gmail.com', 'email')
            ->type('testtest', 'password')
            ->type('testtest', 'password_confirmation')
            ->type($last_name, 'last_name')
            ->type('太郎', 'first_name')
            ->press('保存')
            ->see('会員登録完了');

        $member = \App\Member::where('email', 'success@gmail.com')->first();

        $this->assertEquals($expected, $member->last_name);
    }

    public function providerTrimpInput()
    {
        return [
            [ '山田', '山田'],
            [ ' 山田 ', '山田'],   //半角スペース
            [ ' 山田 ', '山田'], //全角スペース
        ];
    }
}

ユニットテストのデータプロバイダーは、テストとデータを分けることにより、1つのテストでいろいろなデータのテストが可能となります。必要なのは、コメントのphpDocの@dataProviderでデータを供給する関数名を指定することと、そのデータ供給の関数を作成することです。この例では3つのデータを用意することにより、3回テストが実行されます。

テスト実行結果は、以下です。

PHPUnit 4.8.9 by Sebastian Bergmann and contributors.

..F

Time: 360 ms, Memory: 26.50Mb

There was 1 failure:

1) SignupTest::testTrimpInput with data set #2 (' 山田 ', '山田')
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'山田'
+' 山田 '

/vol1/usr/www/shop/webdocs/demo/tests/SignupTest.php:27

FAILURES!
Tests: 3, Assertions: 12, Failures: 1.

2番目の半角のスペースのデータはトリムされ成功となり、3番目の全角スペースが前後にあるデータでは失敗となります。

さて、全角スペースはどう対応しましょうか?

再度、trim関数の仕様を見てみましょう。

 string trim ( string $str [, string $character_mask = " \t\n\r\0\x0B" ] )

この関数は str の最初および最後から空白文字を取り除き、 取り除かれた文字列を返します。
2番目のパラメータを指定しない場合、 trim()は以下の文字を削除します。

    " " (ASCII 32 (0x20)), 通常の空白。
    "\t" (ASCII 9 (0x09)), タブ。
    "\n" (ASCII 10 (0x0A)), リターン。
    "\r" (ASCII 13 (0x0D)), 改行。
    "\0" (ASCII 0 (0x00)), NULバイト
    "\x0B" (ASCII 11 (0x0B)), 垂直タブ

そう、2番目のパラメータに全角スペースを入れてあげればよいですね。

trim($val, ' \t\n\r\0\x0B ');//全角スペースは最後の空白文字

しかし、これはテストしたところうまくいきませんでした。全角のスペースが入る値には対応するようですが、半角の文字列では文字列によっては空白文字でないものもトリムしてしまいます。trimの関数はユニコード対応でないようですね。それならば、mb_trimと思いますが、残念ながらこれが存在しません。

いろいろ調べたところ、preg_replaceが使えそうです。

パターンは、

'/(^\s+)|(\s+$)/u'

\sは空白文字のエスケープシーケンスであり、ドキュメントでは以下の記述があります。

エスケープシーケンス

空白文字とは HT (9)、LF (10)、FF (12)、CR (13)、スペース (32) のことです。 しかし、ロケールを指定したマッチングを行った場合には、128から255までのコードポイントの文字 (たとえば NBSP (A0)) も空白文字とみなされる可能性があります。

ということで、trimで削除する空白文字とともにユニコードの全角スペースも削除してくれます。

ミドルウェアを書き直すと、

namespace App\Http\Middleware; 

use Closure; class TrimInput {

    /** 
     * Handle an incoming request. 
     * 
     * @param \Illuminate\Http\Request $request 
     * @param \Closure $next 
     * @return mixed 
     */ 
    public function handle($request, Closure $next) { 

        $input = $request->all();

        $trimmed = [];

        foreach($input as $key => $val)
        {
            $trimmed[$key] = preg_replace('/(^\s+)|(\s+$)/u', '', $value);
        }

        $request->merge($trimmed);

        return $next($request);
    }
}

先のユニットテストの結果は?

PHPUnit 4.8.9 by Sebastian Bergmann and contributors.

...

Time: 359 ms, Memory: 26.50Mb

OK (3 tests, 12 assertions)

成功です!

次回は、配列の入力値の個々の値の空白文字のトリムの対応です。

入力の空白文字をトリム(1) ミドルウェア

フォームから入力されてくる文字でやっかいなのは、文字列の前後にユーザーのタイプミスで入れられる空白文字。英語では半角のスペース、日本語では全角のスペース。

これらの空白文字を削除してくれる関数はすでにPHPにはあります。

http://php.net/manual/ja/function.trim.php

さて、この関数をララベルのどこで使用するのが適切でしょうか?

まず考えるのは、コントローラ。バリデーションの前に、以下のようにループで空白文字を削除。

public function postSignup(Request $request)
{
    ...

    $trimmed = [];

    foreach($request->all as $key => $val)
    {
        $trimmed[$key] = trim($val);
    }

    $request->merge($trimmed);

    $this->validate($request, $rules, $messages);

    ...

しかし、どこのコントローラでもこれを行うのは面倒です。フォームリクエストという手もありますね。しかし、これもコントローラ1つに対してフォームリクエスト1つを作成すれば、同じ手間。もっとグローバルで対応する手はない?

そこで登場するのは、HTTPミドルウェアです。HTTPミドルウェアは、フォームなどから入力されてくる値をフィルターするためのプログラムです。

フィルターは、

app/Http/Middleware

のディレクトリに存在します。すでに以下のファイルが用意されていて、それぞれのフィルターのカスタマイズを行うことができます。

Authenticate.php ユーザーの認証のため
EncryptCookies.php クッキーの暗号化のため
RedirectIfAuthenticate.php すでに認証されているならリダイレクトするため
VerifyCsrfToken.php セッション乗っ取りを防ぐため

ここに新たなミドルウェアとして空白文字のトリムを入れましょう。


namespace App\Http\Middleware; 

use Closure; class TrimInput {
    /** 
     * Handle an incoming request. 
     * 
     * @param \Illuminate\Http\Request $request 
     * @param \Closure $next 
     * @return mixed 
     */ 
    public function handle($request, Closure $next) { 

        $input = $request->all();

        $trimmed = [];

        foreach($input as $key => $val)
        {
            $trimmed[$key] = trim($val);
        }

        $request->merge($trimmed);

        return $next($request);
    }
}

まず、簡単な対応として、入力されたそれぞれの値に対して半角の空白文字のトリムを実行します。クラス名はファイル名と同様にTrimInputと命名します。既存のクラスを継承する必要はなく、handle()の関数を定義するのみです。

入力の値は、$requestのパラメータからオブジェクトとして取得して、それをトリムした文字列で上書きします。そして、最後には次のフィルターの関数を上書きされたリクエストとともにコールします。

しかし、残念ながらこれだけでは実行されません。以下のファイルにおいて、登録する必要あります。


namespace App\Http; 

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{ 
/**
 * The application's global HTTP middleware stack.
 *
 * @var array
 */ 
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \App\Http\Middleware\TrimInput::class,
    ]; 

/**
 * The application's route middleware.
 *
 * @var array
 */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    ];
}

TrimInputは21行目に登録されています。先にリストした他のミドルウェアも入っていますね。

さて、これだけは十分ではありません。いくつか、対応すべき問題があります。

まず、フォームでは日本語の入力を期待するのに、全角スペースの対応ができていません。

また、フォームなどから入力される値は、たいていが文字列ですがPHPでは、配列の[]を使用することで以下のような同型の複数行フォームの入力も可能です。


<form>
...
<div>あなたの趣味を以下に記入してください:</div>
<ul>
  <li><input type="text" name="hobby[]" value=""></li>
  <li><input type="text" name="hobby[]" value=""></li>
...
</form>

この配列での入力値の対応も先のコードでは無理です。そして、動作確認はどうしましょう。空白文字ゆえに画面での確認は難しいです。フォームのユニットテストが必要ですね。これらを含めて次回に仕上げましょう。

Top