初心者を戒めるPHP

  • 734
    いいね
  • 6
    コメント

この記事は何か

挑発的な文言になってる箇所はあるものの、内容としてはそれなりにまじめに書いたつもり。むしゃむしゃしてやった。いまでは反芻してゐる。

PHPDocは必ず書け

あらゆる再利用可能な手続きは、他人が容易に応用できるように型が明示的でなければいけない。メンバー全員が実装コード全てを把握できるものならそれが理想だけれど、残念ながら時間は有限だ。ヘッダだけを読んでメソッドの仕様が理解でき、またはコードを読む助けになるようなコメントが良い。

有名な事実を紹介すると、多くのコードは数か月(早ければ数日!)も経てば、他人が書いたコードに感じられるほど理解できなくなることがしばしばある。もちろん設計の練度にもよらうが、設計判断について注意を要した点などをコメントに残しておくことで、ひいては未来の自分の役に立てることができる。

お前の先輩は「PHPには型がない」などと知ったかぶって意味不明1なことを言ふかもしれないが、そんなものは戯言だ。うそっぱちだ。入力と出力の定まらない函数/メソッドは存在意義がいともたやすく揺らぐ。実装にとりかかる前に、入力(引数)と出力(返り値)について、必ず型を決めておかなければいけない。

/**
 * 入力値をFizzBuzz文字列化する
 *
 * @param  int    $num
 * @return string FizzBuzz文字列
 *
 * このメソッドは $num が3の倍数なら"Fizz"、5の倍数なら"Buzz"、7の倍数なら"Nass"、
 * これらの数の公倍数ならば、少ない数から該当した順に連結して返す。
 * どの数の倍数でもなければ、 $num を10進数として文字列化した値を返す。
 *
 *     toFizzBuzzNass(1);  // => "1"
 *     toFizzBuzzNass(3);  // => "Fizz"
 *     toFizzBuzzNass(15); // => "FizzBazz"
 *     toFizzBuzzNass(21); // => "FizzNass"
 *     toFizzBuzzNass(35); // => "BazzNass"
 */
function toFizzBuzzNass($num)
{
    static $fizzbuzz_table = [
        'Fizz' => 3,
        'Buzz' => 5,
        'Nass' => 7,
    ];

    $str = '';
    foreach ($fizzbuzz_table as $s => $n) {
        if ($num % $n === 0) {
            $str .= $s;
        }
    }

    return ($str === '') ? "{$num}" : $str;
}

って、自宅警備員の頃に読んだプログラミングの基礎を読んで思った。型推論のあるOCamlの本だけど、「関数定義に対するデザインレシピ」の概念はPHPでこそ活きる。

型の書き型はphpDocumentorのドキュメントの@param@returnDefinition of a ‘Type’を読んでおけば意外にどうにかなる。これはマーケティングなのだけれど、日本語で書かれた技術解説が欲しければWEB+DB PRESS Vol.87がある。

もちろんPHPは「配列または文字列を返す」ことは可能だし@return array|stringと書けば良いが、そんなことをされると受け取り側でis_array()チェックなどをしなければいけなくなって非常にめんどくさい。型をどちらかに統一するか、必要に応じてメソッドを分割した方がめんどくささは軽減できる。

Composerを導入しろ

ComposerがないPHPプロジェクトはレガシーだ。PEARのライブラリが必要ならComposerを使ってもインストールできる。いまどきPHPのライブラリをzipでダウンロードしてきてファイルをひとつひとつincludeするとか絶対やめろ。そんなことはComposerを入れれば(モダンで常識的な構成のライブラリならば)勝手にオートロードしてくれる。

error_reporting(E_ALL)しろ

PHPは未定義変数や配列の存在しないインデックスを参照するとE_NOTICEエラーを発行する。このエラーは無視することも可能だがバグの温床になる。せめて開発環境では有効にしろ。

めんどくさくて$v = isset($ary['key']) ? $ary['key'] : null;なんて書いてられない?
大丈夫、$v = $ary['key'] ?? null;って書けるようになったよ! PHP7から!!

フレームワークを利用しろ

貴様は自分で思ってるほどセキュリティについて知らない。もっとも優れたWebアプリケーションとは顧客の求める機能を安定して提供し続けるものだが、脆弱性は利用者やサーバーにさまざまな不幸を持ち込む。フレームワークのお作法に則ってお行儀のいいコードを書いておけば、大抵の問題はフレームワークがどうにかしてくれる。

ちょっとくらゐPHPで動くコードが書けるようになっていい気になってるのかもしれないが、初心者こそフレームワークが必要なことは確定的に明らか。急げば回れという名セリフを知らないのかよ。

テンプレートとロジックは分けろ

貴様は自分で思ってるほど読みやすいコードは書けない。だからせめて、画面を描画する機能とそれ以外は明確に分けろ。たとへば「データベースの内容をHTMLのtableで表示する」プログラムが書きたいと思ったら、「データベースからデータを取り出して配列にする」部分と「配列をHTMLのtableで表示する」部分を分けるんだ。その程度の役割の分割なら高尚な設計技法を知らない小学生にだってできる。簡単だろ?

PHPはテンプレートエンジンじゃない。調子に乗るな。自動エスケープ機能があるTwigとかBladeとか使っておけば、素のPHPより多少は安全になる。

