バギーPHPコード:PHP開発者が犯す最も一般的な10の間違い

公開: 2022-03-11

PHPを使用すると、Webベースのシステムを比較的簡単に構築できます。これが人気の理由の多くです。 しかし、その使いやすさにもかかわらず、PHPは、開発者を苦しめる可能性のある多くのフレームワーク、ニュアンス、および微妙な点を備えた非常に洗練された言語に進化し、何時間もの髪の毛を引っ張るデバッグにつながりました。 この記事では、PHP開発者が注意する必要のある10の一般的な間違いに焦点を当てています。

よくある間違い#1: foreachループの後にぶら下がっている配列参照を残す

PHPでforeachループを使用する方法がわかりませんか? foreachループで参照を使用すると、反復する配列内の各要素を操作する場合に役立ちます。 例えば:

 $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8)

問題は、注意しないと、望ましくない副作用や結果が生じる可能性があることです。 具体的には、上記の例では、コードが実行された後、 $valueはスコープ内に残り、配列の最後の要素への参照を保持します。 したがって、 $valueを含む後続の操作は、意図せずに配列の最後の要素を変更してしまう可能性があります。

覚えておくべき主なことは、 foreachはスコープを作成しないということです。 したがって、上記の例の$valueは、スクリプトの最上位スコープ内の参照です。 各反復で、 foreach$arrayの次の要素を指すように参照を設定します。 したがって、ループが完了した後も、 $value$arrayの最後の要素を指し、スコープ内に残ります。

これが原因となる可能性のある回避的で紛らわしいバグの例を次に示します。

 $array = [1, 2, 3]; echo implode(',', $array), "\n"; foreach ($array as &$value) {} // by reference echo implode(',', $array), "\n"; foreach ($array as $value) {} // by value (ie, copy) echo implode(',', $array), "\n";

上記のコードは以下を出力します:

 1,2,3 1,2,3 1,2,2

いいえ、それはタイプミスではありません。 最後の行の最後の値は、実際には3ではなく2です。

なんで?

最初のforeachループを通過した後、 $arrayは変更されませんが、上記で説明したように、 $value$arrayの最後の要素へのぶら下がり参照として残されます( foreachループは参照によって$valueにアクセスしたため)。

その結果、2番目のforeachループを通過すると、「奇妙なことが」発生しているように見えます。 具体的には、 $valueは現在valueによって(つまり、 copyによって)アクセスされているため、 foreachは、ループの各ステップで、連続する各$array要素を$valueコピーします。 その結果、2番目のforeachループの各ステップで次のようになります。

  • パス1: $array[0] (つまり、「1」)を$value$array[2]への参照)にコピーするため、 $array[2]は1になります。したがって、 $arrayには[1、 2、1]。
  • パス2: $array[1] (つまり、「2」)を$value$array[2]への参照)にコピーするため、 $array[2]は2になります。したがって、 $arrayには[1、 2、2]。
  • パス3: $array[2] (現在は「2」に等しい)を$value$array[2]への参照)にコピーするため、 $array[2]は引き続き2になります。したがって$arrayには[1 、2、2]。

この種の問題のリスクを冒さずにforeachループで参照を使用する利点を引き続き得るには、 foreachループの直後に変数でunset()を呼び出して、参照を削除します。 例えば:

 $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value no longer references $arr[3]

よくある間違い#2: isset()動作の誤解

その名前にもかかわらず、 isset()は、アイテムが存在しない場合はfalseを返すだけでなくnull値の場合もfalseを返します

この動作は、最初に表示されるよりも問題が多く、一般的な問題の原因です。

次のことを考慮してください。

 $data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }

このコードの作成者は、おそらくkeyShouldBeSet$dataに設定されているかどうかを確認したいと考えていました。 ただし、説明したように、 $data['keyShouldBeSet']設定されていて、 nullに設定されている場合、 isset($data['keyShouldBeSet'])falseを返します。 したがって、上記のロジックには欠陥があります。

別の例を次に示します。

 if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; }

