カテゴリーアーカイブ Laravel

著者:杉浦

【Laravel】factory自動生成ツールlaravel-test-factory-helperの紹介

mpociot/laravel-test-factory-helper: Generate Laravel test factories from your existing models
 Laravelにはfactoryという機能があります。
データベースのテスト 5.5 Laravel#ファクトリの記述
 次の様にfactoryをdatabase/factories/以下に定義し、

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),
    ];
});
// <a href="https://readouble.com/laravel/5.5/ja/database-testing.html#writing-factories">データベースのテスト 5.5 Laravel#ファクトリの記述</a>より引用

 次の様に呼び出すことでデータを生成し、モデルに代入された状態で使用できます。

public function testDatabase()
{
    $user = factory(App\User::class)->make();

    // モデルをテストで使用…
}

 ダミーの値はfakerというライブラリで生成します。
stympy/faker: A library for generating fake data such as names, addresses, and phone numbers.
 有用なライブラリなのですが様々な値生成方法を選べるためfactoryの定義にも悩みがちです。laravel-test-factory-helperはfactoryを自動生成します。

composer require mpociot/laravel-test-factory-helper

とcomposerで導入。

php artisan test-factory-helper:generate

とartisanを実行。するとモデル定義に従ってデータベースから値を取得、適した型のfakerをつけたfactoryをモデル分全て生成します。

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

【Laravel】LaravelでDataTables.jsを楽に使うためのパッケージlaratablesの紹介

 freshbitsweb/laratables: Ajax support of DataTables (Laravel 5.5 – Laravel 5.8) Demo @
 JavaScriptにはDataTables.jsというリッチな表を簡単に作成するライブラリがあります。

DataTables | Table plug-in for jQuery

お手軽に機能付きテーブルを作るJavaScriptライブラリDataTables – 株式会社シーポイントラボ | 浜松のシステム開発会社
 DataTables.jsの基本的な使い方は、あらかじめHTMLコード上にtableタグで値を埋め込んで置き、DataTables.jsを適用して並び替え機能、絞り込み機能、ページネーション機能をつける、です。これは本当に1時間もかからずにリッチな表を作れる様なシンプルないい造りのライブラリなのですが問題がありました。それはあらかじめHTMLに埋め込む以上、初期読み込みにとても時間がかかりやすい点です。常に総数の実体をブラウザの初期段階で取得する必要があるため、総数が万件ある様なデータを対象にすると長い読み込み時間や崩れたデザインが現れだします。
 この問題を解決するには、ajaxを用いて適宜必要なデータのみを取得する基本的でない使い方をするのが良い方法です。しかしこの場合、サーバ側でDataTables.jsの提供するインタフェースに合わせたデータ処理のロジックを作る必要がでてきます。せっかく楽できるライブラリであったのに残念なことです。laratablesはこのDataTables.js用のロジックをLaravel内で簡単に記述するためのパッケージです。
 使い方は簡単。composerでプロジェクトに追加して、

composer require freshbitsweb/laratables

 Bladeに次の様にHTMLにテーブルのヘッダ、DataTablesにajaxの宛先と取得データのプロパティ名を記述、

<table id="simple-datatable-example" class="display" style="width:100%">
    <thead>
        <tr>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Mobile</th>
            <th>Email</th>
            <th>Gender</th>
            <th>Country</th>
        </tr>
    </thead>
</table>

@push('scripts')
    <script>
        $(document).ready(function() {
            $('#simple-datatable-example').DataTable({
                serverSide: true,
                processing: true,
                responsive: true,
                ajax: "{{ route('simple_datatables_users_data') }}",
                columns: [
                    { name: 'first_name' },
                    { name: 'last_name' },
                    { name: 'mobile' },
                    { name: 'email' },
                    { name: 'gender' },
                    { name: 'country' }
                ],
            });
        });
    </script>
@endpush

 ajax宛先のコントローラに次の様にLaratables::recordsOf();を検索対象のモデルクラスを呼ぶだけです。

    /**
     * return data of the simple datatables.
     *
     * @return Json
     */
    public function getSimpleDatatablesData()
    {
        return Laratables::recordsOf(User::class);
    }

 これだけで次のデモページの一部、Simpleのテーブルの完成です。

Laratables
 デモページにある様に、データに無関係なカラム、データの格納されたテーブルに関連したテーブルのデータも使えます。全てを把握できていませんがgithubの説明の文量からしてDataTablesそのものより多機能そうです。

freshbitsweb/laratables: Ajax support of DataTables (Laravel 5.5 – Laravel 5.8) Demo @

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

【Laravel】マイグレーションで扱うカラムにつける属性メソッドの読み取り

 Laravelにはデータベース定義を記述し、作成、ロールバックなどを行えるマイグレーション機能があります。このマイグレーションによるテーブル定義時にはカラムの属性を定義することが出来ます。
データベース:マイグレーション 5.5 Laravel#カラム修飾子
 例えば次です。

Schema::table('users', function (Blueprint $table) {
    $table->string('email')->nullable();
});
<a href="https://readouble.com/laravel/5.5/ja/migrations.html#column-modifiers">データベース:マイグレーション 5.5 Laravel#カラム修飾子</a>より引用

 これでメソッドnullableの動作によりカラムemailはnullを許すカラムになります。しかしnullable()メソッドの実態を見つけることは少し苦労を要します。
 素晴らしいide_helper(皮肉でなく本当にすごく便利)を生成する
