チームビルディングでドラッカー風エクササイズ!(その2)

5月入社された方をチームに迎え入れることとなり、GW明け(5/7)にチームビルディングの一環としてドラッカー風エクササイズを行いました。 今回で2回目となります。この時に工夫したことや、よかったことなどをまとめます。

1回目は、4月に入社された方をチームに迎え入れた時に行いました。

ドラッカー風エクササイズについてや、その時の様子はぜひこちらをご参照ください。

yudy1152.hatenablog.com


目次

今回の様子

前回の立ちっぱなしで疲れるという反省をふまえ、座って会話できる高さで表を書きました(笑)

メンバーは全部で5人いまして、1人はリモートワークをしており、台上のPCごしに見ている状況です。

右の男性(中国出身の方)は決して内職をしてるわけではなく、分からない単語をググっている様子、、、のハズです。

f:id:yudy1152:20190518120111j:plain

工夫したこと

質問項目

前回は初めてのトライということもあり、教科書通りの質問項目で行いました。

  1. 自分は何が得意か?
  2. 自分はどうやってチームに貢献するつもりか?
  3. 自分が大切に思う価値は何か?
  4. チームメンバーは自分にどういう成果を期待していると思うか?

しかし今回は、別の視点で他のメンバーの考えや想いなどを知りたいなと思い、質問項目を一部変えて行いました。

  1. スキルを何か1つマスターできるなら何がいい?
  2. 1ヶ月前から変わったと思うことは?
  3. 自分が大切に思う価値は何か?
  4. 最大のモチベーションは?

質問項目を自分たちで選んだということもあってか、回答がより具体的なものになり、会話も弾んだと思います。

進行

  1. ポストイットに回答を書き出す(10 分)
  2. 一人ひとりの発表(4 分 / 人)
  3. 1ヶ月前から変わったと思うことは?について、他のメンバーから見て変わったと思うことを発表(キャプチャのピンクの付箋)
  4. 気になったことなど、ざっくばらんに会話

3以降でタイムボックスを設けるのを忘れてしまいました・・・。 会話が発散しすぎないようにするためにも、次やるときは全体的にタイムボックスを設けようと思います。

まとめ

今回特によかったこは1ヶ月前から変わったと思うことは?について、他のメンバーから見て変わったと思うことについて会話できたことだと思います。

自身のこの1ヶ月のふりかえるだけでなく、他のメンバーの成長に注目していいところを指摘したり、ありがとうを伝え合う場面もありました。

あるメンバーから「ふりかえりとか、モブプロ とか、チームでやっていくことについて色々取り組もうとしてくれてありがとう」という言葉をもらいました。

泣きそうになったのはナイショですが、むしろいつも私のフワッとした「こういうことやりたいんだよね〜」ということに対し、「よく分からないけど、まずはやってみよう!」と一緒に取り組んでくれるみんなにありがとうを伝えたいです。(ちゃんと伝えたっけ・・・汗)

これからも自身のスキルアップだけでなく、主体的に動けるチーム、強いチームづくりをするための努力は続けていきたいなと改めて思いました。

Laravel の Eloquent 検証(ローカルスコープ)

DB からデータを取得する時、様々な条件のもとに取得してくる事が多いと思います。 その度にwhereをいくつも連結して実装するのも、読み解くのも大変ですよね。

何が大変かと言うと、結局その実装で取得されるデータが何を表しているのか分かりにくい、というのが原因の1つなのではないかと感じました。

そういった事を解決してくれるクエリスコープという機能の1つであるローカルスコープについて検証しました。

参考:Eloquent: Getting Started - Laravel - The PHP Framework For Web Artisans



ローカルスコープの検証

マイグレーションファイルの準備

以下のような users テーブルを用意します。

  • is_active カラム:有効なユーザーかどうかを表すフラグ
  • role カラム(※ 例として以下のような分類とします)

    • admin:管理者
    • prime:一般よりも権限を持つユーザー
    • user:一般ユーザー
  • マイグレーションの定義

<?php

    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->string('id', 36)->primary();
            $table->string('last_name');
            $table->string('first_name');
            $table->boolean('is_active');
            $table->enum('role', ['admin', 'prime', 'user']);
            $table->timestamps();
        });
    }

テストデータの準備

今回は、Fakerを使い、10人分のテストデータを生成しました。

Fakerはランダムなテストデータを生成するのに便利なライブラリです。 以下のサンプルのように人の名前だけでなく、email住所なども生成してくれます。

参考:[PHP] Fakerでランダムなフェイクデータを作成する - Qiita

<?php

$roles = ['prime', 'user'];
$faker = Factory::create('ja_JP');
foreach (range(1, 10) as $i) {
    // 最初の1人だけ admin にし、それ以外はランダムで prime か user ロール。
    $role = $i === 1 ? 'admin' : $roles[rand(0, 1)];
    User::create([
        'id' => $faker->uuid,
        'last_name' => $faker->lastName,
        'first_name' => $faker->firstName,
        'is_active' => $faker->boolean(),
        'role' => $role,
    ]);
}

