OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

勉強会LTはじめました(はじめてました)

今年のフジロックは4日間テント泊にしようと目論んでいる @kalibora です。こんにちは。

さてさて、開発部やそれに類する開発チームで、週1くらいの頻度でミーティングしている組織は多いのではないかと思いますが、 みなさま、どんなアジェンダでミーティングしているのでしょうか?

  • ビジネスとは少し離れた技術的な観点での問題点の話し合い
  • 設計に関する議論
  • 最近の障害の共有

などなど、色々あるかと思います。

弊社ではその開発ミーティングの冒頭15分〜30分間ほど、勉強会と題して開発メンバーの持ち回りでLTのようなことをやっております。

どういった内容のLTをしているかと言うと、特にお題は設けず、ざっくばらん。各メンバーに任せる。としたので 本当にざっくばらんです。一例を挙げると下記のようなお題がありました。

f:id:kalibora:20190624165144p:plain

えーっと、、ユークリッドの互除法からSplatoonまで!幅広く取り揃えております。

このあたりは本当に各メンバーの色が出て面白いところです。

  • そのメンバーが何に興味があるのかを知ることが出来る
  • 自分と興味が被っていなければ、普段とはぜんぜん違う勉強になる
  • 遊びに関するものでも、なぜそれにハマっているのか?ハマる仕組みはなんなのか?などビジネスのヒントになる

と、当初思ってなかったようなメリットもあったので、わりとゆる〜い雰囲気でLTやってみるのも良いのではないかと思っております。

はい。そしてせっかくなので、

「その資料や内容などもブログで公開していけるといいよね〜。」

なんていう流れになったので、今後はこのブログ上で公開していこうと思います。

どうぞみなさま、お手柔らかに。

銭湯でエンジニアの働き方について語る『#エンジニア銭湯』に参加しました!

こんにちは、エンジニアのねっしーです。 平成もじきに終わりですね。

自分は平成生まれなので、これから生まれてくる令和生まれの子たちに「これだから平成のオバサンは...」とか言われるんじゃないかと考えただけで打ち震えます。

だーいぶ日にちが経ってしまいましたが、先週4月6日(土)に開催された『#エンジニア銭湯』というイベントにお手伝いとして参加してきました。その当日の様子とイベントの内容をお届けしようと思います。

connpass.com

まさかの数時間で枠が埋まる

connpassでイベントを公開してからなんと約2時間後には定員の20人を突破する大盛況ぶり。その後増枠してもなお補欠者が増え続けるほど、開催前から注目のイベントでした。

イベント会場

kom-pal.com

会場は大崎にある金春湯さん。大崎の駅付近は再開発が進んでいるため先進的なオフィス街や駅ビルが並んでいるのですが、少し駅から離れると昔ながらの下町が残っています。金春湯も昔ながらの銭湯の良さを感じました。

f:id:nes_801:20190411163625j:plainf:id:nes_801:20190411163707j:plain f:id:nes_801:20190411173952j:plain

f:id:nes_801:20190411173937j:plain

f:id:nes_801:20190415183737j:plain

↑金春湯さんのステッカー、カッコイイのでさっそく貼りました。番台のとこに置かれてるのですが、常連のお爺ちゃんお婆ちゃん達も持ち物に貼ってくれているそう。

オープニング(趣旨説明)

f:id:nes_801:20190411174244j:plain

オープニングはエントランスに集まり、お酒…ではなく銭湯らしくラムネや牛乳で乾杯。イベント中は ハッシュタグ #エンジニア銭湯 でどんどん呟いてね、との案内がありました。

職場の問題かるた大会

エントランスは狭いので、男性脱衣所に移動します。5、6人のグループごとに分かれ新品のかるたを開封。グループ内で優勝した人には大会で使用したかるたがプレゼントされると発表され、会場のテンションが上がります。

f:id:nes_801:20190411174028j:plainf:id:nes_801:20190411174052j:plain

