AWS lambdaでrcloneを動かしてGDriveをS3にバックアップする

CTO室情報システム担当の吉澤です!

AWS Lambda Custom RuntimeとCloudWatch Eventを使ってGDriveをS3にバックアップする自動化を行いました。

Custom Runtimeについては以下を参照にしてください。 docs.aws.amazon.com

ファイル構成

.
├── bootstrap
├── function.sh
├── jq
├── rclone
├── .rclone.conf

function.sh

function handler() {
  ID=`echo $1 | /var/task/jq -r '.id'`
  NAME=`echo $1 | /var/task/jq -r '.name'`
  DATE=`date +%Y%m%d`

  cp /var/task/.rclone.conf /tmp
  /var/task/rclone dedupe --dedupe-mode rename --drive-skip-shortcuts ${ID}:
  /var/task/rclone sync ${ID}: s3:roxx-gdrive-backup/sync/${NAME} --config=/tmp/.rclone.conf --fast-list --drive-alternate-export --drive-skip-shortcuts
}

bootstrapは以下を参照にしてください。 docs.aws.amazon.com

jsonを読み込むためのjqと、rcloneのバイナリも一緒にパッケージ化します。 --drive-alternate-export--drive-skip-shortcutsについては以下を参照してください。 rclone.org

.rclone.confはあらかじめローカルで設定したものをコピペします。

.rclone.conf

[gdrive]
type = drive
scope = drive
token = ***
team_drive = ***

[s3]
type = s3
provider = AWS
env_auth = true
region = ap-northeast-1
acl = private
storage_class = GLACIER

S3のポリシーは、S3 Permissionsの項のExample policyを参考に設定してください。 rclone.org

引数

{'id':'gdrive', 'name':'Googleドライブ'}

今回はIT監査対応でバックアップを作成しました。 rcloneはいろんなストレージに対応しているので、バックアップ手法を模索している方検討してみてください。

reviewdog で構文チェックや静的解析の結果をプルリクのレビューコメントとして出力する

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

www.ritolab.com


プルリクエストを作成した時に構文チェックや静的解析を走らせてソースコードのチェックを行っていた際に、検査に通らなかった場合は結果エラーと表示・通知されますが、その度に GithubActions / CircleCI / TravisCI などの結果画面へ遷移して検査に通らなかった箇所を確認するのって少しだけ面倒だったりします。

そんな時は、reviewdog を使って、検査結果をプルリクのレビューコメントに出力してあげると手間も減って少しだけ幸せになれます。

今回は、reviewdog で構文チェックや静的解析の結果をプルリクのレビューコメントとして出力してみます。

reviewdog

reviewdog は、レビューコメントを出力してくれるツールです。
https://github.com/reviewdog/reviewdog

検証環境

今回使用するツールは以下の通りです

  • reviewdog
  • Github Actions
  • PHP_CodeSniffer(構文チェック)
  • PHPStan(静的解析)

今回は Github Actions に reviewdog を導入して、プルリクエストで構文チェックと静的解析を走らせ、検査に通らないものがあった場合にその結果をレビューコメントとして出力したいと思います。

一部 PHP フレームワーク Laravel のセットアップが出てきますが、特に Laravel のコードは出てきません。プロジェクトルートで各検査を実行している事を明示するために残しているだけなので、他の FW やスクラッチでも設定としては問題ありません。

構文チェックのエラーをコメントに出力する

まずは、PHP_CodeSniffer にて構文チェックを行い、エラーがあればプルリクのコメントに出力するようにします。

reviewdog なしで通常の構文チェックを走らせる場合の Action は以下になっています。(以下すべて yml は該当部分のみ抜粋して表示しています)

.github/workflows/ci.yml

jobs:
  # 構文チェック
  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1 # Laravel setup
    - name: lint
      run: ./vendor/bin/phpcs --report=emacs --standard=phpcs.xml ./

(Laravel setup のところは、個人的に Laravel のセットアップをまとめている Composite Action なので、試す場合は削除して各々の定義を追加してください)

ここに reviewdog を導入してレビューコメントを出力するようにするので、以下のように変更します。

.github/workflows/ci.yml