生成したデータは以下の通りです。

f:id:yudy1152:20190506174824p:plain

検証1:シンプルな定義(有効なユーザーを取得する)

scopeプレフィックスとした function をモデルに定義する必要があります。 ただし、この function を呼び出す時は、scopeを付けません。

モデル定義

ここでは、有効状態のユーザーを操作する頻度が多い事を想定し、 以下のように User モデルにscopeActiveという名前で function を追加します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    〜 略 〜
    protected $casts = [
      'is_active' => 'boolean',
    ];

    public function scopeActive(Builder $query): Builder
    {
        return $query->where('is_active', true);
    }
}

実行

定義したスコープを使って件数を取得します(想定では8人)。

<?php
logger(User::Active()->count());

出力結果

[2019-05-06 08:59:09] testing.INFO: Query Time:15.99ms] select count(*) as aggregate from `users` where `is_active` = ?  
[2019-05-06 08:59:09] testing.INFO: array (
  0 => true,
)  
[2019-05-06 08:59:09] testing.DEBUG: 8  

発行されるクエリがis_activeを条件にしている事、カウントも適切な値が取得できた事を確認できました。

少しハマったポイント

※ ドキュメントにはUser::active()と、aが小文字で書かれており、これを実行すると以下のようなエラーが発生しました。

試しにAと大文字から書き始めたら正常に動くようになりました。

BadMethodCallException: Call to undefined method App\Models\User::acitve()

検証2:スコープに引数を渡せるようにする(動的スコープ)

モデル定義

roleprimeもしくはuserのリストを取得できるようにしたい、といったケースに対応できるようにするため、以下のようにscopeOfTypeという function を追加します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    〜 略 〜
    // $type には `admin`, `prime`, `user` のいずれかを指定します。
    public function scopeOfType(Builder $query, string $type): Builder
    {
        return $query->where('role', $type);
    }
}

実行

定義したスコープを使い、primeなユーザー数数を取得します(想定では3人)。

<?php
logger(User::OfType('prime')->count());

出力結果

[2019-05-06 09:21:46] testing.INFO: Query Time:20.1ms] select count(*) as aggregate from `users` where `role` = ?  
[2019-05-06 09:21:46] testing.INFO: array (
  0 => 'prime',
)  
[2019-05-06 09:21:46] testing.DEBUG: 3  

このように、roleカラムを対象にprimeで絞っており、結果が3である事も確認できました。

もちろん、User::OfType'user'を渡せば、roleを対象にuserで絞る事ができます。

<?php
logger(User::OfType('user')->count());
[2019-05-06 09:39:39] testing.INFO: Query Time:17.09ms] select count(*) as aggregate from `users` where `role` = ?  
[2019-05-06 09:39:39] testing.INFO: array (
  0 => 'user',
)  
[2019-05-06 09:39:39] testing.DEBUG: 6

検証3:作成したスコープの合わせ技

実行

作成した2つの function を合わせて使用し、アクティブでroleuserのユーザー数を取得します。(想定は4人)

合わせる時は単純に繋げて使用する事ができます。

<?php
logger(User::Active()->OfType('user')->count());

出力結果

[2019-05-06 09:43:51] testing.INFO: Query Time:17.5ms] select count(*) as aggregate from `users` where `is_active` = ? and `role` = ?  
[2019-05-06 09:43:51] testing.INFO: array (
  0 => true,
  1 => 'user',
)  
[2019-05-06 09:43:51] testing.DEBUG: 4 

is_activeおよびroleカラムを対象に絞り、結果が4である事を確認できました。

感想

スコープの名称(function 名)を適切なもので定義する事で、その実装がどういうデータを取得してこようとしているか、判断しやすくなるなと感じました。 むしろ判断しやすくなるようなスコープを定義する必要があるかもしれません。

また、例えばあるカラムの名前を変更する事になった場合、そのカラムを利用するクエリ(例:where('カラム名', $value))を、スコープの中に閉じ込めておくことで、ソースコードの修正も容易になるのかなと思いました。

スコープを積極的に使用して、メンテナンス性の高いモデルを実装していきたいなと思いました。

Laravel の Eloquent 検証($dateFormat と $dates)

前回は Laravel の Eloquent を勉強するため、環境構築を行いました。

今回は日付に関する内容で、モデルのプロパティである$dateFormat$datesについて検証を行いました。

yudy1152.hatenablog.com



$dateFormat の検証

参考:Eloquent: Getting Started - Laravel - The PHP Framework For Web Artisans

created_atupdate_atのフォーマットを変更した時の検証を、以下の2パターンで行います。

  • デフォルトの timestamp にミリ秒も利用する(例:2019-05-06 10:10:22.123456)
  • Unix 時間を利用する(例:1557105050)