barryvdh/laravel-ide-helper: Laravel IDE Helper
 を用いても、IDEはこのnullableへの道を見つけることが出来ません。
 これはメソッドnullrableが接続先のDBの種類によって動的に生成されているメソッドであるためです。
PHP: オーバーロード – Manual#メソッドのオーバーロード
 具体的に何が起きているかがわかる場所は、string()、integer()などの各カラム基本定義メソッドの返り値、ないし返り値が継承しているクラスvendor/laravel/framework/src/Illuminate/Support/Fluent.phpの中、そしてvendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.phpの中にあります。
 先のコードは

Schema::table('users', function (Blueprint $table) {
    $fluent = $table->string('email');
    /** @var Fluent $fluent */
    $fluent->nullable();
});

 となっており、Fluent内にnullable()は実装されていません。実装されていないメソッドが呼び出されようとする時、__call()が記述されているならば、__call()内部に記述された動作が実行されます。Fluentの__call()は次です。

    /**
     * Handle dynamic calls to the fluent instance to set attributes.
     *
     * @param  string  $method
     * @param  array   $parameters
     * @return $this
     */
    public function __call($method, $parameters)
    {
        $this->attributes[$method] = count($parameters) > 0 ? $parameters[0] : true;

        return $this;
    }

 Fluentのインスタンス内に与えられた属性を保持するのみです。この仕組みが理由で次の様なコードを書いてもバグは起きません。

Schema::table('users', function (Blueprint $table) {
    $table->string('email')->hogehoge();// 好きな名前でメソッドを呼び出せる
    $table->string('name')->nulable();// タイポしても気付かないかもしれないので、善し悪し
});

 $table->string(‘email’)->nullable();の様なカラム定義が繰り返される度にBlueprintのインスタンスである$tableはカラム定義のインスタンスであるFluentのインスタンスを新たに持ちます。クロージャ内の処理が終わった後、Schema::table()はBlueprint->build()を実行します。Blueprint->build()の中ではBlueprint->toSql()が実行されます。Blueprint->build()とBlueprint->toSql()の中身は次です。

    /**
     * Execute the blueprint against the database.
     *
     * @param  \Illuminate\Database\Connection  $connection
     * @param  \Illuminate\Database\Schema\Grammars\Grammar  $grammar
     * @return void
     */
    public function build(Connection $connection, Grammar $grammar)
    {
        // $connectionにデータベースとの接続、$grammarにデータベースの文法が入っている
        foreach ($this->toSql($connection, $grammar) as $statement) {
            $connection->statement($statement);
        }
    }
    
    /**
     * Get the raw SQL statements for the blueprint.
     *
     * @param  \Illuminate\Database\Connection  $connection
     * @param  \Illuminate\Database\Schema\Grammars\Grammar  $grammar
     * @return array
     */
    public function toSql(Connection $connection, Grammar $grammar)
    {
        $this->addImpliedCommands($grammar);// 文法クラスGrammarの中に入っているコマンドを取得する

        $statements = [];

        // Each type of command has a corresponding compiler function on the schema
        // grammar which is used to build the necessary SQL statements to build
        // the blueprint element, so we'll just call that compilers function.
        $this->ensureCommandsAreValid($connection);// SQLite用処理

        foreach ($this->commands as $command) {
// 文法クラス中のcompileから始まる名前のメソッドらの中に、要求された命令に対応するメソッドがあるならばそれを実行してSQL文の元の配列に追加
            $method = 'compile'.ucfirst($command->name);

            if (method_exists($grammar, $method)) {
                if (! is_null($sql = $grammar->$method($this, $command, $connection))) {
                    $statements = array_merge($statements, (array) $sql);
                }
            }
        }

        return $statements;
    }

 Bladeと同じです。特定のクラス中の特定の名前のメソッドがそのまま対応文法になっています。
【Laravel】Bladeの制御構文の探し方 – 株式会社シーポイントラボ | 浜松のシステム開発会社
 まだ終わりません。これでcompileCreateTable->getColumns->addModifiersと動きます。これが次です。

    /**
     * Create the main create table clause.
     *
     * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
     * @param  \Illuminate\Support\Fluent  $command
     * @param  \Illuminate\Database\Connection  $connection
     * @return string
     */
    protected function compileCreateTable($blueprint, $command, $connection)
    {
        return sprintf('%s table %s (%s)',
            $blueprint->temporary ? 'create temporary' : 'create',
            $this->wrapTable($blueprint),
            implode(', ', $this->getColumns($blueprint))
        );
    }

    /**
     * Compile the blueprint's column definitions.
     *
     * @param  \Illuminate\Database\Schema\Blueprint $blueprint
     * @return array
     */
    protected function getColumns(Blueprint $blueprint)
    {
        $columns = [];

        foreach ($blueprint->getAddedColumns() as $column) {
            // Each of the column types have their own compiler functions which are tasked
            // with turning the column definition into its SQL format for this platform
            // used by the connection. The column's modifiers are compiled and added.
            $sql = $this->wrap($column).' '.$this->getType($column);

            $columns[] = $this->addModifiers($sql, $blueprint, $column);
        }

        return $columns;
    }

    /**
     * Add the column modifiers to the definition.
     *
     * @param  string  $sql
     * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
     * @param  \Illuminate\Support\Fluent  $column
     * @return string
     */
    protected function addModifiers($sql, Blueprint $blueprint, Fluent $column)
    {
        foreach ($this->modifiers as $modifier) {
            if (method_exists($this, $method = "modify{$modifier}")) {
                $sql .= $this->{$method}($blueprint, $column);
            }
        }

        return $sql;
    }

 このaddModifiersでやっと終わりです。compileHogeHoge同様にmodifyHogeHogeが動きます。modifyHogeHogeがカラムに属性を追加するメソッドになります。modifyHogeHogeは文法クラスに詰まっています。文法クラスはvendor/laravel/framework/src/Illuminate/Database/Schema/Grammars以下に詰まっています。この中から使えるメソッドを抜き出し、ide_helperに追加すればIDEの自動補完が使えるようになります。例えばMySQLならば、次の様になります。

