もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

Laravel Webアプリケーション開発を読んだ

はじめに

こんばんは。

今月の目標で、積本を2冊消化するという目標があるので、溜まっている本を眺める時間はもうやめて、重い腰を上げ積み本消化に乗り出しました。

一冊目は、去年辺りから積んでた Laravel Webアプリケーション開発です。

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

本題

目次

  1. Laravelの概要
  2. Laravelアーキテクチャ
  3. アプリケーションアーキテクチャ
  4. HTTPリクエストとレスポンス
  5. データベース
  6. 認可と認証
  7. 処理の分離
  8. コンソールアプリケーション
  9. テスト
  10. アプリケーション運用
  11. テスト駆動開発の実践

読もうと思った理由

ちょうどLaravelの実務が始まるところだったので素振りするために買いました。

ざっくりとした感想

帯には 実践パターン と ユースケース で学ぶ開発現場で使えるプロのテクニック という感じで、パット見玄人向けな本かなと感じてましたが、中はかなり丁寧に書かれており、初心者とはいわないですが、これからLarevel学びたいと思っている経験者の人にはスラスラと入れるような内容でした。

正直Laravel自体使用者も多いので、日本語ドキュメントがわんさと出てきます。

なので、こういった手合の本はなくても良いかな?と思いましたが、本として時間をとって読むのと、サラッと必要なときにwebページを見るのでは違いがあると思いますので、自分としては良かったです。

ただ、いかんせん本を購入してから読み終えるまでにかなり時間が立ってしまっていて、本の内容はある程度実務でこなしたあとだったので、ある程度ベースの知識が出来ている状態で読むことになりました。。。

それでも、今まで雰囲気で使ってたところとかを、おさらいするいいキッカケになったかと思います。

読んで学んだ事

すでに実務で経験済みのところなどが多かったので、比較的知っている内容が多かったです。

ただ、雰囲気で使っていた部分などがあったので、以下のあたりは勉強になりました。

  1. Laravelアーキテクチャ の サービスコンテナバインド周り

  2. Laravelアーキテクチャ の deferプロパティによる遅延実行

  3. 処理の分離の全般

特に、7章の処理の分離にでてくる、イベント系の機能は、自分が実務で実装した箇所には触らなかったので、勉強になりました。

また、よくあるFWの入門書?解説書?と違い、Controller、Modelにベタっと書いちゃうサンプルコードではなく、運用を意識した設計でリファクタリングをするというフェーズが設けられていたのが印象的でした。

自分も今レガシーコードを四苦八苦しながら設計頑張っているので、なるほどなるほどと、とても勉強になりました。

終わりに

話は飛んじゃいますが、同じSlackチームに所属している @budougumi0617さんの、15日間勉強してAWS ソリューションアーキテクト アソシエイト試験に合格した というブログを見て、資格試験に無縁な生活をしていた自分ですが、AWSの試験受けてみようと思いました。

twitter.com

budougumi0617.github.io

とりあえず目下の目標としては、積んでる本全部読んでからですが・・・w

今年の目標として、一つ資格合格を上げておきたいと思います。

では、現場からは以上です。

CricleCIから踏み台サーバを経由して対象サーバへデプロイ

はじめに

こんばんは。

またまた備忘録です。

しかも今回はちょっとした小ネタレベルです。

関わったプロジェクトで、踏み台サーバを経由して本番サーバへデプロイしたいということになり、こんな感じで良いかな?という、以下の方法でまとまりました。

本題

1. CircleCIにPrivate Keyを登録する

CircleCI -> Settings -> Corporation -> ProjectのSSH Permissionsのページで、private keyを登録します。

f:id:kojirooooocks:20190513033154p:plain

2. CircleCI用に使用するssh configファイルを作成

# vi /path/to/project/.circleci/circleci_ssh_config

Host bastion_host
  HostName ************
  User     ************
  IdentityFile /home/circleci/.ssh/id_rsa_{sshkeyを登録した際に表示されるfingerprintsで、「:」を抜いたもの}
  StrictHostKeyChecking no
  UserKnownHostsFile=/dev/null