デフォルトの timestamp にミリ秒も利用する

$dateFormatプロパティにY-m-d H:i:s.uを指定します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public $incrementing = false;

    protected $keyType = 'string';

    protected $guarded = [
        'created_at',
        'updated_at',
    ];

    protected $dateFormat = 'Y-m-d H:i:s.u'; // ← ここを追加
}

デフォルトの$tabe->timestamps();では秒までしか登録されませんが、以下のようにtimestampsに桁数を渡す事で、小数部分も登録されるようになります。

<?php

    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->string('id', 36)->primary();
            $table->string('last_name');
            $table->string('first_name');
            $table->timestamps(6); // ← 桁数を指定
        });
    }
  • データ作成

ユーザーを1件登録します。

<?php
User::create([
    'id' => Uuid::uuid4()->toString(),
    'last_name' => '山田',
    'first_name' => '太郎',
]);

このようにミリ秒も含めて登録される事が確認出来ました。

f:id:yudy1152:20190506111524p:plain

Unix 時間を利用する

  • モデルの設定

$dateFormatプロパティにUを指定します。

<?php

class User extends Model
{
〜 略 〜
    protected $dateFormat = 'U'; // ← ここだけ変更
}

Unix 時間が登録されるように、$tabe->timestamps(); を以下のようにintegerを使って定義するようにします。

<?php

    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->string('id', 36)->primary();
            $table->string('last_name');
            $table->string('first_name');
            $table->integer('updated_at');
            $table->integer('created_at');
        });
    }
  • データ作成

ユーザーを1件登録します。

<?php
User::create([
    'id' => Uuid::uuid4()->toString(),
    'last_name' => '山田',
    'first_name' => '太郎',
]);

このように Unix 時間で登録される事が確認出来ました。

f:id:yudy1152:20190506105029p:plain

感想

外部のサービスと連携する時など、サービスによっては日時のフォーマットが異なる場合もあるかと思います。 そして、外部サービスから受け取ったデータをそのまま DB へ保存したい時などに使えるのではないかと思いました。


$dates の検証

参考:Eloquent: Mutators - Laravel - The PHP Framework For Web Artisans

Eloquent は、データを取得するとデフォルトでcreated_atupdated_atCarbonインスタンスへ変換してくれます。

created_atupdated_at以外に日時を扱うカラムを追加した際に、$datesでそのカラムを指定する事で、その指定したカラムもCarbonインスタンへ変換してくれるようになります。

マイグレーションファイルおよびデータの準備

User のマイグレーションファイルに、以下のようなカラムを追加して検証を行います。

  • カラム名logged_at(Null を許可)
    • ユーザーがログインした日時を保存しておく想定
<?php
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->string('id', 36)->primary();
            $table->string('last_name');
            $table->string('first_name');
            $table->timestamp('logged_at')->nullable(); // ← 追加
            $table->timestamps();
        });
    }

次に、テーブルを作成後、以下のようなレコードを作成しておきます。

f:id:yudy1152:20190506125034p:plain

$dates を設定しなかった場合の挙動

まずは、User モデルに$datesを設定せずにデータを取得してみます。

<?php
$user = User::find('01a2b401-b03b-4471-9fe3-c015e22f0552');

logger($user->logged_at);
logger(gettype($user->logged_at));

出力内容

[2019-05-06 03:33:23] testing.DEBUG: 2019-05-06 03:27:48  
[2019-05-06 03:33:23] testing.DEBUG: string

このようにデフォルトではstringで取得されます。 そのため、例えば2019年05月06日と表示したい場合に、自前でCarbonを使ったりdate関数を駆使してフォーマットの変換を行わなければなりません。

$dates を設定した場合の挙動

それでは、以下のように User モデルに$datesを設定します。

<?php

class User extends Model
{
〜 略 〜
    protected $dates = [
        'logged_at'
    ];
}

再度、出力内容を少し変えて取得してみます。

<?php
$user = User::find('01a2b401-b03b-4471-9fe3-c015e22f0552');

logger($user->logged_at);
logger(get_class($user->logged_at));
logger($user->logged_at->format('Y年m月d日'));

出力内容

[2019-05-06 04:06:02] testing.DEBUG: 2019-05-06 03:27:48  
[2019-05-06 04:06:02] testing.DEBUG: object  
[2019-05-06 04:06:02] testing.DEBUG: Illuminate\Support\Carbon  
[2019-05-06 04:06:02] testing.DEBUG: 2019年05月06日

このようにCarbonインスタンスへ変換されたものが取得され、formatメソッドで簡単に書式を整える事ができました。

感想

正直言いますと、今まで取得したstringの日付を頑張ってフォーマットしていました・・・。 もっと早く知っていれば!と後悔です。