読み上げCDを使って全グループ同時にスタート。読み上げるのは声優の戸松遥さん。この読み上げ音声の収録・編集にオトバンクは関わっています。

f:id:nes_801:20190411174008j:plain

前職のSIerにいた頃を思い出して「あぁ、あるあるだなぁ...」と感じたのは、

  • 【お】「おかしい!」....ってだれも思わないのかなぁ
  • 【し】 資料のための、資料を作る
  • 【と】 「隣のあの人、だれですか?」

この辺りですかね...。大会中も「うーん...w」とメッチャ頷く人や苦笑いする人などもいて、どんな職場でも同じような問題を抱えているのだと分かりました。

LT大会 〜エンジニアの働き方

登壇者は本物の番台の中から発表をします。「番台に入りたかったら第2回3回エンジニア銭湯で登壇してね!」という魅力的な告知もありました。

皆さんのお話ししていた内容は、近藤佑子さんのグラレコが要点を捉えていてとても分かりやすかったので、ここでは感想ベースです。

1人目 角屋文隆さん

f:id:nes_801:20190411174108j:plain

「銭湯は心のリファクタリング」 「ととのいフレームワーク」などのパワーワードが次々と出てきました。

エンジニアはコードの悪い所は見つけられても自分の心の悪いところは意外と見つけられない、というのは心当たりがありすぎてドキッとしました。

「サウナ」「水風呂」「休憩」をループするととのいフレームワークの分かりすぎるサンプル

2人目 後藤大輔さん

f:id:nes_801:20190411174123j:plain

静的ルールと動的ルールの違いを銭湯に例えるのが、とても分かりやすかったです。

誰しもマイルールを持っていて、それを持ち寄って動的ルールができる。仕事でもそうやって職場のルールをみんなで作っていけたら楽しいし理想的ですよね!

3人目 佐渡あまねさん

f:id:nes_801:20190411174141j:plain

ダム際で仕事をする魅力を語り、「あなたにとってのダム際はなんですか?」という投げかけがのちのトークライブに続きます。

「自分にとっての勝ちパターンを見つける」ということはとても重要ですよね。自分としては全員が9時に出社したり、社内勤務なのにスーツを強要するなどの就業規則で誰かの勝ちパターンを潰してしまうのは勿体ないな、と感じます。

4人目 久保田裕也さん

f:id:nes_801:20190411174156j:plain

「満員電車禁止令」や「wikiでの情報公開」など、入社後に自分の中で段々と当たり前になってきたことの特異さを改めて実感しました。

「働き方改革」でなく「働き方創造」、押し付けの制度ではなく、問題ひとつひとつに向き合って解決策を考えることがこれからの働き方には大切なのかもしれないですね。

トークライブ 〜エンジニアの働き方を湯ったり語る

f:id:nes_801:20190411174216j:plain

登壇者を中心にぐるっと囲んで座り、ざっくばらんに参加者も交えてトークセッションを行いました。 佐渡さんの話の中でも出てきた「あなたにとっての勝ちパターンは?」という話題で盛り上がりました。

「会社以外の場所で仕事をする」「朝は苦手なので午後から業務開始する 」などは就業規則の範囲のため実行するは難しいですが、参加者の中からは「1日のうちにあそびの時間をつくる」「(勉強会があるので)定時で上がると周りに宣言する」などの自分で操作できる範囲の勝ちパターンも出てきてとても参考になりました。

まだまだ話し足りなかったのですが、ここでいったんイベントは中締め。 登壇者と運営メンバーで写真撮影をしました。

f:id:nes_801:20190411173814j:plain

入浴タイム

中締め後は希望者で残り、入浴タイム。 営業開始時間に並ぶ常連さんに配慮して、一旦会場の外に出ます。

ぬるめなお風呂があったおかげで、熱いのが苦手な自分でもだいぶ長風呂できました。お風呂に浸かりながら参加者の方とゆっくりお話ができました。