namespace Illuminate\Support{
    /**
     * @method Fluent after()
     * @method Fluent charset()
     * @method Fluent collate()
     * @method Fluent comment()
     * @method Fluent default()
     * @method Fluent first()
     * @method Fluent increment()
     * @method Fluent nullable()
     * @method Fluent srid()
     * @method Fluent storedAs()
     * @method Fluent unsigned()
     * @method Fluent virtualAs()
     */
    class Fluent {}
}

 よく使うuseCurrent()が抜けているので不完全です。少なくともこれで多少はカバーできます。

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

【Laravel】Laravel開発者ツール拡張Clockworkの紹介

Clockwork
itsgoingd/clockwork: Clockwork – php dev tools integrated to your browser – server-side component
 ClockworkはLaravel内で実行された様々なデータを表示する開発者ツールの拡張です。
 とりあえずの使い方は次の様にcomposerでプロジェクトにインストールして

composer require itsgoingd/clockwork

 次のリンクから拡張をブラウザに追加するのみです。(Larvel5.5以前はconfig/app.phpにProvider追加が必要です。)
Clockwork – Chrome ウェブストア
Clockwork – Get this Extension for 🦊 Firefox (en-US)
 これだけで基本機能は使えます。詳細な機能は製作者のドキュメントとソースコード参照です。
 基本機能だけでも強力です。何ができるかというと次の5つです。
 一つ目はリクエストの中身を見ることです。ルートとコントローラとの対応を見ることができますが、これ自体はChrome標準のNetworkタブの機能と大差ありません。タブ切り替えがいらない利便性が主目的でしょう。

 二つ目は実行時間です。Laravel内部の実行内容で時間を区切れる点が特長です。図はいささか単純ですが、複雑化してくるとボトルネックを探すことが容易になり便利です。

 三つ目はコード内で発行しているログの内容の取得です。図に使ったコードではデータベースを待ち受けてログファイルに吐き出す機能がついており、このようになっています。わざわざサーバにログファイルを取りにいかなくて済む、GUIで整理されて見やすい、といった点が特長です。

 四つ目は発行されたSQL文、その発行場所と実行時間の一覧です。これは特に便利です。SQLの観点から何を改善すれば高速化できるのかがとても分かりやすいです。長時間かかるクエリ、異様に多く発行されるクエリあたりが見つかったら赤信号で、これはそれらを見つけやすくしてくれます。

 五つ目はセッション情報です。普段サーバに隠れている部分であり、いちいちdumpする必要がなくなります。

 基本的な機能は前述の5つですが、xdebugによるコードのステップ実行や、Clockwork備え付けのメソッドを使って特別なログを出力することも可能です。

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

【Laravel】Cookieのsecure属性とLaravelビルトインサーバ

 Cookieにはいくつか属性がつけられます。この記事で注目するのはsecure属性です

The Secure attribute limits the scope of the cookie to “secure”
channels (where “secure” is defined by the user agent). When a
cookie has the Secure attribute, the user agent will include the
cookie in an HTTP request only if the request is transmitted over a
secure channel (typically HTTP over Transport Layer Security (TLS)
[RFC2818]).

Although seemingly useful for protecting cookies from active network
attackers, the Secure attribute protects only the cookie’s
confidentiality. An active network attacker can overwrite Secure
cookies from an insecure channel, disrupting their integrity (see
Section 8.6 for more details).RFC 6265 – HTTP State Management Mechanism

 要はsecure属性付きのCookieはHTTP通信では送信されず、HTTPS通信では送信されるようにするオプションです。これは特に通信を盗聴されることが致命的な情報、セッション情報(これを盗ると通信を乗っ取れる)などのために用いられます。

 Laravelはartisan serveコマンドによって組み込みサーバを動かせます。このサーバはPHPのビルトインウェブサーバの仕組みを利用しています。
PHP: ビルトインウェブサーバー – Manual
 実装コードは次です。

/** vendor/laravel/framework/src/Illuminate/Foundation/Console/ServeCommand.php */
    /**
     * Execute the console command.
     *
     * @return int
     *
     * @throws \Exception
     */
    public function handle()
    {
        chdir(public_path());

        $this->line("<info>Laravel development server started:</info> <http://{$this->host()}:{$this->port()}>");

        passthru($this->serverCommand(), $status);

        return $status;
    }
    
    /**
     * Get the full server command.
     *
     * @return string
     */
    protected function serverCommand()
    {
        return sprintf('%s -S %s:%s %s',
            ProcessUtils::escapeArgument((new PhpExecutableFinder)->find(false)),
            $this->host(),
            $this->port(),
            ProcessUtils::escapeArgument(base_path('server.php'))
        );
    }

 要は

