はらへり日記

腹に弾丸

PHPでISO8061に準拠した日付フォーマットをバリデーションする

この記事は

PHPアドベントカレンダー13日目の記事です。大遅刻ですごめんなさい。

qiita.com

正直、なぜか投稿した気になってしまってました…ちゃんとやらなきゃダメですよね気をつけます…。

したいこと

すいません。タイトルちょっと厳密に言うと違います。もっというとPHP関係ないかもしれない。

厳密には「ISO8061に従い、かつ年月日秒とタイムゾーンまで指定されているかどうか」バリデーションする方法です。

例えば以下のような書式はISO8061に準拠、かつ上記条件を満たしています。

2016-09-30T12:00:00+09:00

ですが、以下の書式はISO8061に準拠しながらも分秒数は指定されていません。

2016-09-30

とある実装で、分秒数まで指定された状態のバリデーションをかけたい場面があったのでどう実現するか考えました。

※ 今後、「ISO8061に従い、かつ年月日秒とタイムゾーンまで指定されているかどうか」といちいち言うと長いので便宜上、「ISO8061完全形」と呼称します。

そもそもISO8061とは

ISOとは皆様御存知、国際レベルでの標準化団体です。

それによって定められたISO8061はざっくり言うなら日付の表記方法を定めたものです。

仕様書を探して初めて知ったんですが、ISOの仕様書は有料みたいですね。なので今回はWikipediaの情報を元に進めました。

ISO 8601 - Wikipedia

方法を考える

まず、行わなければいけないバリデーションは2つです。

  • フォーマットがISO8061完全形であるかどうか
  • 存在する日付かどうか(9/31とかじゃないか的な)

この2つを実現するために、ざっくり以下の方法が考えられます。

  • 既存ライブラリで頑張る
  • 自分で正規表現とか頑張る
  • (あるなら)PHP標準関数とかで頑張る

順番に考えます。

既存ライブラリで頑張る

PHPの日付系ライブラリで真っ先に思いつくのはCarbonです。

carbon.nesbot.com

柔軟なパーサーや豊富でシンプルなAPIやテスタビリティの高さから使ってる人は非常に多いのではないでしょうか。

Carbonを知らないよって方はこの紹介記事がおすすめです。

blog.asial.co.jp

ただ、Carbonにはバリデーションの仕組みはほぼなく、文字列がISO8061完全形かどうかのチェックはできなそうです。

(あったら教えてください)

例えばCarbonは文字列を渡すことでインスタンスを生成できますが、その解釈が柔軟すぎてそれをテキストフォーマットのバリデーションとして使用するのは無理そうです。

例えば2016 12/25という文字列を渡してみるときちんと2016-12-25 20:16:00といった感じでデータが作成されます。

<?php

require __DIR__.'/vendor/autoload.php';

use Carbon\Carbon;

$date = new Carbon('2016 12/25');
echo (string) $date;

では、存在しない日付をチェックする手段としてはどうでしょう?

例えば2016-09-31T12:00:00+09:00はISO8061完全形としては正しいですが、9/31という日付は存在しません。

そこで、ISO8061完全形としてのバリデーションが通った文字列が日付的に有効かどうか調べる方法としてCarbonインスタンスの生成がうまくいくかどうか試してみます。

ちなみにでたらめな文字列を投げると例外を投げてくれるのでおかしな日付もきっと投げてくれるはず。

<?php

require __DIR__.'/vendor/autoload.php';

use Carbon\Carbon;

// Exception
$invalidDate = new Carbon('でたらめ');

では試しにこんなコードを実行してみます。

<?php

require __DIR__.'/vendor/autoload.php';

use Carbon\Carbon;

$date = new Carbon('2016-09-31T12:00:00+09:00');
echo (string) $date;

するとこうなります。

gyazo.com

なぜなのか\(^o^)/

CarbonはDateTimeクラスを継承してる

DateTimeクラスとはPHPの標準クラスです。

PHP: DateTime - Manual

