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

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

cakephp3でcsrfの制御を行う

はじめに

こんばんは。

今回もまたcakephp3の話です。

外部とのやり取りを行う開発をしており、 外部からのコールバック時に csrfチェックをスルーさせるようなロジックが必要になりました。

その際の対応を備忘録として残しておきます。

book.cakephp.org

本題

現在3.6系のcakephp3を使用中で、その際は、 Application.phpの以下の場所で CsrfProtectionMiddleware を使用しています。

github.com

cakephp4以降になると、CsrfProtectionMiddlewareに whitelistCallback()というメソッドが実装され、ホワイトリスト形式でcsrf保護を通すようなロジックを書ける場所を提供してくれています。

    /**
     * Set callback for allowing to skip token check for particular request.
     *
     * The callback will receive request instance as argument and must return
     * `true` if you want to skip token check for the current request.
     *
     * @param callable $callback A callable.
     * @return $this
     */
    public function whitelistCallback(callable $callback)
    {
        $this->whitelistCallback = $callback;

        return $this;
    }

ちなみに、4.1以降になると、 whitelistCallback() は非推奨になり、代わりに、 skipCheckCallback() を使うように勧められます。

    /**
     * Set callback for allowing to skip token check for particular request.
     *
     * The callback will receive request instance as argument and must return
     * `true` if you want to skip token check for the current request.
     *
     * @param callable $callback A callable.
     * @return $this
     */
    public function skipCheckCallback(callable $callback)
    {
        $this->skipCheckCallback = $callback;

        return $this;
    }

ただし、cakephp3にはこれはありません。 なので、それっぽいのを作る感じになります。

今回作ったのは、 CsrfProtectionMiddleware をextendsした、 AppCsrfProtectionMiddleware です。

<?php
namespace App\Middleware;

use Cake\Http\Middleware\CsrfProtectionMiddleware;
use Cake\Http\Response;
use Cake\Http\ServerRequest;

class AppCsrfProtectionMiddleware extends CsrfProtectionMiddleware
{
    private const WHITE_LIST_REQUESTS = [
        'Example'   => '*',
        'Example2' => ['index', 'callback'],
    ];

    public function __invoke(ServerRequest $request, Response $response, $next)
    {
        $requestParams = $request->getAttributes()['params'];
        foreach (self::WHITE_LIST_REQUESTS as $controller => $actions) {
            if ($requestParams['controller'] !== $controller) {
                continue;
            }

            if ($actions === '*') {
                return $next($request, $response);
            }

            foreach ($actions as $action) {
                if ($requestParams['action'] === $action) {
                    return $next($request, $response);
                }
            }
        }

        return parent::__invoke($request, $response, $next);
    }
}

WHITE_LIST_REQUESTSController => Action という感じで定義していくことで、該当のActionに対しては csrfの保護を外すことが出来ます。

終わりに

ということで、こんなのを実装しましたが、cakphp4系になればこんな独自実装は必要なくなるので、早めにあげられたらいいなと思う日でした。

現場からは以上です。

cakephp3でeventを使用する

はじめに

こんばんは。

今回 CakephpでEventを使用する機会があったので、やり方を備忘録として残しておきます。

book.cakephp.org

もともとCakephp3 には 以下のイベントが用意されています。

MVCにそって開発していくとよく使うのはモデルイベントかもしれません。

今回は独自イベントの設定をやってみました。

本題

まず、EventをDispatchするクラスを作成します。

EventDispatcher.php

<?php
namespace App;

use Cake\Event\Event;
use Cake\Event\EventManager;
use Kojirock5260\Event\EventDispatcherInterface;
use Kojirock5260\Event\EventInterface;

class EventDispatcher implements EventDispatcherInterface
{
    /** @var EventManager  */
    private $eventManager;

    /**
     * EventDispatcher constructor.
     * @param EventManager $eventManager
     */
    public function __construct(EventManager $eventManager)
    {
        $this->eventManager = $eventManager;
    }

    /**
     * @param EventInterface $event
     */
    public function dispatch(EventInterface $event): void
    {
        $this->eventManager->dispatch(new Event($event->eventName(), null, ['data' => $event->eventData()]));
    }
}