$ cd ~/public_html
$ php -S [host]:[port] /hoge/fuga/project_root/server.php

をartisanで実行しやすくしているだけです。呼び出しているserver.phpが次です。

/** server.php */
<?php

/**
 * Laravel - A PHP Framework For Web Artisans.
 * @author   Taylor Otwell <taylor@laravel.com>
 */
$uri = urldecode(
    parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
);

// This file allows us to emulate Apache's "mod_rewrite" functionality from the
// built-in PHP web server. This provides a convenient way to test a Laravel
// application without having installed a "real" web server software here.
if ('/' !== $uri && file_exists(__DIR__.'/public'.$uri)) {
    return false;
}

require_once __DIR__.'/public/index.php';

 安全にpublic/index.phpを呼び出しているだけです。public/index.phpはHTTPリクエスト全てを受け取る窓口です。つまりLaravelの組み込みサーバはPHPのビルトインウェブサーバのラッピングであり、そのサーバとしての機能はほとんど素のPHPのビルトインウェブサーバの素のままです。
 このためPHPのビルトインウェブサーバの不便な点はLaravelのビルトインサーバの不便な点と言えます。業務用web開発において特に不便な点はHTTPSによるセキュアな通信が出来ない点です。


 セキュアな通信ができないということは、セキュアな通信でなければ渡されない情報を使えない、ということです。その様な情報には冒頭で述べたSecure属性がtrueなCookieが含まれます。またセキュアなcookieであるべきものには盗聴されるとセッションの乗っ取りを容易にする情報であるセッション情報が含まれます。これによりLaravelビルトインサーバ上ではセキュアなプログラムを組んだままログイン情報を持ちまわせません。Laravelビルトインサーバではログインした状態でブラウザでどうこうすることができません。

 書いている途中で気づきましたが、artisan serveにはオプションがあります。

artisan serve --env=notsecure

 このように記述すると、Laravelは.env.notsecureの環境変数を読み込んでビルトインサーバを立ち上げます。.env.notsecureを読んだ時限定でHTTPSとHTTPを同等に扱えるようなオプションとコードを記述すればビルトインサーバでも大過なくテストできそうです。ただCSRFがらみは念入りにセキュアにされてるっぽそうな。
 とはいえ業務を考えた場合、コードには十分に安全なプログラムであることが要求されます。結局のところセキュアなことを確認するためにビルトインサーバを用いることはできないので、マシン本体、仮想マシン、外部サーバなりなんなりにapacheなどの運用状態と同等の環境を用意するのが業務としては安全で確実そうです。

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

【Laravel】LaravelのソースコードでよくあるArrayAccessを実装するClass設計

 Laravelのソースコード内を

implements[^\n]+ArrayAccess

という正規表現でググるとArrayAccessを満たすClassを軒並み見つけられます。この記事ではこれらのClassで使われているArrayAccessの実装方法とその設計について述べます。
 ArrayAccessというのはPHPの組み込みインタフェースの一つです。
PHP: ArrayAccess – Manual
 このインタフェースを満たしたClassのインスタンスは配列として値を呼び出せるようになります。例えば次です。

<?php
class obj implements ArrayAccess {
    private $container = array();

    public function __construct() {
        $this->container = array(
            "one"   => 1,
            "two"   => 2,
            "three" => 3,
        );
    }

    public function offsetSet($offset, $value) {
        if (is_null($offset)) {
            $this->container[] = $value;
        } else {
            $this->container[$offset] = $value;
        }
    }

    public function offsetExists($offset) {
        return isset($this->container[$offset]);
    }

    public function offsetUnset($offset) {
        unset($this->container[$offset]);
    }

    public function offsetGet($offset) {
        return isset($this->container[$offset]) ? $this->container[$offset] : null;
    }
}

$obj = new obj;

var_dump(isset($obj["two"]));
var_dump($obj["two"]);
unset($obj["two"]);
var_dump(isset($obj["two"]));
$obj["two"] = "A value";
var_dump($obj["two"]);
$obj[] = 'Append 1';
$obj[] = 'Append 2';
$obj[] = 'Append 3';
print_r($obj);

 配列として呼び出し、代入、isset()、empty()、unset()が使えるようになります。注意するのは配列となっているわけではないことです。上のコードの続きで

array_pop($obj);