CarbonはこのDateTimeクラスを継承しているので、インスタンス生成に渡される文字列の解釈はDateTimeの仕様に依存しています。

そして、DateTimeではなぜか9/31が通るようになっています。

そしてもっというと9/32は例外を投げます。

<?php

$date1 = new \DateTime('2016-09-31'); // OK
$date2 = new \DateTime('2016-09-32'); // Throws Exception

中のコードまでは読んでないので推測ですが、おおかた正規表現(0[1-9]|[12][0-9]|3[01])みたいなチェックをしているだけな気がします。

ということでCarbonはISO8061完全形のバリデーション、及び存在する日付かどうかのバリデーションには使えなさそうです。

すごく念のためですがCarbon sageなわけではないです。むしろCarbon無いと生きていけない

自分で正規表現とか頑張る

一番避けたいと思いつつ、結論から言うと今回はISO8061完全形フォーマットになっているかどうかのバリデーションはこれで実装しました。

というのも、

  • ISO8061形に対してバリデーションしてるよさげなcomposerライブラリが見つけられなかった
  • PHP標準関数にもISO8061完全形かどうかチェックしてくれるものはなさそうだった

という理由からです。

そもそもISO8061は特定のフォーマットでなく、いくつかのフォーマットがあるのでそれに対してバリデーションというと今回求めてる形式以外も許容しなければなりません。

仕様書に目を通していない以上、適当なことを事実として断言はできませんが世の認識的にも様々な表現方法がある、といった認識が一般的な気がします。

ISO 8601 - Wikipedia

というわけで正規表現ドーン!

<?php

$matches = [];

$isValidFormat = preg_match(
    '/^(\d{1,4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-4]):[0-5][0-9]:[0-5][0-9](Z|(\+|-)([01][0-9]|2[0-4]):?([0-5][0-9])?)$/',
    $date, $matches
) === 1;

$matchesは後ほど、存在する日付かどうかのチェックに使用するためにいくつか文字列を抜き出しています。

読むの嫌になりますよね。読まなくていいです。

こういうものはコードを舐める用に読んでも絶対にミスする可能性があるのでテストケースでカバーしましょう。

思いつく範囲のテストケースを書き出して、ユニットテストでこの正規表現の質を担保しています。

正常系は省略して、異常系のみ書いてます。(以降も同じく)

<?php

$testCases = [
    // 日付のみ指定のケース
    '2016-09-30',
    // 日付と時刻の間のTが抜けてるケース
    '2016-09-01 12:00:00+0900',
    // タイムゾーンが抜けてるケース
    '2016-09-30T12:00:00',
];

あとは日付が存在するかどうかのチェックをすればよさそうです。

PHP標準関数とかで頑張る

日付が実在するかどうかのチェックですが、PHPにはcheckdate()という標準関数があります。

PHP: checkdate - Manual

先ほど実装した正規表現と合わせてこの関数を使用すれば9/31といった存在しない日付も弾くことができます。

<?php

$matches = [];

$isValidFormat = preg_match(
    '/^(\d{1,4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-4]):[0-5][0-9]:[0-5][0-9](Z|(\+|-)([01][0-9]|2[0-4]):?([0-5][0-9])?)$/',
    $date, $matches
) === 1;

$isValidDate = true;

try {
    $isValidDate = checkdate($matches[2], $matches[3], $matches[1]);
} catch (\Exception $e) {
    $isValidDate = false;
}

return $isValidFormat && $isValidDate;

先ほどの正規表現で抜き出した年、月、日をcheckdate()で判別することで存在しない日付のチェックは通らないようになっています。

もちろん、PHPの標準関数を使ってるからと油断せずにテストケースも追加しておきます。

<?php

$testCases = [
    // 日付がでたらめなケース
    '2016-15-01T12:00:00+0900', // 月
    '2016-09-31T12:00:00+0900', // 日
    '2016-09-01T25:00:00+0900', // 時
    '2016-09-01T12:61:00+0900', // 分
    '2016-09-01T12:00:61+0900', // 秒
    '2016-09-31T12:00:00+0900', // 存在しない31日
];