あとPHPファイルは <?php 〜 ?> で囲まなきゃいけないとか思ってるなら顔を洗って出直してきてくれ。テンプレートエンジンじゃないんだからクラス定義しかしないファイルに ?> なんて書く必要あるわけないだろ。

安易にキャストするな

必要もないのにPHPで(int)とかintval()とか書き始めたら負けフラグ。貴様は何のために"3" == 3trueになると思ってるんだ。$a = []; $b = $a + 1;と書いておけばFatal errorが発生してコード上のバグに気付けたものを、わざわざ無意味にキャストして$b = (int)$a + 1;などと書いてしまったら気付けなくなる。

わざわざキャストなんかしなくても貴様のコードはちゃんと動くから安心しろ。PHPはそういう言語だ。キャストとかそんな配管工事はフレームワークやO/Rマッパーに任せておけ。あるいはメインのロジックからは慎重に分割して、目につかないところでひっそりとやれ。残念なお知らせだが、私たちの好きなPDOには自動キャストのような気のきいた機能は存在しない。

ちなみになぜかキャスト(float)よりもfloatval()のような函数形式が好きな輩が居るが、どちらも大差はない。どちらかと言ったら前者の方がちょっとはましだ。後者の出番はまったく皆無だし、array_map('floatval', range(1, 10))と書くためだけに存在してるのだと私は確信する。

リファレンス(参照)はやめろ

明示的なリファレンスを使ってうれしいことなんか滅多にない。引数のリファレンス渡しはやめろ。返り値で返せ。foreachのリファレンス。リファレンス代入とかやめろ、絶対やめろ。どうしてもやりたければ俺の屍を超えてPHP: リファレンスが行うことは何ですか? - Manualを読んで100%理解してから行け。

<?php

// だめなパターン (リファレンス使用)
$ary = range(1, 5);
foreach ($ary as &$a) {
    $a = $a * 2;
}
var_dump($ary); //=> [2, 4, 6, 8, 10]

// 許されるパターン (リファレンス不使用)
$bry = range(1, 5);
foreach ($bry as $i => $b) {
    $bry[$i] = $b * 2;
}
var_dump($bry); //=> [2, 4, 6, 8, 10]

どうして下が許されて上のパターンが禁止なのかって? このコードの一番下に$a = 100; var_dump($ary);を追加して実行してみるといいよ!

念のために言及しておくけど、preg_match()のような標準函数のリファレンスを利用することは特に制約しない。一見して未使用変数を使ってるように見える以外に害はないので。

追記
もうちょい詳しく: PHPのリファレンス(参照&)の傾向と対策、あるいはさよなら

継承がそんなに嬉しいか?

私も若い頃はお前のように継承を活用したモジュールの再利用が最高だと思ってた頃があったが、膝に矢を受けてしまってな… クラスの差分によるプログラミングは他人に把握させるのもなかなか難しい。それよりはinterfaceと、必要があれば委譲を利用することで同様のことをシンプルに実現できることが多い。

そんなにarray_mapが書きたいか?

中途半端にRubyやらカンスー型言語やらにかぶれやがって。ここはPHPだぞ。

// array_mapを使ったパターン
$ary = array_map(
    function ($n) { return $n * 2; },
    range(1, 5)
);
var_dump($ary);

// foreachを使ったパターン
$bry = range(1, 5);
foreach ($bry as $i => $b) {
    $bry[$i] = $b * 2;
}
var_dump($bry); //=> [2, 4, 6, 8, 10]

どちらが読みやすいか、どちらが書きやすいか、胸に手を当てて深呼吸して熟考してみてほしい。

お前はそんなにmapが嫌なのかって? 違ふね

そんなにforeachが嫌ならarray_mapなんて低機能な半端もん使ってないでいきがってないで、nikic/iterでも使って徹底的にやりががれってんだこんちくしょう。

<?php
use function iter\fn\operator as op;

$ary = iter\toArray(iter\map(op('*', 2), iter\range(1, 5)));

まあ、ふつうに配列とforeachを使っておくのが無難ですよ…

あとがき

ギョームやら趣味やらでPHPを書きはじめて四年めだけど、過去に書いてきたコードを顧みてからこの記事を読み返すと胸が痛い。とても痛い。

脚注


  1. この発言は頻繁に見かける言説だが、主語がない。「PHPの変数には型がない」、つまり「変数$nint型として定義」といったことは基本的にはできない(SPL Typesの存在を完全に見てみぬふりをした場合)ので、これに限っては正しい。しかし、PHPの値には厳密なる型が存在する。PHPの世界では1"1"は歴然とした別の型だ。また、PHP7の新機能として引数の型宣言など型定義や返り値の型宣言が導入された。この概念についてはWhat is Gradual Typing: 漸進的型付けとは何か - Qiitaを参照されたい。この記事はPHPと同様に「動的型」と俗称されるPythonについての記事だが、PHPについてもおよそ同様だ。PHPでの詳細な事情については【導入決定!】PHP7で実装されるスカラー型宣言とは? | 東北ギークPHP7で追加される整数型、浮動小数点型タイプヒントの問題点 | yohgaki's blogも参考にされたい。