"OK Google" + Nature Remo で部屋の照明をコントロールする

ROXX エンジニアの匠平 (@show60)です

新型コロナの感染防止対策のため、弊社エンジニアメンバーも多分に漏れず自宅作業がメインとなっています。

せっかく長く過ごす自宅環境なのでもっと楽しくしていこうと思い、Nature Remo の API で遊んでみました。

経緯

もう2年前になりますが、ROXX 社内の LT 大会で1位をいただき、賞品にスマートリモコン Nature Remo をいただきました。

家電をアプリでコントロールできるという代物なんですが、当時持っていた家電が古すぎてほとんど使えていませんでした。家時間が増えたことも相まって、今年になってエアコンと照明を新調したため対応家電が増えて遊べる環境が整ってまいりました。

今回はスマホで「OK Google, 休憩入ります」と話すと部屋の照明を消してくれる遊びをやってみます。

環境

  • Android スマートフォン (Pixel3)
  • Nature Remo (第二世代)
  • 照明
  • IFTTT
  • AWS API Gateway

Nature Remo の設定、照明との連携は先に済ませておきます。

想定する動作

Google Assistant での音声リクエストから消灯までの流れは以下のようになります。

  1. Google Assistant で Webhook を叩く。
  2. Webhook が AWS API Gateway にリクエスト。
  3. AWS API Gateway が Nature Remo の Web API を叩く。
  4. Nature Remo が作動して照明を操作する。

準備

Nature Remo

Nature Remo の利用を開始する際にアカウントを作成します。以下の URL から同じアカウントでログインし、アクセストークンを発行します。

アクセストークンは再表示できないので忘れずにメモっておきます。

https://home.nature.global/

Nature Remo の OpenAPI 情報はこちらにまとまっています。

https://developer.nature.global/

家電の ID を取得してみます。

エンドポイントは https://api.nature.global/1/appliances で、先ほどのアクセストークンを付けてリクエストします。

f:id:show-hei:20201223030429p:plain

登録されている家電の情報を取得してきました。アイリスオーヤマの照明であることが分かりますね。

ここで表示される家電の ID をメモっておきます。また、家電の操作の情報も記述されているので確認しておきます。

[
    {
        "id": "{家電 ID}",
        "device": {},
        "model": {},
        "light": { // ① 照明として登録されている場合、このキー名になっている
            "buttons": [
                {
                    "name": "on", // ②
                    "image": "ico_on",
                    "label": "Light_on"
                },
                {
                    "name": "off", // ②
                    "image": "ico_off",
                    "label": "Light_off"
                },
                {
                    "name": "on-100", // ②
                    "image": "ico_light_all",
                    "label": "Light_all"
                },,,,
            ]
        }
    }
]

照明を操作するエンドポイント URL は https://api.nature.global/1/appliances/{家電 ID}/①?button=② のような構成になります。

今回の場合だと https://api.nature.global/1/appliances/{家電 ID}/light?button=off となります。

接続している家電の情報を取得するだけでなく、それらの家電を操作する API も存在しており、今回は照明を消灯する操作の API を使用することにします。

API Gateway

AWS API Gateway から [API を作成] > REST API [構築] を選択します。任意の API 名を入力し作成します。

※ 今回の API は IFTTT で使うのみで他者には共有しないため、プライベート (VPC からのみアクセス可) にはしていません。

照明の操作は POST リクエストのため、[リソース] > [アクション] から POST メソッドを選択します。

メソッドの設定は以下のように行います。

  • 統合タイプ: HTTP
  • HTTP メソッド: POST
  • エンドポイント URL: https://api.nature.global/1/appliances/{家電 ID}/light?button=off
  • HTTP ヘッダー: Authorization : Bearer {アクセストークン}

設定したら再度 [リソース] > [アクション] を開き、[API のデプロイ] を選択します。デプロイするステージなど適宜入力すると API の完成です。

IFTTT

API Gateway で作った URL を IFTTT の Webhook に叩かせましょう。

IFTTT の create から新規の Applet を作っていきます。

ifttt.com

以下が Applet の作成手順になりますが、シンプルな UI であまり迷わずに直感的に作ることができると思います。

Applet 作成: "If This" 項目

  1. If ThisGoogle Assistant を選択。
  2. trigger として Say a simple phrase を選択。
  3. trigger 設定: What do you want to say?休憩入ります と入力。(句読点は入れないよう注意)
  4. trigger 設定: What do you want the Assistant to say in response?ゆっくり休んでね と入力 (ここは空欄でも可)
  5. trigger 設定: Language で Japanese を選択し、Create trigger をクリック。

Applet 作成: "Then That" 項目

  1. Then ThatWebhooks を選択。
  2. action として Make a web request を選択。
  3. web request 設定: URL に API Gateway で作成した API を入力。
  4. web request 設定: Method -> POST, Content Type -> application/json を選択し、Create action をクリック。

完成

以上の工程で完成となります。

手持ちのスマホや Google home などで「OK Google, 休憩入ります」と話せば「ゆっくり休んでね」と応えてくれて消灯までしてくれるようになりました。

さいごに

Nature Remo を声で動かすだけであればシンプルに IFTTT だけで実装できますが、そこから伝搬して他の処理につなげていけるのは自分で実装する楽しさでもあります。

ここから拡張して休憩時間の終わりになると、アラーム代わりに照明を点けてくれて、ついでに Spotify API 経由で音楽を流してくれる、みたいにすると午後も楽しくお仕事できそうです。

Laravelの環境構築が一瞬で終わった

この記事は個人ブログの転記です。

Laravelの環境構築をしようとしたら、一瞬で終わって感動したという感想メモです。
公式サイトに書いてあること以上のことは出ません。

環境構築手順

