gorogoroyasu

福岡の開発会社で働いている。

はじめてのPHPプロフェッショナル開発 を読んだ

書評なんて偉そうなことを言える立場ではございませんが、
購入し本日届いたのでざっと感想のようなものを書いていきます。

f:id:adiboy:20190228232941j:plain
買った本

全体の感想

  • PHP の歴史やPSR の説明、DockerやCI、静的解析ツールの使い方などモダンな開発環境が紹介されている
  • Slackの紹介や障害との付き合い方など、実際に開発現場でしか味わえないような内容も書いてあり、親近感が湧く
  • 解説がとても丁寧なので、独学の人にも学びやすそうに思える
  • 普段業務で使っている内容を公開してくれている感がある(CakePHPTravis CI, コンテナの運用など)

読んでいて著者の一人がブログでおっしゃられている内容を確実に実現している本だと感じました。
itosho525.hatenablog.com

裏を返せば、

  • 完全なる初心者向けではない
  • フレームワークの使い方に特化した内容ではない
  • 帯に短し襷に長し(失礼だけど、御本人がおっしゃってるので泣)

ということを念頭に置いて読まなくてはなりません。
自分も1~2時間ぐらいでサラッと読むような感じだったので(休みの日にサンプルコード動かしてみます)、自分よりも熟練の人にとっては物足りないかと思います。

では、この本は誰におすすめか。
それはまさに、これから入ってくる新人の方たちです(上から目線ですいません)。
この時期に発売されたのも、会社に入ってくる新人の人たちを狙い撃ちにしている感じがします笑
これは全然悪い意味ではなく、この本を人数分買って渡しておけば、勝手にぐんぐん成長してくれることでしょう。

自分の勝手な妄想ですが、

現状モダンな開発ができている組織

幅広い知識が必要ですが、この本で網羅的に(広く浅く)触れられているので、
必要性・重要性の説明、Hello World ぐらいまでの教育コストが削減できると思います。

モダンな開発にこれから取り組みたい

新人にモダンな開発手法があることを学んでもらい、「これは、最新の本にも書かれていない高等テクニックですか?(煽り)」的な感じで追い立てられることで組織全体にモダンな開発が広がる(妄想強め)

というような効果が期待できると思います。

あと、これはあまり大きな声では言えないことなのですが、
入社した会社が、モダンな開発をできているかどうか? を測る物差しにもなると思います。
自分の会社は他の会社と比べて開発しやすい環境にあるのか?
ということを知ることができる本です。

もちろん、5年目の人がよんでも10年目の人が読んでも学びはあるでしょう。
しかし、上記のような理由で新人の方にぜひ読んでいただきたい本です。
全国にあるPHPの会社に入社する新入社員の分だけ売れることを願ってやみません。

細かい感想

しっかり読んだ章、さっと読み飛ばした章などばらつきがあるので、
読んだところはしっかりと、飛ばしたところは正直に書いていこうと思います。
また、いくつか自分はこうしてる といった補足情報なども書いていこうと思います。

導入編

Chapter01 進化するPHP

  • 01-01 PHPの歴史
  • 01-02 PHPの特徴

自分はPHP の歴史についてほとんど何も知らなかったのだということを実感させられました。
<?= ?>シンタックスは PHP5.4~ なのか、など、興味深い発見がありました。
誰かの伝記を読んでいる感じでした。

Chapter02 PHPのエコシステム

  • 02-01 モダンなPHPを支えるコミュニティの力
  • 02-02 PHP-FIGとPSR
  • 02-03 PHPのパッケージ管理
  • 02-04 PHPのアプリケーションフレームワーク

PSR は、名前はよく聞くしなんとなく知っていたけど、
何種類あるのか? などということは恥ずかしながら知らなかったので、とても勉強になりました。
逆にこの辺は、何年かPHP を触っている不勉強な人間が読んだほうが面白いと思います。

Chapter03 PHPをはじめよう

  • 03-01 エディタ
  • 03-02 DockerでPHPの開発環境を整える

自分はPhpStorm を使ってます。
あと、開発環境構築も丁寧に解説されており、これならあまりつまずくことなく環境構築ができるのではないかと感じました。

Chapter04 モダンPHPの文法と基礎文法

  • 04-01 基本的な構文
  • 04-02 型と演算
  • 04-03 分岐処理
  • 04-04 繰り返し処理
  • 04-05 関数
  • 04-06 PHP7の新機能

