pixiv insideは移転しました! ≫ https://inside.pixiv.blog/

PHP初心者が仕事で躓いた4つの罠

こちらはピクシブ株式会社 Advent Calendar 2015の2日目の記事です。

こんにちは。Vimエンジニアの kana です。

さて、皆さんもご存知の通り、WebサービスのpixivにはPHPが使用されています:

f:id:ka-nacht:20151202201442p:plain

PHPについては様々な噂を聞き及んでいた為、 これまでPHPとは関わらないように注意して過ごしてきましたが、 pixiv.netの開発ではPHPを避けて通ることは出来ません。

仕方なくPHPを使うことになる訳ですが、 実際に使ってみると……これが予想していた以上に様々な方向から毎日新鮮な驚きを届けてくれます。 今回は実際に遭遇したPHP初心者が躓くポイントを幾つか紹介しようと思います。

switch の中で continue したら switch の直後に飛ぶ

大量のデータをループでぶん回して処理するのはよくある話です。 その中で特定の種類のデータについては処理をスキップすることもよくある話です。 例えば以下の様なコードを書いたとしましょう:

for ($i = 0; $i < 5; $i++) {
  switch ($i) {
    case 2:
    case 4:
      continue;
  }
  print "$i\n";
}
//==> 0
//    1
//    2
//    3
//    4

あれれ……2と4はスキップされると思ったのに全部 print されてしまってます。 不思議に思いつつ continue のリファレンスを確認すると:

Note: In PHP the switch statement is considered a looping structure for the purposes of continue.

どうしてそうなった。

一応 continue 2;switch の外のループを回すことは出来るのですが、 continue 2; という字面が意味不明ですし、 PHPに詳しい先輩エンジニアから「PHPの switch は危ない」とも言われたので、 これ以降 switch を使うのは止めました。

クロージャーで使う外側の変数を明示しないと駄目

私はScheme大好き人間なので、データ列の処理は空気を吸う様に map して filter するのですが、 pixiv内部のソースコードには array_map 等を使わず愚直に foreach で処理している箇所が散見されます:

$xs = [1 => 'aaa', 2 => 'bbb', 3 => 'ccc', 4 => 'ddd'];
$ids = [1, 2, 3, 4];
$ys = [];
foreach ($ids as $id) {
  $ys[] = $xs[$id];
}
print implode(', ', $ys) . "\n";
//==> aaa, bbb, ccc, ddd

こういう箇所は「来た時よりも美しく」の精神で無意識のうちにリファクタリングしていまいます:

$xs = [1 => 'aaa', 2 => 'bbb', 3 => 'ccc', 4 => 'ddd'];
$ids = [1, 2, 3, 4];
$ys = array_map(function ($id) {return $xs[$id];}, $ids);
print implode(', ', $ys) . "\n";
//==> , , ,

おやおや、不思議な結果になってます。 困惑しつつAnonymous functionsのリファレンスを確認すると:

Closures may also inherit variables from the parent scope. Any such variables must be passed to the use language construct.

ふぁーーーーー何だそれーーーーーー!

なるほど、そりゃあ愚直に foreach で皆コードを書いてる訳だ……

未初期化の変数を配列として使うと配列になる

一人で開発している時も勿論ですが、大人数で開発しているとなおさら万人が分かり易いようにコードを書くことになります。 ところが極稀に以下の様なコードに遭遇します。

$illust_id = 123;
$options['size'] = 'small';   // <== ???
$options['include_metadata'] = true;
$illust = fetch_illust($illust_id, $options);

う、うん? $options はどこからも初期化されておらず、それをいきなり配列として使っています。 どういうことだと思いつつ変数の基本に関するリファレンスを確認すると:

It is not necessary to initialize variables in PHP however it is a very good practice. Uninitialized variables have a default value of their type depending on the context in which they are used - booleans default to FALSE, integers and floats default to zero, strings (e.g. used in echo) are set as an empty string and arrays become to an empty array.

お、おう……そうか……

この仕様を知っていれば便利に使える事があるかも知れませんが、 単なる書き忘れと区別が付かないことが殆どなので、こういう箇所は見つけ次第撲滅してます。

ユーザー定義関数の名前の大文字小文字は区別されない

pixivのコードベースは巨大なので、 気付かないうちにどこからも使われなくなった関数がどうしても増えていきがちです。 という訳でここ最近のpixivでは「未使用関数削除祭り」と称してコードの整理を行っています。 機械的に抽出した未使用関数の数々に対してざっくり担当エンジニアを決め、 各自半日程度の時間を取って本当に不要な関数かどうかを確認・削除しています。

抽出対象はgrepして定義行しか引っかからなかった関数なので、基本的にはただ定義を削除するだけで済む作業です。 とはいえ万が一の可能性も無くはないので、念の為に関連するコードも確認します。 私も自分の担当分について一通り確認した上であれこれ削除していきました。

ところがある関数を削除したものを本番環境へデプロイした途端に毎秒のペースでエラーが発生するようになりました。 一旦revertしてから原因の調査を始めるのですが、 「grepしても引っかからない関数なのに削除したらエラーが出る」 ということは実際にはどこか呼んでいる箇所が存在するということになります。

まさか……と思いつつユーザー定義関数のリファレンスを確認すると:

Note: Function names are case-insensitive, though it is usually good form to call functions as they appear in their declaration.

ウッ。

恐る恐る実際のソースコードを確認すると……

$ grep -i getSomethingByUID
.../foo.php: function getSomethingByUID(...)
.../bar.php: $something = getSomethingByUId(...);

なんでそこ d 小文字にした!

(そっと修正してデプロイし直しておきました)

まとめ

上記の4点以外にも色々とPHPで躓いた話はあるはずなのですが、毎日驚きの連続で記憶から飛んでしまいました。PHP凄い。

次回は saturday06 さんによる凄い趣味の話です。お楽しみに!