上記のコードは、$ _ POST $_POST['active']trueを返す場合、 postDataが必ず設定されるため、 isset($postData)trueを返すことを前提としています。 したがって、逆に、上記のコードは、 isset($postData)falseを返す唯一の方法は、 $_POST['active']falseを返した場合であると想定しています。

いいえ。

説明したように、 $postDatanullに設定されている場合、 isset($postData)falseを返します。 したがって、$ _ POST $_POST['active']trueを返した場合でも、 isset($postData)falseを返す可能性あります。 繰り返しになりますが、上記のロジックには欠陥があります。

ちなみに、副次的な点として、上記のコードの目的が$_POST['active']がtrueを返すかどうかを再度確認することである場合、これをisset()に依存することは、いずれの場合もコーディングの決定としては不十分でした。 代わりに、 $_POST['active'] ;を再チェックする方がよいでしょう。 すなわち:

 if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; }

ただし、変数が実際に設定されているかどうかを確認すること重要な場合(つまり、設定されていない変数とnullに設定されている変数を区別するため)、 array_key_exists()メソッドの方がはるかに堅牢です。解決。

たとえば、上記の2つの例の最初の例を次のように書き直すことができます。

 $data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // do this if 'keyShouldBeSet' isn't set }

さらに、array_key_exists( array_key_exists()get_defined_vars() )を組み合わせることで、現在のスコープ内の変数が設定されているかどうかを確実に確認できます。

 if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }

よくある間違い#3:参照によるリターンと値によるリターンに関する混乱

このコードスニペットについて考えてみましょう。

 class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];

上記のコードを実行すると、次のようになります。

 PHP Notice: Undefined index: test in /path/to/my/script.php on line 21

どうしたの?

問題は、上記のコードが、参照による配列の戻りと値による配列の戻りを混同していることです。 参照によって(つまり、 &を使用して)配列を返すようにPHPに明示的に指示しない限り、PHPはデフォルトで配列を「値によって」返します。 これは、配列のコピーが返されるため、呼び出された関数と呼び出し元が配列の同じインスタンスにアクセスしないことを意味します。

したがって、上記のgetValues()の呼び出しは、 $values配列への参照ではなくコピーを返します。 そのことを念頭に置いて、上記の例の2つの重要な行をもう一度見てみましょう。

 // getValues() returns a COPY of the $values array, so this adds a 'test' element // to a COPY of the $values array, but not to the $values array itself. $config->getValues()['test'] = 'test'; // getValues() again returns ANOTHER COPY of the $values array, and THIS copy doesn't // contain a 'test' element (which is why we get the "undefined index" message). echo $config->getValues()['test'];

考えられる修正の1つは、 getValues()によって返された$values配列の最初のコピーを保存し、その後そのコピーを操作することです。 例えば:

 $vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];

このコードは正常に機能します(つまり、「未定義のインデックス」メッセージを生成せずにtestを出力します)が、実行しようとしている内容に応じて、このアプローチが適切な場合と適切でない場合があります。 特に、上記のコードは元の$values配列を変更しません。 したがって、変更('test'要素の追加など)が元の配列に影響を与えるようにたい場合は、代わりにgetValues()関数を変更して、 $values配列自体への参照を返す必要があります。 これは、関数名の前に&を追加することで実行されます。これにより、参照を返す必要があることを示します。 すなわち:

 class Config { private $values = []; // return a REFERENCE to the actual $values array public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];

予想通り、これの出力はtestになります。

ただし、混乱を招くために、代わりに次のコードスニペットを検討してください。

 class Config { private $values; // using ArrayObject rather than array public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];

これにより、前のarrayの例と同じ「未定義のインデックス」エラーが発生すると推測した場合は、間違っていました。 実際、このコードは問題なく機能します。 その理由は、配列とは異なり、 PHPは常にオブジェクトを参照によって渡すためです。 ( ArrayObjectはSPLオブジェクトであり、配列の使用法を完全に模倣しますが、オブジェクトとして機能します。)

これらの例が示すように、PHPでは、コピーを扱っているのか参照を扱っているのかが必ずしも完全に明らかであるとは限りません。 したがって、これらのデフォルトの動作(つまり、変数と配列は値で渡され、オブジェクトは参照で渡されます)を理解し、呼び出している関数のAPIドキュメントを注意深くチェックして、値を返しているかどうかを確認することが重要です。配列のコピー、配列への参照、またはオブジェクトへの参照。

とはいえ、配列またはArrayObjectへの参照を返す方法は、呼び出し元にインスタンスのプライベートデータを変更する機能を提供するため、通常は避ける必要があることに注意することが重要です。 これはカプセル化の「顔を飛ばす」。 代わりに、古いスタイルの「ゲッター」と「セッター」を使用することをお勧めします。例:

 class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue('testKey', 'testValue'); echo $config->getValue('testKey'); // echos 'testValue'

このアプローチにより、呼び出し元は、それ以外の場合はプライベートな$values配列自体へのパブリックアクセスを提供せずに、配列内の任意の値を設定または取得することができます。

よくある間違い#4:ループでクエリを実行する

PHPが機能していない場合、次のようなものに遭遇することは珍しくありません。

 $models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }

ここではまったく問題がないかもしれませんが、コードのロジックに従うと、上記の$valueRepository->findByValue()を無邪気に呼び出すと、最終的に次のようなクエリが発生することがあります。

 $result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);