こちらは女湯だったのでまったり雰囲気でしたが、壁の向こう側の男湯がワイワイとても楽しそうで少し羨ましかったです。でも洗い場とか脱衣所とかちょっと窮屈そう...

お風呂上がりはエントランスに戻り、参加者の方が差し入れで下さったお饅頭とコーヒー牛乳を頂きました。

花見

f:id:nes_801:20190411174304j:plain

さらに残った人たちで食事会、と思っていたら桜が綺麗とのことでお花見に変更。コンビニでつまみやお酒を買い、目黒川沿いまで歩きました。

桜の時期の目黒川と言えば中目黒〜五反田間の混雑を想像してしまい「座るところあるのかな...」なんて思っていましたが、五反田よりさらに下った大崎駅周辺は人もまばらながら綺麗な桜が眺められる穴場スポットでした。

↑ ほろよい1本で仕上がる自分

f:id:nes_801:20190411174230j:plain

f:id:nes_801:20190411174320j:plain

まとめ・感想

年代もエンジニア歴もスキルツリーも異なる人同士が「理想の働き方ってなんだろう?」という共通の問いでざっくばらんに話せた今回のイベント(とお花見)は大変有意義なものでした。

今回定員オーバーで残念ながら参加できなかった方や、#エンジニア銭湯 というワードがTLに流れてきて気になった人にも会場の盛り上がりや楽しさが伝われば幸いです。

第1回から大盛況だった本イベント、好評につき第2回、3回の開催も予定されているようです。 その時はぜひ一緒に風呂上がりの牛乳を飲みましょう!

PHPStan で Doctrine Criteria で使ってるフィールドを検証できるようにした

全国1億2000万のDoctrineファンのみなさん。こんにちは @kalibora です。

Doctrine 使ってますか!! Eloquent な皆さんはここで帰っても大丈夫です。

さて、 Doctrine を使っているのであれば Criteria を使っているという方も多いかと思います。
Criteria ってどんなんだっけ?という方は

Doctrine2 四方山話 ( Fetch mode, Index by, Criteria について) - OTOBANK Engineering BlogCriteria の項目。

または

Doctrine Criteriaの使いドコロ | QUARTETCOM TECH BLOG

あたりを読んでみてください。

僕は待ちます。

読みましたか?

OK! では続けます。

はい、そんな便利な Criteria ですが、私が思わずハマってしまった落とし穴がありました。
次節から実際のコードを使って確かめます。

エンティティ(スキーマ)の定義

サンプルとして使うエンティティは下記のように定義しました。

f:id:kalibora:20190225122126p:plain

顧客(Customer)が注文(Order)を複数持つ。というシンプルなもので、メソッドの意味は以下のとおりです。

  • Customer::order() : 顧客が商品を注文する
  • Customer::getRecentOrders() : 顧客の最新の注文を取得する(デフォルトは5件分)
    • 今回このメソッドで Criteria を使います
  • Order::__toString() : 注文内容を文字列化(デバッグ目的で注文の中身をダンプしたいため)

実際のソースコードはすべて こちら に置きましたが、実際のエンティティのソースコードを記述します(※名前空間など一部は省略します)と

Customer.php

<?php

/**
 * @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
 * @ORM\Table(name="customers")
 */
class Customer
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\OneToMany(targetEntity="Order", mappedBy="customer", cascade={"persist", "remove"})
     */
    private $orders;

    public function __construct(string $name)
    {
        $this->name = $name;
        $this->orders = new ArrayCollection();
    }

    public function order(\DateTimeImmutable $date, string $product) : self
    {
        $this->orders[] = new Order($date, $product, $this);

        return $this;
    }

    public function getRecentOrders($limit = 5) : Collection
    {
        /* ここで Criteria を使ってるよ!!! */
        $criteria = Criteria::create()
            ->orderBy(['date' => Criteria::DESC])
            ->setMaxResults($limit)
        ;

        return $this->orders->matching($criteria);
    }
}

Order.php