今までは、Laravelの環境構築のために、

  1. PHPを入れる
  2. Laravelコマンドを入れる
  3. Laravelをインストールする
  4. データベースとかも準備する
  5. 必要であればnodeやらyarnやらも準備する
  6. ローカルが汚れないように仮想環境を準備する

みたいな面倒なやつが必要でした。
そして、ちょいわけあって新規の環境を用意する必要があり、「面倒だなー」と思いながら環境構築をしたのですが、一瞬で終わってびっくりしました。

今やLaravelの環境構築は3ステップで終わります。

  1. Docker for Mac を入れる
  2. curl -s https://laravel.build/example-app | bash
  3. ./vendor/bin/sail up

.env.example のコピーや artisan key:generate は必要ありません。
composer install も不要です。
PHPを入れる必要すらありません。

この sail がLaravel8から入った新機能のようでした。

できる環境

Laravelの開発環境を作るライブラリのようです。

デフォルトでは

  • PHP8.0
  • MySQL8.0
  • Redis
  • MailHog
  • node ( npm )

の環境が出来上がります。
デフォルトではコメントアウトされていますが、Laravel Dusk のために selenium のコンテナも建てられます。
ちなみにPHPバージョンは7.4でも作れるようです。

dockerがわかる人であれば、 docker-compose.yml を読めば「ああなるほどね」となると思います。

操作体系

docker環境となると、「dockerわからん人どうするんじゃい!」という話が上がりそうですが、そこらへんもわかりやすくまとまってました。
./vendor/bin/sail がエントリポイントとなり、色々とできるようです。

やりたいこと 今までのコマンド sailでのコマンド
環境立ち上げ docker-compose up / php artisan serve sail up
環境破棄 docker-compose down / ( serveを終了する ) sail down
PHPコマンド php info.php sail php info.php
Composer操作 composer update sail composer update
npm操作 npm i sail npm i
Artisanコマンド php artisan make:model User sail artisan make:model User
Laravel Dusk php artisan dusk sail dusk
Laravel Tinker php artisan tinker sail tinker
Shellコマンド docker-compose exec sh sail shell

だいたいのことはできそうです。 ./vendor/bin/sail が起点になるので、エイリアスシンボリックリンクを用意すると良いと思います。

まとめ

個人開発でサクッと環境構築するにはとても便利そうです。
プロダクションで使用する場合は、もうちょっとコンテナの中を覗いたりする必要がありそうです。

PHP 8.0 の新機能を試してみよう

この記事は個人ブログと同じ内容です

www.ritolab.com


2020 年 11 月 26 日に PHP 8.0 が正式リリースとなりました。

今回は PHP 8.0 の新しい機能を使ってみようと思います。

実行環境

Docker でコンテナを作成して PHP 8.0 を動作させます。

今回は適当な場所に php80 というディレクトリを作成してそこで行っています。

php80/
└─ src/
    └─ index.php

以下のコマンドでコンテナを起動

docker run \
    -v /path/to/php80/src:/var/www/html \
    -p 8080:80 \
    -d --name test-php80-container php:8.0-apache

起動したコンテナの PHP バージョン

$ php -v
PHP 8.0.0 (cli) (built: Dec  1 2020 03:24:11) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies

Named Arguments(名前付き引数)

関数のデフォルト値の一部を変更したい時とかに、この名前付きの引数を使用する事で引き数の指定が簡略化されます。

https://wiki.php.net/rfc/named_params

例えば以下のような関数があったとして

<?php

function sample(int $value = 1, int $value_int = 1, string $valueString = 'hoge')
{
    $number = $value + $value_int;

    return "{$valueString}: {$number}";
}

sample();
// => hoge: 2

3つの引き数にはデフォルト値が指定されているので、この関数を使用時に引き数を渡さなければそれぞれデフォルト値が代入されて、文字列 hoge: 2 が返ります。

この時に例えば「最後の引き数だけを指定したい」といった場合には 3 つ全ての引き数を指定してあげなければなりませんでした。

<?php

// 第三引数だけを変更したいけど第一引数と第二引数も渡してあげる必要がある
sample(1, 1, 'foo');
// => foo: 2

これを名前付き引き数を使用することで、第三引数のみを指定する事ができるようになりました。

<?php

// 第三引数の変数名を指定して引数として渡す
sample(valueString:'abc');
// => abc: 2

「最後の引数」というのは例なので、例えばこの関数の第二引き数のみを指定する事も可能です。

<?php

// 第二引数の変数名を指定して引数として渡す
sample(value_int: 10);
// => hoge: 11

複数指定することもできます。

<?php

// 複数指定する
sample(value_int: 5, valueString:'xyz');
// => xyz: 6

関数で定義されている引数の順番でなくてもいけます。

<?php

// 指定順がバラバラでもいける
sample(valueString:'AAA', value_int: 3, value: 4);
// => AAA: 7

Constructor property promotion(コンストラクタのプロパティ昇格)

クラスをインスタンス化する際にコンストラクタでセットするプロパティへの代入と定義が簡略化できるようになりました。

https://wiki.php.net/rfc/constructor_promotion

例えば、Member クラスをインスタンス化する際に id と name を渡すとすると、これまでは以下の記述で行っていました。

<?php

class Member
{
    public int $id;

    public string $name;

    public function __construct(int $id, string $name)
    {
        $this->id   = $id;
        $this->name = $name;
    }
}
  1. プロパティ(メンバ変数)を宣言する
  2. コンストラクタでそれぞれ id と name をプロパティへ代入する

これが PHP 8 からは以下のように簡略化して記述することができるようになりました。

<?php

class Member
{
    public function __construct(
        public int $id,
        public string $name,
    ) { }
}

コンストラクタの引数のところでプロパティを宣言しているようなイメージですね。

これで「プロパティの宣言」「コンストラクタで受け取る引数の型・順番の指定」「受け取った値をプロパティへ代入する」をまとめて行っている感じ。なんかすごいな。