また、それぞれ使用するInterfaceを作成します。

EventDispatcherInterface.php

<?php
namespace Kojirock5260\Event;

interface EventDispatcherInterface
{
    /**
     * @param EventInterface $event
     */
    public function dispatch(EventInterface $event): void;
}

EventInterface.php

<?php
namespace Kojirock5260\Event;

interface EventInterface
{
    /**
     * @return string
     */
    public function eventName(): string;

    /**
     * @return array
     */
    public function eventData(): array;
}

次に、EventListenerを作成します。

NotificationEventListener.php

<?php
namespace App\EventListener;

use Cake\Event\EventListenerInterface;

class NotificationEventListener implements EventListenerInterface
{
    /**
     * @return array|string[]
     */
    public function implementedEvents(): array
    {
        return [
            'Notification.Slack' => 'notificationSlack',
        ];
    }

    /**
     * @param $event
     * @param array $data
     */
    public function notificationSlack($event, array $data): void
    {
        // Slackにデータを送信する
    }
}

次に、例えばのビジネスロジックとして、ユーザー登録用のユースケースを作成します。

RegisterUser.php

<?php
namespace Kojirock5260\User\UseCase;

use Kojirock5260\User\Domain\Repository\UserRepositoryInterface;
use Kojirock5260\User\Domain\Event\RegisterUserEvent;
use Kojirock5260\Event\EventDispatcherInterface;

class RegisterUser
{
    /** @var UserRepositoryInterface  */
    private $userRepository;

    /** @var EventDispatcherInterface  */
    private $eventDispatcher;

    /**
     * RegisterUser constructor.
     * @param UserRepositoryInterface $userRepository
     * @param EventDispatcherInterface $eventDispatcher
     */
    public function __construct(UserRepositoryInterface $userRepository, EventDispatcherInterface $eventDispatcher)
    {
        $this->userRepository   = $userRepository;
        $this->eventDispatcher = $eventDispatcher;
    }

    /**
     * @param  array $registerData
     * @throws \Exception
     * @return int
     */
    public function __invoke(array $registerData): int
    {
        $user = $this-userRepository->register($registerData);
        $this->eventDispatcher->dispatch(new RegisterUserEvent($user));

        return $user['id'];
    }
}

ユーザーが新規作成された際に実行したいイベントクラスを作成します。

RegisterUserEvent.php

<?php
namespace Kojirock5260\User\Domain\Event;

use Kojirock5260\Event\EventInterface;

class RegisterUserEvent implements EventInterface
{
    /** @var array  */
    private $user;

    /**
     * RegisterUserEvent constructor.
     * @param array $user
     */
    public function __construct(array $user)
    {
        $this->user = $user;
    }

    /**
     * @return string
     */
    public function eventName(): string
    {
        return 'Notification.Slack';
    }

    /**
     * @return array
     */
    public function eventData(): array
    {
        return $this->user;
    }
}

最後に、ContorllerからEventをセットしつつビジネスロジックを実行します。

ExampleController.php

<?php
namespace App\Controller;

use App\Repository\UserRepository;
use App\EventListener\NotificationEventListener;
use App\EventDispatcher;
use Kojirock5260\User\UseCase\RegisterUser;

class ExampleController extends AppController
{
    public function indexAction()
    {
        $this->getEventManager()->on(new NotificationEventListener());
        $userId = (new RegisterUser(
            new UserRepository($this->UsersTable),
            new EventDispatcher($this->getEventManager())
        ))($this->request->getData('userData'));
    }
}

これで、実際にEventが実行されるようになると思います。

Cakephpのsrc内ですべて完了させるのであれば、こんなに複雑にはしなくていいですが、FWとビジネスロジックを切り離したい場合はこの形が良いかと思いました。

終わりに

愚痴になりますが、今やってる仕事で開発の人が来なくなり自分がすべてを開発することになりました。。。

今年は1人アドベントカレンダーは無理そうです。。。

現場からは以上です。

PuPHPeteerを試した

はじめに

こんばんは。

今回は、Web+DB Press vol.118で紹介されていた PuPHPeteer を試してみました。

WEB+DB PRESS Vol.118

WEB+DB PRESS Vol.118

  • 発売日: 2020/08/24
  • メディア: Kindle