などとした場合、引数がArray型でないと怒られ、PHPが致命的なエラーを起こします。

 Laravelのソースコード中のいくつかのClassではこのArrayAccessを実装しています。例えばデータベース定義を行うクラスであるFluentでは次の様になっています。

    /**
     * Determine if the given offset exists.
     *
     * @param  string  $offset
     * @return bool
     */
    public function offsetExists($offset)
    {
        return isset($this->attributes[$offset]);
    }

    /**
     * Get the value for a given offset.
     *
     * @param  string  $offset
     * @return mixed
     */
    public function offsetGet($offset)
    {
        return $this->get($offset);
    }

    /**
     * Get an attribute from the fluent instance.
     *
     * @param  string  $key
     * @param  mixed   $default
     * @return mixed
     */
    public function get($key, $default = null)
    {
        if (array_key_exists($key, $this->attributes)) {
            return $this->attributes[$key];
        }

        return value($default);
    }

    /**
     * Set the value at the given offset.
     *
     * @param  string  $offset
     * @param  mixed   $value
     * @return void
     */
    public function offsetSet($offset, $value)
    {
        $this->attributes[$offset] = $value;
    }

    /**
     * Unset the value at the given offset.
     *
     * @param  string  $offset
     * @return void
     */
    public function offsetUnset($offset)
    {
        unset($this->attributes[$offset]);
    }

 いずれもattributesへのアクセス方法です。attributesにはカラムの定義が羅列されます。
 ブラウザに返すためのwebページを定義するクラスViewでは次の様になっています。

    /**
     * Determine if a piece of data is bound.
     *
     * @param  string  $key
     * @return bool
     */
    public function offsetExists($key)
    {
        return array_key_exists($key, $this->data);
    }

    /**
     * Get a piece of bound data to the view.
     *
     * @param  string  $key
     * @return mixed
     */
    public function offsetGet($key)
    {
        return $this->data[$key];
    }

    /**
     * Set a piece of data on the view.
     *
     * @param  string  $key
     * @param  mixed   $value
     * @return void
     */
    public function offsetSet($key, $value)
    {
        $this->with($key, $value);
    }

    /**
     * Add a piece of data to the view.
     *
     * @param  string|array  $key
     * @param  mixed   $value
     * @return $this
     */
    public function with($key, $value = null)
    {
        if (is_array($key)) {
            $this->data = array_merge($this->data, $key);
        } else {
            $this->data[$key] = $value;
        }

        return $this;
    }

    /**
     * Unset a piece of data from the view.
     *
     * @param  string  $key
     * @return void
     */
    public function offsetUnset($key)
    {
        unset($this->data[$key]);
    }

 いずれもdataへのアクセス方法です。dataにはHTMLコードのテンプレートファイルに渡す変数が羅列されます。
 ここで挙げたのは二例ですが、Collection、Modelなど他にも同様の実装はあります。この実装の方針は、Classの中に主目的のデータらを格納した配列を持ち、その配列を直接扱う方法の一つとしてArrayAccessを実装、というものです。このため次の様にClassを作るとLaravel内のコードと違和感なく扱えるままClassを拡張していくことができます。

class MyModel implements ArrayAccess
{
    /** @var array 主データ */
    public $data;

    //
    // $dataを操る色々
    //

    /**
     * {@inheritdoc}
     * @param  mixed $offset
     * @return bool
     */
    public function offsetExists($offset)
    {
        // この実装は疑問。LaravelではoffsetExistsの返り値がarray_key_existsの返り値であることがしばしばある。
        // しかしArrayAccessの実装によってisset($this[$offset])とした時、$this->offsetExists($offset)が評価される。
        // Laravelに従った場合、$this->data[$offset]=nullの時にisset($this->data[$offset])===false、isset($this[$offset])===trueとなる。
        // この実装の場合、isset($this->data[$offset])===isset($this[$offset])が担保できる。
        return isset($this->data[$offset]);
    }

    /**
     * {@inheritdoc}
     * @param  mixed $offset
     * @return mixed
     */
    public function offsetGet($offset)
    {
        return $this->data[$offset];
    }

    /**
     * {@inheritdoc}
     * @param  mixed $offset
     * @param  mixed $value
     * @return void
     */
    public function offsetSet($offset, $value)
    {
        return $this->data[$offset] = $value;
    }

    /**
     * {@inheritdoc}
     * @param  mixed $offset
     * @return void
     */
    public function offsetUnset($offset)
    {
        unset($this->data[$offset]);
    }
}

 このやり方はリレーショナルデータベース以外のデータリソースを参照するModelを作る時などで割と便利です。EloquentをそのままExtendすると不意にクエリビルダが走る事故が起きますが、この書き方なら起きません。また適切に拡張した場合、Controller等のModelを外部から参照する際に他のEloquentModelと記述上の区別なしに扱うことができます。

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

【Laravel】Bladeで直書きJavaScriptのコンポーネントを作る時の勧め

BladeはシンプルながらパワフルなLaravelのテンプレートエンジンです。
Bladeテンプレート 5.7 Laravel

 BladeはLaravelで用いられるテンプレートエンジンであり、PHPの構文を書きやすく、読みやすい形でHTMLに埋め込みます。例えば

<?php if(auth()->guard('admin')->check()): ?>
  <div class="alert alert-danger">
    <?php echo e($slot); ?>

  </div>
<?php endif; ?>

ならばBladeでは

@auth('admin')
<div class="alert alert-danger">
    {{ $slot }}
</div>
@endauth

と記述できます。
 Bladeはコンポーネント指向――多数の部品を組み合わせることで完成品を作る考え――の元にwebページを作ることが出来ます。例えば、

@section('link_button')
    <a class="btn btn-primary font-weight-bold text-white" href="{{$link}}">
        {{$text}}
        <i class="fa fa-angle-right fa-lg right -pull-right"></i>
    </a>
@overwrite
@yield('link_button')

と定義すると