Union types(Union型)

型宣言を複数記述することができるようになりました。

https://wiki.php.net/rfc/union_types_v2

関数の引数・戻り値、クラスのプロパティに型宣言が行えますが、これまでは型が1つに確定している場合のみ(もしくは ? で |nullを表現)記述できる状態でした。PHP 8 からは、複数の型を記述できるようになりました。

<?php

class Sample
{
    public int|float $a;

    public function set(int|float $a)
    {
        $this->a = $a;
    }

    public function get(): int|float|null
    {
        return $this->a;
    }
}

Match expression(match式)

match 式が追加されました。

switch 式と似ているけれど、より洗練された感じになっています。

<?php

$number = 1;


/* switch 式 */
switch ($number) {
    case 1:  $result = 'one';   break;
    case 2:  $result = 'two';   break;
    case 3:  $result = 'three'; break;
    default: $result = 'other';
}

echo $result;
// => one


/* match 式 */
$result = match ($number) {
    1       => 'one',
    2       => 'two',
    3       => 'three',
    default => 'other',
};

echo $result;
// => one

match が switch と違う点

  • 値を返す
  • 比較が厳密( == ではなく === )に行われる
  • フォールスルーしないので break を書かなくて良い

シンプルな分、間違いが起こりにくい印象を受けました。 比較部分を軽視してて予期せぬ挙動したりとか、単純に break 付け忘れて意図していない挙動するとか、そういうのは無くせそうですね。

Nullsafe operator(Null 安全演算子

JavaScript 等ではお馴染みな Null 安全演算子PHP でも使えるようになりました。

<?php

$country = $session?->user?->getAddress()?->country;

「?」をアローの手前に挟むやつです。

<?php

class User
{
    private int $id;

    private string $name;

    public function __construct(int $id, string $name)
    {
        $this->id   = $id;
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }
}

class Member
{
    public User|null $user = null;
}

$member = new Member();


$member->user->getName();
// => Fatal error: Uncaught Error: Call to a member function getName() on null

$member->user?->getName();
// => null

上記の例では、Member クラスの $userに User クラスがセットされていない場合、getName() を参照できないので Fatal error が発生しますが、 Null 安全演算子で記述することで、$member->user が null の場合は getName() を参照せずに null を返してくれるようになります。

str_contains()

対象文字列に、指定した文字列が含まれているかどうかを調べる str_contains() 関数が追加されました。

https://www.php.net/manual/ja/function.str-contains.php

<?php

$target = 'I love Grape,Apple,Orange,Strawberry and Cherry.';

$search = 'Orange';

str_contains($target, $search);
// => true

これまで strpos とか preg_match を使っていたことを考えたらシンプルになって良いかなと思います。

str_starts_with() / str_ends_with()

以下2つの関数も追加になっています。

<?php

$target = 'I love Grape,Apple,Orange,Strawberry and Cherry.';


$start = 'I';

str_starts_with($target, $start);
// => true

$end = 'Cherry.';

str_ends_with($target, $end);
// => true

他にもいろいろ

ここで取り上げたもの以外でも様々な変更点があるので PHP マニュアルを参照してみてください。

PHP: PHP 7.4.x から PHP 8.0.x への移行 - Manual

日々の「ふりかえり」のために、タスクでやることやったことの履歴を残す

agent bank 開発チームのさかもとです。

最近チームで取り組み始めたアクションで、とても効果を感じれたことがあったので書きます。

ふりかえり大事

仕事をしていく上で振り返りを行うことは、とても重要なことです。

なぜ「ふりかえり」を行うのか。
個人やチームの成果が出るまでの過程をふりかえってみることで、良くなかった点、改善できる点の改善を行い、良かった点はそれを続けられるように強化をしていきます。

ふりかえりは、チームや個人の反省会ではなく、次を良くするために行われるものです。  

はなしたいこと

slackのワークフローを使って、タスク着手時に「やること宣言」と「やったこと履歴」残すのが、ふりかえりをする際やチームでタスクを進めていく上でとても効果を感じれた。

きっかけ

チームの現状説明(タスクの進め方や、振り返りのタイミング)

agent bank開発チームでは現在こんな感じで開発を進めています

  • フルリモート
  • スクラム開発(スプリントの単位は1週間)
  • discordのボイスチャットつなぎっぱで開発
  • ATLASSIANのJiraバックログ管理
  • 一日の朝・昼・夕のタイミングでチーム内の現状把握の場を設定している

リモートワークを円滑にすすめるための実践とツール - ROXX(旧SCOUTER)開発者ブログ こっち見るともう少し詳しくどんな感じで進めてるかわかるかと思います。

ふりかえりであがったproblem

一日に最大3回チームの現状把握の場を設定して、「計画通りに進めているか?」「想定外の事態が発生していないか?」などのチェックをして、計画の修正やアサインの調整などを行っているんですが、先日のスプリントの振り返りでこのようなproblemが上がりました。

f:id:skmtko:20201111142339p:plain
進捗の遅れから想定外のタスク、隠れた仕様・懸念点などを洗い出したい

背景としては、タスクに対してのポイントの付け方を以下のように、ストーリーにポイントが付いていればポイント付けを行い、タスクにポイントがつくのはストーリーに紐付かない場合のときだけにしているのが、関係してそうでした。

f:id:skmtko:20201111144255p:plain
タスクへのポイントの付け方

※リファインメントの際に、ストーリーにポイント付を行い、プランニングの際にポイントが紐付かないタスクにポイント付を行っています

結果として1日の中で現状把握の場を設けてはいるが、実際にタスクにかけるつもりだった時間と、実際に掛かった時間が見えずらい状態が続いていました。

深く記憶に残るような、よほどの大事故でもない限り、一日のふりかえりで振り返ることは難しいですね。「きのうの晩ごはん」を思い出すのも難しくなってしまった我々にとっては、タスクの中でいくつも発生する些細な出来事や、タスクの開始時間、経過時間を記憶だけですべて鮮明に振り返るのはおそらく不可能です。

「想定よりもタスクが大きかったが検知できるようになる」をゴールとしたアクションを出そうといういことで、今回の「slackにやること宣言する」をすることになりました。

やったこと

やったことはシンプルでこんな感じのことをやっていました

  1. チームのタスク進捗用のslackチャネルを作成した

  2. チームメンバーはタスク着手時に、slackワークフローを用いて、やること、着手開始時間、完了目標時間などを投稿

  3. タスクを進めながら、必要に応じて都度進捗をスレッドに投稿する

  4. タスクが完了したら、完了したことを書く

  5. 区切りの時間(昼休み後の昼会や夕会)の振り返りに、活用する

実際にはこんな感じ

ワークフローの入力

f:id:skmtko:20201111150053p:plain
ワークフロー入力のイメージ

終了時刻は任意にしているが、開始から終了までに2時間以上かかっていた場合には、ふりかえりの際に、なぜ時間超過してしまったのかを話すトリガーにしている。

ワークフロー投稿の活用イメージ

f:id:skmtko:20201111152909p:plain
こんな感じでスレッド作ってふりかえれるようにする

終了したことと、終了した時刻だけは必ずわかるようにして、作業中のスレッドは各自自由に投稿するようにしている

今のところ、進捗の投稿を行うかどうかは各自に任せて自由に投稿している。
 例)「〇〇完了した」「✗✗が判明したのでタスクを追加起票した」「△△が複雑で延長した」