不勉強ながら、PHP7 の新機能の中には知らないものもあり、大変勉強になりました。
早速明日から使っていこうと思います。

入門編

Chapter05 チームのための開発環境構築

Chapter06 設計から始める

Chapter07 CakePHPを使ってみよう

Chapter08 質問と回答機能の実装

Chapter09 ユーザー管理機能の実装

Chapter10 テストコードを書く

ごめんなさい。ぜんぶ飛ばしました。
ほとんど見たことあるような内容だったので。。(ここを飛ばしたのがサラッと読めた原因というか理由) 後日動かしてみます!

 実践編

Chapter11 チーム開発の現場

  • 11-01 個人開発とチーム開発の違い
  • 11-02 GitHubを使った課題の「見える化
  • 11-03 Slackを使った日々のコミュニケーション
  • 11-04 ツールの選定方法

自分がこの業界に入ったのは、Rebuild.fm で聞く話にワクワクしたから という部分が大きいわけですが、
この章の話は、その頃の気持ちを思い出させてくれるような生き生きとした内容でした。

Chapter12 Pull Request 駆動によるコードレビュー

  • 12-01 コードレビューの必要性
  • 12-02 Pull Requestを利用したコードレビューの方法
  • 12-03 コードレビューをしてみよう

この章は、チーム開発をしたことがない人に特におすすめだと思います。短いですがw
LGTMの解説があったのは笑いました!

ちなみに、typo 見つけましたw

f:id:adiboy:20190228233045j:plain
typo

Chapter13 開発に役立つツール

  • 13-01 なぜツールを使うのか
  • 13-02 PHP_CodeSniffer:コーディング規約チェックツール
  • 13-03 PHPStan: コード解析ツール

このあたりは、必読だと思います。
ちなみに自分はScrutinizer というサービスを使って静的解析をやってます。
PHP_CodeSniffer は、git の pre-commit に入れてます(ときどきめんどくさくて切ってるのは墓まで持っていく内緒のうちの一つです)。

scrutinizer-ci.com

Chapter14 継続的インテグレーション

自分はCircle CI を使ってます。
ただ、せっかくなのでTravis CI も触ってみようと思います。

Chapter15 デプロイの自動化

  • 15-01 Webアプリケーションの公開
  • 15-02 ソフトウェアのデプロイメントサイクル
  • 15-03 デプロイ自動化のメリット
  • 15-04 コンテナベースのビルド&デプロイ
  • 15-05 コンテナのデプロイサイクル
  • 15-06 コンテナのデプロイを楽にするオーケストレーションツール

ここが一番の驚きでした。
普通にコンテナを運用されてるんですね。
もちろん要件にもよりますが、自分はアプリケーションをコンテナで運用した経験がないので、とても興味深かったです。
Ansible + Deployer といったタイプの(なんと呼べばいいのかわからない)話も併記しておいてほしかったなと思います。
そんなこと言ってる時点でもはやモダンではないのかもしれませんが。

発展編

Chapter16 障害と向き合う

  • 16-01 障害は突然やってくる
  • 16-02 障害についての重要な考え方
  • 16-03 障害と向き合うためのツール

自分はこのあたりを利用しています。(いつもお世話になっております) github.com

github.com

Chapter17 SQLチューニング

Chapter18 PHPとセキュリティ

このあたりは、Web システムを語る上では外せない という理由で入っているのでしょうが、
それぞれの内容で本が書けるぐらい奥が深い話なので、より詳細な本へのイントロといった内容だと思います。
ただ、書いてある内容は厳選されており、知らないと話にならないような内容なので、とても重要だと思います。

Chapter19 外の世界に飛び出そう

  • 19-01 OSSへの貢献
  • 19-02 最新情報のキャッチアップ
  • 19-03 更にその先へ

いやー。いいこと書いてありますね。
自分は最近全く登壇できていない(というか、勉強会にも行っていない)ので、一度初心にかえろうと思います。

ということで、ざっとではありますが、感想を書かせていただきました。
明日出社したら、早速新人教育の担当に渡してみようと思います。

すべての新人PHPer がこの本を手に取り、素敵なスタートダッシュを切れることを願ってやみません。

自作のValidatorを使う

CakePHPドキュメントには、 再利用可能なバリデーターを作成するという説明書きがあります。
PR を送るのは後回しにして 今回の記事では、その使い方を書いていこうと思います。

ドキュメントに書いてあるコードはこんな感じです。

namespace App\Model\Validation;