おまけ

プライマリキーは UUID で

検証で使う User モデルの id を、デフォルトのもの(int)ではなく、以前勉強したUUIDで利用できるようにしています。

モデルの置き場所を変更

モデルはapp/Models下に置くように変更しました。

参考:【Laravel】Model ファイルのディレクトリ構成変更時にやること | ブロックチェーンエンジニアのブログ

実行された SQL を確認したい

下記参考サイトを真似させて頂きました。

※ 出力される SQL の実行時間($query->time)の単位はミリ秒です。

<?php

namespace App\Providers;

use Illuminate\Support\Facades\{DB, Log};
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * サービスプロバイダの登録
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * アプリケーションサービスの初期処理
     * 参考サイト:https://daiki-sekiguchi.com/2018/07/26/laravel-sql-log/
     *
     * @return void
     */
    public function boot()
    {
        if (config('app.env') !== 'production') {
            DB::listen(function ($query) {
                Log::info("Query Time:{$query->time}ms] $query->sql");
                Log::info($query->bindings);
            });
        }
    }
}

Docker で Laravel の環境構築をしました

やりたい事

  • Laravel の Eloquent の機能を色々試したい
  • 単体テスト形式でコードを書いて試したい
  • テスト実行時には、開発用 DB とは別にテスト用の DB を用意したい

Eloquent を勉強するため、まずは検証環境を用意する事から着手しました。 その時の過程やハマったポイント等をまとめました。

環境情報

  • Docker version 18.09.2
  • docker-compose version 1.23.2

アプリ(Laravel)の環境構築

こちらの記事を参考にさせて頂きました。構成はほぼ同じです。

ただ、PHP のバージョンは 7.3 で作成したかったため、一部変更して実行しました。

まずはアプリのみを構築(Laravel をインストールするまで)

  • docker-compose.yml
version: '3'

services:
  php:
    container_name: php
    build: ./docker/php
    volumes:
      - ./server:/var/www

  nginx:
    image: nginx
    container_name: nginx
    ports:
      - 80:80
    volumes:
      - ./server:/var/www
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - php

# db に関するものはいったん削除しています。(後で追加)

docker/php/Dockerfile を変更

PHP 7.3 を使いため、イメージ指定を変更しました。