Host prod_server
  HostName     ************
  User         ************
  IdentityFile /home/circleci/.ssh/id_rsa_{sshkeyを登録した際に表示されるfingerprintsで、「:」を抜いたもの}
  ProxyCommand ssh -W %h:%p bastion_host
  StrictHostKeyChecking no
  UserKnownHostsFile=/dev/null

3. CircleCIのconfig.ymlにsshkeyとsshconfigを登録する

executors:
  deploy:
    working_directory: ~/workspace

commands:
  # sshKeyをCircleCIのconfigに設定する
  deploy_ssh_setting:
    steps:
      - add_ssh_keys:
          fingerprints:
            - "{sshkeyを登録した際に表示されるfingerprints}"
      - run: cat ~/workspace/.circleci/circleci_ssh_config >> ~/.ssh/config

jobs:
  deploy:
    executor:
      name: deploy
    steps:
      - checkout
      - deploy_ssh_setting

これで一応狙ったことは出来ます。

以前あげたLaravel-Deployerでのデプロイでまさにこの方法でbackend_serverへデプロイしてます。

終わり

もっとスマートなやり方があればぜひ教えてくださいm( )m

小さなネタになりましたが、現場からは以上です。

Effective DevOpsオンライン輪読会はじめました

f:id:kojirooooocks:20190513005434j:plain

こんばんは。

自分が所属している challenge-every-month というSlackチームのメンバーで、 Effective DevOps のオンライン輪読会が開催され、その会に参加させてもらいました。

Effective DevOps ―4本柱による持続可能な組織文化の育て方

Effective DevOps ―4本柱による持続可能な組織文化の育て方

参加しましたといっても、#1 は自己紹介や輪読会の進め方などのすり合わせでした。

参加者は、 @tadaさんと@kdnaktさんと、自分です。

twitter.com

twitter.com

開催のきっかけは、 @tadaさんが@kdnaktさんのブログを読んで、オンラインで一緒に読み進めませんか?と話していたところを僕が空気を読まず突っ込んだという感じです。

kdnakt.hatenablog.com

f:id:kojirooooocks:20190513005630p:plain

f:id:kojirooooocks:20190513005656p:plain

#1 では、前述した通り、各自の自己紹介と今後の進め方などを話しました。

進め方は以下。

  • 1週間に1回開催
  • 1回の読む量は1章分で30〜40ページ程度
  • 初回については、1~4章分(各章の内容によって、読む量は変動する)

輪読会でやりたいことは以下。

  • 読んだ章の中身の理解のすり合わせ
  • 実際の現場での実践の話

また、途中で輪読会に参加したいという方が現れた場合も、読み進めている場所からスムーズに参加できる人であれば、歓迎しますというルールにしました。

↑こんな偉そうなこと言ってますが、Effective DevOps については、自分は全く読んだこともなく、 DevOpsについても分かっているような分かってないようなくらいの知識です。

なので、とにかく読み進めつつ、お二人から諸々吸収できればという気持ちでいますw

また、初回の開催でわかったのですが、自分のマイクがとにかく小さいみたいなので、これを気にヘッドセットを買いました。

次からは少しはクリアになるかと。。。!

次回は5/19に開催予定なので、その時の感想も ブログにしようとおもいます。

現場からは以上です。

PS

@tadaさん @kdnaktさん

初回の開催は本当は5/11だったのにいきなりリスケさせてすいませんでした...

Laravelでjson web token試してみました

はじめに

こんばんは。

今回も前回同様、作業した備忘録です。

今回試したのは LaravelでのJWTです。

こちらの jwt-auth というライブラリです。

設定自体はめちゃめちゃ簡単です。

公式のドキュメントに書いているとおりで、ほぼ迷うことはありません。

本題

1. インストール

$ composer require tymon/jwt-auth

2. 設定

# configファイル生成
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"


# secret key作成
$ php artisan jwt:secret

# auth設定
# vi config/auth.php
# multi-authだとこんな感じ?

...

'guards' => [
    'admin' => [
        'driver'   => 'jwt',
        'provider' => 'admin',
        'hash'     => true,
    ],
    'customer' => [
        'driver'   => 'jwt',
        'provider' => 'customer',
        'hash'     => true,
    ],
],