use Cake\Validation\Validator;

class ContactValidator extends Validator
{
    public function __construct()
    {
        parent::__construct();
        // バリデーションのルールを加える
    }
}

なるほど。ContactValidatorというバリデーターを作ることができるのかということがわかります。 つまり、この機能を使うと、

public function __construct()
{
    parent::__construct();
    $this
        ->scala('name')
        ->requirePresence('name')
        ->notBlank('name');
}

という書き方ができるようになります。

これをうまいこと呼ぶと、いつもモデルで書いているおなじみの、

class UsersTable extends Table
{
    public function validationDefault(Validator $validator)
    {
        $validator
            ->scala('name')
            ->requirePresence('name')
            ->notBlank('name');

        return $validator;
    }
}

が、

class UsersTable extends Table
{
    public function validationDefault(Validator $validator)
    {
        return $validator;
    }
}

というふうに書けそうです。 では、$validator を ContactValidator に変更するにはどのようにすればよいのでしょうか?

答えは、モデルの設定にあります。

use App\Validation\ContactValidator;

class UsersTable extends Table
{
    // この行を追加
    protected $_validatorClass = UsersValidator::class;

    public function validationDefault(Validator $validator)
    {
        return $validator;
    }
}

$_validatorClass というプロパティに呼び出したいValidator をセットします。 あとは、自動でいい感じに $validator が ContactValidator のインスタンスに差し替えられます。

これを使うと、明示的に宣言しつついい感じでValidation を隠蔽できます。 更に、Behavior を new する必要すらないので、テストが(前回よりも更に)書きやすい!

ちなみに、

public function validationAdd(Validator $validator)
{
    // default とは違う処理
    return $validator;
}

という処理を書きたいことがあります。 そんなときは、

use App\Model\Validator\UsersAddValidator;

class UsersTable extends Table
{
    protected function createValidator($name)
    {
        if ($name == 'add') {
            $this->_validatorClass = UsersAddValidator::class;
        }
        return parent::createValidator($name);
    }
}

という感じで、$this->_validatorClass を上書きしてあげると良いでしょう。

Migration に書く処理を共通化した親クラスを作りたい

CakePHP3 のマイグレーションについて

CakePHP3 は、デフォルトで Phinx というマイグレーションツールを使用しています。

docs.phinx.org

マイグレーションファイルの雛形は以下のようになっており、

$ bin/cake migrations create MyCustomMigration

これを実行すると、

config/Migrations/20160121164955_my_custom.php

というファイルが作成されます。

ファイルの中身は、

<?php

use Migrations\AbstractMigration;

class MyCustomMigration extends AbstractMigration
{
    public function change()
    {
    }
}

のようになっています。

今回の話は、 extends AbstractMigration を変更したい という話です。

やるべきこと

親クラスを作る

config/Migrations/AppMigrations.php

<?php
namespace Config\Migrations;

use Migrations\AbstractMigration;

class AppMigrations extends AbstractMigration
{
    protected function commonUtil()
    {
        // 共通処理
    }
}

ドキュメント https://book.cakephp.org/3.0/ja/migrations.html#id2 にある通りに、カラムの情報を書こうとすると、 けっこうコードが長くなるので、割と共通化のしがいがあります。
特に、オプションとかは共通なものも多いでしょうし。

親クラスを継承する

config/Migrations/20160121164955_my_custom.php をちょこっと編集します。

<?php

// use Migrations\AbstractMigration; # 消す
                                                           
class MyCustomMigration extends AppMigrations // ← 変更する

composer.json を編集

    "autoload": {
        "psr-4": {
            "App\\": "src/",
            "Config\\": "config/" # この行を追加!
        }
    },

composer.json の autoloadに Config を追加します。

composer install

composer install をします。
これにより、 Config という名前の namespace が認識されます (詳細はよくわかってない)

以上! ここまでできたら、Migration を実行できます。

そこそこめんどくさいですが、 別で class を作ったりすると、 いちいち $table を渡さなくてはならなかったりして見た目が美しくありません。

Trait を使ったり、Plugin を使ったりと、他にも色々な方法はあると思いますが、
特定の案件だけで用いる特殊な処理を共通化したい場合は 今回のように親クラスを作ってあげるとさっと進めて良いのだろうなと思います。

IteratorをGeneratorと関数内static変数で実装する

Excel のカラムは、 A から始まって Z までいくと次は AA となり、 AZ、BA、BZ・・・と続いていきます。