jobs:
  # 構文チェック
  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    # 1. reviewdog の setup action を追加
    - uses: reviewdog/action-setup@v1
      with:
        reviewdog_version: latest
    - name: lint
      env:
        # 2. reviewdog が コメントを書き込めるように token をセットする
        REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      # 3. 構文チェックの結果を reviewdog へ渡す
      run: ./vendor/bin/phpcs --report=emacs --standard=phpcs.xml ./ | reviewdog -reporter=github-pr-review -efm='%f:%l:%c:%m'
  1. reviewdog の setup action を追加して reviewdog を導入しています
  2. reviewdog が コメントを書き込めるように GITHUB_TOKEN をセットしています
  3. 構文チェックの結果を reviewdog へ渡しています

ポイントとしては、コメントを書き込める用に環境変数 REVIEWDOG_GITHUB_API_TOKEN に GITHUB_TOKEN 渡してあげることがまず一つです。ただ結果を渡すだけでは、書き込みの権限がなくて失敗してしまいます。

2つ目は、lint 結果のフォーマットです。

通常のエラーレポートの出力フォーマットでは reviewdog 側で簡単に読み取ることができないので、--report=emacs を指定して、出力フォーマットを通常のものから変更しています。

その上で、コメントのフォーマットを reviewdog 側で指定しています。(-efm='%f:%l:%c:%m')

Reporting
https://github.com/squizlabs/PHP_CodeSniffer/wiki/Reporting

これで、構文チェックに引っかかったものが、プルリクのレビューコメントに出力されるようになります。

f:id:ro9rito:20200902095337p:plain

静的解析のエラーをコメントに出力する

次は、PHPStan にて静的解析を行い、エラーがあればプルリクのコメントに出力するようにします。

まず、reviewdog なしで通常の構文チェックを走らせる場合の Action は以下になっています。

.github/workflows/ci.yml

jobs:
  analyse:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    - name: analyse
      run: ./vendor/bin/phpstan analyse

各々セットアップして、PHPStan を実行しているだけの、ベーシックな定義です。

これに、reviewdog を導入するために以下の記述に変更します。

.github/workflows/ci.yml

jobs:
  # 静的解析
  analyse:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    # 1. reviewdog の setup action を追加
    - uses: reviewdog/action-setup@v1
      with:
        reviewdog_version: latest
    - name: analyse
      env:
        # 2. reviewdog が コメントを書き込めるように token をセットする
        REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      # 3. 静的解析の結果を reviewdog へ渡す
      run: ./vendor/bin/phpstan analyse --error-format=raw --no-progress | reviewdog -reporter=github-pr-review -f=phpstan

基本的な部分は構文チェックの時と変わりませんが、一つだけポイントがあります。

run: ./vendor/bin/phpstan analyse --error-format=raw --no-progress | reviewdog -reporter=github-pr-review -f=phpstan

reviewdog は PHPStan 用のメッセージフォーマットを既に持っているので、-f=phpstan とするだけで済むのですが、今回の書き方の場合、PHPStan 側のエラー出力がデフォルトでは読み取れないので、--error-format=raw を指定してエラーの出力フォーマットを変更してあげる必要があります。

PHPStan - Output Format
https://phpstan.org/user-guide/output-format

これで、静的解析に引っかかったものが、プルリクのレビューコメントに出力されるようになります。

f:id:ro9rito:20200902102925p:plain

補足

PHP_CodeSniffer と PHPStan のエラー出力のフォーマットをデフォルトから変更しましたが、基本的デフォルトでは検査結果は人が確認しやすいような書式で出力されるので、シンプルな出力に変更する事で reviewdog から読み取れるようにしています。

ただ、reviewdog 自体は checkstyle を読めるので、今回のように出力フォーマットを変更しなくても設定次第でいけるのかもというのは一応補足しておきます。(検証はしていません)

(とはいいつつ、-f=phpstan で使う場合、PHPStan の場合は raw フォーマット前提のようです)
https://github.com/reviewdog/errorformat/pull/22/files

まとめ

reviewdog を導入するとそのプルリクを出した際の検査結果がコメントが書き込まれるので、おそらく最も早くレビューのコメントがつく。という事を考えると、人が実際にレビューを行う前に基本的な部分が自動で指摘されるので、全体的なレビュー労力の削減にもなりそうですね。

本当はこういった類のものはプルリク出す前に全て潰してしまえたら最良ですが、漏れる時もありますし、reviewdog にかわいく(コメントに犬のアイコンが出てきます)指摘してもらうのも良いかもしれません。

Github Actions の複合ステップアクションを利用してアクションを分離・再利用する

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

www.ritolab.com