'providers' => [
    'admin' => [
        'driver' => 'eloquent',
        'model'  => App\Models\AdminUser::class,
    ],
    'customer' => [
        'driver' => 'eloquent',
        'model'  => App\Models\Customer::class,
    ],
],

3. routing設定

<?php

Route::group(['middleware' => ['auth:admin']], function () {
    Route::get('/staff', 'StaffController@list')->name('StaffList');
    Route::get('/auth/refresh', 'AuthController@refresh')->name('AuthenticateRefresh');
});

Route::post('/auth/login', 'AuthController@login')->name('Authenticate');
Route::delete('/auth/logout', 'AuthController@logout')->name('AuthenticateLogout');

4. Controller設定

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class AuthController extends Controller
{
    /**
     * Login Action.
     * @throws UnauthorizedHttpException
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(): \Illuminate\Http\JsonResponse
    {
        $requestData = request(['email', 'password']);
        $credentials = [
            'mailaddAddress' => $requestData['email'],
            'password'       => $requestData['password'],
        ];

        if (!$token = auth()->attempt($credentials)) {
            throw new UnauthorizedHttpException('Invalid credentials', '認証失敗');
        }

        return $this->respondWithToken((string) $token);
    }

    /**
     * Logout Action.
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout(): \Illuminate\Http\JsonResponse
    {
        auth()->logout();
        return response()->json('OK');
    }

    /**
     * RefreshToken.
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh(): \Illuminate\Http\JsonResponse
    {
        return response()->json([
            'accessToken' => auth()->refresh(),
            'expiresIn'   => auth()->factory()->getTTL() * 60,
        ]);
    }

    /**
     * Get the token array structure.
     *
     * @param string $token
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken(string $token): \Illuminate\Http\JsonResponse
    {
        $adminUser = auth()->user();
        return response()->json([
            'accessToken' => $token,
            'firstName'   => $adminUser->firstName,
            'lastName'    => $adminUser->lastName,
            'id'          => $adminUser->id,
            'mailAddress' => $adminUser->mailAddress,
            'expiresIn'   => auth()->factory()->getTTL() * 60,
        ]);
    }
}

5. Model設定

<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class AdminUser extends Authenticatable implements JWTSubject
{
    use Notifiable;

    protected $table = 'admin_users';

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims(): array
    {
        return [];
    }
}

f:id:kojirooooocks:20190510014611p:plain

expiresIn はローカルなのでとんでもない値に設定していますw

終わり

簡単ですが、こんな感じです。

あと備忘録として残しておかないといけないのなんだったけかな・・・?

とりあえず現場からは以上です。

Laravel-Deployerを使ったデプロイ

はじめに

こんにちは。

この記事は challenge-every-month全員でアウトプット芸人 Advent Calendar 8日目の記事です。

前日の記事は、激アウトプット芸人 の @kdnaktさんの記事です。

twitter.com

kdnakt.hatenablog.com

4月からGW明けまで異常に忙しく、なかなかブログや挑戦を行えていない僕です。

今回のブログも作業の備忘録的なブログでございます。

Laravelプロジェクトのデプロイをどうしようかと考えていたのですが、Deployerを試してみようと思い、Laravel-Porjectに特化したLaravel-Deployerなるものを発見したのでこちらを使用しました。

Laravel-DeployerはCapistranoと同じくシンボリックリンク型のデプロイツールで、そのデプロイ方法は大きく5つ存在するようです。

  1. Basic strategy
  2. First deploy strategy
  3. Locally-built strategy
  4. Git pull only strategy (no zero downtime)
  5. Upload strategy

全部検証しているわけではないのですが、基本的にHostサーバ側で git pullしちゃう形になってるみたいです。

また、.envstorage などは sharedに登録されるようですが、 基本的には .env は .gitignore対象になっていると思うので、 git pullしたあとだと .env が存在しません。

.env がない場合は、勝手に空の .env を作成して sheardFileに登録されます。

それによりもれなくエラーとなります。

このあたり参考サイトをみてても手動で解決しているようでした。

この流れで自分が変えたかったのは、以下です。

  • Host側でgit pullしちゃうところ(単純に時間短縮したい)
  • ファイルがない場合勝手に .env 作っちゃう(期待している .env を作ったり上書きしたりしたい)

こちらを対応するため、先ほど紹介したデプロイ方法をベースにカスタムのデプロイ方法を作りました。

本題

1. インストール

$ composer require lorisleiva/laravel-deployer
$ php artisan deploy:init

2. deploy.php修正

今回は自分が変えたかったところを対応するために、 Default deployment strategy を変更します。

# config/deploy.php

<?php

declare(strict_types=1);

return [
    /*
    |--------------------------------------------------------------------------
    | Default deployment strategy
    |--------------------------------------------------------------------------
    |
    | This option defines which deployment strategy to use by default on all
    | of your hosts. Laravel Deployer provides some strategies out-of-box
    | for you to choose from explained in detail in the documentation.
    |
    | Supported: 'basic', 'firstdeploy', 'local', 'pull'.
    |
    */

    'default' => 'custom',

3. recipeとなるphpファイル修正

基本ベースは upload strategy をベースにしており、host側で git pullするところを、 git pullせずrsyncを行うように変更しています。

# recipe/custom_strategy.php

<?php

declare(strict_types=1);

namespace Deployer;

set('writable_dirs', [
    'bootstrap/cache',
    'storage',
    'storage/app',
    'storage/app/public',
    'storage/app/tmp',
    'storage/framework',
    'storage/framework/cache',
    'storage/framework/sessions',
    'storage/framework/views',
    'storage/logs',
]);

desc('Custom Strategy');
task('strategy:custom', [
    'hook:start',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'custom:upload',
    'deploy:shared',
    'deploy:vendors',
    'deploy:writable',
    'hook:ready',
    'deploy:symlink',
    'deploy:unlock',
    'cleanup',
    'hook:done',
]);

set('custom_upload_path', __DIR__ . '/../');
set('custom_upload_vendors', false);
set('custom_upload_options', function () {
    $options = [
        '--exclude=.git',
        '--exclude=.circleci',
        '--exclude=.env.develop',
        '--exclude=.env.local',
        '--exclude=.env.production',
        '--exclude=.env.staging',
        '--exclude=.env.testing',
        '--exclude=server.php',
        '--exclude=sg.sh',
        '--exclude=tests',
        '--exclude=recipe',
        '--exclude=.editorconfig',
        '--exclude=.gitattributes',
        '--exclude=.gitignore',
        '--exclude=.php_cs.cache',
        '--exclude=.php_cs.dist',
        '--exclude=_ide_helper.php',
        '--exclude=codecov.yml',
        '--exclude=coverage.xml',
        '--exclude=phpcs.xml',
        '--exclude=phpstan.neon.dist',
        '--exclude=phpunit.xml',
        '--exclude=readme.md',
    ];

    if (!get('custom_upload_vendors')) {
        $options[] = '--exclude=/vendor';
    }

    return compact('options');
});

desc('Upload a given folder to your hosts');
task('custom:upload', function (): void {
    $configs = array_merge_recursive(get('custom_upload_options'), [
        'options' => ['--delete'],
    ]);

    upload('{{custom_upload_path}}/', '{{release_path}}', $configs);
});

4. デプロイ実行

デプロイ自体は以下のコードで実行できます。

$ php artisan deploy

今回は CircleCIからデプロイするようにしましたので、CircleCI上で checkoutした状態のコードをデプロイします。

このデプロイフローの中で、 .env の問題も解決させています。

# .circleci/config.yml

  # デプロイ実行
  execute_deployer:
    steps:
      - run: cp ~/workspace/.env.production ./.env
      - run: echo "AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID" >> ./.env
      - run: echo "AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY" >> ./.env
      - run: echo "DB_PASSWORD=$DB_PASSWORD" >> ./.env
      - run: php artisan deploy:run deploy:unlock
      - run: php artisan deploy -vv

特に変わったことはしておらず、事前に用意している production環境用の .envファイルを .env ファイルとしてコピーしつつ、デプロイを行うという方法です。

その際に、awsのアクセスキーやシークレットキー・本番DBのパスワードなども環境変数からコピーしています。

deployコマンドを実行する前に deploy:unlock を実行しているのは、万が一デプロイ中に何かしら失敗した場合に .lockファイルが残っている可能性があり、そのファイルを消さないと次のデプロイが失敗してしまうから、必ず消すようにしています。

5. デプロイ結果

実際のデプロイ結果は以下です。

$ ls -la
合計 412
drwxr-xr-x 10 ec2-user ec2-user    207  426 21:48 .
drwxrwxr-x  7 ec2-user ec2-user     55  426 21:46 ..
-rw-r--r--  1 ec2-user ec2-user   1487  426 21:48 .env
drwxr-xr-x 10 ec2-user ec2-user    131  426 21:44 app
-rwxr-xr-x  1 ec2-user ec2-user   1686  426 21:44 artisan
drwxr-xr-x  3 ec2-user ec2-user     34  426 21:44 bootstrap
-rw-r--r--  1 ec2-user ec2-user   2882  426 21:44 composer.json
-rw-r--r--  1 ec2-user ec2-user 400814  426 21:44 composer.lock
drwxr-xr-x  2 ec2-user ec2-user   4096  426 21:44 config
drwxr-xr-x  4 ec2-user ec2-user     37  426 21:44 database
drwxr-xr-x  2 ec2-user ec2-user     40  426 21:44 public
drwxr-xr-x  3 ec2-user ec2-user     18  426 21:44 resources
drwxr-xr-x  2 ec2-user ec2-user    102  426 21:44 routes
lrwxrwxrwx  1 ec2-user ec2-user     20  426 21:46 storage -> ../../shared/storage
drwxrwxr-x 51 ec2-user ec2-user   4096  426 21:46 vendor

終わりに

今回はこのような感じです。

このあたりブログに書き留めるために作業中にまとめておきたかったのですが、とにかく忙しく思い出しながら書きました。。。

嗚呼。忙しい。

現場からは以上です。

参考サイト

忙しくてブログ書けない日々が続いております。

ブログのタイトル通りです。

4月中盤から今日そして、GW明けまでは本当に忙しくなかなか自分の勉強の時間が取れない状況です。

現在入っている

運動頑張るチーム

チャレンジチーム

ブログチーム

のすべてが、ほぼほぼ活動できてない状況で、大変申し訳なく思っております...

本も溜まってるので、読まなきゃいけないのですが...

とりあえずネタはあるのですが、時間がない状況なので、GW終わって、落ちついてからまた頑張り始めようと思います。

ブログというよりTwitterのつぶやきレベルですが、現場からは以上です。

PHPUnit + Phake

はじめに

こんばんは。今回も積本消化月間 vol.1で手を進みながら勉強した、PHPUnit + Phakeのログ的なものをきれいに整形してブログにしました。

WEB+DB PRESS Vol.108

WEB+DB PRESS Vol.108

  • 作者: 中野暁人,山本浩平,大和田純,曽根壮大,ZOZOTOWNリプレースチーム,権守健嗣,茨木暢仁,松井菜穂子,新多真琴,laiso,豊田啓介,藤原俊一郎,牧大輔,向井咲人,大島一将,上川慶,末永恭正,久保田祐史,星北斗,池田拓司,竹馬光太郎,粕谷大輔,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/12/22
  • メディア: 単行本
  • この商品を含むブログを見る

phpは普段業務で使っているので、ハマるところはなくスムーズに進みました。

今回も前回同様やってみたログなので、プログラムコードに関しては、本で紹介されている内容をほぼほぼ再現しています。

本題

assert()を使用したテスト

phpunitなどを使わず、assert()を使用した場合のテスト方法を紹介されています。

# sum.php
<?php

function sum(int $a, int $b): int
{
    return $a + $b;
}

# test_sum.php
<?php

require_once 'sum.php';

$result = sum(1, 2);
assert($result === 4, '1 + 2は3を返す');

自分の環境では、普通に実行しただけでは何も表示されなかったのですが、本書で紹介されている、 assert.exception = 1zend.assertions = 1 を指定することで、狙い通り例外が発生しました。

php -d assert.exception=1 -d zend.assertions=1 test_sum.php 
PHP Fatal error:  Uncaught AssertionError: 1 + 23を返す in /path/to/phpunit_phake/test_sum.php:6
Stack trace:
#0 /path/to/phpunit_phake/test_sum.php(6): assert(false, '1 + 2\xE3\x81\xAF3\xE3\x82\x92\xE8\xBF\x94...')
#1 {main}
  thrown in /path/to/phpunit_phake/test_sum.php on line 6

phpカンファレンス@t_wada さんが話していた、 表明プログラミングの部分で登場していた assert()関数でした。

https://www.youtube.com/watch?v=17i1EL9pBwA

youtubeでも紹介されているのですが、 zend.assertions-1 にしてあげれば、assert() のコードは生成されない動作になります。

なので、開発時に assert() をテストとして使用して、本番では -1 にしてあげてassert()が生成されない動作にしてあげることが出来ます。

自分が考えるこれの旨味は、testファイルとして分けなくても、実際に使用されるクラスなり関数なりに直接書くことができるというところですが、いくら運用時に生成されないとはいえ、assert() のコード自体は存在しているので、ちょっとみにくくなるなと思いました。

assert()を「テストツール」として使用するのではなく、youtubeでも紹介されている通り、「暗黙の前提を表明するツール」と割り切って、別でテストツールを使うのが現実的だと思いました。

phptを使ったテスト

これは自分知らなかったのですが、phpの標準テストツールの phpt を使ったテストが紹介されていました。

ただ、こちらはpearのインストールが必要とのことでした。

おそらくですが、今後pearを使うことはないと思うので、これに関しては読み飛ばしました。

PHPUnitを使ったテスト

自分毎回お世話になっている PHPUnitを使用したテストです。

インストール等はググれば死ぬほど乗ってるので、省略します。

# Sum.php
<?php

namespace Kojirock;

class Sum
{
    public static function sum(int $a, int $b): int
    {
        return $a + $b;
    }
}

# SumTest.php
<?php

use Kojirock\Sum;

class SumTest extends \PHPUnit\Framework\TestCase
{
    /**
     * @test
     */
    public function 2つの値を足し算できること()
    {
        $this->assertSame(4, Sum::sum(1, 2));
    }
}
$ php vendor/bin/phpunit tests
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 54 ms, Memory: 4.00 MB

There was 1 failure:

1) SumTest::2つの値を足し算できること
Failed asserting that 3 is identical to 4.