以前行った案件でExcel出力を行う際に github.com

を使わせていただいていましたが、

        'col' => 'B',

を指定するのがかなり大変でした。

というのも、カラムが AX ぐらいまであり、今回の改修で、そのうちのいくつかの項目を削ることになったのです。

つまり、

        'col' => 'D',

という値を消すと、以降の

        'col' => 'E',
        'col' => 'F',
        'col' => 'G',

をすべて

        'col' => 'D',
        'col' => 'E',
        'col' => 'F',

に書き換えなくてはなりません。
そして、今回、機能追加を行うことになりました。
そんなことやってられるかー (怒)

ということで、2つ対策を考えてみました。

俗に言う、Enum ってやつなのかな?
イメージとしては、Golang のiota 的なやつ。

package main

import (
    "fmt"
)

const (
    a = iota
    b
    c
    d
)
func main() {
    fmt.Println(a, b, c, d)
        # => 0, 1, 2, 3
}

今回は、 Excel のカラムに沿った値を返すところが違います。

Generator を使う解決策

class GenColumn
{
    public function __construct()
    {
        $this->gen = $this->excelColumnGenerator();
    }

    public function getCurrent()
    {
        return $this->gen->current();
    }

    public function getNext()
    {
        $this->gen->next();
        return $this->gen->current();
    }

    private function excelColumnGenerator()
    {
        $ary = [];
        for ($a="A"; $a != "AZ"; $a++) {
            $ary[] = $a;
        }
        foreach($ary as $column) {
            yield $column;
        }
    }

}

$column = new GenColumn();
print($column->getCurrent());
print($column->getNext());
print($column->getCurrent());
print($column->getNext());
print($column->getCurrent());

getCurrent() というメソッドを追加して、現在の値を取得することができるようにしています。

これを 関数内static変数を使うバージョンで書き直すと、

関数内static変数を使う解決策

class GenColumn
{
    private static $ary = [];
    
    private static function genAry()
    {
        $ary = [];
        for ($a="A"; $a != "AZ"; $a++) {
            $ary[] = $a;
        }
        self::$ary = $ary;
    }

    public function getColumnName($current=false)
    {
        static $a=1;
        if (empty(self::$ary)) {
            self::genAry();
        }
        if ($current) {
            return self::$ary[$a - 1];
        }
        
        $a++;
        return self::$ary[$a - 1];
    }
}
$column = new GenColumn();
print($column->getColumnName(true));
print($column->getColumnName(false));
print($column->getColumnName(true));
print($column->getColumnName(false));
print($column->getColumnName(true));

という風に書けます。

両者に一長一短があります。

ちなみに、10000回ループした際の実行速度(5回の平均)は

Generator: 0.089s 関数内static変数: 0.023s

と、関数内static変数の方で実装したほうが4倍ほど早かったです。

メリットとデメリット

Generator を使う解決策は、コードが長くなりますが、 - $column->getCurrent() - $column->getNext() と、行う操作にわかりやすい名前をつけることができます。

一方、コードが長くなり、実行に時間がかかります。

関数内static変数 を使う解決策は、コードは短く実行時間も早いです。 ただし、現在の値とインクリメントした値を取得する際に、 $column->getColumnName($current) という風に、$current の true/false で取れる値が変わります。 可読性は決して高くないです。

まとめ

結局、2つの実装方法を検討しましたが、より安全性が高い Generator で実装を行いました。 ただ、 関数内static変数 の使い方はあまり良く知らなかったので、調べてみてよかったです。

LINE公式アカウント の料金を調査するWebサイト作った。

昨日、LINE 社より 【重要】LINE@サービス統合のお知らせ というお知らせが発表されました。

blog-at.line.me

そこには、 「LINE@」は「LINE公式アカウント」「LINE ビジネスコネクト」「LINE カスタマーコネクト」とサービス統合し、名称を「LINE公式アカウント」として生まれ変わります。 と書かれています。

つまり、今までと料金体系が変わるということです。

フリープランでも、今まで上位プラン・もしくはビジネスコネクトでしか使えなかった機能が使えるようになる半面、
メッセージの送信が従量課金制になります。

料金プランも同時に発表されましたが、そこそこ分かりづらい。
無料枠の取扱とかが難しいです。

ということで、プランとメッセージ数を入力したら月いくらかかるか試算してくれるアプリ(と呼べるほどのものでもないが。。。)を作りました。