その結果、上記のループを繰り返すたびに、データベースへの個別のクエリが発生します。 したがって、たとえば、1,000個の値の配列をループに指定した場合、リソースに対して1,000個の個別のクエリが生成されます。 このようなスクリプトが複数のスレッドで呼び出されると、システムが停止する可能性があります。

したがって、コードによってクエリが実行されていることを認識し、可能な場合は常に値を収集してから、1つのクエリを実行してすべての結果をフェッチすることが重要です。

非効率的に(つまり、ループ内で)クエリが実行されるのに遭遇するかなり一般的な場所の1つの例は、フォームが値のリスト(IDなど)とともに投稿される場合です。 次に、各IDの完全なレコードデータを取得するために、コードは配列をループし、IDごとに個別のSQLクエリを実行します。 これは多くの場合、次のようになります。

 $data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); }

ただし、次のように、単一のSQLクエリで同じことをはるかに効率的に実行できます。

 $data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }

したがって、コードによって直接または間接的にクエリが実行されていることを認識することが重要です。 可能な限り、値を収集してから1つのクエリを実行して、すべての結果をフェッチします。 ただし、そこでも注意を払う必要があります。これにより、次の一般的なPHPの間違いにつながります…

よくある間違い#5:メモリ使用量のヘッドフェイクと非効率

一度に多くのレコードをフェッチする方が、フェッチする行ごとに1つのクエリを実行するよりも確実に効率的ですが、このようなアプローチでは、PHPのmysql拡張機能を使用するときにlibmysqlclientで「メモリ不足」状態が発生する可能性があります。

実例を示すために、限られたリソース(512MBのRAM)、MySQL、およびphp-cliを備えたテストボックスを見てみましょう。

次のようにデータベーステーブルをブートストラップします。

 // connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // create table of 400 columns $query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT'; for ($col = 0; $col < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ');'; $connection->query($query); // write 2 million rows for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ', ' . mt_rand(1000000000, 9999999999); } $query .= ')'; $connection->query($query); }

では、リソースの使用状況を確認しましょう。

 // connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo "Before: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo "Limit 1: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo "Limit 10000: " . memory_get_peak_usage() . "\n";

出力:

 Before: 224704 Limit 1: 224704 Limit 10000: 224704

いいね。 クエリはリソースの観点から内部で安全に管理されているようです。

ただし、念のため、もう一度制限を引き上げて100,000に設定しましょう。 ええとああ。 これを行うと、次のようになります。

 PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11

どうしたの?

ここでの問題は、PHPのmysqlモジュールの動作方法です。 これは実際には、汚い作業を行うlibmysqlclientの単なるプロキシです。 データの一部が選択されると、そのデータは直接メモリに送られます。 このメモリはPHPのマネージャーによって管理されていないため、 memory_get_peak_usage()は、クエリの制限を超えてもリソース使用率の増加を示しません。 これにより、上記のような問題が発生し、メモリ管理が適切であると考えて自己満足に陥ります。 しかし実際には、メモリ管理に重大な欠陥があり、上記のような問題が発生する可能性があります。