/path/to/tests/SumTest.php:12

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

よく見る結果が出てきました。

本書では、よく使われる assertメソッドが紹介されていました。

知らないものが多かったというか、自分は正直、assertSame()assertTrue()|assertFalse()expectException() expectExceptionMessage または、 expectExceptionObject を使うくらいなので、その他はほぼ使いません。。。

フィクスチャを使用する

# TaxCalculator.php
<?php
namespace Kojirock;
use DateTimeInterface;
use DateTime;

class TaxCalculator
{
    private $tax3perDate;
    private $tax5perDate;
    private $tax8perDate;
    private $tax10perDate;

    public function __construct()
    {
        $this->tax3perDate = new DateTime('1989-04-01');
        $this->tax5perDate = new DateTime('1997-04-01');
        $this->tax8perDate = new DateTime('2014-04-01');
        $this->tax10perDate = new DateTime('2019-04-01');
    }

    public function calc(int $amount, DateTimeInterface $current):int
    {
        if ($this->tax3perDate <= $current && $current < $this->tax5perDate) {
            return $this->multiple($amount, 103);
        } elseif ($this->tax5perDate <= $current && $current < $this->tax8perDate) {
            return $this->multiple($amount, 105);
        } elseif ($this->tax8perDate <= $current && $current < $this->tax10perDate) {
            return $this->multiple($amount, 108);
        } elseif ($this->tax10perDate <= $current) {
            return $this->multiple($amount, 110);
        }

        return $amount;
    }