Github が 2020 年 8 月 7 日に、複合ステップアクションという機能をリリースしました。

GitHub Actions: Composite Run Steps
https://github.blog/changelog/2020-08-07-github-actions-composite-run-steps/

なかなか便利そうだったのでこれを試してみたいと思います。

複合ステップアクション(Composite run steps actions)

複合ステップアクションとは、1つのアクション内で複数のワークフローの実行手順を組み合わせることができる機能です。

よく使うアクションを1つにまとめておいて、それを必要な箇所で使用(再利用)することができます。

ワークフローを切り出しておけば、例えば別のリポジトリで同じような環境で開発を行ったときにそれを再利用できるという感じです。

https://docs.github.com/en/actions/creating-actions/about-actions#composite-run-steps-actions

検証環境

今回は、Laravel で構築した PHP アプリケーションの CI を回す辺りを想定してこの機能を利用してみたいと思います。

また、Github Actions では以下のチェックを走らせようと思います。

ワークフローの定義

まずは、従来の方法でワークフローを定義してみます。

今回の場合、Github Actions で CI を回す場合に必要なのは、アプリケーションのセットアップとユニットテスト用の DB のセットアップ、そして各チェックの実行です。

各チェックごとにジョブを分けて実行するとして定義すると、例えば以下のような感じになります。

laravel/.github/workflows/ci.yml

name: App check

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  # 構文チェック
  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
    - name: Generate key
      run: php artisan key:generate
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
    - name: lint
      run: composer lint
  # 静的解析
  analyse:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
    - name: Generate key
      run: php artisan key:generate
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
    - name: analyse
      run: composer analyse
  # ユニットテスト
  tests:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
    - name: Generate key
      run: php artisan key:generate
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
    - name: Create Database
      run: |
        mkdir -p database
        touch database/database.sqlite
    - name: Execute tests (Unit and Feature tests) via PHPUnit
      env:
        DB_CONNECTION: sqlite
        DB_DATABASE: database/database.sqlite
      run: vendor/bin/phpunit

そして CI を回すとそれらは成功します。

f:id:ro9rito:20200816090231p:plain

ただし、この yml の書き方の場合、ジョブごとに同じアプリケーションのセットアップが記述されていて冗長です。

ここを、複合ステップアクションで共通化していきます。

複合ステップアクションの作成

詳細な手順は公式に案内があります。
https://docs.github.com/en/actions/creating-actions/creating-a-composite-run-steps-action
これに沿って進めていこうと思います。

複合ステップアクションを作成するには、リポジトリが必要です。まずは、切り出すアクションを設置するためのパブリックリポジトリを作成します。

リポジトリを作成したら、そこに設置するアクションを作成します。

(ちなみに、適当に v1 というブランチを切って進めています。)

master
 \
    v1

Laravel をセットアップするアクションを定義します。

action.yml

inputs:
  name:
    description: 'Setup Laravel'
runs:
  using: "composite"
  steps:
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
      shell: bash
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
      shell: bash
    - name: Generate key
      run: php artisan key:generate
      shell: bash
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
      shell: bash

アクションを作成したらリモートリポジトリへ push します。

ラベルを付与する

リモートリポジトリへブランチを push したら、プルリクを作成して、そこで「v1」というラベルを付与(無い場合は作成)します。

f:id:ro9rito:20200816090524p:plain

これで準備は完了です。(ブランチを生かしておけばプルリクはマージしても大丈夫です。)

複合ステップアクションの利用

切り出したアクションを実際に利用してみます。

laravel/.github/workflows/ci.yml

name: App check

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    - name: lint
      run: composer lint
  analyse:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    - name: analyse
      run: composer analyse
  tests:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    - name: Create Database
      run: |
        mkdir -p database
        touch database/database.sqlite
    - name: Execute tests (Unit and Feature tests) via PHPUnit
      env:
        DB_CONNECTION: sqlite
        DB_DATABASE: database/database.sqlite
      run: vendor/bin/phpunit

Laravel のセットアップの部分をまるっと以下に置き換えています。

- uses: rito328/composite-run-steps-action-laravel-setup@v1

「 rito328/composite-run-steps-action-laravel-setup 」はリポジトリ名で「 v1 」はラベルです。(v1 とか v2 とか以外が使えるかは試していません)

冗長な記述が無くなったのでとてもすっきりしました。

これを Github Actions で動かすと、複合ステップアクションが読み込まれ、アプリケーションのセットアップやチェックも行われている事が確認できます。