@include('link_button',['text'=>'hoge','link'=>'/hoge'])
@include('link_button',['text'=>'fuga','link'=>'/fuga'])
@include('link_button',['text'=>'foo','link'=>'/foo'])

と呼び出した時に/hoge, /fuga, /fooそれぞれに飛んでいくボタンの見た目のリンクが作られます。この仕組みはJavaScriptに応用できます。
 やり方はscriptタグを書いたコンポートを定義するだけです。例えば、

@section('alert_by_javascript')
<script>
    if (document.readyState !== 'loading') {
      alertHoge()
    } else {
      document.addEventListener('DOMContentLoaded', alertHoge);
    }
    function alertHoge(){
      alert({{$hoge}});
    }
</script>
@overwrite
@yield('alert_by_javascript')

と定義して

@include('alert_by_javascript',['hoge'=>'hoge'])
@include('alert_by_javascript',['hoge'=>'fuga'])
@include('alert_by_javascript',['hoge'=>'foo'])

 と呼び出すと、hoge, fuga, fooと文字列を表示するalertが発生します。パッと使うだけならこの通り簡単なのですが、注意点は素のJavaScriptを書いている点で問題点が発生します。深刻なのはグローバル汚染です。全て同じ階層にあるため他コンポーネントと変数名、関数名が被った時に容易にバグります。
 解決方法の一つは常に即時関数で囲みスコープを作ることです。こうするとコンポーネントの外に漏れることはありません。しかし、外部で使いたい時もあります。グローバル汚染を防ぎながら外部でJavaScriptを適宜呼び出せるようにする方法が次です。

@section('auto_redirect_to_top_script')
  @php
    // グローバル汚染回避のためにランダムな文字列か指定された文字列を名前に持つ関数を作る
    $function_name = $function_name ?? str_random();
  @endphp
  <script type="text/javascript">
    if (document.readyState !== 'loading') {
      {{$function_name}}();
    } else {
      document.addEventListener('DOMContentLoaded', {{$function_name}});
    }
    function {{$function_name}}(){
      alert({{$hoge}});
    }
  </script>
@overwrite
@yield('alert_by_javascript')

 関数の名前を呼び出す際に関数名を決めます。こうするとスコープは閉じたままで、外から任意に呼び出せます。

@include('alert_by_javascript',['hoge'=>'hoge','function_name' => 'hoge_function'])// hoge_function()が走る
@include('alert_by_javascript',['hoge'=>'fuga','function_name' => 'fuga_function'])// hoge_function()が走る
@include('alert_by_javascript',['hoge'=>'foo'])// `${ランダムな文字列}`()が走る

 この手法の注意点はやはり素のJavaScriptを書いている点です。古いIEを代表とするいくらかのブラウザは近年のJavaScriptの記述を読み取れず、実行できません。このコンポーネントに書かれているJavaScriptはほぼそのままwebページに表示されます。なんとなくで記述を行うといくらかのブラウザではJavaScriptを実行してくれません。他にも圧縮、難読化、コメントなど素のJavaScriptそのままならではの問題点が出てきます。

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

【Laravel】DBに依存させずにEloquentモデルをテストする

 EloquentはLaravelに備わっているORM(Object-relational mapping)です。
Eloquent:利用の開始 5.7 Laravel
 オブジェクト関連マッピングとはオブジェクトとDB(データベース)の関連のマッピングのことであり、その都合上オブジェクトはデータベースと密接な関係にあります。

<?php
class Post extends Model
{
    // これだけでpostsテーブル中の各レコードを呼べる。
}


 この密接な関係のため、モデルをテストする際にDBが付きまといがちです。何を呼び出すにしても、DBの中を見に行きます。しかしモデルのメソッドのテストはDBから切り離されるべきです。これはモデルのテストがそれ以上掘り下げるべき最小単位のユニットテストであり、テスト失敗の原因がテストやソースコードのエラーかDBのエラーか考える手間を省くべきでもあるからです。
 次の様なコードを記述することでDBからEloquentモデルを切り離してテストできます。

<?php

namespace Tests\Unit\App\Models;

use App\Models\User;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection;
use Tests\TestCase;

class UserTest extends TestCase
{
    public function testScopeFindOrFailPort()
    {
        self::assertInstanceOf(User::class, UserMock::scopeFindOrFailPort(UserMock::query(), '20100'));
        try {
            UserMock::scopeFindOrFailPort(UserMock::query(), 'hoge');
            self::assertTrue(false, 'not foundでなかった');
        } catch (ModelNotFoundException $e) {
            self::assertTrue(true);
        }
    }
}

/**
 * モデルUserのモック。データベースに関わらず一定のものを返すようにしている。
 */
class UserMock extends User
{
    public $mock_data;
    public $mock_return_data;

    /**
     * {@inheritdoc}
     * UserMock constructor.
     * @param array $attributes
     */
    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
// テストデータ定義
        $mocks = [];
        for ($i = 0; $i < 5; ++$i) {
            $mocks[] = (new User())->forceFill([
                'id' => $i,
                'group_key' => $i % 3,
                'hash' => 'hash_'.$i,
                'port' => ['1234', '12345', '20100'][$i % 2],
                'created_at' => now(),
                'updated_at' => now(),
                'deleted_at' => null,
            ]);
        }
        $this->mock_data = new Collection($mocks);
    }