github.com

本を見るまで、存在も知りませんでした。

本では、 E2Eテストを実行するところまで丁寧に説明されていますが、今回は PuPHPeteer を試すところまでやりました。

本題

環境構築

$ mkdir puphpeteer-example
$ cd puphpeteer-example 

$ npm init 
$ npm install @nesk/puphpeteer

$ composer init
$ composer require nesk/puphpeteer

example1 (ページを開いてみる)

このブログを開いて、スクショを撮ってみます。

<?php

require_once './vendor/autoload.php';

$puppeteer = new \Nesk\Puphpeteer\Puppeteer();
$browser   = $puppeteer->launch();

$page = $browser->newPage();
$page->goto('https://kojirooooocks.hatenablog.com/');
$page->screenshot(['path' => 'example.png']);
$browser->close();

gyazo.com

出来ました!

example2 (検索フォームを実行してみる)

<?php

require_once './vendor/autoload.php';

$puppeteer = new \Nesk\Puphpeteer\Puppeteer();
$browser   = $puppeteer->launch();

$page = $browser->newPage();
$page->goto('https://kojirooooocks.hatenablog.com/');
$page->type('.search-module-input', 'laravel');
$page->click('.search-module-button');
$page->screenshot(['path' => 'example2.png']);
$browser->close();

gyazo.com

検索も問題なさそうです。

example3 (スクショのサイズ大きくする)

<?php

require_once './vendor/autoload.php';

$puppeteer = new \Nesk\Puphpeteer\Puppeteer();
$browser   = $puppeteer->launch();

$page = $browser->newPage();
$page->setViewport([
    'width'  => 1080,
    'height' => 1920
]);
$page->goto('https://kojirooooocks.hatenablog.com/');
$page->type('.search-module-input', 'laravel');
$page->click('.search-module-button');
$page->screenshot(['path' => 'example3.png']);
$browser->close();

gyazo.com

画像が大きくなりました。

example4 (指定要素の文字を取得)

<?php

require_once './vendor/autoload.php';

$puppeteer = new \Nesk\Puphpeteer\Puppeteer();
$browser   = $puppeteer->launch();

$page = $browser->newPage();
$page->goto('https://github.com/kojirock5260?tab=repositories');

$repositories = [];
foreach ($page->querySelectorAll('.wb-break-all') as $element) {
    $repositories[] = trim($element->querySelectorEval('a', 
        \Nesk\Rialto\Data\JsFunction::createWithParameters(['element'])->body('return element.innerHTML')
    ));
}
$browser->close();
var_dump($repositories);
$ php example4.php
/path/to/puphpeteer-example/example4.php:18:
array(24) {
  [0] =>
  string(19) "laravel-breadcrumbs"
  [1] =>
  string(16) "symfony5_example"
  [2] =>
  string(14) "paypay-example"
  [3] =>
  string(11) "tdd-example"
  [4] =>
  string(14) "nuxt_example01"
  [5] =>
  string(17) "slack-app-backlog"
  [6] =>
  string(28) "laravel-json-schema-validate"
  [7] =>
  string(23) "github_mention_to_slack"
  [8] =>
  string(4) "docs"
  [9] =>
  string(14) "type-converter"
  [10] =>
  string(21) "sample__object-mapper"
  [11] =>
  string(18) "object-type-parser"
  [12] =>
  string(24) "simple_line_flex_message"
  [13] =>
  string(4) "tbls"
  [14] =>
  string(14) "github_mention"
  [15] =>
  string(12) "cron-doc-gen"
  [16] =>
  string(28) "write-blog-every-week-remind"
  [17] =>
  string(27) "phpunit-snapshot-assertions"
  [18] =>
  string(13) "sample__react"
  [19] =>
  string(24) "sample__chatwork-webhook"
  [20] =>
  string(24) "sample__pillow-draw-text"
  [21] =>
  string(35) "sample__excel-tool-check-php-python"
  [22] =>
  string(31) "sample__google_cloud_vision_api"
  [23] =>
  string(17) "ansible-lamp-php7"
}

なんか無理矢理感、ありますが、なんとか出来ました...w

終わりに