f:id:ro9rito:20200816090651p:plain

アクションを分離させて再利用できました。

まとめ

ドキュメント見ながらざっと試してみました。個人的な感想としては、

  • 切り出したアクションのリポジトリが private だと利用側で読み込めなかった。それはそうかという気持ち反面、同じオーナーなら許してほしい気持ち反面。
  • ラベルつけるのが面倒。ラベル(@v1)無しでもできないのかな。

といったところでした。

とはいえ、アクションを分離できて再利用できるのはとても有用だなと思います。

同じ環境のアプリケーションがどんどん増えていくような環境下だと、再利用が捗って活躍しそうですね。

ちなみにリリースはまだファーストステップみたいで、これからも機能強化が行われていくようです。 https://github.com/actions/runner/issues/646

現場からは以上です。

HSTSってなんだっけ

この記事は個人ブログの転載になります。

kotamat.com

HSTSに関して、話題に上がったことがあったので、改めてHSTSは何なのかをまとめてみました。

TL; DR

  • HTTPSを強制するもの
  • TLDベースとレスポンスヘッダーベースがある
  • 導入するときは気をつけないといけない

HSTSの導入モチベーション

HTTPSが有効ではないと、中間者攻撃をされる危険性があることは承知の上で議論を進めていきます。 わからない方はhttps httpとかでググってみよう

ただ、例えばHTTPでアクセスしたあとにHTTPSにリダイレクトするようなサイトを構築している場合、最初のアクセスのタイミングで中間者攻撃されるような危険性のある環境でクライアントが接続しに来ると、いくらHTTPSで疎通していたとしても攻撃される危険性が残ってしまいます。

機微なCookieに対してSecure属性をつけるなど他での対策はできますが、当然それだけでは不足です。

ここで、「初回アクセス時からHTTPSを強制する」事ができれば、上記のような危険性を排除できます。これがHSTSが達成したいモチベーションです

HSTSするには

TLDベースで対処する

.dev.app など、比較的最近発行されたTLDではHTTPSを強制することが標準で備わっていることがあります。こういったドメインを用いればHSTSは強制的に有効になります。

レスポンスヘッダーで対処する

とはいえ大抵のアプリケーションは .com だったり .co.jp だったりを使っていると思います。こういったドメインはHSTSを強制する力はないため、独自で設定する必要があります。 「初回リクエスト時にHTTPSで接続する」ということがブラウザに知られている必要があるため、レスポンスヘッダーを用いて「このウェブサイトはHSTSだよ」ということを事前に伝えて置く必要があります。

そこで用いられるヘッダーが Strict-Transport-Security ヘッダーです。

使い方

このヘッダーには3つのディレクティブがあります。

ディレクティブ 説明
max-age ブラウザが記憶する、HTTPSに強制する時間(秒)
includeSubDomains 当該ドメインだけではなく、サブドメインもすべて対象にする(省略した場合は当該ドメインだけ)
preload preload対象に入れる(後述)

このディレクティブを適切に設定し、任意のリクエストのレスポンスヘッダーに追加するだけで、「次からの接続は」HTTPSが強制されるようになります

注意点

ここで大事なのはmax ageとincludeSubDomainsです。 max ageがながければ長いほど当然セキュアではあるのですが、その間証明書が失効するなどHTTPSで接続できない時間があると、ブラウザはそのウェブサイトへ接続できなくなってしまいます。 またincludeSubDomainが設定されればサブドメインにもその効果が波及されます。一つのドメインを踏むだけで、他のサブドメインに対しても効果が波及するのでよりセキュアではありますが、サブドメインがHTTPのみしか許可できない場合は、当然そこには接続できません。

例えば簡易的に CNAME などで外部サービスにサブドメインを発行しているケースだと、Let's EncryptなどでHTTPS化していないケースが有り、そういったサービスを使う場合はHTTPS化されません。 そのようなドメインは局所的にしか使わないものかもしれませんが、一度includeSubDomain下に入ってしまえばそのドメインも対象になってしまうので、その外部サービスが使えなくなってしまいます。

またこれはmax-ageが消えるまで有効です。つまり初期のタイミングでは特にそのようなサービスと提携しておらず、問題なく接続できたとしても、後日そのようなサービスと連携する際に障害になるといったケースも発生しえます。