https://gorogoroyasu.github.io/lineat_price/

必要最低限の機能しかないし、バリデーションもしていない。 メッセージも不親切だしデザインもなにもない。と、不親切極まりない感じですが。。。

ちょこちょこ修正して、いい感じにしていこうと思っています。

一応、Vue.js を使っています。
使ったことないので、間違った使い方してるかも。
(そもそもJSをHTML 内に書いてる時点でというのはおいといてw)

テスト

上記のLINE社の発表の中で、いくつか例示されている金額があります。
手動ではありますが、その金額がただしいことは確認しています。 逆に言うと、他の金額のことは考えていないので、違うかもしれません。

算出根拠とかも出るようにしたほうがいいですね。 改修します。

終わりに

プルリク送ってください。

tensorflowのrandom crop で、2枚の画像の同じ箇所を切り取る

画像が2枚あります。
画像A が入力データ 画像B が正解データ。

f:id:adiboy:20181026102449p:plain 画像引用: https://helpx.adobe.com/jp/photoshop-elements/kb/222341.html

この画像をrandom crop するときに、どのようにするとうまくcrop できるのか試しました。
そして、たどり着いた答えが以下のコード。
もっといい方法があるのかもですが、とりあえずこれで動きます。

import tensorflow as tf
import numpy as np
import random

# この二つの配列を、画像と見立てる
img1 = np.arange(300).reshape((10, 10, 3))
img2 = np.arange(300).reshape((10, 10, 3)) + 300

i1 = tf.constant(img1)
i2 = tf.constant(img2)

with tf.Session() as s:
    s.run(tf.global_variables_initializer())
    seed = random.randint(0, 1000)
    croped_img1 = tf.random_crop(i1, [5, 5, 3], seed=seed)
    croped_img2 = tf.random_crop(i2, [5, 5, 3], seed=seed)
    c1 = s.run(croped_img1)
    c2 = s.run(croped_img2)    
"""
c2 - c1 => array([[[300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300]],

       [[300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300]],

       [[300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300]],

       [[300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300]],

       [[300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300]]])
"""

おそらくこれで切れているんだと思います。 ということで、random_crop に seed を入れてあげれば解決。 以上。

追記。

俺のやりたかったことは、そんなんじゃない!!!! こんな記事、嘘っぱちだ。

dataset API の中で、どうやってそれ使えばいいんだ(怒)

ということで、調べた。

import tensorflow as tf
import numpy as np
import random

# 画像 (10枚)
img1 = np.arange(3000).reshape((-1, 10, 10, 3))
img2 = np.arange(3000).reshape((-1, 10, 10, 3)) + 300

def _parse_function(img1, img2):
    r = tf.get_seed(1)
    img1 = tf.random_crop(img1, [5, 5, 3], seed=r[0])
    img2 = tf.random_crop(img2, [5, 5, 3], seed=r[0])
    return img1, img2

dataset = tf.data.Dataset.from_tensor_slices((i1, i2))
dataset = dataset.map(_parse_function, num_parallel_calls=2)
dataset = dataset.shuffle(buffer_size=(len(img1))).batch(1).prefetch(1).repeat()
iterator = dataset.make_one_shot_iterator().get_next()

with tf.Session() as sess:
    for i in range(5):
        x, y = sess.run(iterator)
        print(x[0, 0, 0])
print(y - x)

結果

[2823 2824 2825]
[675 676 677]
[1932 1933 1934]
[2232 2233 2234]
[2436 2437 2438]
[[[[300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]]

  [[300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]]

  [[300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]]

  [[300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]]

  [[300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]]]]

ということで、ランダムに画像を crop しつつ、対象の2枚の画像については同じ箇所で切り出すことができた。

メモ golang の defer 2

package main

import (
    "fmt"
)

type Hoge struct {
    a string
}


func main() {
    h := &Hoge{"a"}
    defer fmt.Println("no func", h.a)
    defer func () {fmt.Println("with func", h.a)}()
    h.a = "b"
}

出力は、

with func b
no func a

以前の記事に書いたが、defer は、後入れ先出しで処理される。
よって、 with func が先に実行され、
no func が後で実行される。

1つ目の defer、with func については、h.a = "b" の結果が表示され、 2つ目のdefer、 no func については、 h.a = "a" の結果が表示されている。

これは、 defer 文が、式の実行を遅らせるだけで、式の評価自体は defer 文が呼ばれた段階で行われていることを示唆している。
たぶん。