// DBにアクセスするLaravel内部に隠れている部分を引っ張り出して置き換え

    /**
     * {@inheritdoc}
     * @return User|Builder|UserMock
     */
    public static function query()
    {
        return new self();
    }

    /**
     * {@inheritdoc}
     * @param  array                    $columns
     * @return User|Model|object|null
     */
    /** @noinspection PhpHierarchyChecksInspection */
    public function first($columns = [])
    {
        return $this->mock_data->first();
    }

    /**
     * {@inheritdoc}
     * @param  array|Closure|string $column
     * @param  string|null          $operator
     * @param  null                 $value
     * @param  string               $boolean
     * @return User|UserMock
     */
    /** @noinspection PhpHierarchyChecksInspection */
    public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        if (func_num_args() === 2) {
            $value = $operator;

            $operator = '=';
        }
        $this->mock_data = $this->mock_data->where($column, $operator, $value);

        return $this;
    }
}

 重要なのは、テスト対象のモデルを継承しているがテスト対象のモデル内で定義されたメソッドを変更していないクラス、をテストコードに記述することです。このUserMockクラスは、UserクラスのDBにアクセスしてデータを取ってくる部分を、テストコード中で定義する値を取ってくる様に上書きしたクラスです。テスト対象そのものを置き換えている様にも見えますが、この置き換えている部分は大過ないことが担保されているLaravel内部のテスト済みコードのModelクラスの部分です。

 テスト用クラスUserMockはテスト対象クラスUserで定義されたコードを変更せず、Userの更に奥のModelから継承したコードを変更しています。これによりUserクラスを破壊することなく、DBのアクセスの代わりにローカル変数へのアクセスを行うUserクラスであるUserMockクラスが作れます。
 このようにするとデータベースに依存することなくEloquentのテストが出来ます。こういったコードはローカルのPHPのみで完結しており、実行速度が速い、他のデータリソースを汚染しない、と便利です。ここまで小さいテストならばモデル全体を通しても10秒とかからずにテストが終わります。おかげでGitにコミットする度にローカルで走らせても苦になりません。

DBへのアクセスメソッドは大体\Illuminate\Database\Eloquent\Builder以下にそろっています。

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

【Laravel】artisan view:cacheでBladeの中身を読めるようにする

 LarvelはBladeなるテンプレート言語を備えています。
Bladeテンプレート 5.7 Laravel
 便利は便利なのですが独自な上、説明もざっくばらんなので詳細に中身を把握するのは難しいです。
 artisan view:cacheはresources以下のBladeファイルのキャッシュを生成するコマンドです。このコマンドで生成されるPHPファイルはBladeをPHP言語にコンパイルした結果です。コンパイル後のファイルを読むことでBladeが何をやっているのか少しわかりやすくなります。
 例えば次のコードをコンパイルすると

@section('header')
@endsection
@yield('header')

@section('footer')
@show

@section('body')
@overwrite
@yield('body')

次の様になります。

<?php $__env->startSection('header'); ?>// @section
<?php $__env->stopSection(); ?>// @endsection
<?php echo $__env->yieldContent('header'); ?>// @yield

<?php $__env->startSection('footer'); ?>// @section
<?php echo $__env->yieldSection(); ?>// @show

<?php $__env->startSection('body'); ?>// @section
<?php $__env->stopSection(true); ?>// @overwrite
<?php echo $__env->yieldContent('body'); ?>// @yield

 こうやってPHPコードに直すとやっていることを追いかけられるようになり、中身を知ることが出来ます。
 @endsection, @yield, @show, @overwriteはそれぞれ@sectionと密に関わるディレクティブです。
Bladeテンプレート 5.7 Laravel#レイアウト定義
 @endsectionはsectionで定義を始めたコードの終点を示し、@yieldはsectionの出力を示し、@showはsectionで定義を始めたコードの終点であり即時展開を示し、overwriteは同名sectionの上書きを行うsection定義の終点を示します。これだけだとどういうこととなりますがコードを読むと少しわかります。@sectionの中身であるstartSectionの内容が次です。

    /**
     * Start injecting content into a section.
     *
     * @param  string  $section
     * @param  string|null  $content
     * @return void
     */
    public function startSection($section, $content = null)
    {
        if ($content === null) {
            if (ob_start()) {// ここが大事
                $this->sectionStack[] = $section;
            }
        } else {
            $this->extendSection($section, $content instanceof View ? $content : e($content));
        }
    }

 obstart()でこれ以降の出力(ブラウザに映る予定のHTMLコードなど)をバッファにため込みます。これは改めて命令されるまで出力されません。ついで@endsection,@overwriteの中身であるstopSection()が次です。

    /**
     * Stop injecting content into a section.
     *
     * @param  bool  $overwrite
     * @return string
     *
     * @throws \InvalidArgumentException
     */
    public function stopSection($overwrite = false)
    {
        if (empty($this->sectionStack)) {
            throw new InvalidArgumentException('Cannot end a section without first starting one.');
        }

        $last = array_pop($this->sectionStack);

        if ($overwrite) {
            $this->sections[$last] = ob_get_clean();
        } else {
            $this->extendSection($last, ob_get_clean());
        }

        return $last;
    }

 ob_get_clean()でバッファの内容を取得し、出力バッファを削除します。@overwriteはこの$overwrite=trueでstopSectionを呼び出しています。これにより問答無用でプロパティsectionsの末尾にバッファを書き込んでいます。@overwriteに引数がないのは必ず末尾を参照していたせいです。
 $overwrite=falseの時はextendSection()へ回って、もし以前に定義していればそちらを呼び出すようにしています。
 @showの中身であるyieldSection()、@yieldの中身であるyieldContent()が次です。

    /**
     * Stop injecting content into a section and return its contents.
     *
     * @return string
     */
    public function yieldSection()
    {
        if (empty($this->sectionStack)) {
            return '';
        }

        return $this->yieldContent($this->stopSection());
    }
    
    /**
     * Get the string contents of a section.
     *
     * @param  string  $section
     * @param  string  $default
     * @return string
     */
    public function yieldContent($section, $default = '')
    {
        $sectionContent = $default instanceof View ? $default : e($default);

        if (isset($this->sections[$section])) {
            $sectionContent = $this->sections[$section];
        }

        $sectionContent = str_replace('@@parent', '--parent--holder--', $sectionContent);

        return str_replace(
            '--parent--holder--', '@parent', str_replace(static::parentPlaceholder($section), '', $sectionContent)
        );
    }

 yieldSectionは短縮形の様なものです。stopSection、yieldContentの順に呼び出し、最後に宣言したsectionを閉じ、出力しています。
 yieldContentはプロパティ$this->sectionsの中から目当てのsectionを選び、整形して返しているのみです。
 @endsection, @yield, @show, @overwriteはBlade内部では糖衣構文として役割に応じた大きく違う名前を与えられていましたが、実際にはちょっとした違う引数で呼び出される同じ関数らであることがわかります。

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