また、レスポンスヘッダーに付与されるという特性上、ブラウザにとっての初回アクセス時はどうしてもHTTP接続になるケースが発生しえます。これを防ぐ仕組みとしてpreloadというものが提供されています。

preload

preloadは仕様書では定義されていないものですが、Googleによって提供され、昨今の主要なブラウザには搭載されている機能となっています。 サポートしているブラウザはこちら

preloadをつけることによって、「そのブラウザ」ではなく「全ブラウザ」に対してHTTPS強制することが伝わります。 つまり誰かが生贄になってそのサイトに接続したあとは(タイムラグはありますが)それ以降そのサイトに訪れる人は全員HTTPSが矯正されるということです。

タイムラグを極力減らしたい方は https://hstspreload.org にて対象ドメインを設定することでその速度を早めることができます。

当初達成したかった目的はこのpreloadを使うことによって達成しうるのですが、当然上記で上げた注意点を考慮する必要があります。

preloadの設定ができるということは当然削除もできます

https://hstspreload.org/removal/

にて削除リクエストを送ることができますが、このリクエストが処理されるのはChromeユーザで6~12週間、他のブラウザはそれ以上の時間がかかるので、より一層設定には注意を払う必要があります。

無効化

Strict-Transport-Security: max-age=0 を設定することによって無効化できます。

まとめ

HTTPSが当たり前になってきている中で、HSTSの対応が求められるケースは増えて生きているかと思います。 上記注意点を考慮し、設定していきましょう。

Mockeryでのモックの作り方を調べてみた

この記事は個人ブログの転載になります。

toyo.hatenablog.jp

とある方から、「なんで静的メソッドはモックできないんですか?」ときかれたときに、「そういえば、Mockがどういう原理で動いているかいまいち知らないなー」と思ったので、モックがどのように作られているのかを調べてみました。

僕は普段はMockeryを使用しているので、Mockeryでのモックの生成のされ方をまとめます。

docs.mockery.io

Mockeryは色々な種類のモックが作れるので、今回は一番ベーシックな「スタブやモック」の生成過程を追ってみました。

コードでいうと、以下のような感じのやつ。

<?php
Mockery::mock(Post::class);

以下ではこのコードを例にとって説明していきます。

大まかな流れ

雑にまとめると、以下です。

  1. Postクラスを継承したモッククラスの定義を作成する
  2. 定義したモッククラスをロードする
  3. ロードしたモッククラスのインスタンスを生成する

です。もっと Refrection Class が関わってくるのかなと身構えていたのですが、とてもシンプルでした。

モッククラスの定義

Mockeryではモッククラスごとにクラスの定義を行っています。
つまり、

<?php

// あくまで疑似コードです
class MockedPost {
}

みたいなコードを、都度生成しているということです。

モッククラスのテンプレート

ただし、モッククラスには、 shouldRecieveandReturn などのメソッドが共通して必要です。
そのため、モッククラスの定義のテンプレートのようなものが存在しています。
このテンプレートが、 Mockery\Mock です。

github.com

このクラスの中で shouldRecieve などの定義も書かれています。
つまり、 Mockery::mock(Post::class) で生成したインスタンスの振る舞いは、このクラスの内容を読めばわかるということです。

テンプレートの書き換え

Mockeryではモッククラスごとにクラスの定義を行っているため、クラス名の衝突については考えないといけません。
また、PHPには型が存在しているため、それぞれのモッククラスは、引数で渡されたクラスの型である必要があります。
今回でいうと、生成されたモッククラスは Post 型である必要があります。

なので、テンプレートを書き換える必要があります。
テンプレートのファイルを file_get_contentsで読み込み、文字列として変数に入れます。
その文字列を str_replace などを用いて書き換えていきます。

元のテンプレートは以下のような感じです。

<?php

// ...略

class Mock implements MockInterface
{
    // ...略
}
クラス名の書き換え

クラス名が衝突するとエラーになるため、ユニークなクラス名が必要です。
そのため、テンプレートのクラス名を書き換える必要があります。

クラス名は Mockery_0_Post という命名になります。

0 の部分は連番です。
たとえばモックを2個生成した場合は、以下のようになります。

<?php

// Mockery_0_Post クラスのインスタンス
$post1 = Mockery::mock(Post::class);

// Mockery_1_Commentable クラスのインスタンス
$post2 = Mockery::mock(Commentable::class);

よって、クラス定義は以下のような状態になります。

<?php

// ...略

class Mockery_0_Post implements MockInterface
{
    // ...略
}
型情報の追加