<?php

/**
 * @ORM\Entity(repositoryClass="App\Repository\OrderRepository")
 * @ORM\Table(name="orders")
 */
class Order
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="date")
     */
    private $date;

    /**
     * @ORM\Column(type="string")
     */
    private $product;

    /**
     * @ORM\ManyToOne(targetEntity="Customer", inversedBy="orders")
     * @ORM\JoinColumn(name="customer_id", referencedColumnName="id")
     */
    private $customer;

    public function __construct(\DateTimeInterface $date, string $product, Customer $customer)
    {
        $this->date = $date;
        $this->product = $product;
        $this->customer = $customer;
    }

    public function __toString(): string
    {
        return sprintf('%s: %s - %s', $this->id, $this->date->format('Y-m-d'), $this->product);
    }
}

こんな感じです。次にサンプルで使うFixtureを定義します。

Fixture の定義

AppFixtures.php

<?php

class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        $customer = (new Customer('kalibora'))
            ->order(new \DateTimeImmutable('2019-01-01'), 'おはぎ')
            ->order(new \DateTimeImmutable('2019-01-02'), 'ようかん')
            ->order(new \DateTimeImmutable('2019-01-03'), '茶饅頭')
            ->order(new \DateTimeImmutable('2019-01-04'), '芋ようかん')
            ->order(new \DateTimeImmutable('2019-01-05'), 'ようかん')
            ->order(new \DateTimeImmutable('2019-01-06'), 'とらまき')
            ->order(new \DateTimeImmutable('2019-01-07'), 'みたらし団子')
            ->order(new \DateTimeImmutable('2019-01-08'), 'とらまき')
            ->order(new \DateTimeImmutable('2019-01-09'), '磯辺団子')
            ->order(new \DateTimeImmutable('2019-01-10'), '茶饅頭')
        ;

        $manager->persist($customer);
        $manager->flush();
    }
}

はい。正月早々元旦から毎日和菓子を注文し続ける。というフィクスチャーになっております。

ここまででテストデータが揃ったので、次節から実際にコマンドを実行し、挙動を確認します。

コマンド実行

TestCriteriaCommand.php

<?php

namespace App\Command;

class TestCriteriaCommand extends Command
{
    protected static $defaultName = 'test:criteria';

    private $customerRepository;

    public function __construct(CustomerRepository $customerRepository)
    {
        $this->customerRepository = $customerRepository;

        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $customer = $this->customerRepository->findOneByName('kalibora');

        /* ここに注目 */
        foreach ($customer->getRecentOrders() as $order) {
            echo $order, PHP_EOL;
        }
    }
}

このように、 getRecentOrders を使い最新5件分の注文を出力すると、

$ ./bin/console test:criteria
10: 2019-01-10 - 茶饅頭
9: 2019-01-09 - 磯辺団子
8: 2019-01-08 - とらまき
7: 2019-01-07 - みたらし団子
6: 2019-01-06 - とらまき

はい、ちゃんと5件出力されました。いいですね。とても便利。

では次に私がハマったパターンのコマンドを実行してみます。

エラーとなるコマンドの実行

TestErrorCriteriaCommand.php

<?php

class TestErrorCriteriaCommand extends Command
{
    protected static $defaultName = 'test:error-criteria';

    private $customerRepository;

    public function __construct(CustomerRepository $customerRepository)
    {
        $this->customerRepository = $customerRepository;

        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $customer = $this->customerRepository->findOneByName('kalibora');

        /* ここに注目 */
        $customer->order(new \DateTimeImmutable('now'), 'ようかん');

        foreach ($customer->getRecentOrders() as $order) {
            echo $order, PHP_EOL;
        }
    }
}

分かりますかね。 getRecentOrders の前に最新の注文を1件追加しています。

このコマンドを実行してみます。

$ ./bin/console test:error-criteria

In ClosureExpressionVisitor.php line 90:

  Cannot access private property App\Entity\Order::$date