代わりにmysqlndモジュールを使用することで、少なくとも上記のヘッドフェイクを回避できます(ただし、それ自体ではメモリ使用率は向上しません)。 mysqlndはネイティブPHP拡張としてコンパイルされ、PHPのメモリマネージャーを使用します

したがって、 mysqlではなくmysqlndを使用して上記のテストを実行すると、メモリ使用率のより現実的な図が得られます。

 Before: 232048 Limit 1: 324952 Limit 10000: 32572912

ちなみに、それよりもさらに悪いです。 PHPのドキュメントによると、 mysqlはデータを格納するためにmysqlndの2倍のリソースを使用するため、 mysqlを使用する元のスクリプトは実際にはここに示されているよりもさらに多くのメモリを使用しました(約2倍)。

このような問題を回避するには、クエリのサイズを制限し、反復回数が少ないループを使用することを検討してください。 例えば:

 $totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); }

このPHPの間違いと上記の間違い#4の両方を考慮すると、コードが理想的に達成する必要のある健全なバランスが、一方ではクエリが細かく反復的すぎることと、それぞれの個々のクエリが大きすぎます。 人生のほとんどのものに当てはまるように、バランスが必要です。 どちらの極端も良くなく、PHPが正しく機能しないという問題を引き起こす可能性があります。

よくある間違い#6:Unicode/UTF-8の問題を無視する

ある意味で、これはPHPのデバッグ中に遭遇する問題よりも、PHP自体の問題ですが、適切に対処されたことはありません。 PHP 6のコアはUnicode対応にする予定でしたが、2010年にPHP6の開発が中断されたときに保留になりました。

しかし、それは決して開発者がUTF-8を適切に処理し、すべての文字列が必然的に「単純な古いASCII」であるという誤った仮定を回避することを免れるものではありません。 非ASCII文字列を適切に処理できないコードは、コードに危険な特異なバグを導入することで有名です。 単純なstrlen($_POST['name'])呼び出しでも、「Schrodinger」のような姓の誰かがシステムにサインアップしようとすると、問題が発生する可能性があります。

コードでこのような問題を回避するための小さなチェックリストを次に示します。

  • UnicodeとUTF-8についてよく知らない場合は、少なくとも基本を学ぶ必要があります。 ここには素晴らしい入門書があります。
  • 古い文字列関数の代わりに、常にmb_*関数を使用してください(「マルチバイト」拡張子がPHPビルドに含まれていることを確認してください)。
  • データベースとテーブルがUnicodeを使用するように設定されていることを確認してください(MySQLの多くのビルドは引き続きデフォルトでlatin1を使用します)。
  • json_encode()は非ASCIIシンボルを変換しますが(たとえば、「Schrodinger」は「Schr \ u00f6dinger」になります)、 serialize() ()は変換しないことに注意してください。
  • 文字列をハードコードまたは構成された文字列定数と連結するときの衝突を回避するために、PHPコードファイルもUTF-8でエンコードされていることを確認してください。

この点で特に価値のあるリソースは、このブログのFranciscoClariaによるPHPおよびMySQL用のUTF-8入門書です。

よくある間違い#7: $_POST POSTに常にPOSTデータが含まれていると仮定する

その名前にもかかわらず、 $_POST POST配列には常にPOSTデータが含まれているとは限らず、空であることが簡単にわかります。 これを理解するために、例を見てみましょう。 次のように、 jQuery.ajax()呼び出しを使用してサーバー要求を行うと仮定します。

 // js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' });

(ちなみに、ここではcontentType: 'application/json'に注意してください。データはJSONとして送信されます。これは、APIで非常に人気があります。たとえば、AngularJS $httpサービスに投稿する場合のデフォルトです。)

この例のサーバー側では、 $_POST配列を単純にダンプします。

 // php var_dump($_POST);

驚いたことに、結果は次のようになります。

 array(0) { }

なんで? JSON文字列{a: 'a', b: 'b'}はどうなりましたか?