生成されたモッククラスはPost型である必要があります。
ただ、この段階でモッククラスの型は Mockery_0_Post, MockInterface だけです。
そのため、Postクラスを継承することで解決します。
つまり、ドキュメントにも書いてありますが、 final キーワードのついているクラスのモックは(継承できないので)作成できません。1

クラス定義は以下のような状態になります。

<?php

// ...略

class Mockery_0_Post extends Post implements MockInterface
{
    // ...略
}
メソッド呼び出しの抑制

継承をすることで型を付与することはできますが、問題がひとつ残ります。
それは、Postクラスのメソッドが継承されてしまうということです。

たとえば、PostクラスにgetTitle()というメソッドが存在していたとして、Postをモックしたインスタンスを呼ぶと、そのままPost::getTitle()が呼ばれてしまいます。
別の挙動に差し替えるためにモックを使いたいので、これではモックの意味がありません。

モッククラスの定義に、Postで定義されているすべてのメソッドを再定義(オーバーライド)することで、この問題に対応します。
Postで定義されているすべてのメソッドの取得は、Postクラスのリフレクションクラスインスタンスを作成し、そのインスタンスgetMethods メソッドを使用することで実現できます。

https://www.php.net/manual/ja/reflectionclass.getmethods.php

ちなみに、メソッドの内容はほぼ固定で、以下が入ります。

<?php

$argc = func_num_args();
$argv = func_get_args();
$ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv);
return $ret;

最終的に、クラス定義は以下のようになります。

<?php

// ...略

class Mockery_0_Post extends Post implements MockInterface
{
    // ...略
    
    public function getTitle(): string {
        $argc = func_num_args();
        $argv = func_get_args();
        $ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv);
        return $ret;
    }
}

クラス定義のロード

クラス定義はできましたが、これは現状ただの文字列です。
これをどうにか読み込まないと、クラスとして使えません。

「どうやって読み込むんだろう・・・?」と思っていましたが、

<?php
eval("?>" . $definition->getCode());

github.com

のように、evalを使用して読み込んでいる感じでした。

PHPevalのドキュメントには

PHP 開始タグを含めてはいけません。

とありますが、クラス定義の先頭はPHP開始タグで始まっているので、 ?> を先頭につけて相殺しているようです。

モックインスタンスの生成

さて、モッククラスの定義をし、定義の読み込みもできたので、あとはインスタンスを生成するだけです。

ただし、Postクラスにコンストラクタが宣言されていた場合を考えないといけません。

<?php

class Post
{
    private string $title;

    public function __construct(string $title)
    {
        $this->title = $title;
    }
    
    public function getTitle(): string
    {
        // ...略
    }
}

このような場合、モッククラスはPostクラスを継承しているので、インスタンスを生成するためには $title が必要になります。
モックとしては、コンストラクタの処理は実行させたくない場合が多いと思います。
また、その際はそもそも引数を渡す意味もなく、引数の生成のためのコード2も書きたくないですよね。
なので、コンストラクタを呼ばずにインスタンスを生成します。

そのため、モッククラスのリフレクションクラスのインスタンスを作成し、 newInstanceWithoutConstructor メソッドを使用して、コンストラクタ無しにモックインスタンスを作成します。

https://www.php.net/manual/ja/reflectionclass.newinstancewithoutconstructor.php

これで無事にモックのインスタンスを生成できました。

まとめ

僕はまだモックを知らなかった頃は

<?php

$mock = new class extends Post
{
    public function getTitle(): string
    {
        return 'dummy';
    }
};

のように無名クラスを使用して書いていました。

今回は有名なモックライブラリの中を調べてみたわけですが、基本的な発想は同じなんだなと思いました。
簡単な発想にどこまで向き合えるか、が有名ライブラリへの道なのかもしれません。

おまけ1: インターフェースのモック

さて、Postはクラスでしたが、インターフェースのモックの場合がどのようになるかも追ってみました。

<?php

interface Commentable
{
    function writeComment(string $content): void;
}

Mockery::mock(Commentable::class);

基本的にはクラスの場合と同じです。

ただし、クラス定義の部分で extends していた部分は implements になります。

- class Mockery_0_Post extends Post implements MockInterface
+ class Mockery_0_Commentable implements MockInterface, Commentable

その他は大きな違いはありませんでした。

おまけ2: なぜ静的メソッドはモックできないのか