本家の puppeteer はほんの少しだけ触ったことがありましたが、慣れ親しんでいる phpで書けるのはいいですね。

本では実際にphpunitを使ってE2Eテストをするところまで紹介されているので、興味ある方はぜひ!

別件で、ちょっとハマったのが、僕は ESET セキュリティソフトを入れているのですが、このファイヤーウォールが邪魔して、以下のエラーが発生していました。

PHP Fatal error:  Uncaught Socket\Raw\Exception: Socket operation failed: Resource temporarily unavailable (SOCKET_EAGAIN) in /path/to/puphpeteer-example/vendor/clue/socket-raw/src/Exception.php:59
Stack trace:
#0 /path/to/puphpeteer-example/vendor/clue/socket-raw/src/Exception.php(28): Socket\Raw\Exception::createFromCode()
#1 /path/to/puphpeteer-example/vendor/clue/socket-raw/src/Socket.php(247): Socket\Raw\Exception::createFromSocketResource()
#2 /path/to/puphpeteer-example/vendor/nesk/rialto/src/ProcessSupervisor.php(409): Socket\Raw\Socket->read()
#3 /path/to/puphpeteer-example/vendor/nesk/rialto/src/ProcessSupervisor.php(382): Nesk\Rialto\ProcessSupervisor->readNextProcessValue()
#4 /path/to/puphpeteer-example/vendor/nesk/rialto/src/ProcessSupervisor.php(167): Nesk\Rialto\ProcessSupervisor->executeInstruction()
#5 [internal function]: Nesk\Rialto\ProcessSupervisor->__ in /path/to/puphpeteer-example/vendor/nesk/rialto/src/ProcessSupervisor.php on line 431
PHP Stack trace:
PHP   1. {main}() /path/to/puphpeteer-example/example.php:0

同じくこのエラーが出ている場合は、セキュリティソフトの可能性があるので、見てもらえたらと思います!

現場からは以上です。

github actionでlaravelのunit test

はじめに

こんばんは。

今回は github actionで laravelのunit testを対応した備忘録です。

参考サイト

techracho.bpsinc.jp

本題

まずは例のごとくいきなりコードどかっと載せます。

name: Unit Test

on:
  push:
    branches: [ develop ]
  pull_request:
    branches: [ develop ]

jobs:
  unit-test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0.19
        ports:
          - 3306:3306
        options: --health-cmd "mysqladmin ping -h 127.0.0.1" --health-interval 20s --health-timeout 10s --health-retries 10
        env:
          MYSQL_ROOT_PASSWORD: rootパスワード
          MYSQL_DATABASE: データベース名
          MYSQL_USER: ユーザー名
          MYSQL_PASSWORD: パスワード
    steps:
      - uses: actions/checkout@v2
      - name: Copy .env
        run: php -r "file_exists('.env.testing') || copy('.env.testing.example', '.env.testing');"

      - name: Change Directory Permissions
        run: chmod -R 777 storage bootstrap/cache

      - name: Install Libraries
        run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist

      - name: Generate Key
        run: php artisan key:generate --env=testing

      - name: Execute Test
        run: composer test

github actionのworkflowテンプレートでは sqliteを使用しているバージョンが紹介されていたのですが、今回は、mysql8のwindow関数とかを使ってたので、mysqlを使用したバージョンになります。

testの実行は composer のscriptsに適当にセットしております。

また、今回、共通で実行したいSeederがあったので ベースの TestCaseクラスを少し修正しています。

<?php

declare(strict_types=1);
namespace Tests;

use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;
    use DatabaseTransactions;
    private static $init = false;

    protected function setUp(): void
    {
        parent::setUp();

        if (!self::$init) {
            $this->artisan('migrate:fresh --seed');
            self::$init = true;
        }
    }
}

最初は static変数せずに直で artisanコマンドを実行していたのですが、そうすると DatabaseTransactionTraitが毎回きれいに消してくれて、実行時に毎回 seed実行が走ってしまったため、このような対応になりました。

終わりに

もうすぐ10月になります。

例年ならば1人アドカレそろそろ仕込み始める時期ですが、今年は子供のことで少しプライベートが忙しく、ちょっと無理かもしれません...