他のタスクを実施しているメンバーが、共有や依頼を投げたりにも使ってみている
 例)「Aのタスクで演る予定だったけど、こちらで〇〇の実装もお願い!」

うれしかったこと

みんなが嬉しかったこと

実際に2週間運用してみたところ、メンバーの全員が好感触でした。こんなかんじ。

  • 忘れがちになタスク着手時の時系列を残せてふりかえれるのがよい
  • デザインタスクをひとりでやることが多いが、共有できて安心する(開発チームにデザイナーも一人います)

ふりかえりの材料が目に見える状態で残っているというのが、とても大きかったみたいです。
何もわからないことは、怖いですもんね。
slackのワークフローで最初に投稿するようにしたのが、着手時の投稿ハードルを下げたんですかね。着手宣言のアクションを初めて1日後にワークフローをすぐ作ってくれたメンバーに感謝です。

他の人はしらんけど俺はこれがうれしかったこと

ふりかえりの際の材料になることはもちろんとても良かったんですが、その他にも個人的に、以下のような嬉しさがありました。

  • 他のメンバーが何をやっているかがすぐ分かる
  • 着手時に何をすべきなのかが整理できる
他のメンバーが何をやっているかがすぐ分かる

ボイスチャットつなぎっぱといえども、皆がずっと話しっぱで、逐一何しているか話しているわけではありません。(たまにずっと喋りながらタスクを進めるメンバーがいたり、全員でモブですすめるみたいなケースはありますが...)

おおよそ誰がどのタスクを着手するかをわかるようにしていますが、計画通りに行くとは限りません。
誰が何をやっているのかすぐにわからない状態では、次何をするかを迷ってしまいがちでした(わたしが)

着手宣言と経過を見えるようにすることで、その日のたすくをより効率的にすすめるための判断がやりやすくなりました。
(どんどんタスクをすすめるべきか、ほかメンバーのサポートに入るべきか、計画を修正するべきかなどの判断がやりやすくなったとおもう)

 着手時に何をすべきなのかが整理できる

着手時に完了目標時間を入れることによって、タスク開始時に再度タスクのプランニングを行うことになります。
可能ならば30分とか10分単位で完了できることを整理し直すことで、タスクの中で明確に何をやるべきなのかが見えてくると思います。
タスクが大きすぎる場合は、ワークフローで投稿する際に宣言しなくとも、スレッド内に「次の30分で〇〇を終わらせる」などと宣言してタイムボックスを設定すれば、細かくやることを整理も可能でさらには、ふりかえりの際に、より詳細な出来事として振り返ることが可能です。

あと、タスクに紐づく共有をできたのも良かった

個人的には、15分スプリント*1にトライしてみたい感あったんですが(やれるイメージが、まだあまり浮かばないけど)、とりあえずはslackの「やること宣言」だけでも今の顕在化した課題は解決できそう。

さいごに

「slackにタスクのやることやったことの履歴を残す」といったシンプルなアクションだったんですが、現在のチームとしてはすごく効果を感じることができました。このアクションが、これからのふりかえり、改善のために役立って行くことと思います。

また続けていくうちに、違った方法になるかもしれませんが、もしそうなるとしたらチームがより良くなるための動きの一つだと思います。

今回のアクション自体も、実行の1日目から「slack投稿先の変更」「ただのslack投稿→ワークフローで投稿」「ワークフローの入力項目変更」などと、ふりかえりを重ねることに、今のチームがより良くできるように変化を重ねていました。

この調子で、さらにプロダクトをより良くできるチームになれるように、開発を進めていきたいところです。

おまけ

弊社では開発チームのメンバーをとても募集しています。 ちょっとでも興味があって、話くらいなら聞いてやるか!という方は私のtwitterにでも連絡くれると、とてもありがたいです。

こちら さかもとこうへい (@12kohe3i) | Twitter

Next.jsでStrict CSPを実現する

この記事は個人ブログと同じ内容です。

kotamat.com

業務では Nuxt.js を使用しているのですが、CSP の nonce の対応ができておらず、Next.js だとどうなんだろうと触ってみたらメチャクチャ簡単に設定できる事がわかったので、その方法について紹介します。

やりたいこと