答えは、 PHPがPOSTペイロードを自動的に解析するのは、コンテンツタイプがapplication/x-www-form-urlencodedまたはmultipart/form-dataのみであるということです。 この理由は歴史的なものです。PHPの$_POSTが実装された数年前に使用されたのは、基本的にこれら2つのコンテンツタイプだけでした。 したがって、他のコンテンツタイプ( application/jsonなど、今日非常に人気のあるものでも)では、PHPはPOSTペイロードを自動的にロードしません。

$_POSTはスーパーグローバルであるため、一度オーバーライドすると(できればスクリプトの早い段階で)、変更された値(つまり、POSTペイロードを含む)はコード全体で参照可能になります。 $_POSTはPHPフレームワークとほとんどすべてのカスタムスクリプトでリクエストデータを抽出および変換するために一般的に使用されるため、これは重要です。

したがって、たとえば、コンテンツタイプがapplication/jsonのPOSTペイロードを処理する場合、次のように、リクエストのコンテンツを手動で解析(つまり、JSONデータをデコード)して$_POST変数をオーバーライドする必要があります。

 // php $_POST = json_decode(file_get_contents('php://input'), true);

次に、 $_POST POST配列をダンプすると、POSTペイロードが正しく含まれていることがわかります。 例えば:

 array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }

よくある間違い#8:PHPが文字データ型をサポートしていると考える

このサンプルコードを見て、何が出力されるかを推測してみてください。

 for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; }

「a」から「z」と答えた場合、自分が間違っていたことに驚かれるかもしれません。

はい、「a」から「z」までを出力しますが、「aa」から「yz」まで出力します。 理由を見てみましょう。

PHPにはcharデータ型はありません。 stringのみが使用可能です。 それを念頭に置いて、PHPでstring zをインクリメントすると、 aa :が生成されます。

 php> $c = 'z'; echo ++$c . "\n"; aa

さらに混乱させるために、 aaは辞書式順序でzよりも小さいです:

 php> var_export((boolean)('aa' < 'z')) . "\n"; true

そのため、上記のサンプルコードでは、文字aからzが出力されますが、 aaからyz出力されます。 zaに達すると停止します。これは、 zより「大きい」最初の値です。

 php> var_export((boolean)('za' < 'z')) . "\n"; false

その場合、PHPで値「a」から「z」を適切にループする1つの方法を次に示します。

 for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; }

または代わりに:

 $letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; }

よくある間違い#9:コーディング標準を無視する

コーディング標準を無視しても、PHPコードをデバッグする必要が直接生じるわけではありませんが、それでもおそらくここで説明する最も重要なことの1つです。

コーディング標準を無視すると、プロジェクトで多くの問題が発生する可能性があります。 せいぜい、一貫性のないコードになります(すべての開発者が「自分のことをしている」ため)。 しかし、最悪の場合、動作しない、またはナビゲートするのが難しい(場合によってはほとんど不可能な)PHPコードを生成するため、デバッグ、拡張、保守が非常に困難になります。 そしてそれは、多くの無駄な(または少なくとも不必要な)努力を含む、チームの生産性の低下を意味します。

PHP開発者にとって幸いなことに、次の5つの標準で構成されるPHP標準勧告(PSR)があります。

  • PSR-0:自動読み込み標準
  • PSR-1:基本的なコーディング標準
  • PSR-2:コーディングスタイルガイド
  • PSR-3:ロガーインターフェース
  • PSR-4:オートローダー

PSRは元々、市場で最も認知されているプラ​​ットフォームのメンテナからのインプットに基づいて作成されました。 Zend、Drupal、Symfony、Joomlaなどがこれらの標準に貢献し、現在はそれらをフォローしています。 それまで何年もの間標準になろうとしていたPEARでさえ、現在PSRに参加しています。

ある意味では、標準に同意してそれに固執する限り、コーディング標準が何であるかはほとんど問題ではありませんが、プロジェクトに他の方法でやむを得ない理由がない限り、PSRに従うことは一般的に良い考えです。 。 ますます多くのチームとプロジェクトがPSRに準拠しています。 Ttは、現時点で大多数のPHP開発者によって「標準」として確実に認識されているため、これを使用すると、新しい開発者がチームに参加するときにコーディング標準に精通し、快適に使用できるようになります。