完成形

全部のコードをがっちゃんこするとこんな感じです。

<?php

/**
 * @param string  $date
 * @return bool
 */
function validateDate (string $date) 
{
    $matches = [];
    $isValidFormat = preg_match(
        '/^(\d{1,4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-4]):[0-5][0-9]:[0-5][0-9](Z|(\+|-)([01][0-9]|2[0-4]):?([0-5][0-9])?)$/',
        $date, $matches
    ) === 1;
    $isValidDate = true;

    try {
        $isValidDate = checkdate($matches[2], $matches[3], $matches[1]);
    } catch (\Exception $e) {
        $isValidDate = false;
    }

    return $isValidFormat && $isValidDate;
}

テストケースはこんな感じ。

<?php

$testCases = [
    // 日付のみ指定のケース
    '2016-09-30',
    // 日付と時刻の間のTが抜けてるケース
    '2016-09-01 12:00:00+0900',
    // タイムゾーンが抜けてるケース
    '2016-09-30T12:00:00',
    // 日付がでたらめなケース
    '2016-15-01T12:00:00+0900', // 月
    '2016-09-31T12:00:00+0900', // 日
    '2016-09-01T25:00:00+0900', // 時
    '2016-09-01T12:61:00+0900', // 分
    '2016-09-01T12:00:61+0900', // 秒
    '2016-09-31T12:00:00+0900', // 存在しない31日
];

テストはPHPUnitなりでよしなに🙏

まとめ

PHPの話というよりは文字列をparseする話になってしまいましたが、いろいろと勉強になりました。

ISO8061を知ってるようで知らなかったことやPHP標準関数とかを知れたのでよかったです。

後はテストの大事さですね。こんな正規表現絶対読めない。