モックの生成過程が見えてくれば、静的メソッドのモックができない理由もみえてきます。

<?php

function getAllPosts(): array
{
    return Post::all();
}

たとえばこのようなコードがあったとします。
Postのモックインスタンスを作成することは可能です。
Postのモッククラスの静的メソッドの内容を差し替えることも可能でしょう。

ただし、モッククラスは Mockery_0_Post クラスであり、 Post クラスではないのです。
つまり、 Mockery_0_Post::all() は自由に定義できても、 Post::all() の内容はそのままです。

そのため、 Post::all() をそのままモックすることはできません。

エイリアスモック

これを解決するために、Mockeryでは「エイリアスモック」という機能があります。

<?php
Mockery::mock('alias:' . Post::class);

このように書くと、モッククラスの定義が以下のように変化します。

- class Mockery_0_Post extends Post implements MockInterface
+ class Post extends \stdClass implements MockInterface

引数で渡したクラス名がそのままモッククラスのクラス名になりました。

当然、すでにPostクラスが読み込まれている場合には、クラス名が衝突してエラーになります。
なので、エイリアスモックは、Postクラスが読み込まれる前に読み込む必要があります。3

つまり、「本物のコードが読み込まれる前に、モックのコードを読ませて、本物のコードが読み込まれないようにしちゃえ」という作戦なわけですね。

エイリアスモックとフレームワーク

エイリアスモックを使えば静的メソッドもモック可能」なわけですが、実コードではなかなか上手くいかないことが多いです。

というのは、たいていのプロダクトではLaravelなどのフレームワークを使用していると思います。
そして、テストコードを書く際も、フレームワークの機能を使用して書く場合が多いでしょう。
そのため、テストの実行時には、フレームワークの初期化などを行われますが、その際に、モックしたいクラスが読み込まれてしまうことが多いのです。
たとえばLaravelの場合だと、サービスプロバイダによってユーザが定義したクラスが読み込まれます。
その場合、個々のテストケースに処理が渡ってきたときにはすでにモックしたいクラスが読み込まれてしまっているため、 エイリアスモックを使えないことが多いのです。

エイリアスモックという手段は存在しているものの、「基本的には静的メソッドはモックできない」と考えたほうが良いかもしれません。


  1. ドキュメントにもありますが、finalキーワードがついている場合は プロキシパーシャルモック を作成できます。

  2. 今回はstringなので生成が楽でいいのですが、オブジェクトを引数に取る場合は、実引数に指定するオブジェクトを事前に生成する必要があり、コードが冗長になります。

  3. ここらへんの挙動についてはPHPのオートロードについて理解しておく必要があります。

php-fpmのアクセスログにリクエストされたURIを書き込む

backcheck事業部の前田です。
今回は簡単なTips程度の話です。
結論だけ見たい人は結果の見出しへ

問題

私はよくnginx+php-fpmの構成で、Laravelを使います。
その際に、php-fpm側でもアクセスログを出しているのですが、デフォルトの設定だと以下のようになります。

172.18.0.7 -  31/Jul/2020:14:48:45 +0900 "GET index.php" 200
172.18.0.7 -  31/Jul/2020:14:48:47 +0900 "GET index.php" 200
172.18.0.7 -  31/Jul/2020:14:48:58 +0900 "PATCH index.php" 204

まぁ、すべてのリクエストを意図的にindex.phpに集めてるから、そうなるよねー・・・・という話です。
ただ、実際の運用では「どのファイルにアクセスしたか」よりも、「どのURIでアクセスしたか」が知りたいと思います。
なので、ここの index.php の部分を実際にリクエストされたURIに変更したいと思います。

アクセスログのフォーマット指定

さて、URIをログに書き込むためには、ログのフォーマット形式を変更する必要がありそうです。
php-fpmのアクセスログのフォーマット指定は、php-fpmの設定ファイルで指定できます。

https://www.php.net/manual/ja/install.fpm.configuration.php#access-format

ここで、ログ形式のデフォルト値は

"%R - %u %t \"%m %r\" %s"

と書かれています。が、なんと肝心の各記号の意味が書かれていません!!
色々と探してみると、php-fpmのデフォルトの設定ファイルの中に書かれているようでした。

github.com

説明が大量にあるので、デフォルトのログの部分の説明だけ拾ってみます。

