PHPのempty()でゼロの取り扱いに対する注意、代替と対策
PHPのempty()関数でゼロの取り扱いについては散々出尽くしているネタではあるが、システムを組んでいたときにempty()関数の恐ろしさに改めて気が付いた。
CMSのテンプレートとかを作る際には非常に便利なempty()関数だが、システムでこいつを乱立させるとひどく痛い目にあう。
多くの場合、配列が空きのときのチェックや、数字の絡まない文字列が空っぽのときのチェックに利用するのは良いものの、数値や論理値とかがチェックにかかってくる場合にはこれは絶対に使用してはならない。
文字列型でもゼロが単一で入ってくる可能性がある場合も然り。
というか、意識してその部分に使わないようにしても、「気がついたら使っていた!あかんやんけ!」ってことはPHPでプログラムを組んでいる際にはよくある話だと思う。
このempty()関数はゼロを空値として処理してしまうってのは非常に有名な話で色んな所で取り沙汰されていて、
$what_empty = 0; empty( $what_empty ); // true ... 数値のゼロは空っぽとして評価される $what_empty = '0'; empty( $what_empty ); // true ... 文字列でもゼロなら空っぽとして評価される $what_empty = false; empty( $what_empty ); // true ... 論理値でもfalseなら空っぽ、trueの場合は値が入っているとみなされる
こんなことをしてもいずれも空として評価されてしまう。
論理値(boolean)については内部的には「falseは0」であり「trueは1」であるといった形がPHPでは行われている。そして、この値をempty()が評価している模様。
論理値で評価を行う際は、falseかそうではない(trueか文字列が入ってくるか)の条件を数字で切り分ける場合、
$data = 0; if ( ! $data ) echo 'これはfalseです'; $data = 1; if ( $data ) echo 'これはtrueです'; $data = '0'; if ( $data ) echo 'これはtrueです'; $data = '1'; if ( $data ) echo 'これはtrueです'; $data = ''; if ( $data ) echo 'これはtrueです';
こういった結果となるので、例えばある条件を満たした場合は空白や文字列型ゼロ等を入れ、そうでなければfalseを入れるといったものを評価に入れるといった方針をコーディング上で一貫しているなら、これについては特に問題とはならんと思う。
少し脱線した話を挟んでしまったが、CMSだとempty()で評価される対象の変数やオブジェクトについては、多少乱暴な書き方をしたとしてもうまいこと処理されるようになっていたりする。
というか、テンプレートとかをわずかな知識で誰でも作れるようにするためか、そうなっているものが多い印象がある。
が、システムを作る際には自分でGET/POSTメソッドの送信や、ひいてはSQL文などを全て自分で発行して処理しなければならない。
その都合上、CMSの延長線の感覚でシステムを作ろうとして、パラメータ値やテーブルカラム値を判定する際にゼロを取り扱うところに(またはゼロが入ることを作り手である自分が予期できないところに)誤って使用してしまうと、ふとした所で「これおかしくね?何が原因??」となる。
GET/POSTメソッドの場合は、直感的・視覚的にHTMLにvalueが入るため、比較的気付きやすく直しやすいが、特に問題になるのはSQLのクエリをPHPに落とし込んでくるとき。
自分が遭遇した実例に近い形なら、
// $min_qcountに、ある商品グループの集計を出すためのSQLの結果を代入 // SELECT quanty.group, Min(quanty.count) AS minnum FROM quanty WHERE quanty.group = 'food' GROUP BY quanty.group; $min_qcount = PHPによるSQL実行; // テーブルでの当該の列の型はINT型であり、最小値として0が入ってくると過程 // 以下はレコードがない場合に備えて、単純にempty()をレコードがあるかどうかだけ評価として利用したいが・・・ if ( ! empty ( $min_qcount['minnum'] ) ) { if ( 0 < $min_qcount['minnum'] ) { echo '当該の商品グループでは在庫がゼロのものはありません。'; } else { echo '当該の商品グループで在庫がゼロになった商品があります'; } }
上記はempty()で評価する変数(SQL実行結果を格納)を単純にレコードの存在チェックだけに利用することを想定しているが、この変数に結果ゼロが入ってきた場合はメッセージは表示されない。
(上例のように単純なものであるなら、変数にレコードが存在するかの判定にはそもそもisset()使えばええやんって話やけど・・・。)
SQLだとデータベースのテーブル定義でINT型にしているところへ不特定多数のデータが入る場合、非常に気付きづらいし直しづらい。
CMSで多少複雑なテーマを作れる程度で調子こいて、システムに同じ感覚でこういった条件の評価を乱立させると完全に収拾がつかない事態に発展する。
(えらそーにいっている俺もその道は既に通っている。自分の場合は当該の関数がゼロのときは空判定することは過去の経験から知っていたものの、今回はSQL周りをガッツリ書いているときに不意打ちを食らった・・・。)
というか、そもそもempty()だけでそういったデータを全て評価して処理しきろうというのも無理がある話ではある。
ただ、同じことを行おうとしてもPHPは空っぽであることを判定できるのは、このempty()関数か、もしくはNULLを判定するisset()関数くらいしかない。
なので、代替と対策としてゼロをempty()の範囲外にするために、これを拡張してユーザ定義関数としてシステム上に突っ込んでおくことが肝要。
function is_empty( $var = null ) { if ( empty( $var ) && 0 !== $var && '0' !== $var ) { // 論理型のfalseを取り扱う場合は、更に「&& false !== $var」を追加する return true; } else { return false; } }
手続き型のコーディングでもオブジェクト指向型のコーディングでも、とりあえずメインルーチンのファイルインクルードする際に別ファイルにしたこの関数を突っ込む。
関数名はis_emptyでもvacantでもnullpoでも、PHPの定義済の関数や、将来的に実装されないと思われる関数の名前なら何でもいい。
定義済の関数の拡張なのにわざわざこれをオブジェクト指向型に合うようにライブラリ化とかする必要もない。
実際のところ、PHPは手続き型だろうが、オブジェクト指向型だろうが、どっかのタイミングで関数を使うことには変わらないのでこの程度でオッケー。
後は、empty()を書いた部分でゼロを評価していそうなところをこのユーザ定義関数に置き換える。
もし、その箇所が多岐に渡って散見しているのであれば、開発段階ならいっそグレップして思考停止で全てを置き換えてしまってもいいかもしれない。
置き換えて仮に不具合が出たとしても、多少野暮ったい書き方になることは承知の上で、ユーザ定義関数の条件をempty()だけにすればええだけやし(´・ω・`)
で、この際なのでこぼれ話。
前回のこちらの記事でのブログ更新からだいぶ日が空いてからの更新になったけど、システムの案件で大きいものを正式に受注することになりました。
仕事の出所はいつもやり取りしている人ではなく、完全にcattlemute個人で受けてきて、はっきり言っていつもやり取りしている人から受けるお仕事の遥か比ではない人と物と予算と時間が動きます。(胃が痛い・・・)
ってか、大きい案件を正式に受注したってところで、物を作るってことのモチベーションは過去現在含めて最高潮であるんだけど・・・その際に気づいたこと。
俺、やっぱりプログラムって嫌いだわ。
プログラムで「ものづくり」することそのものは好きだけどね。
手段と目的の分別って大事。
正直、今回のこの記事にしてもそうだけど、システム関係で遭遇したTIPSがかなり溜まっていて時間があれば書いていきたい・・・が。
今現在、このシステムのために色んな意味でほぼ煮詰まった缶詰になっているんだよね。
(昼時・夜時と空腹感を覚えながらも、三度の飯よりそのままただひたすらプログラム作っている時間が長くて、俺自身がそのまま干物になりそうだ・・・)
悠長にブログなんか更新していて良いんだろうかwwww