(snip)

はい。エラーになりました!
Order::$date は private なプロパティだからアクセスできないぜ!というエラーです。

なぜエラーになったか?

Order の定義をもう一度見てみましょう。

再掲: Order.php

<?php

/**
 * @ORM\Entity(repositoryClass="App\Repository\OrderRepository")
 * @ORM\Table(name="orders")
 */
class Order
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="date")
     */
    private $date;

    /**
     * @ORM\Column(type="string")
     */
    private $product;

    /**
     * @ORM\ManyToOne(targetEntity="Customer", inversedBy="orders")
     * @ORM\JoinColumn(name="customer_id", referencedColumnName="id")
     */
    private $customer;

    public function __construct(\DateTimeInterface $date, string $product, Customer $customer)
    {
        $this->date = $date;
        $this->product = $product;
        $this->customer = $customer;
    }

    public function __toString(): string
    {
        return sprintf('%s: %s - %s', $this->id, $this->date->format('Y-m-d'), $this->product);
    }
}

確かに $dateprivate ですね。
基本的にはプロパティは公開せず、必要に応じてアクセサを用意するのが鉄則なので、
プロパティの定義としてはこれでよいのですが、今回外からアクセスするためのアクセサがなかったためにエラーとなりました。

だから getDate() メソッドを定義してあげれば今回のエラーは出ず、望み通りの結果となります。

じゃあなんで1回めのコマンド実行ではエラーにならなかったか?

<?php
// (snip)

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $customer = $this->customerRepository->findOneByName('kalibora');

        /* この行があるとエラー、なければ成功。 $customer->order(new \DateTimeImmutable('now'), 'ようかん'); */

        foreach ($customer->getRecentOrders() as $order) {
            echo $order, PHP_EOL;
        }
    }

このようにDBからの取得後、さらに注文を追加してから getRecentOrders するとエラーになります。
追加しなければエラーになりません。

前者ではPHPのレイヤで(オブジェクトとしてロードされてから)Criteriaによる処理が走ります。
後者ではDBのレイヤで(SQLレベルで)Criteriaによる処理が走ります。

永続化される前のデータがある場合、DBのレイヤで処理することはできないのでこのような挙動になります。

PHPのレイヤではオブジェクトのアクセサを通して絞り込みやソートが行われるのでアクセサの定義が必須ですが、
DBのレイヤではDoctrineのORMの定義を通してSQLに変換されて絞り込みやソートが行われるのでアクセサは不要です。(もちろん @ORM\Column(type="date") のようなORMの定義は必要)

というわけで私はこのような事象にハマってしまったわけです。

PHPStan で解決する

なんだか 漏れのある抽象化 にやられているような気もしますが、
Criteria が便利であることも事実なのでここはエンジニアリングで解決しましょう。

ということで作りました。

otobank/phpstan-doctrine-criteria - Packagist

です。

これをインストールしてちょちょっと設定すると、
Criteriaで使用しているフィールドが、DBのレイヤでもPHPのレイヤでも存在する問題のないものだ。ということを検証してくれます。

ただし…

これを実現するために、オリジナルの Criteria クラスは使えないようにしています。

その理由ですが、 Criteria を定義したタイミングでは、どのエンティティのコレクションに適用するかが定まらず、
コレクションの matching メソッドに渡されたタイミングで初めてエンティティが定まります。

ということは、matching メソッドに渡された変数 $criteria の中身を知らないといけないことになり、
これを静的解析でやるのは難しいと判断しました。

代わりの解決方法として、 オリジナルの Criteria クラスを禁止し、
どのエンティティに対して適用するCriteriaなのかを定義の時点で定める TargetAwareCriteria というクラスを作り、
アプリケーション側では必ずそれを継承したものを使用する。という方式にしています。

これはオリジナルを使うよりも冗長になりますが、
一方でより明示的な Specification パターンになるようにも思うので、そんなに悪くない選択肢なのではないかなと思います。

ではまた。