; %R: remote IP address
; %u: remote user
; %t: server time the request was received
;   it can accept a strftime(3) format:
;   %d/%b/%Y:%H:%M:%S %z (default)
;   The strftime(3) format must be encapsuled in a %{<strftime_format>}t tag
;   e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t
; %m: request method
; %r: the request URI (without the query string, see %q and %Q)
; %s: status (response code)

・・・ということで、 %rindex.php に該当していることがわかります。
ここを、リクエスURIに置き換えれば良さそうです。

リクエスURIの取得

置き換える場所がわかったので、リクエスURIはどのように取得すれば良いかを調べます。
先ほどの各記号の説明をざっと読んでもそれらしきものは見当たらなかったのですが、じっくり読むと、以下が使えそうです。

; %e: an environment variable (same as $ENV or $SERVER)
;   it must be associated with embraces to specify the name of the env
;   variable. Some examples:
;   - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e
;   - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e

リクエスURI$_SERVER['REQUEST_URI'] に入っているので、 %{REQUEST_URI}e と書けば良さそうです。

結果

まとめると、以下のような修正を行いました。

[www]
;access.format = "%R - %u %t \"%m %r\" %s"
access.format = "%R - %u %t \"%m %{REQUEST_URI}e\" %s"

そしてログを確認すると・・・

172.18.0.7 -  31/Jul/2020:14:48:45 +0900 "GET /api/posts" 200
172.18.0.7 -  31/Jul/2020:14:48:47 +0900 "GET /api/posts/5/comments" 200
172.18.0.7 -  31/Jul/2020:14:48:58 +0900 "PATCH /api/posts/5/comments/12" 204

無事にアクセスログにリクエスURIを残せました!!🎉🎉🎉

GASのWebアプリケーションでGoogleDriveのフォルダ一覧をSelectBoxで出力する

CTO室情報システム担当の吉澤です!

GoogleDriveを操作をする際に、SelectBoxの項目を動的にDrive内からとってくると視覚的に扱いやすいと思い、GASのWebアプリケーションを利用したツールを作成しました。

公式のHtmlServiceクラスのcreateHtmlOutput()を使うと表示できたので、メモとして残します。

developers.google.com

今回はこのマイフォルダの中にあるフォルダのリストをWebアプリケーション上のSelectBoxで選択できるようにします。

f:id:wakanayoshizawa:20200715233123p:plain

Webアプリケーションを開いた時はfunction doGet()を実行します。 createHtmlOutput()で全てのHTMLを上書きしてしまうため、 html内の特定のワードをreplaceしたものを引数として渡します。 今回は<putFolderSelectBox>に生成したSelectBoxのHTMLを渡して表示します。

コード.gs

function doGet() {
  return HtmlService.createHtmlOutput(this.setSelectBox());
}

function setSelectBox(){
  var html = HtmlService.createHtmlOutputFromFile('index').getContent();
  var lists = this.ls();
  var select = '<select name="folderId">';
  for (list of lists){
    select += '<option value="' + list[0] + '">' + list[1] + '</option>';
  }
  select += '</select>';
  return html.replace('<putFolderSelectBox>', select);
}

function ls() {
  var currentFolder = DriveApp.getFolderById('フォルダID');
  var folders = currentFolder.getFolders();
  var files = [];
  while(folders.hasNext()) {
    var folder = folders.next();
    files.push([folder.getId(), folder.getName()]);
  }
  return files;
}

DriveApp.getFolderById();に入れるフォルダIDは、Driveもしくはフォルダにアクセスし、URLからコピペします。

f:id:wakanayoshizawa:20200715232021p:plain

getFolders()でフォルダのListが取れるので、hasNext()で回し、ファイルのIDと名前を取得し、 SelectBoxのHTMLのValueと選択肢に当てはめていきます。

index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <putFolderSelectBox>
  </body>
</html>

HTML内のReplaceしたいところに<putFolderSelectBox>を入力します。

f:id:wakanayoshizawa:20200715232032p:plain

公開からWebアプリケーションとして導入を選択します。

このようにフォルダ一覧がSelectBoxとして表示されました。

f:id:wakanayoshizawa:20200715232052p:plain

f:id:wakanayoshizawa:20200715232102p:plain

Selectで選択したものを受け取る場合は、 Formで囲んで、action先はWebアプリケーションのURLに、 コード.gs内でdoPost() を用意すると、引数で受け取ることができます。

GASはまだ慣れていないのですが、Driveの情報をHTMLで視覚的に表現ができるのは便利でいいですね👍