CSP の対応のなかで Google が提唱しているやり方としてStrict CSPというのがあります。 これは簡単に言うと下記のように CSP の設定を行えば モダンなブラウザにおいて プロダクションレベルのセキュリティーが担保できるよというものです。

Content-Security-Policy:
  object-src 'none';
  script-src 'nonce-{random}' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
  base-uri 'none';

このなかで大事なものとしては strict-dynamic が挙げられます。これは、CSP Level3 から導入されたディレクティブで、ほかのドメインホワイトリストは無視した上で、'nonce-{random}'で指定された <script> タグのスクリプトを許可し、かつその先で生成された別のスクリプトに関しても同様に許可するというものです。

この挙動により、開発者はトップレベルのスクリプトに関して nonce をつければ良くなり、攻撃者はその nonce を奪えない限り XSS ができなくなるため、ホワイトリスト形式での設定より安全になるということで、非常に便利に対策ができるようになります。

注意点としては、モダンなブラウザでなければ strict-dynamic は解釈してくれないので、そういったブラウザでは脆弱性は防ぎきれないというところになります。 また Safari はまだ対応していません。(モダンなブラウザではないということですかね…?)

https://caniuse.com/?search=strict-dynamic

対応方法

/pages/_document.tsx にて、nonce の生成と、Head,NextScript への反映を行います。

import { randomBytes } from "crypto";
import Document, { Html, Head, Main, NextScript } from "next/document";

type WithNonceProp = {
  nonce: string;
};
export default class MyDocument extends Document<WithNonceProp> {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    const nonce = randomBytes(128).toString("base64");
    return {
      ...initialProps,
      nonce,
    };
  }
  render() {
    const nonce = this.props.nonce;
    const csp = `object-src 'none'; base-uri 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http: 'nonce-${nonce}' 'strict-dynamic'`;

    return (
      <Html>
        <Head nonce={nonce}>
          <meta httpEquiv="Content-Security-Policy" content={csp} />
        </Head>
        <body>
          <Main />
          <NextScript nonce={nonce} />
        </body>
      </Html>
    );
  }
}

これだけです。

Next.js では、SSR では当然のこと、SSG する際にもこの処理を適応してくれるため、例えば下記のようなページコンポネントがあったとしても

import { GetStaticProps } from "next";
import { FC } from "react";

type Props = {
  name: string;
};
const SSG: FC<Props> = ({ name }) => {
  return <div>{name}</div>;
};

export default SSG;

export const getStaticProps: GetStaticProps<Props> = async (ctx) => {
  return { props: { name: "hoge" } };
};

next export 後のファイルに nonce がちゃんと反映されています。 ただ、当然ですが、SSG だと nonce の本来の持つべき役割である 「リクエストごとに生成し直す」ことができないため、SSG を前提としたプロダクトでは攻撃者の攻撃にさらされる危険性があります。

まとめ

Next.js にて nonce を用いた StrictCSP を実装する方法を紹介させていただきました。 モダンではないブラウザが対応してくれれば、相当対策をしやすくなるので、ブラウザの対応に期待したいですね。

余談

Nuxt.js では、この辺で HTML タグが直書きされていることによって nonce を入れる余地がないため、今回のやりたいことはできない感じになっています。 その代わり inlineScript の hash 値を CSP に埋め込む形で実装されているため、アプリケーションのソースコードに閉じる限りはそれなりに硬い対策になるようになっているようです。

Laravel 8 で刷新された ModelFactory でテストデータを簡単に作成する

こちらのブログは個人ブログと同じ内容です

www.ritolab.com


Laravel には ModelFactory(モデルファクトリ)といって Eloquent のモデルを使って簡単に開発用のデータやテスト時のデータを作成できる仕組みがあります。

このモデルファクトリが Laravel 8 で刷新されなかなか良い感じになっていたので、今回は実際に使用してみたいと思います。

モデルとテーブルの作成

まずは ModelFactory を作成するために、モデルと DB のテーブルを作成しつつ仕様を決めておきたいと思います。

今回は、Book モデルを作成して、それについてのモデルファクトリを定義しようと思います。

まずは books テーブルのマイグレーションを作成

# books テーブル マイグレーション作成
php artisan make:migration create_books_table --create=books

マイグレーション(カラム)は以下のように定義しました。

database/migrations/xxxx_xx_xx_xxxxxx_create_books_table.php

<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateBooksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        Schema::create('books', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('author');
            $table->date('bought_at')->nullable();
            $table->date('started_at')->nullable();
            $table->date('ended_at')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down(): void
    {
        Schema::dropIfExists('books');
    }
}
  • id
    • 主キー
  • title
    • 本のタイトル
  • author
    • 著者
  • bought_at
    • 購入した日
  • start_at
    • 読み始めた日
  • end_at
    • 読み終わった日
  • created_at
    • 作成日
  • updated_at
    • 更新日

続いて、モデルを作成します。

# book モデル作成
php artisan make:model Book

app/Models/Book.php

<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    use HasFactory;
}

今回は factory を試すことがメインのため、特に事前の設定は不要なので生成されたままで記述は変えていません。

この Book モデルからテストデータについて考えると以下のような感じで作れたら良いなと考えます。

  • 本のタイトルと著者は入っていれば何でも良い
  • 購入日・読み始めた日・読み終わった日はそれぞれが関連するので、state で予め定義しておいて簡単にデータを作成できるようにしたい

前者はランダムで良いので Faker で作成すれば良いですね。

後者は、例えば「読み始めた日」に日付が入っているということは、当然既に購入済み(レンタルは想定しません)なので「購入日」にも日付が入っている必要がある。みたいなことなので、ここは state を定義しておいて、呼び出すだけで簡単にテストデータを作成したいねというお話です。

このあたりまで決めたら、factory の実装に移れそうです。

Laravel 7 までのモデルファクトリ