    private function multiple(int $amount, int $hundredTimesTaxRate): int
    {
        $multi = $amount * $hundredTimesTaxRate;

        return (int)floor($multi / 100);
    }
}

# TaxCalculatorTest.php
<?php

use Kojirock\TaxCalculator;

class TaxCalculatorTest extends \PHPUnit\Framework\TestCase
{
    private $taxCalculator;

    public function setUp()
    {
        $this->taxCalculator = new TaxCalculator();
    }

    /**
     * @test
     */
    public function 税率5パーセントの金額を計算できる()
    {
        $date = new DateTime('2014-03-31');
        $this->assertSame(105, $this->taxCalculator->calc(100, $date));
    }
    /**
     * @test
     */
    public function 税率8パーセントの金額を計算できる()
    {
        $date = new DateTime('2018-10-31');
        $this->assertSame(108, $this->taxCalculator->calc(100, $date));
    }
    /**
     * @test
     */
    public function 税率10パーセントの金額を計算できる()
    {
        $date = new DateTime('2019-10-01');
        $this->assertSame(110, $this->taxCalculator->calc(100, $date));
    }
}
$ php vendor/bin/phpunit tests
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 39 ms, Memory: 4.00 MB

OK (3 tests, 3 assertions)

こちらもよく使用する setUp の紹介でした。

