はじめに
CSVデータを扱うシステムの開発、特にCSVインポート機能を実装する際に、empty()
の挙動で「えっ?」となったこと、ありませんか?
特に "0"
(ゼロ)が入っているとき、empty("0")
は true を返してしまい、「値が入ってるのに空扱い!?」となるケースです。
Laravel 11 を使って開発中のプロジェクトで、CSVのインポート処理を実装していたところ、ある列の値が "0"
だったのに、それが「空」として処理されてしまうという問題にぶち当たりました。そこでこの問題をtraitにまとめて再利用できるようにした方法を紹介します。
問題:empty("0") は true になる
var_dump(empty("0")); // true になる 😱
CSVデータには "0"
や "0.0"
のような値が普通にあり得ます。
しかし empty()
を使って null や 空文字と一緒に判定すると、0 も「空」と判定されてしまいます。
目的:null や ""(空文字) は「空」として判定したいけど、"0" は除外したい
そこで、独自の空判定関数をtraitで定義して、プロジェクト全体で使い回せるようにしました。
traitとは
PHPのtraitは、単一継承言語であるPHPにおいて、クラス間でメソッドを再利用するための機能です。PHP公式ドキュメントによると:
トレイトは、PHP のような単一継承言語でコードを再利用するための仕組みのひとつです。 トレイトは、単一継承の制約を減らすために作られたもので、 いくつかのメソッド群を異なるクラス階層にある独立したクラスで再利用できるようにします。
トレイトはクラスと似ていますが、トレイトは単にいくつかの機能をまとめるためだけのものです。 トレイト自身のインスタンスを作成することはできません。
つまり、継承とは異なるアプローチでコードの再利用を実現する仕組みです。継承(extends)が「is-a関係」を表現するのに対し、traitは単に機能の共有を目的としています。
解決策:traitで isActuallyEmpty() を作成
この問題を解決するために、CSVデータの値を厳密に判定するtraitを作成しました:
namespace App\Traits;
trait StrictEmptyCheck
{
/**
* null または 空文字列("") のみを "空" と判定する
* CSVインポート時の値チェックに特に有用
*
* @param mixed $value
* @return bool
*/
public function isActuallyEmpty($value): bool
{
return $value === null || $value === '';
}
}
これで "0"
や "false"
、0
、false
などの値は空と判定されなくなります。
「本当に空かどうか」を判定するのにちょうど良い関数になります。CSVから読み込んだデータがエクセルなどで空欄だった場合は空文字として渡されるので、それをきちんと判定できます。
使用例
コントローラやサービスクラスで使うときは、traitを use
するだけです。
use App\Traits\StrictEmptyCheck;
class CsvImportService
{
use StrictEmptyCheck;
public function processRow(array $row)
{
if ($this->isActuallyEmpty($row['name'])) {
// nameがnullまたは空文字のときだけ処理
}
// その他のロジック...
}
}
traitの活用メリット
今回の例のように、複数のクラスで使いたい機能はtraitを使うことで以下のメリットがあります:
- コードの再利用: 同じ機能を複数クラスで使い回せる
- 単一責任の原則: 各クラスは本来の責務に集中でき、空値チェックのような共通機能は分離できる
- 継承の複雑さ回避: 複数継承ができないPHPで、継承関係を複雑にせず機能を追加できる
- メンテナンス性: 機能の修正が一箇所で済む
まとめ
- CSVデータ処理では、
empty()
が"0"
を空と判定してしまう罠がある - CSVインポート時には、null や 空文字だけを判定したいときが多く、自前で判定関数を用意するのがベスト
- traitにしておけば、複数のCSV処理クラスやその他のクラスでも再利用できて便利