現場からは以上です。

DynamoDBのテーブルを別テーブルにコピーする

はじめに

こんばんは。

今回はタイトルの通り DynamoDBのテーブルを別テーブルにコピーする方法を備忘録で残しておきます。

wbewのデータはDynamoDBに保存しています。

現在wbewのスクリプトGolangからJSに切り替える作業を 行っています。

ほぼほぼ @kdnaktさんにやってもらっている感じで、自分はほんとちょこちょこやってるくらいです...w

スクリプトの修正後、現在使用中のテーブルに影響を与えず別テーブルで実際にデータが反映されるかをチェックするために、テーブル自体をコピーすればいいかなと思い、調べてやってみました。

本題

調べてわかったのですが、DynamoDBは csvのエクスポートや バックアップなど 今あるテーブル に対してのアプローチはかなり豊富なんですが、 csv importや、今回やりたいコピーなどは、ちょっと工夫が必要そうでした。

pythonスクリプトを自作で書いて対応したり、S3にアップした際に、 Data Pipelineを使用して importするようにしたりなど、こんなめんどくさいのかーと思って、もっとカジュアルに簡単にできるものないかなーと思ってたらありました。

コチラのスクリプトです。

github.com

download後以下のコードを実行すると、簡単にテーブルコピーが出来ました。

$ AWS_ACCESS_KEY_ID=xxxxxxxxxx AWS_SECRET_ACCESS_KEY=zzzzzzzzzz AWS_DEFAULT_REGION=ap-northeast-1 python3.8 dynamodb-copy-table.py 対象のテーブル コピー先のテーブル(作成されていないこと)

f:id:kojirooooocks:20200920181155p:plain

f:id:kojirooooocks:20200920181205p:plain

終わりに

一旦DBのコピーは終わったんですが、まだまだリプレイスは終わってないので、 @kdnaktさんに聞きながら、開発進めていこうと思います。

ただ、リリースが近づいてきて、なかなか動けずやばい...

現場からは以上です。

2つの輪読会始まった

はじめに

こんばんは。

今週から2つの輪読会が始まりました。

1つ目はクリーンアーキテクチャ輪読会

yourmystar.connpass.com

2つ目はレガシーコードからの脱却輪読会

リンクなし

クリーンアーキテクチャの方は、以前も輪読会したのですが、今お仕事を頂いている会社主催の輪読会だったので、復習のために再度参加しました。

本題

クリーンアーキテクチャ #1

第1章 設計とアーキテクチャ

気づき
  • ソフトウェア設計の目的
    • 求められるシステムを構築・保守するために必要な人材を最小限に抑えること
  • 崩壊のサイン
    • アーキテクチャ構造の配慮にかけているものをリリースし続けると、リリース毎にコストが上昇していく
    • 経営者視点でみても、コストが上昇する毎に開発者単価も上がっていくため、経営にもダメージがある
  • 後々対応する。の罠
    • 設計の配慮を欠いているコードが世に出ることを理解した上で、「後でキレイにするからとりあえずリリースしよう」と、リリースしたものは、後でキレイにされることはない。競合他社のプレッシャーに打ち勝つため、次の機能のリリースをどんどん開発し、走り続ける必要があるから。
    • 早期リリースのために設計配慮の欠けたコードを書くという行為は、事実誤認。実際は短期的にも長期的にも、設計に配慮されたコードを書くほうが早い。
    • 一から再設計するという方針もおすすめしない。現実はそれほどうまくいかない。

第2章 2つの価値のお話

気づき
  • ソフトウェアの定義

    • ソフトウェアはソフトに なるように考案されたものだから、簡単に振る舞いを変更できなければならない。簡単に変更できないものはハードウェアだ。
  • 振る舞いと構造

    • 大事なものは振る舞い(機能が動作すること)ではなく、構造(機能を提供しつつ変更に強い)である
  • アイゼンハワーマトリックスでわかる緊急度・重要度

    • 振る舞いは緊急だが、常に重要とは限らない。構造(アーキテクチャ)は重要だが、常に緊急とは限らない。緊急かつ重要というのが最優先だが、ビジネスマネージャは、振る舞いを 緊急かつ重要 に昇格させることが多々ある。構造の重要だが、緊急ではない を判断できていない。ここをわかっていないと崩壊のスタートになる。ビジネスマネージャにきちんと判断させるために、きちんと発言できるアーキテクトの存在が重要。