Laravel 7 までの場合をおさらいすると、以下のように定義していました。

laravel7/database/factories/BookFactory.php

$factory->define(Book::class, fn(Faker $faker)  => [
    'title'  => $faker->sentence(),
    'author' => $faker->name(),
]);

これを factory 関数を使ってテストデータを作成していました。

/** @var \App\Models\Book $book */
$book = factory(Book::class)->create();

factory state(ファクトリステート)と呼ばれる、データの値を予め定義しておく場合も、以下のように記述していました。

laravel7/database/factories/BookFactory.php

$now = CarbonImmutable::now();

/**
 * 購入済み
 */
$factory->state(Book::class, 'bought', [
    'bought_at' => $now
]);

/**
 * 読んでいるところ
 */
$factory->state(Book::class, 'started', [
    'bought_at'  => $now->subDay(),
    'started_at' => $now,
]);

/**
 * 読み終わった
 */
$factory->state(Book::class, 'ended', [
    'bought_at'  => $now->subDays(2),
    'started_at' => $now->subDay(),
    'ended_at'   => $now,
]);

そしてテストデータ作成時に state()(複数ある時は states() )メソッドをチェーンさせて呼び出します。

/** @var \App\Models\Book $book */
$book = factory(Book::class)->state('ended')->create();

ModelFactory 実装

ではここから Laravel 8 にてモデルファクトリを定義していきたいと思います。

まずは、artisan コマンドで生成した時点での BookFactory を見てみます。

laravel8/database/factories/BookFactory.php

<?php

namespace Database\Factories;

use App\Models\Book;
use Illuminate\Database\Eloquent\Factories\Factory;

class BookFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Book::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            //
        ];
    }
}

なんということでしょう。ModelFactory がクラス化されています。Laravel 8 になって刷新されたというのは、こういうことだったのですね。

では実装します。まずは基本部分から

laravel8/database/factories/BookFactory.php

public function definition(): array
{
    return [
        'title'  => $this->faker->sentence(),
        'author' => $this->faker->name(),
    ];
}

definition() メソッドに値のデフォルト値を定義することで、テストデータを作成できます。

利用側はこんな感じ

/** @var \App\Models\Book $book */
$book = Book::factory()->create();

factory 関数ではなく、モデルから static に呼び出すことでテストデータを作成していますね。

続いてステートの定義をしていきます。

laravel8/database/factories/BookFactory.php

/**
 * 購入済み
 *
 * @return BookFactory
 */
public function bought(): BookFactory
{
    return $this->state(fn ()  => [
        'bought_at' => CarbonImmutable::now()
    ]);
}

/**
 * 読んでいるところ
 *
 * @return BookFactory
 */
public function started(): BookFactory
{
    $now = CarbonImmutable::now();

    return $this->state(fn ()  => [
        'bought_at'  => $now->subDay(),
        'started_at' => $now,
    ]);
}

/**
 * 読み終わった
 *
 * @return BookFactory
 */
public function ended(): BookFactory
{
    $now    = CarbonImmutable::now();

    return $this->state(fn ()  => [
        'bought_at'  => $now->subDays(2),
        'started_at' => $now->subDay(),
        'ended_at'   => $now,
    ]);
}

メソッドを作成することで、それぞれの state を定義することができるようになっています。

こちらも、テストデータ作成時にメソッドをチェーンさせて呼び出します。

/** @var \App\Models\Book $book */
$book = Book::factory()->bought()->create();

state に引数を渡す

ModelFactory がクラス化され state がメソッド化されたことで、state 指定時に引数を渡せるようになっています。

laravel8/database/factories/BookFactory.php

/**
 * 読んでいるところ
 *
 * @param int $startDays 読み始めるまでの日数
 *
 * @return BookFactory
 */
public function started(int $startDays=1): BookFactory
{
    $now = CarbonImmutable::now();

    return $this->state(fn (array $attributed)  => [
        'bought_at'  => $now->subDays($startDays),
        'started_at' => $now,
    ]);
}

/**
 * 読み終わった
 *
 * @param int $endDays   読み終わるまでの日数
 * @param int $startDays 読み始めるまでの日数
 *
 * @return BookFactory
 */
public function ended(int $endDays=1, int $startDays=1): BookFactory
{
    $end    = CarbonImmutable::now();
    $start  = $end->subDays($endDays);
    $bought = $start->subDays($startDays);

    return $this->state(fn (array $attributed)  => [
        'bought_at'  => $bought,
        'started_at' => $start,
        'ended_at'   => $end,
    ]);
}

今回の Book モデルで具体的に説明すると、これまで state では単純に「購入日」「読み始めた日「読み終わった日」にそれぞれ 1 日違いで値を挿入し、データとして整合性がとれた状態のみを作り出していました。

上記の場合では、引数に日数を与える事にとって、任意の日数差にてデータを作成できるようにしています。

state は本来、決まったデータのパターンを定義するものと私は理解しているので、基本的には可変的に設定するシーンは多くはないと認識はしているものの、それでもあるケースのテストを行いたい場合に、たまにこういうの欲しくなる事もあります。

もちろん、make() や create() の引数で値を指定すれば済む話ではあるのですが、テストやテストデータ作成の数が多くなってくると、この辺の記述が結構膨れてきて設定の意図がわかりづらくなり、他の値の設定とかも入ってきて可読性が下がるのが微妙だなと思っていました。

なので外から値を投げられるのは結構うれしいなと感じました。

// 購入日から 3 日後に読み始め、読み始めてから 10 日後に読み終わる
/** @var \App\Models\Book $book */
$book = Book::factory()->ended(10, 3)->create();

今回の state の場合はサンプル的に作っているのでニーズから作成したものではないですが、例えば、普段は日にちさえ入っていればそれで良いのだけれど、あるテストケースで「いつ読んだ」とか「読むのに何日以上かかった」あるいは日付範囲で絞る。みたいな検索・抽出の機能のテストを書く際のテストデータがほしいなって思った時とかに使えるかな。