また、apt-get でインストールするものにlibzip-devを追加しました。(ハマったポイントその1

  • docker/php/Dockerfile
FROM php:7.3-fpm # ← 7.2-fpm から変更
COPY php.ini /usr/local/etc/php/

RUN apt-get update \
    # ↓ libzip-dev を追加
    && apt-get install -y zlib1g-dev libzip-dev mysql-client \
    && docker-php-ext-install zip pdo_mysql

# Composer install
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
RUN php -r "if (hash_file('sha384', 'composer-setup.php') === '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
RUN php composer-setup.php
RUN php -r "unlink('composer-setup.php');"
RUN mv composer.phar /usr/local/bin/composer

ENV COMPOSER_ALLOW_SUPERUSER 1

ENV COMPOSER_HOME /composer

ENV PATH $PATH:/composer/vendor/bin

WORKDIR /var/www

RUN composer global require "laravel/installer"

後は、参考サイト通りに Laravel をインストールし、動作確認(Laravel と書かれたページが表示)まで実施します。

※ 次に行く前にいったん環境を停止しておきます。

$ docker-compose down

DB の環境構築

開発用 DB とは別にテスト用 DB を作成するため、また、テスト実行時にはちゃんとテスト用 DB へ向ける必要があるため、以下のサイトを参考にして進めました。

以下のルールで構築します。

  • 開発用 DB 名は study
  • テスト用 DB 名 study_testing

DB コンテナの定義を追記

MySQL は root ユーザーで運用したいため、MYSQL_USER,MYSQL_PASSWORDを削除しました。(※ 他ユーザーへの権限付与等の手間を省きました。手を抜きました。。。)

  • docker-compose.yml
version: '3'

services:
  php:
    〜 略 ~

  nginx:
    〜 略 〜

  db:
    image: mysql:5.7
    container_name: db-host
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: study # DB 名を変更
      # MYSQL_USER, MYSQL_PASSWORD は削除
      TZ: 'Asia/Tokyo'
    # ↓ utf8mb4_general_ci に変更
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    volumes:
      - ./docker/db/data:/var/lib/mysql
      - ./docker/db/my.conf:/etc/mysql/conf.d/my.cnf
      - ./docker/db/sql:/docker-entrypoint-initdb.d
    ports:
      - 3306:3306

server/config/database.php を変更

テスト用 DB への接続情報を追記します。

  • server/config/database.php
<?php

    'connections' => [

        'sqlite' => [
            'driver' => 'sqlite',
            'database' => env('DB_DATABASE', database_path('database.sqlite')),
            'prefix' => '',
            'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
        ],

        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_general_ci', // ← utf8mb4_general_ci に変更
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

        // ↑ の 'mysql' をキーとするものをコピーして、キーを変更します。 
        'mysql_testing' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_general_ci',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

server/.env を変更

DB_HOST が 127.0.0.1 だったので、DB のコンテナ名を指定します。(※ ハマったポイントその2

  • server/.env
〜略〜
DB_CONNECTION=mysql
DB_HOST=db-host # DB コンテナの名前を指定
DB_PORT=3306
DB_DATABASE=study
DB_USERNAME=root
DB_PASSWORD=root
〜略〜

server/.env.testing ファイルを作成

server/.env ファイルをコピーして作成します。テスト時はstudy_testingを見にいくようにします。

  • server/.env.testing
〜略〜
DB_CONNECTION=mysql_testing # ← テスト用の方を見るように指定します。
DB_HOST=db-host
DB_PORT=3306
DB_DATABASE=study_testing # ← テスト用 DB を見るように指定します。
DB_USERNAME=root
DB_PASSWORD=root
〜略〜

テスト用 DB 作成 SQL を用意(※ハマったポイントその3

コンテナ起動時にテスト用 DB が作成されるように SQL ファイルを以下の場所に作成します。

CREATE DATABASE IF NOT EXISTS study_testing CHARACTER SET utf8mb4;

コンテナを起動する

$ docker-compose up -d

これで、ローカルから MySQL Workbench 等を使って DB コンテナへアクセスすれば、studystudy_testing という DB が作成されている事が確認出来るかと思います。

マイグレーション

まだ各 DB の中身は空っぽなので、Laravel のマイグレーションを利用してテーブルを作ります。

アプリ(Laravel)のコンテナに入る

以下のコマンドでコンテナに入ります。

$ docker-compose exec php bash

開発用 DB に対してマイグレーション

$ php artisan migrate

これで DB study 内に users テーブル等が作成されます。

テスト用 DB に対してマイグレーション

--env=testing を指定する事で、server/.env.testing ファイルを読み込んで実行する事が出来ます。

$ php artisan migrate --env=testing

これで、開発用 DB と同様に、study_testing にもテーブルが作成されます。

以上で、おおかた環境を整える事が出来ました。

Eloquent 10 本ノックの本編は次回に書きたいと思います!

ハマった事

ハマったポイントその1

  • $ docker-compose up -d 実行時にエラー

最初、libzip-dev は追記せずにイメージだけを変更して、

$ docker-compose up -d

を実行したところ、以下のようなエラーが出ました。

〜略〜
checking for pkg-config... /usr/bin/pkg-config
checking for libzip... not found
configure: error: Please reinstall the libzip distribution
ERROR: Service 'php' failed to build: The command '/bin/sh -c apt-get update     && apt-get install -y zlib1g-dev mysql-client     && docker-php-ext-install zip pdo_mysql' returned a non-zero code: 1

PHP 7.3 では、このlibzip-devが必要なようですが、そのソースまでは辿り着けませんでした。。

ハマったポイントその2

  • $ php artisan migrateSQLSTATE[HY000] [2002] Connection refused

エラー内容の詳細

$ php artisan migrate

   Illuminate\Database\QueryException  : SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = database and table_name = migrations)

  at /var/www/vendor/laravel/framework/src/Illuminate/Database/Connection.php:664
    660|         // If an exception occurs when attempting to run a query, we'll format the error
    661|         // message to include the bindings with SQL, which will make this exception a
    662|         // lot more helpful to the developer instead of just the database's errors.
    663|         catch (Exception $e) {
  > 664|             throw new QueryException(
    665|                 $query, $this->prepareBindings($bindings), $e
    666|             );
    667|         }
    668| 

  Exception trace:

  1   PDOException::("SQLSTATE[HY000] [2002] Connection refused")
      /var/www/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  2   PDO::__construct("mysql:host=127.0.0.1;port=3306;dbname=database", "root", "root", [])
      /var/www/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  Please use the argument -v to see more details.

DB にアクセス出来ないよ、というエラーが発生しました。

この時、ローカルの MySQL Workbench からは接続が出来て、空っぽのstudyデータベースが見れる事を確認しました。 そのため、php コンテナから db-host コンテナへのアクセスする設定が何かしら足りないんじゃないかと疑いました。

そして、server/.env ファイルを見ると、

DB_HOST=127.0.0.1

となっていたので、これを DB のコンテナ名 db-host にする事で解決出来ました。

ハマったポイントその3

  • ./docker/db/sql に置いた SQL ファイルが実行されない問題

テスト用の DB を用意する前に Laravel の環境を構築していたわけですが、その際にはもちろん DB が構築されており、 docker/db/data にデータが作成されている状態でした。

以下の参考サイトにもあるように、データが永続化されている場合には、初めての起動時にしか ./docker/db/sql に置いた SQL は実行されないようです。

開発段階では、データを永続化したいですし、

今更ではありますが、開発用 DB とテスト用 DB を分けて考える場合、同一コンテナではなくて別々のコンテナを用意してあげればよかったのかもしれません。。

まとめ

  • やはり環境構築は骨が折れます。。(慣れるしかないかな!?)
  • Docker 使ってるおかげでいくらでも作って、消してが試せる!(ローカルが汚れる心配なし!)
  • 次回は、いよいよ Eloquent を触って行きます!

AliEatersOkinawa Meetup #1 で LT しました!

2019年4月24日(水)に、沖縄で初となる Alibaba Cloud Developers Community のミートアップを開催しました。

そこで人生2回目の LT をしましたので、その時の発表資料や、言い忘れた事(汗)、補足したい事などを書きたいと思います。

alieaters-okinawa.connpass.com

Alibaba Cloud についてはコチラ

jp.alibabacloud.com

いざ、LT!(発表資料)

発表時の風景

f:id:yudy1152:20190428111628j:plain
なんだかムーディ・・・

f:id:yudy1152:20190428111659j:plain
こんな姿まで撮られていたとは・・・

発表内容の概要

Alibaba Cloud の以下のサービスを使って 簡単な API を作ってみました。

作った API はユーザーを1件作成するものと、ユーザーリストを取得する、という簡単なものです。

発表途中で、ソースコードをデプロイし、デプロイされた API を Postman を使って実行する、といったことを行いました。

※ Postman は API を開発する時にとても便利なツールです。API を実行するだけでなく、テストも書けてしまうという優れものです。

www.getpostman.com

こんな感じで API を実行しました。

まずは、登録したいユーザーの情報を Body に JSON で定義して POST。

f:id:yudy1152:20190428115737p:plain
ユーザーを1件登録する POST

次に、リストを取得する GET して、先ほど作成したユーザーが取得されました。

f:id:yudy1152:20190428115813p:plain
ユーザーリストを取得する GET

伝えたかった事

正直サービスを触ったばかりですし、このサービスのこの機能が素晴らしいんだよ!といった事は言えません。。

しかし、いくつかつまづいたポイントがあるにしても、Serverless な API をサクッと作成+デプロイまで持っていけるのは便利だなと感じました。

API や NoSQL(Table Store)の勉強にも役立てる事ができますし、業務の中でも、プロトタイプをサクッと作りたい時なんかに有効活用できるんじゃないかと思いました。

まとめ

意識した事

  • 発声

まだ LT 2回目だし発表内容の質はこれから上げていこう、という甘えを抱きつつも、 せめて何を言っているか聞き取りやすいように、腹式呼吸を心がけました。(カラオケの時みたいに)

後日、LTを聞いてくれていた同僚に言葉が聞き取りやすかったか聞いてみたところ、「聞き取りやすかったよ〜」と言ってくれたので、ひと安心しました。 次からも意識していこうかな、と。

反省点(次に繋げる事)

  • 時間オーバー

10分の枠でしたが、デモの操作で時間がとられてしまい、枠を越えてしまいました。 デモ部分を動画にするなどの工夫が必要かなと思いました。

参考にしたサイト

asmsuechan.hatenablog.com

asmsuechan.hatenablog.com

Laravel の主キーで UUID を利用する時にハマった事、調べた事

起きた問題

テーブルの id カラムを UUID で登録できるようにしていました。

例えば、以下のような users テーブルがあるとします。

テーブル名:users

id name
62ce6b67-f8e3-4279-a2a1-0a23f3ca5730 はてな 太郎
830fd685-58dc-46b5-b7df-16e06f3c1b34 はてな 花子

単純にユーザーの一覧を取得したく、User::all() を実行してみたところ、id が上記のような 36 桁の文字列ではなく、以下のように id が欠落された状態で取得されてしまいました。

{
    "data": [
        {
            "id": "62",
            "name": "はてな 太郎"
        },
        {
            "id": "830",
            "name": "はてな 花子"
        }
    ]
}

これを解決すべく調べた事や、試した事などをまとめます。

原因

まさにコレだ!というものが Laravel のドキュメントに書かれていました。

Eloquentは主キーを自動増分される整数値であるとも想定しています。つまり、デフォルト状態で主キーは自動的にintへキャストされます。

Eloquent:利用の開始 5.8 Laravel の「主キー」

この仕様のため、各 id の最初の数字部分(83062)だけが取得された事になります。

私はコレで解決しました

上記の Laravel のドキュメントと同じ箇所にこのように書かれていました。

自動増分ではない、もしくは整数値ではない主キーを使う場合、モデルにpublicの$incrementingプロパティを用意し、falseをセットしてください。主キーが整数でない場合は、モデルのprotectedの$keyTypeプロパティへstring値を設定してください。

これを読んだ時に正直思った事は、 $incrementing$keyType の両方を設定しないといけないの?それとも片方だけでいいの?でした。

まずは、他の方のサイトも参考にしつつ、両方のプロパティを設定する事にしました。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public $incrementing = false; // ← 追加

    protected $keyType = 'string'; // ← 追加

    protected $fillable = ['id', 'name'];
}

これらのプロパティを追加後にデータ取得し直してみると・・・

{
    "data": [
        {
            "id": "62ce6b67-f8e3-4279-a2a1-0a23f3ca5730",
            "name": "はてな 太郎"
        },
        {
            "id": "830fd685-58dc-46b5-b7df-16e06f3c1b34",
            "name": "はてな 花子"
        }
    ]
}

といった具合に、今度は id の一部が欠落されることなく取得することが確認できました。

以上です。。。

だけだとつまらないので、最初に思った疑問「プロパティ両方必要?それとも片方だけ?」を少し試しました。

プロパティ両方必要?それとも片方だけ?

結果を先に言いますと、どちらか片方だけでも想定通りの動き(id がちゃんと文字列 36 桁で取得)をしました。

    // public $incrementing = false;

    protected $keyType = 'string';

でも

    public $incrementing = false;

    // protected $keyType = 'string';

データの登録、取得で問題なく動きました。

しかし、なんだか腑に落ちないのでもう少し調べてみました。

Model に定義されているデフォルト値

Laravel の Illuminate\Database\Eloquent\Model の中身を見てみたところ、以下のようにデフォルト値が設定されていました。

なので、どちらかのプロパティを設定して想定通りに動いたとしても、そのモデルのプロパティ値が気持ち悪い状態だな、と思いました。

$incrementing は false で、$keyType は int であれば id に自前で用意した数値を入れるんだな、という感じですが、 $incrementing は true だけど $keyType は string の時に、矛盾しているように感じました。

    /**
     * The "type" of the auto-incrementing ID.
     *
     * @var string
     */
    protected $keyType = 'int';

    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = true;

そこで、これらのプロパティが使われている(何かしらの条件で使われている)箇所を調べてみたところ、以下のような処理が実行されていることが分かりました。

Model クラスの 782 行目(私の環境では)

        // If the model has an incrementing key, we can use the "insertGetId" method on
        // the query builder, which will give us back the final inserted ID for this
        // table from the database. Not all tables have to be incrementing though.
        $attributes = $this->getAttributes();

        if ($this->getIncrementing()) {
            $this->insertAndSetId($query, $attributes);
        }

        // If the table isn't incrementing we'll simply insert these attributes as they
        // are. These attribute arrays must contain an "id" column previously placed
        // there by the developer as the manually determined key for these models.
        else {
            if (empty($attributes)) {
                return true;
            }

            $query->insert($attributes);
        }

$this->getIncrementing() は、モデルの $incrementing の値を返しています。

そのため、例え $keyType が string に設定していて動いていても、この insertAndSetId が実行されてしまいそうです。 これ以上先は追いきれませんでした・・・。

insertAndSetId が実行されても問題なく動くのですが、ムダな処理が実行されてしまいそうです。

なのでやはり id に UUID を登録したい時には、両方のプロパティを設定した方が良さそうに思いました。

補足

Laravel のマイグレーションファイルで、id を UUID が登録できるようにするために、以下のように定義してテーブルを作成しました。

Schema::create('users', function (Blueprint $table) {
    $table->string('id', 36)->primary();
    $table->string('name');
    $table->timestamps();
});

まとめ

今回はちょっとした事をきっかけに、フレームワークの中身を少しだけですが追ってみました。

ソースコードを目で見て追うだけでなく、実際にどう動いているのかを確認できたら面白いんだろうな、と感じました。

この辺は次なる課題として、、、もっと手を動かして実験して、まとめていけるようになりたいなと思います。

PHP で遊ぶための環境を構築してみた

はじめに

4月に入り、PHP のスキルを上げ、開発スピードを上げよう!というチーム目標を立てました。

勉強は各自に任せて、だと何だか気持ち的な負担が高くなるような気がしたので、皆で PHP の勉強会をするようになりました。

そこで必要になったのが実行環境です。

本を読むだけでなく、実際に手を動かして試せる環境が欲しいなと思ったので、Docker の勉強がてら Docker + Composer + PHPUnit を使った環境構築に挑戦してみました。

それぞれを簡単にざっくり言うと・・・


準備するもの

ローカル環境に以下のものがインストールされている事

  • Docker

    • ※ 私の環境のバージョンは 18.09.2 でした
  • ディレクトリ構成

- test-php
    - app
        - src
            Calculator.php
        - tests
            SampleTest.php
    - docker
        - php7
            Dockerfile
    - html
        index.php
      composer.json
      docker-compose.yml

以下は各ファイルの内容です。

  • docker-compose.yml
version: '3'

services:
  test-php7:
      build: docker/php7
      container_name: 'test-php7'
      ports:
        - '8000:80'
      working_dir: /workspace
      volumes:
        - ./:/workspace
        - ./html:/var/www/html
  • docker/php7/Dockerfile
FROM php:7-apache

RUN apt-get update && apt-get install -y \
    # phpunit をインストールする時に必要
    zip

# root でのインストールを許可する設定
ENV COMPOSER_ALLOW_SUPERUSER 1;

# composer をインストールする
RUN curl -s http://getcomposer.org/installer | php \
    && mv composer.phar /usr/local/bin/composer
{
    "require-dev": {
        "phpunit/phpunit": "^8.1"
    },
    "scripts": {
        "test": [
            "phpunit"
        ]
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    }
}

以下の PHP ファイルは単純なサンプルコードなので省略しています。

Calculator.php

<?php

namespace App\Src;

class Calculator
{
    /**
     * 足し算をする
     *
     * @param int $a
     * @param int $b
     * @return int
     */
    public static function add(int $a, int $b): int
    {
        return $a + $b;
    }
}

SampleTest.php

<?php

namespace App\Tests;

use App\Src\Calculator;
use PHPUnit\Framework\TestCase;

class SampleTest extends TestCase
{
    public function testSample()
    {
        // 期待する値
        $expected = 3;

        // テストしたい function を実行
        $result = Calculator::add(1, 2);

        $this->assertEquals($expected, $result);
    }
}


仮想環境の構築

コンテナを起動する

  • まずは、test-php ディレクトリでコンンテナを起動します。
    • -d はバックグラウンドで実行するためのオプションです。
    • これを実行する事で、PHP が実行でき、さらに Composer がインストールされた状態の仮想環境が構築されます。
$ docker-compose up -d

PHPUnit をインストールする

  • Composer を使って PHPUnit をコンテナ内にインストールします。
$ docker exec test-php7 composer update

※メモ docker exec を使い、コンテナ内のコマンド(composer update)を実行しました。

$ docker exec [コンテナ名] [コマンド]

テストを実行

  • ローカルからテストを実行する
$ docker exec test-php7 composer test -- app/tests

※ コンテナ内のコマンドにオプションを渡す時は、-- で一旦区切りを入れてから指定します。 この場合 $ docker exec test-php7 composer test app/tests はエラーになります。

テスト実行結果

> phpunit 'app/tests'
PHPUnit 8.1.2 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 247 ms, Memory: 4.00 MB

OK (1 test, 1 assertion)

これで、ローカルに PHP の実行環境を構築する事ができました。

今回の構築手順としては、

  1. PHP + Composer がインストールされた状態の仮想環境を Docker で用意する
  2. 仮想環境上の Composer を利用し、PHPUnit をインストールする

という大きく分けて2手間が必要な手順になりました。

Docker もまだまだ不明な点がたくさんあるので勉強しつつ、 今後の展望としては、DB(MySQL)のコンテナを用意して DB 接続を含めたテストを書いたりできるようにしてみたいなと思います。

ハマったポイント

1. PHPUnit をインストールする時に発生した警告!

コンテナの中に入って、手動で PHPUnit をインストールしてみた時に以下のようなメッセージが表示されました。

$ composer require phpunit/phpunit --dev
Do not run Composer as root/super user! See https://getcomposer.org/root for details

このメッセージが出ても中断される事なく PHPUnit はインストールされました。 どうやら root ユーザーでインストールを行う時に、セキュリティ的によろしくないよ、という事で警告が出ているようです。

www.itblog.jp

ではどう対応するのがベストなのか?

・・・

ローカルで動かすだけですし、、、すぐには分からなかったため、Dockerfile に以下の設定を付けました。 (メッセージが出なくなるだけで、根本的な解決ではないと思いますが)

# root でのインストールを許可する設定
ENV COMPOSER_ALLOW_SUPERUSER 1;

2. テスト実行時に Class Not Found!

PHP のクラスを作っている時には、IDEの補完が効いてくれていたので、つい一方のクラスからもう一方のクラスの参照が出来ているものと思ってしまいました。

実は以前にも同じような事にハマった事がありまして、エラーが出た時に「あぁあれね」と気付けたのですぐ解決できました。(一瞬ヒヤっとしましたが・・・)

※ 以前ハマった時の記事はこちら

解決するために、composer.json に以下のように autoload の設定を追加しました。

    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    }

まとめ

  • 環境構築は不慣れなため、ちょっとしたコマンドを用意するだけでも時間がかかってしまいました。
  • 時間がかかってしまった分、達成感を味わえたと思います。
  • ひとまず動くところまでできたけれども、この構成で良いのか?という不安があります。
  • まずは第一歩という事で、これからDBのコンンテナ用意したり、色々試して拡張して行けたらいいなと思います。