Phakeを使ったテスト

Mockeryとかは Laravelプロジェクトで少々触ったことがあったのですが、Phakeは使ったことありませんでした。

PHPUnit標準で搭載されているモックツールよりも使い勝手が良いとのことで、試してみます。

インストール

$ composer require --dev phake/phake:3.1.3
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing phake/phake (v3.1.3): Downloading (100%)         
phake/phake suggests installing doctrine/common (Allows mock annotations to use import statements for classes.)
phake/phake suggests installing hamcrest/hamcrest-php (Use Hamcrest matchers.)
Writing lock file
Generating autoload files
——

実装コード

# CostReport.php
<?php

namespace Kojirock;

class CostReport
{
    private $repository;

    private $taxCalculator;

    public function __construct(CostRepository $repository, TaxCalculator $taxCalculator)
    {
        $this->repository = $repository;
        $this->taxCalculator = $taxCalculator;
    }

    public function report(): array
    {
        $rows = $this->repository->findAll();
        return array_map(function (array $row) {
            return $this->taxCalculator->calc($row['value'], new \DateTime($row['date']));
        }, $rows);
    }
}

# CostRepository.php
<?php
namespace Kojirock;

class CostRepository
{
    private $pdo;

    public function __construct(\PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function findAll()
    {
        $stmt = $this->pdo->query('SELECT * FROM report_table');
        $stmt->execute();
        return $stmt->findAll();
    }
}

# CostReportTest.php
<?php

use Kojirock\TaxCalculator;
use Kojirock\CostReport;
use Kojirock\CostRepository;


class CostReportTest extends \PHPUnit\Framework\TestCase
{
    private $costReport;
    private $mockRepository;

    public function setUp()
    {
        $this->mockRepository = Phake::mock(CostRepository::class);
        $this->costReport     = new CostReport($this->mockRepository, new TaxCalculator());
    }

    /**
     * @test
     */
    public function 税率8パーセントのコストをレポートできる()
    {
        Phake::when($this->mockRepository)
            ->findAll()
            ->thenReturn([[
                'date' => '2018-10-31',
                'value' => 100
            ]]);

        $this->assertSame([108], $this->costReport->report());
    }
}
php vendor/bin/phpunit tests
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 59 ms, Memory: 4.00 MB

OK (1 test, 1 assertion)

PDOを使用してDBに接続するCostRepositoryというクラスをmockしてテストできるようにしました。

自分が使用したことのある Mockery と直感的にあまり変わりませんでした。

だいたいmockを使用するのって外部API連携などの部分かと思います。

終わりに

最近仕事がマジで忙しくて、ブログはもちろん、1月1回チャレンジ、毎週運動が全然出来てない状況です。

とりあえずGWすぎれば落ち着くはずなので、それまでは簡単なブログのみになっちゃいそうです。。

現場からは以上です。