よくある間違い#10: empty()誤用

一部のPHP開発者は、ほぼすべてのブールチェックにempty()を使用することを好みます。 ただし、これが混乱につながる場合があります。

まず、配列とArrayObjectインスタンス(配列を模倣)に戻りましょう。 それらの類似性を考えると、配列とArrayObjectインスタンスは同じように動作すると簡単に推測できます。 ただし、これは危険な仮定であることがわかります。 たとえば、PHP5.0では次のようになります。

 // PHP 5.0 or later: $array = []; var_dump(empty($array)); // outputs bool(true) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false) // why don't these both produce the same output?

さらに悪いことに、PHP5.0より前の結果は異なっていたでしょう。

 // Prior to PHP 5.0: $array = []; var_dump(empty($array)); // outputs bool(false) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false)

このアプローチは残念ながら非常に人気があります。 たとえば、これは、ドキュメントが示唆しているように、 TableGateway::select()の結果でcurrent()を呼び出すときに、ZendFramework2のZend\Db\TableGatewayがデータを返す方法です。 開発者は、このようなデータでこの間違いの犠牲になる可能性があります。

これらの問題を回避するには、空の配列構造をチェックするためのより良いアプローチは、 count()を使用することです。

 // Note that this work in ALL versions of PHP (both pre and post 5.0): $array = []; var_dump(count($array)); // outputs int(0) $array = new ArrayObject(); var_dump(count($array)); // outputs int(0)

ちなみに、PHPは0falseにキャストするため、 if ()条件内でcount()を使用して、空の配列をチェックすることもできます。 PHPでは、 count()は配列の定数の複雑さ( O(1)操作)であることに注意してください。これにより、それが正しい選択であることがさらに明確になります。

empty()が危険な場合の別の例は、それをマジッククラス関数__get()と組み合わせる場合です。 2つのクラスを定義し、両方にtestプロパティを設定しましょう。

まず、通常のプロパティとしてtestを含むRegularクラスを定義しましょう。

 class Regular { public $test = 'value'; }

次に、magic __get()演算子を使用してtestプロパティにアクセスするMagicクラスを定義しましょう。

 class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } }

では、これらの各クラスのtestプロパティにアクセスしようとするとどうなるか見てみましょう。

 $regular = new Regular(); var_dump($regular->test); // outputs string(4) "value" $magic = new Magic(); var_dump($magic->test); // outputs string(4) "value"

これまでのところ元気です。

しかし、これらのそれぞれでempty()を呼び出すとどうなるか見てみましょう。

 var_dump(empty($regular->test)); // outputs bool(false) var_dump(empty($magic->test)); // outputs bool(true)

うーん。 したがって、 empty()に依存している場合、 $magictestプロパティは空であると誤解される可能性がありますが、実際には'value'に設定されています。

残念ながら、クラスがmagic __get()関数を使用してプロパティの値を取得する場合、そのプロパティ値が空であるかどうかを確認する確実な方法はありません。 クラスのスコープ外では、実際にはnull値が返されるかどうかのみを確認できます。これは、実際にはnull設定されている可能性があるため、対応するキーが設定されていないことを必ずしも意味しません。

対照的に、 Regularクラスインスタンスの存在しないプロパティを参照しようとすると、次のような通知が表示されます。

 Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0

したがって、ここでの主なポイントは、 empty()メソッドは注意して使用する必要があるということです。注意しないと、結果が混乱したり、誤解を招く可能性があるためです。

要約

PHPの使いやすさは、開発者を誤った安心感に陥らせ、言語の微妙な違いや特異性のために、長いPHPデバッグに対して脆弱なままにする可能性があります。 これにより、PHPが機能しなくなり、ここで説明するような問題が発生する可能性があります。

PHP言語は、20年の歴史の中で大きく進化してきました。 その繊細さに精通することは、作成するソフトウェアがよりスケーラブルで、堅牢で、保守しやすいことを保証するのに役立つため、価値のある取り組みです。