state() に渡すクロージャの引数

ちなみに、$this->state() に渡すクロージャには、引数として array $attributed が渡りますが、ここには、この state が実行されるまでに設定された値が入ってきます。

ですので definition() で作成されたデータ + この state が実行されるまでに実行された state のデータが渡ってくる事になります。

例えば以下を実行した場合に、

Book::factory()->bought()->ended()->create();

ended() の ファクトリステートで渡ってくる array $attributed は以下のようになります。

$attributed => Array
(
    [title] => abcd
    [author] => ABCD
    [bought_at] => 2020-11-03 10:14:17
)

Laravel 8 と Laravel 7 以前のモデルファクトリの違い

これまで部分的にしかコードを記していなかったので、最後に Laravel 8 と Laravel 7 以前のモデルファクトリの違いを全体感で俯瞰できるように、今回作成したそれぞれのコードをここで表示します。

Laravel 7 以前の BookFactory

<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Book;
use Carbon\CarbonImmutable;
use Faker\Generator as Faker;

$factory->define(Book::class, fn(Faker $faker)  => [
    'title'  => $faker->sentence(),
    'author' => $faker->name(),
]);

$now = CarbonImmutable::now();

/**
 * 購入済み
 */
$factory->state(Book::class, 'bought', [
    'bought_at' => $now
]);

/**
 * 読んでいるところ
 */
$factory->state(Book::class, 'started', [
    'bought_at'  => $now->subDay(),
    'started_at' => $now,
]);

/**
 * 読み終わった
 */
$factory->state(Book::class, 'ended', [
    'bought_at'  => $now->subDays(2),
    'started_at' => $now->subDay(),
    'ended_at'   => $now,
]);

Laravel 8 の BookFactory

<?php

declare(strict_types=1);

namespace Database\Factories;

use App\Models\Book;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * Class BookFactory
 *
 * @package Database\Factories
 */
class BookFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Book::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition(): array
    {
        return [
            'title'  => $this->faker->sentence(),
            'author' => $this->faker->name(),
        ];
    }

    /**
     * 購入済み
     *
     * @return BookFactory
     */
    public function bought(): BookFactory
    {
        return $this->state(fn (array $attributed)  => [
            'bought_at' => CarbonImmutable::now()
        ]);
    }

    /**
     * 読んでいるところ
     *
     * @param int $startDays 読み始めるまでの日数
     *
     * @return BookFactory
     */
    public function started(int $startDays=1): BookFactory
    {
        $now = CarbonImmutable::now();

        return $this->state(fn (array $attributed)  => [
            'bought_at'  => $now->subDays($startDays),
            'started_at' => $now,
        ]);
    }

    /**
     * 読み終わった
     *
     * @param int $endDays   読み終わるまでの日数
     * @param int $startDays 読み始めるまでの日数
     *
     * @return BookFactory
     */
    public function ended(int $endDays=1, int $startDays=1): BookFactory
    {
        $end    = CarbonImmutable::now();
        $start  = $end->subDays($endDays);
        $bought = $start->subDays($startDays);

        return $this->state(fn (array $attributed)  => [
            'bought_at'  => $bought,
            'started_at' => $start,
            'ended_at'   => $end,
        ]);
    }
}

クラス化されたことで各々のファクトリがしっかりすみ分けできた感じがいいなあ。

Laravel <= 7 から 8 への移行

さて、Laravel 8 にて刷新されたモデルファクトリですが、Laravel 7 以前の記法とは互換性がありません。

なので ver. 8 へアップグレードをするためにはここを全て書き直さないといけないわけですが、調整箇所が多いと物量との戦いになるため、なかなか辛くなります。

ただ、モデルファクトリを使用している部分は開発用のデータやテストデータの作成部分であり、直接的にプロダクトとして価値を提供している部分ではないので、ここの移行で ver. 8 へのアップグレードがスタックするのはなるべく避けたいところです。

そこで、Laravel 8 でも、旧式のモデルファクトリの記述を使えるように、パッケージが提供されています。

laravel/legacy-factories
https://github.com/laravel/legacy-factories

ある程度運用されているシステムであれば、基本的にアップグレードで変更や調整が必要となる箇所はモデルファクトリだけではないと思うので、ひとまずこのパッケージを導入してモデルファクトリ以外を Laravel 8 に対応させてしまい、その後でこの部分だけを対応させていくという流れが良いかなと思いました。

ちなみに試してみたところ、Laravel 8 でも以前のモデルファクトリが問題なく動作しました。ただし旧記述と新記述を共存させることはできなかったので、新しい記述に対応する際にはパッケージを削除して一気に全てのモデルファクトリを修正する必要がありそうです。

まとめ

Laravel 8 で刷新されいい感じになったモデルファクトリ。開発用のデータやテストデータの作成って作成元も利用側も散らかり傾向になりがちなので、これですっきりまとめて見通し良く開発していきたいですね。

リモートワークを円滑にすすめるための実践とツール

こんにちは皆さん。 niisan-tokyoです。

現在の新型コロナの脅威の中で、弊社も当然のごとくリモートワークに移行しまして、引きこもり傾向のある私としては特に苦もなく移行は出来たのですが、チームとしてリモートワークを進めるには、やはりいろんな困難があるわけです。 今回は、我々agent bank 開発チームが、リモートワークを円滑に進めるために実践していることと、それに伴って使っているツールなどを紹介します。

リモートワークを円滑にすすめる実践

チーム全員リモートにする

これは意外に大事なのですが、リモートワークをする場合は、全員をリモートにし、一部の人だけリモート、みたいな状況を作らないほうがいいです。 リアルで顔を合わせるメンバーがいると、それ以外のメンバーをどうしてもないがしろにしてしまいますし、リモートメンバーはその状況を改善するために、余分に自分の状況をアピールする必要があり、その結果としてメンバー間のコミュニケーションに齟齬が生まれたり、疲労が溜まったりします。