LaravelをNginxで動作させる際の設定

LaravelではどのURLを指定されても、(自分の担当するパス配下へのリクエストであれば)Laravel自身を起動させるために必ずpublic/index.phpを経由させる必要があります。

この設定はPHPだけでは限界があるため、Webサーバー側の設定で対応する必要があります。

Laravelのパッケージにはこの”index.phpを必ず経由させる”という設定が書かれた設定ファイルがデフォルトで含まれています。
具体的には

public/.htaccess

上記のファイルがこの設定ファイルにあたります。
ところが、この.htaccessファイルは基本的にApacheなどのごく一部のWebサーバーでしか認識しないので、たとえは先日の記事のようにnginxにPHPをインストールした環境で動作させようとしても、そのままではうまく動いてくれません。

この場合、.htaccessに書かれている設定と同様の働きになる設定をWebサーバーの設定ファイルに指定する必要が出てきます。
ただし、その記述の仕方もApacheとは異なってきますので、各Webサーバー用の書き方に書き換えないといけません。

nginxの場合、.htaccessの中身をnginx用の設定に書き換えてくれるWebサービスがあり、まず最初にこのサービスを使って変換した設定ファイルを使って設定を行ってみました。

元の.htaccess

<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews -Indexes
    </IfModule>

    RewriteEngine On

    # Handle Authorization Header
    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    # Redirect Trailing Slashes If Not A Folder...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} (.+)/$
    RewriteRule ^ %1 [L,R=301]

    # Handle Front Controller...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

上記サイトで変換後のnginxコンフィグ

# nginx configuration

location / {
  if (!-e $request_filename){
    rewrite ^(.*)$ /%1 redirect;
  }
  if (!-e $request_filename){
    rewrite ^(.*)$ /index.php break;
  }
}

実際に入れ込むと以下のようになります。

server {

  listen        80;
  listen        443 ssl;

  server_name  example.com;

  ssl_certificate       /path/to/ssl/fullchain.pem;
  ssl_certificate_key   /path/to/ssl/privkey.pem;

  root /path/to/docroot;
  client_max_body_size 1g;
  location / {
        if (!-e $request_filename){
                rewrite ^(.*)$ /%1 redirect;
        }
        if (!-e $request_filename){
                rewrite ^(.*)$ /index.php break;
        }      
        index index.php index.htm index.html;
  }
  location ~ \.php$ {
    if (!-e $request_filename){
        rewrite ^(.*)$ /%1 redirect;
    }
    if (!-e $request_filename){
        rewrite ^(.*)$ /index.php break;
    }
    fastcgi_pass   unix:/var/run/php-fpm.sock;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    include        fastcgi_params;
    allow all;
  }
}

ところが、上記のような設定を行ったところ、リダイレクトループに陥ってしまいます。

いろいろ調べたところ、Nginx向けのLaravel用の設定が公開されていました。

nginxをLaravel5.4用に設定する – Qiita

上記の記事を基に今回の場合の設定ファイルを書き換えてみます。

server{
   listen        80;
   listen        443 ssl;

   server_name  example.com;

   ssl_certificate       /path/to/ssl/fullchain.pem;
   ssl_certificate_key   /path/to/ssl/privkey.pem;

   root /path/to/docroot;

   location / {     
        index  index.php index.html index.htm;
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass   unix:/var/run/php-fpm.sock;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

全然違うじゃん…

やはり、ただ設定を変換しただけではうまく行かないみたいですね。

参考元の記事ではLaravel5.4となっていましたが、5.6の環境でも同様の設定で問題なく動作しました。

Laravel + Nginxの構築でお困りの方の参考になれば幸いです。

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