とにもかくにも投稿遅れてほんとスイマセン。来年はもっと勉強して「DateTimeクラスの実装直したぜ(ドヤァ」ぐらいの記事書きたいですね。

それでは皆様、メリークリスマス!

身内向けにCTF開催した

CTF開いた

某コミュニティでCTF開いた

やるきっかけとしては

  • ISUCONで人権を失う
  • SECCON予選で人権を再び取り戻そうという決意を固める
  • でもCTF力全然ないし練習会しよう

という感じ。

学生の頃にちょっとだけCTFに参加したことがあったのと、ちょっと使ってみたいOSSがあったので主催した。

忙しいのに合間を縫ってyagihashooには問題を作ってもらったり、当日参加して解説してくれたりした。ありがとう。

ちなみに同コミュニティではISUCONの練習会もやってたり(こちらはhoto氏が開いてくれた\(^o^)/)。

ISUCON 練習会をするために InfluxDB + Grafana でポータルサイトを作った - ほとラボ

CTFを開いたきっかけ

一番のきっかけは私がずっと前からこれを使ってみたかったというのが実はあった。

github.com

CTFをやったことある人はわかるんだけど、Flagを送信したり得点を表示するポータルサイトなるものが必要になる。

これはFacebookが作ったポータルサイトOSSでめっちゃカッコイイ。

gyazo.com

これが実際の画面。プロビジョニングするだけですぐできるし機能も多くて、もしCTFやる人がいたらぜひオススメしたい。

AWSのEC2インスタンス立ち上げてgit cloneしてshell叩いてLet's Encryptしたら終わりです。

AWS慣れてないので金銭感覚わからないけど7, 8人参加で2000円くらいしかかからなかった。

問題構成

CTFやったことない人が大半、かつWebの人が多かったので全ジャンルの初級編 + Webの中~上級編という感じで15問出した。

とはいいつつ私も全然CTFしたことないのでいわゆる良問なるものを探したり、昔自分が解いたやつを参考にしたりして作った。

いろんな問題とかWriteupとか読んで、世のCTFerはすごいなぁと思った|ω・`)

やってみて

CTFをやるって決まった時は実装で死ぬかなぁと思ってたけど、どっちかっていうといい問題を作ることに頭を使ってる時間のほうが長かった。

「この問題が解けたらこんな学びがあるように」みたいなことを考えつつ、脆弱性を作り込むのは結構楽しかった。

そしてPHPとDocker超便利。本当に便利。

CentOS7でPHP5.2をビルドしようとして絶望してたけどDocker使ったら瞬殺だった。

当日まで楽しんでもらえるかめっちゃ不安だったし、学びも何もない会になったら飛び降りるしかないと思ってたけど楽しいと言ってもらえたのでよかった。

まとめ

そもそも自分も全然できないのに企画するの不安だったり、運営ぐだぐだだったりしたけど学びが多かったしCTFに興味ある人いたらぜひオススメしたい。

唯一はしゅに作ってもらった問題が解けなくてやはり人権得られなかったので修行していきたい。

頑張って来年のSECCON予選でるぞい!

Laravelでセッターインジェクションする

この記事は

Laravelアドベントカレンダー8日目の記事です。

qiita.com

前提知識

この記事ではDIパターンを実現する1つの手段であるセッターインジェクションをLaravelで実現する方法を紹介します。

なのでDIパターンやDIコンテナを知らない方は先にこれらの記事を読んでいただくと理解が進むと思います。

Inversion of Control コンテナと Dependency Injection パターン

さくっと知りたい方は私が今年発表したスライドの55枚目までを流し読みしていただければと思います。

LaravelにおけるDependency Injection

LaravelはHTTPリクエストが来たタイミングでDIコンテナが起動します。

それがアプリケーションコードのほぼ全ての依存を解決するので、開発者はインスタンスの生成方法を意識せずにコーディングすることが可能となっています。

例えば以下のようなコントローラークラスがあるとしましょう。

<?php

namespace App\Http\Controllers;

use App\Services\SampleService;

/**
 * Class SampleController
 */
class SampleController 
{
    /**
     * @param SampleService  $sampleService
     *
     * @return \Illuminate\Http\Response
     */
    public function main(SampleService $sampleService)
    {
        $data = $sampleService->getData();
        
        return view('main', compact('data'));
    }
}

なんてことはない、mainという名前のViewを必要なデータを入れてレンダリングして返すコントローラーメソッドです。

このコントローラーをもしLaravelを使わずに使おうと思うとおそらくこんな感じのコードを書かなければなりません。

(コードはイメージ)

<?php

namesapce App\Http;

use App\Http\Controllers;
use App\Services\SampleService;
use App\Repositories\SampleRepository;

/**
 * Class OriginalRoute
 */
class OriginalRoute
{
    /**
     * @return array
     */
    public function route()
    {
        return [
            '/main' => function (\Illuminate\Http\Request $request) {
                $controller = new SampleController;
                $response = $controller->main(
                    new SampleService(new SampleRepository)
                );
                return $response;
            },
        ];
    }
}

注目してほしいのは連想配列/mainに指定しているClouser部分です。

実装者はSampleControllerインスタンスを実装し、mainメソッドが依存しているインスタンスを自分の手で生成する必要があります。

小規模なアプリケーションであればこのような方法で実装するのは問題にはなりづらいかもしれません。

しかしクラス数が増えたら?インスタンスの生成方法が変わったら?実装の差し替えが起こったら?

そういったことを考えるとこのような方法では将来的につらいことになる可能性が高いです。

しかしLaravelだとこんなことはしなくても大丈夫です。

auto wiring

先ほど述べたようにLaravelではHTTPリクエストが来た際にDIコンテナが立ち上がります。

そのDIコンテナがアプリケーションコード中でDIパターンによって明示的に指定されている依存関係を全てよしなに解決します。

詳しい仕組みはコードを読むとよいと思いますが、LaravelのDIコンテナはReflection等を活用して自動で必要な依存関係を調べるauto wiringという仕組みで動いています。

詳しくは下記リンクを読むと理解が進むかと思います。

Aura.Di/auto.md at 3.x · auraphp/Aura.Di · GitHub

Auto Wiring - Container

これによって何が嬉しいかというと、開発者は例えばSampleController::main()メソッドが必要としているSampleServiceインスタンスを生成する必要が無いということです。

普段は意識することは少ないかもしれませんが、これを覚えておくと設定なしにInterfaceやスカラー型をタイプヒントしてもDIコンテナからインジェクションしてくれない理由がわかると思います。

セッターインジェクション

セッターインジェクションとはDIパターンを実現するための1つの手段です。

Laravelでよく使われる手段としてコンストラクタインジェクションがあります。

<?php

namespace App\Services;

use App\Repositories\SampleRepository;

/**
 * Class SampleService
 */
class SampleService
{
    /** @var SampleRepository */
    protected $sample;

    /**
     * ConstructorでタイプヒントしておくとLaravelのauto wiringにより
     * インスタンスが注入される
     *
     * @param SampleRepository  $sample
     */
    public function __construct(SampleRepository $sample)
    {
        $this->sample = $sample;
    }
}

これをセッターメソッドを用意して行うのがセッターインジェクションです。

上記のコードをセッターインジェクションを用いたコードに書き直すとこんな感じ。

<?php

namespace App\Services;

use App\Repositories\SampleRepository;

/**
 * Class SampleService
 */
class SampleService
{
    /** @var SampleRepository */
    protected $sample;

    /**
     * Constructor
     */
    public function __construct()
    {
        //
    }

    /**
     * @param SampleRepository  $sample
     */
    public function setSampleRepository(SampleRepository $sample)
    {
        $this->sample = $sample;
    }
}

こうすることで以下のような形でSampleRepositoryインスタンスをDIすることができます。

<?php

$sampleService = new \App\Services\SampleService;

// Dependency Injection!!
$sampleService->setSampleRepository(new SampleRepository);

簡単ですよね。

Laravelでのセッターインジェクションのやり方

ここからがこの記事の本編です。

Laravelではセッターインジェクションに対する定義を行うAPIが用意されていません(あったら教えてください…)。

なので方針としては

といった感じでやります。

対象のクラスは先ほど出てきたSampleServiceを利用します。

<?php

namespace App\Services;

use App\Repositories\SampleRepository;

/**
 * Class SampleService
 */
class SampleService
{
    /** @var SampleRepository */
    protected $sample;

    /**
     * Constructor
     */
    public function __construct()
    {
        //
    }

    /**
     * @param SampleRepository  $sample
     */
    public function setSampleRepository(SampleRepository $sample)
    {
        $this->sample = $sample;
    }
}

このクラスをそのままLaravelで利用したら、普通に動きます。

しかしながらセッターは実行されないのでインスタンス生成直後にセッターメソッドを実行したい。

そういった場合はIlluminate/Container/Container::extendを利用します。

任意のServiceProviderでこんな処理を書きます。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class DependencyServiceProvider extends ServiceProvider
{
    public function register()
    {
        // set SampleRepository instance
        $this->app->extend(\App\Services\SampleService::class, function ($sampleService, $app) {
            $sampleService->setSampleRepository(new \App\Repositories\SampleRepository);
            return $sampleService;
        });
    }
}

また、extend等を通じて完全に依存解決された後にセッターインジェクションしたい場合にはIlluminate/Container/Container::resolvingを使用できます。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class DependencyServiceProvider extends ServiceProvider
{
    public function register()
    {
        // set SampleRepository instance
        $this->app->resolving(\App\Services\SampleService::class, function ($sampleService) {
            $sampleService->setSampleRepository(new \App\Repositories\SampleRepository);
            return $sampleService;
        });
    }
}

こうすることでDIコンテナによって生成されたSampleServiceインスタンスにセッターインジェクションを行うことができました!

セッターインジェクションの利用場面

このセッターインジェクションですが、普通の実装をしているとあまり使う場面は出てきません。

というのも大抵のインスタンスはコンストラクタインジェクションで事が足りるからです。

しかし、この方法を覚えておくと設計の幅が広がります。

例えばインスタンス生成の方法が複雑でコンストラクタの拡張が困難な場合などはセッターを生やした継承クラスを作成し、bindした上でセッターインジェクションするといったことが可能です。

他にも例えばログやトランザクションといった汎用的な処理を行うインスタンスのセッターをTraitで作成し、DIコンテナでセッターインジェクションすればクラス本体を汚さずに欲しいインスタンスを注入することも可能です。

まとめ

Laravelの良さはDIコンテナの柔軟さだと思ってます。

DIコンテナをしっかり利用できれば開発も楽になりますし何より楽しくなるので、依存解決で難解な場面に出くわすことがあればぜひこのセッターインジェクションという方法を思い出してみてください。

明日の記事はIganinTeaの記事です。お楽しみに!

アイスタイルを退職します

近況報告 兼 ポエムです。悪しからず

アイスタイルを退職します

新卒として入って2年目ですが、今月一杯でアイスタイルを退職することになりました。

退職の動機として、「やめたいから転職する」というより「行きたい場所があるから転職する」というニュアンスが非常に近いです。

アイスタイルでの2年間

じつは学生の頃のアルバイト期間から合わせるとぴったし2年程、勤めた計算になります。

最初の頃は本当にポンコツで、SQLがわからないだの設計って何だのconfigって何だの、本当にひどい有様でした。

ですが技術研修や業務中の指導をしていただいたりですとか、初歩的な質問をしても快く答えてくれる同僚達のおかげでちょっとは使い物になるエンジニアになれたのかなと今は思っています。

特に@ytakeさんには感謝しきれてもし足りません。この方の指導がなければ転職どころかまともにコーディングもできないままだった気さえするくらい、いろいろご指導頂きました。

本当にありがとうございます。

新卒というレッテルを捨てること

今回の転職でいろんな変化が起こると思っているのですが、一番大きな変化は新卒のレッテルがなくなることかなと思ってたりします。

特にアイスタイルに関して言えば新卒を特別扱いする文化が良くも悪くも存在します。

新卒で入った人は部署が違っても気にかけてくださる先輩社員も多く居たり、新卒で入った同士の縦のつながりが非常に強かったりと本当に過ごしやすいのです。

一方で新卒という立場だからこそ普通の人以上に注意を受けたりもするのですが、それも悪いことではなくて、むしろ注意されなくなったら社会人として終わり感があるので今では本当にありがたかったなと感じています。

新卒バッジをつけながらアイスタイルで働くのは本当に居心地が良くてかわいがってもらえることが非常に多かったです。

今回それを捨てることになるわけですが、一生に一度の物を捨てる惜しさもありつつ僕自身はワクワクしていたりします。

仕事(というよりもエンジニアリング)に関して言えば僕はドMと言われてもしょうがない性格をしていて、例えばダメなコードはダメとはっきり言われたいしフィードバックは常に求めているし、願わくば一生誰かに「お前のコードは本当にダメだな!」と言われながら仕事をしたいと思っています。

それを考えると私にとって新卒ではなく「中途社員」という立場に武者震いをしています。

中途のいわば即戦力としてお賃金をもらうわけですから、会社としてはコストをかけて育てる対象ではないわけです。

となると私が成すべきことはもらっている対価以上の価値をすぐにでも生み出すことです。今のお休み期間に死ぬほど勉強しなきゃですね。

といっても今までも「新卒だからしょうがないよね」みたいな扱いはほぼされたことはないので実態は変わらないかもしれません。僕のメンタルの問題なんでしょうかね。

今までありがとうございました

アイスタイルではエンジニアがやめる時はプチ送別会的なものを開いて上長からありがたいお言葉を頂いたり、辞める人がちょびっと話したりする文化があるのですが、それに来ていただいた方が想像以上に多くて本当に多くの人に支えられてたんだなという気持ちです。

最終日前日にあいさつ回りする人を数えたら180人いたりして、たかだか2年と思っていたりしたんですが意外と重いんだなと思ったり。

時間が合わなかったりして挨拶できなかった方も何人かいらっしゃって、本当に申し訳なかったのですがまたどこかでお会いしたらシカトしないでいただけると嬉しいです。

今後共何卒よろしくお願いします。

次の会社のことはまた入社エントリポエムでも書くのでその時に。。。

おまけ

伝統芸能らしいので貼るだけ貼っておきます。

amzn.asia

社内で横断的に使えるPHPライブラリを書こう!

この記事は

アイスタイルアドベントカレンダー3日目の記事です。

タイトル的に技術知見っぽいんですがどちらかと言うとポエムに近いのであしからず。。。

qiita.com

社内の共有ライブラリを作る

弊社といえば化粧品クチコミサイトの@cosmeを運営していることで有名だと思うのですが、

実は@cosme以外にもかなり多くのサイトを運営しています。

それらの大半はPHPで実装されており、そのバージョンや使用しているフレームワークは多種多様です。

そうなると問題になってくるのが社内で同じような処理を実装したものが各所で発生することです。

弊社の例でいうと以下のような実装コードが数々のリポジトリで見られるようになりました。

  • タグのバリデーション実装
  • 社内APIとの通信処理
  • OAuth認証のロジック

せっかく言語が共通なのにこれらの処理とテストを新しく実装するたびに作っているのはもったいないなと感じ、

いくつか社内で使いまわせるライブラリを作りました。

この記事ではその時に得られたノウハウを共有したいと思います。

目指すこと

まず初めに、社内ライブラリを作る際に目指すことを考えます。

  • Composerによる配布を前提とする
  • 保守性の高さを意識する
  • 属人性を排除する

Composerによる配布

この時点でPHP5.3.2+が必須になってしまいますが、下位互換性を意識すると出来ることの幅が狭くなるのでここでは意識しません。

PHP5.5以下はサポート切れてますし…ね…

社内ライブラリなのでOSS化できない場合はPrivateな環境で配布する必要があります。

この際、Gitを導入していればComposer配布が可能になります。

まずは作成したリポジトリをComposerで落とせるようにするところまでやってみます。

Composerパッケージを作る

Gitリポジトリを作成したらカレントディレクトリでcomposer initを実行します。

すると対話式で作成するComposerパッケージについて聞かれるので答えていきましょう。

入力が完了するとcomposer.jsonが自動で生成されます。

これで最低限の配布準備は完了です。簡単!

Composerライブラリを落とす

Composerは通常、Packagistでパッケージを配布し、インストールします。

ですが、明示的にgitリポジトリのURLを指定することでPackagistからでなくgitサーバーからパッケージをインストールすることが可能になります。

まず、インストールしたいリポジトリcomposer.jsonrepositoriesという項目に追記します。

{
  "repositories": {
    "type": "git",
    "url": "https://github.com/private_organization/sample_repo"
  }
}

これでcompsoer require private_organization/sample_repoを叩くことでパッケージを落とすことができます。

これでPrivateなgitサーバーからComposerパッケージを配布することができます。

開発する

ここから実際に実装したいコードを開発していくことができますが、1年間やってきてこうすればよかったよというのを述べていきます。

第三者がプルリクを出せる環境を作る

社内で共有ライブラリを作り、浸透していくとバグ修正や機能追加のプルリクが飛んで来ることがあります。

その際、必ずしもレビューするのはライブラリ作成者ではないのでライブラリの品質を担保できなくなる場面が時たまあります。

例えばテスト未実行によるデグレやコーディング規約の揺れ、ドキュメント不足等がそれにあたります。

頑張ってそれらを取り締まる共有ライブラリ警察をやってもいいのですが、せっかくプログラマーをしてるので自動化できる部分は自動化してしまうとよいです。

弊社の流れで言うと

  • GitHubにプルリクエストが来る
  • Jenkinsでテストジョブを実行する
    • このジョブが通らないとマージボタンが押せないようになっている
  • PHPUnitによる単体テスト、cs-fixerによる構文チェック等が行われる

と言った形で、テストの実行漏れやコーディング規約の制約をブランチテストという形で課しています。

また、GitHubを使用している場合はCONTRIBUTING.mdを追加することでプルリクを出す際にそれを読むよう促してくれたり、.githubディレクトリにテンプレートを入れるとPull Requestのテンプレートを追加できたりします。

ドキュメントの追加等、開発者にしてほしい作業があればそのテンプレートにチェックボックス形式で追加することで作業を促しています。

バージョン管理を厳密に行う

ライブラリが社内に浸透していくと破壊的変更を行った時の影響範囲が大きくなります。

なのでバージョニングはセマンティックバージョニングに従い厳密に行います。

どんなに些細な変更でもAPI変更があればメジャーバージョンをあげることでバージョン変更によるバグが起きない用心がけましょう。

コードレビューは誰がするのか

作成するライブラリがどのプロジェクトにも該当しない場合、コードレビューを誰に頼めばいいかわかりづらい状況でした。

なので弊社ではSlackに#codereviewチャンネルを作成し、「自分の分かる言語ならレビューしていいよ」という人を募ってそこにレビュー依頼を投げるようにしました。

そうすることでレビューを投げる先が明確になり、放置されるPull Requestの数が減りました。

布教する

個人的に、これがかなり大事だと思っています

テストも完璧、ドキュメントも完璧な品質の高い便利ライブラリを作っても誰にも使われなければ時間の無駄になってしまうし保守もされなくなっていきます。

なので社内ライブラリを作ったら必ず社内に宣伝するようにしています。

社内wikiで「こんな便利ライブラリを作りました」と言って使い方を詳しく解説したり、その記事をSlackの#generalチャンネルに貼り付けたり。

また、自分がそのタイミングで入ってるPJTで採用できそうなら積極的に採用し、運用実績を作ると結構みんなノリ気になってくれることが多いように感じます。

社内ライブラリを作ってよかったこと

業務改善や再実装の無駄の排除はもちろんなのですが、それ以外にもよかったことがいくつかありました。

自分の勉強になる

ライブラリを作成するとフレームワークに頼って開発してるときと違い、全て1から自分で考えなければなりません。

どんなフォルダ構成にするのか。ライブラリのInterfaceはどうするのか。どこまで汎用化、抽象化するのか。

そういったことを考えるために普段使ってるライブラリのソースコードを読んだり、社内のできる先輩に設計を相談することでコーディングの知見を多く得られました。

社内に文化を広める

弊社は元々、OSS指向の人は多くなくGitHubによるプルリクエスト開発も導入して何年も経っていません。

なので隣のPJTのコードを読むとか、それにプルリクを出すと言ったことはほとんどありませんでした。

しかし、社内ライブラリが少しずつ増え、お互いコードレビューをしていくうちにその知見を各PJTで持ち帰ってもらったり、テストの書き方を学んでもらったりと社内の技術知見の風通しが改善されました。

普段はなかなか関わらない人と「このライブラリこう使いたいんだけど」みたいな相談をしたりとコミュニケーションも増え、100人規模になった弊社としては非常によい効果をもたらしていると感じています。

自分の名前を社内に残せる

これは完全にネタなんですが、私が作ったlaravel/socialiteを社内のOAuth向けに拡張したライブラリがあるのですが、そのGitHubリポジトリの作成を上長に依頼した時に

上長1「名前何にしましょう」

私「socialiteの拡張なのでis_auth_socialiteでお願いします」

上長2「sugi_socialite」

上長3「sugiurasocialite」

上長1「了解です」

私「ちょっと何言ってるかよくわからないです」

上長2「sugiura」

上長1「sugiulite」

上長3「sugiuliteいいですね!それでいきましょう。ぜひ」

私「マネージャー×3にいじめられてる…社会ってコワイ…」

って茶番がSlackに繰り広げられた末に弊社にはsugiuliteというライブラリが存在します。私の名前は杉浦です。

というわけでみなさんも積極的に社内ライブラリを作っていきましょう。楽しいです👏

明日は

明日はanntoque君の「changefinderの詳細について書く」だそうです。

お楽しみに!