感想

1〜2回読んだことがあるので、復習という感じで読めました。

はっとしたのは、クリーンアーキテクチャもレガシーコードからの脱却も、レガシコードや、それの影響について似たような文章があったことでした。

最初は2つ同時期に輪読会読みすすめることは少し不安でしたが、こういった気付きがあったのでやってよかったと思いました。

レガシーコードからの脱却 #1

0章 何かが間違っている

気づき
  • クリーンアーキテクチャの冒頭でも、レガシーコード(設計が練られておらず、修正にコストがかかるコード)について似たような記載があった気がした
  • ウォーターフォール手法が、ソフトウェア開発にマッチしないということを理解できた。
  • What(何)コメントではなくWhy(なぜ)コメントを残す。Whatはコードが明瞭に表現されているべき(他書でも良く言われているけど言い回しが一番しっくりきた)
悩んだこと
  • 本書でも書かれているウォーターフォールでの開発を経験したこと無いのだけど、経験した方はいるのか?どんな感じだったのか?また、今で結構あるものなのか。

感想

自分はガチガチのウォーターフォールを経験してなかったので、こういった現場は今でも結構あるのか?と思ったりしたのですが、参加者の方とお話して、ここまではないにしても、気づいてなかったけど小さい粒度でウォーターフォール開発をしていたというのを気づきました。

ただ、 小さい粒度のウォーターフォール開発ってなに?それがアジャイル開発? という話題が広がっていき、そもそもアジャイル開発とはどんなものなのか?という謎が続いていきました。

僕は アジャイル開発とはなにかを説明してください と言われて、きちんと説明ができないので、アジャイル開発がどんなものかを理解していないのだとはっきりわかりました。

終わりに

色々と気づきがある輪読会でした。

あと HackMD便利。

現場からは以上です。

テスト駆動開発 読み終えた

はじめに

こんばんは。

7/8 からテスト駆動開発の輪読会を行って、先週やっと終わりました。全7回でした。

第1回

yourmystar.connpass.com

第7回(最終回)

yourmystar.connpass.com

テスト駆動開発

テスト駆動開発

本題

第一部(多国通貨)は、例となるプロジェクトを、写経しながらTDDの体験が出来るようになってました。

第二部(xUnit)は、Pythonを使って実際にテスティングフレームワークを作成できるようになっていました。

第三部(テスト駆動開発のパターン)は、TDDを実践する上でのよく使うテクニックやパターンなどを紹介していました。

個人的には第一部の写経と、第一部に説明されていた文章がとても勉強になりました。

また、本の前半で書かれていた以下の文章もとても印象的でした。

TDDはテスト技法ではない。TDDは分析技法であり、設計技法であり、実際には開発のすべてのアクティビティを構造化する技法なのだ

ボク個人の印象としては 分析技法 というのが一番しっくりきました。

「先にテストを書いていくことがTDDだ」という勘違いが最近でも生じているかもしれませんが、この本の第一部をじっくり写経することで

そうではない。それはただの手段であって、TDDで開発する上での目的は別のものなんだ

ということに気づくことが出来ると思います。

どうしても最初は、いきなり実装からガッと書きたいなと思ってしまいますが、そこは経験値を積めば考えが変わってくるかもしれません。

また、第一部の写経を終えた後、輪読会メンバーで高速道路の料金算出ロジックを実際にTDDでモブプロする機会があり

「ここは明白な実装でいいだろ?」 「いや、ここは仮実装で堅実に行こう」

などワイワイ言いながら進められて、写経以外にも実りがある輪読会でした。

終わり

2017年に発売された本ですが、2020年現在でもとてもためになる良書でした。

また、コードはjavaで書かれていますが、内容は言語に寄らない情報だと思いますので、言語違いのアレルギーを出さず手を出してほしいです。

実際自分は第一部をphpで写経をしてました。

github.com

次は、レガシーコードからの脱却の輪読会が控えてます。

もっともっと勉強しないとなぁ...

現場からは以上です。