働き方改革なんかで、リモートか出社かを選べる状況もあるかとは思いますが、少なくともチーム単位では、全員リモートか全員出社かのどちらかにしたほうが、楽だと思います。

一つのツールにのみ依存しないようにする

リモートワークを進める上で、ボイスチャットなどのツールを使うと思うのですが、一つのツールに依存してしまうと、それが使えないとなったときに業務が止まりますので、複数のツールで業務ができるようにしておくべきです。

例えば、我々のチームだと、基本的にはボイスチャットに使用しているdiscordで画面共有もやっているのですが、discordの画面共有と何故か相性の悪いメンバーがいて、そのメンバーたちは画面共有を google meet でやっていたりします。

情報共有の場を意図的に設定する

開発チームの仕事は開発なのですが、開発するにあたっては、仕様・実装のイメージ・運用のイメージ・現在の成果状況といったものについて、メンバー間で共通認識が取れていないと、開發が滞ったり、手戻りが発生したりして、効率も品質も落ちます。

我々のチームだと主に朝会と夕会で現状の認識を合わせるようにしています。

朝会のアジェンダは以下のようになっています。

  • 新規タスクの確認: 前日からの持ち越し・分離タスク、差し込まれたタスクなどの確認
  • その日一日でやることの設定: 何を、どのように進めて、どこまでのタスクをこなすかを決める

一方、夕会のアジェンダは以下のとおりです。

  • タスクの実施状況: 朝会で設定した内容に対しての進捗と現在の成果状況を確認、遅延・想定外状況の発生したタスクのチェック
  • 振り返り: 翌日以降の開発を良くするための案出し
  • ゴール決め: 各メンバーの当日作業の終了目標を決める。持っているタスクが大きすぎれば、タスクを分割してその日終わらせるものと翌日に持ち越すものを分離

このように、定期的に場を設けて、少なくとも一日に一回は現状把握する時間を作っています。

リモートに役立つツール

ここからは我々のチームで使っているツールを紹介し、どんなふうに使っているのかを簡単に見ていきます。

discord

discord.com

ゲームとかでよく使われているボイスチャットのアプリです。 画面共有もできるようになっており、ペアプロや夕会などの定期的なミーティングにもこれ一つで行けてます。 基本ボイスチャットなので、メンバーの顔を見たい場合はgoogle meet のほうを利用しています。

肝心の使い方ですが、基本的には以下のようにしています。

  • 基本的に全員同じチャンネルで作業
  • 一日中繋ぎっぱなし
  • 違うタスクをしているメンバーの音量を下げる
  • ドライバーは、画面共有できる場合はする

全員同じチャンネルで作業しているのは、情報の齟齬が発生したときに、それをキャッチしやすくするためであるのと、互いに質問や状況を聞きやすくするためです。チャットを切る必要もないので、休憩時間以外は常につないでいます。

f:id:niikura23:20201104123614p:plain
別タスクメンバーの音量を下げる

ただし、現在のタスクとは違う内容の会話が混じると進めにくいので、違うタスクのメンバーについては音量を下げ、会話が混じらないようにしています。

なお、PC側で重たい処理( ビルドとか ) が走っている状況だと、discordの音声処理が遅延するのか、音が遅れたり、ハウったりするので、そういう場合は一旦マイクを切って、重たい処理が終るのを待ちます。

Google Meet

apps.google.com

Google Meetはリモート会議をするためのツールで、ブラウザで動かすことが出来ます。 音質はdiscordのほうがいい感じですが、Google Calendar と連携させて、インスタントな会議を設定することが出来ます。 このため、我々は主に他部署のステークホルダとのリモート会議で使用しています。

f:id:niikura23:20201104125345p:plain
カレンダーとmeetの連携

カレンダーをおさえて、あとは、meetに来てくれればオッケーみたいな感じです。

あと、前述したとおり、discordで画面共有が出来ない場合、meetで画面だけ共有して、音声はdiscordでやってます

Miro

miro.com

Miroはオンラインで使えるホワイトボードです。 前回も紹介しましたが、我々はストーリー詳細化という、開発するべきことを詰めていく作業があるわけで、この作業において、ホワイトボードのように使えるMiroを使っています。

techblog.roxx.co.jp

Miroは可視化ツールとしてはかなり優秀で、おかげで代替が難しい面もあります。 我々のチームでも多くのケースで使われており、実例を上げてしまうと、ストーリー詳細化などは全面モザイクになったりしますので、難しいですが、使用例の一部を紹介しておきます。

振り返り

f:id:niikura23:20201104131842p:plain
振り返りの例

振り返りでもMiro使ってます。 我々が現在使っている手法は普通のKPTAになっています。

ワイヤーフレームの作成

ストーリー詳細化の際にワイヤーフレームを作ることがあり、そこでもMiroは活躍しています。 特に、既存のUIに対する変更においては、クリッピングが役に立ちます。

f:id:niikura23:20201104143541p:plain
クリッピングの様子

クリッピングした画像同士の間に、新しい要素を入れると、簡易的ですが既存UIに対する追加を表現できます。

まとめ

リモートワークに移行してはっきり認識できたことは、チーム内、もしくは関係者間で共通認識を持てるかどうかが、物事をすすめる上では大事であるということです。 本記事の実践していることにおいても、使っているツールにおいても、いかにお互いの認識を合わせられるかを主眼にしています。

今回はこんなところです。

おまけ

弊社では開発チームのメンバーを募集しています。 ちょっとでも興味があって、話くらいなら聞いてやるか!っていう方は私のtwitterにでも連絡くれると、とてもありがたいです。

twitter.com