PHP は非常に人気のあるオープン ソースのサーバーサイド スクリプト言語であり、World Wide Web 上で見られる Web サイトのほとんどは PHP を使用して開発されています。この記事では、PHP 開発で最も一般的な 10 の問題を紹介します。あなたの友人に役立つことを願っています。
foreach ループで、反復される要素を変更する必要がある場合、または効率を向上させる必要がある場合は、参照を使用するのが良い方法です。
1 2 3 4 5 |
$arr = array (1, 2, 3, 4);
foreach ( $arr as & $value ) {
$value = $value * 2;
}
// $arr is now array(2, 4, 6, 8)
|
多くの人が混乱するであろう質問があります。ループが終了した後、$value は破棄されません。$value は実際には配列内の最後の要素への参照です。これを知らないと、その後の $value の使用時に不可解なエラーが発生します。以下のコードを見てください:
1 2 3 4 5 6 7 8 |
$array = [1, 2, 3];
echo implode( ',' , $array ), "n" ;
foreach ( $array as & $value ) {} // by reference
echo implode( ',' , $array ), "n" ;
foreach ( $array as $value ) {} // by value (i.e., copy)
echo implode( ',' , $array ), "n" ;
|
上記のコードを実行した結果は次のとおりです:
1 2 3 |
1,2,3
1,2,3
1,2,2
|
正解でしたか?なぜこのような結果になったのでしょうか?
分析してみましょう。最初のループの後、$value は配列内の最後の要素への参照になります。 2 番目のサイクルが始まります:
要約すると、最終結果は 1,2,2 です
このエラーを回避する最善の方法は、ループの直後に unset 関数を使用して変数を破棄することです。
$arr = array (1, 2, 3, 4);
foreach ( $arr as & $value ) {
$value = $value * 2;
}
unset( $value ); // $value no longer references $arr[3]
|
$data = fetchRecordFromStorage( $storage , $identifier );
if (!isset( $data [ 'keyShouldBeSet' ]) {
// do something here if 'keyShouldBeSet' is not set
}
|
別の例を示します:
if ( $_POST [ 'active' ]) {
$postData = extractSomething( $_POST );
}
// ...
if (!isset( $postData )) {
echo 'post not active' ;
}
|
1 2 3 4 5 6 7 8 9 |
if ( $_POST [ 'active' ]) {
$postData = extractSomething( $_POST );
}
// ...
if ( $_POST [ 'active' ]) {
echo 'post not active' ;
}
|
変数が実際に設定されているかどうかを判断するには(値が設定されていない場合とnullに設定されている場合を区別する場合)、array_key_exists()関数の方が良いかもしれません。上記の最初の例を次のようにリファクタリングします。
1 2 3 4 |
$data = fetchRecordFromStorage( $storage , $identifier );
if (! array_key_exists ( 'keyShouldBeSet' , $data )) {
// do this if 'keyShouldBeSet' isn't set
}
|
さらに、get_define_vars() 関数と組み合わせると、変数が現在のスコープに設定されているかどうかをより確実に検出できます。
if ( array_key_exists ( 'varShouldBeSet' , get_defined_vars())) {
// variable $varShouldBeSet exists in current scope
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Config
{
private $values = [];
public function getValues() {
return $this ->values;
}
}
$config = new Config();
$config ->getValues()[ 'test' ] = 'test' ;
echo $config ->getValues()[ 'test' ];
|
上記のコードを実行すると、次の内容が出力されます:
1 |
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
|
何が問題なの?問題は、上記のコードが戻り値と戻り参照を混同していることです。 PHP では、戻り参照を明示的に指定しない限り、PHP は配列のコピーである配列の値を返します。したがって、上記のコードが返された配列に値を割り当てる場合、実際には元の配列ではなく、コピーされた配列に値が割り当てられます。
1 2 3 4 5 6 7 |
// 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 2 3 |
$vals = $config ->getValues();
$vals [ 'test' ] = 'test' ;
echo $vals [ 'test' ];
|
元の配列を変更するだけ、つまり配列参照を返したいだけの場合はどう対処すればよいでしょうか?方法は、指定された戻り参照を表示することです:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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' ];
|
変更後、上記のコードは期待どおりにテストを出力します。
さらに混乱させる別の例を見てみましょう:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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' ];
|
上記のように「未定義のインデックス」エラーが出力されると思ったら大間違いです。コードは通常「test」を出力します。その理由は、PHP はデフォルトで値ではなく参照によってオブジェクトを返すためです。
まとめると、関数を使って値を返すとき、それが値の戻りなのか参照の戻りなのかを把握する必要があります。 PHP のオブジェクトの場合、デフォルトでは参照によって返され、配列と組み込みの基本型はデフォルトで値によって返されます。これは他の言語とは区別する必要があります (多くの言語は配列を参照によって渡します)。
Java や C# などの他の言語と同様、クラス プロパティにアクセスまたは設定するにはゲッターまたはセッターを使用する方が良い解決策です。もちろん、PHP はデフォルトではサポートしていないため、自分で実装する必要があります。
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'
|
エラー 4: ループ内で SQL クエリを実行しています
$models = [];
foreach ( $inputValues as $inputValue ) {
$models [] = $valueRepository ->findByValue( $inputValue );
}
|
$result = $connection ->query( "SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue );
|
1 2 3 4 5 |
$data = [];
foreach ( $ids as $id ) {
$result = $connection ->query( "SELECT `x`, `y` FROM `values` WHERE `id` = " . $id );
$data [] = $result ->fetch_row();
}
|
ただし、同じ目的を SQL でより効率的に達成できます。コードは次のとおりです。
1 2 3 4 5 6 7 |
$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 つのレコードを取得するよりも、1 つの SQL クエリで複数のレコードを取得する方が確実に効率的ですが、PHP で MySQL 拡張機能を使用している場合、一度に複数のレコードを取得するとメモリ オーバーフローが発生する可能性があります。
実験するコードを書くことができます (テスト環境: 512MB RAM、MySQL、php-cli):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 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 );
}
|
次に、リソース消費を見てみましょう:
1 2 3 4 5 6 7 8 9 |
// connect to mysql
$connection = new mysqli( 'localhost' , 'username' , 'password' , 'database' );
echo "Before: " . memory_get_peak_usage() . &quot;n&quot; ;
$res = $connection ->query( 'SELECT `x`,`y` FROM `test` LIMIT 1' );
echo "Limit 1: " . memory_get_peak_usage() . &quot;n&quot; ;
$res = $connection ->query( 'SELECT `x`,`y` FROM `test` LIMIT 10000' );
echo "Limit 10000: " . memory_get_peak_usage() . &quot;n&quot; ;
|
出力結果は次のとおりです:
1 2 3 |
Before: 224704
Limit 1: 224704
Limit 10000: 224704
|
メモリ使用量から判断すると、すべて正常のようです。より確実にするには、一度に 100000 レコードを取得しようとすると、プログラムは次の出力を取得します。
PHP Warning: mysqli::query(): (HY000/2013):
Lost connection to MySQL server during query in /root/test.php on line 11
|
問題は、PHP の mysql モジュールの動作方法にあります。mysql モジュールは、実際には libmysqlclient のプロキシです。複数のレコードを取得するためにクエリを実行すると、これらのレコードはメモリに直接保存されます。このメモリはPHPのメモリモジュールで管理されていないため、memory_get_peak_usage()関数を呼び出して得られる値が実際のメモリ使用量の値ではないため、上記問題が発生します。
mysql の代わりに mysqlnd を使用できます。Mysqlnd は PHP 独自の拡張機能にコンパイルされ、そのメモリ使用量は PHP メモリ管理モジュールによって制御されます。 mysqlnd を使用して上記のコードを実装すると、メモリ使用量がより現実的に反映されます:
Before: 232048
Limit 1: 324952
Limit 10000: 32572912
|
1 2 3 4 5 6 7 8 |
$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" );
}
|
上記のエラー4を踏まえると、実際のコーディングプロセスでは、機能要件を満たすだけでなく、パフォーマンスを確保するためにバランスをとる必要があることがわかります。
PHP プログラミングでは、非 ASCII 文字を扱うときにいくつかの問題が発生します。慎重に扱わないと、どこでもエラーが発生します。簡単な例として strlen($name) を考えます。$name に非 ASCII 文字が含まれている場合、結果は多少予想外になります。このような問題を回避するための提案をいくつか示します:
そのような問題をより詳しく紹介しているこちらの記事をお勧めします: UTF-8 Primer for PHP and MySQL
PHPの$_POSTには、フォームPOSTで送信されたデータが必ずしも含まれているわけではありません。 jQuery.ajax() メソッドを介してサーバーに POST リクエストを送信するとします。
// js
$.ajax({
url: 'http://my.site/some/path' ,
method: 'post' ,
data: JSON.stringify({a: 'a' , b: 'b' }),
contentType: 'application/json'
});
|
// php
var_dump( $_POST );
|
1 |
array (0) { }
|
なぜこのような結果になったのでしょうか? json データ {a: ‘a’, b: ‘b’} はどこに行ったのでしょうか?
答えは、PHP は Content-Type が application/x-www-form-urlencoded または multipart/form-data である HTTP リクエストのみを解析するということです。その理由は、歴史的な理由によるもので、PHP が最初に $_POST を実装したとき、上記の 2 つのタイプが最も一般的でした。したがって、現在、一部の型 (application/json など) が非常に人気がありますが、PHP には自動処理がまだ実装されていません。
$_POSTはグローバル変数なので、$_POSTを変更するとグローバルに反映されます。したがって、Content-Type が application/json であるリクエストの場合は、json データを手動で解析して、$_POST 変数を変更する必要があります。
1 2 |
// php
$_POST = json_decode( file_get_contents ( 'php://input' ), true);
|
この時点で、$_POST 変数を出力すると、期待する出力が得られます。
array (2) { [ "a" ]=> string(1) "a" [ "b" ]=> string(1) "b" }
|
for ( $c = 'a' ; $c <= 'z' ; $c ++) {
echo $c . &quot;n&quot; ;
}
|
はい、上記のコードは「a」から「z」を出力しますが、さらに「aa」から「yz」も出力します。なぜこのような結果になったのかを分析してみましょう。
PHPにはcharデータ型はなく、string型のみです。これを理解してから「z」をインクリメントすると、結果は「aa」になります。文字列の大きさの比較については、C を勉強したことがある人なら、'aa' が 'z' より小さいことを知っているはずです。これは、上記の出力結果が得られる理由も説明します。
'a' を 'z' に出力したい場合は、次の実装が良い方法です:
for ( $i = ord( 'a' ); $i <= ord( 'z' ); $i ++) {
echo chr ( $i ) . &quot;n&quot; ;
}
|
1 2 3 4 5 |
$letters = range( 'a' , 'z' );
for ( $i = 0; $i < count ( $letters ); $i ++) {
echo $letters [ $i ] . &quot;n&quot; ;
}
|
コーディング標準を無視してもエラーやバグが発生することはありませんが、特定のコーディング標準に従うことは依然として重要です。
統一されたコーディング標準がなければ、プロジェクトには多くの問題が発生します。最も明白なことは、プロジェクトのコードに一貫性がないことです。さらに悪いことに、コードのデバッグ、拡張、保守が難しくなります。これは、無意味な作業を多く行うなど、チームの効率が低下することも意味します。
PHP開発者にとっては、比較的幸運です。 PHP コーディング標準勧告 (PSR) があり、次の 5 つの部分で構成されているためです。
場合によっては、コーディング スタイルを使用し、それに固執している限り、どのようなコーディング標準を使用しても問題ありません。ただし、自分で作成する特別な理由がない限り、PSR 標準に従うことは良い考えです。現在、PSR を使用し始めているプロジェクトがますます増えており、ほとんどの PHP 開発者も PSR を使用しています。そのため、PSR を使用すると、チームの新しいメンバーがより早くプロジェクトに慣れ、より快適にコードを作成できるようになります。
エラー 10: empty() 関数の間違った使用
まず、PHPの配列Arrayと配列オブジェクトArrayObjectを見てみましょう。違いはないようで、どれも同じです。これは本当にそうなのでしょうか?
// 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?
|
1 2 3 4 5 |
// Prior to PHP 5.0:
$array = [];
var_dump( empty ( $array )); // outputs bool(false)
$array = new ArrayObject();
var_dump( empty ( $array )); // outputs bool(false)
|
残念なことに、上記の方法は非常に人気があります。たとえば、Zend Framework 2 では、TableGateway::select() 結果セットに対して current() メソッドを呼び出してデータ セットを返すときに、ZendDbTableGateway がこれを実行します。開発者は簡単にこの罠に陥る可能性があります。
これらの問題を回避するために、配列が空かどうかを確認する最後の方法は count() 関数を使用することです:
1 2 3 4 5 |
// 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は値0をブール値falseとみなすため、if条件文の条件判定にcount()関数を直接使用して配列が空かどうかを判定することができます。さらに、 count() 関数の配列の複雑さは O(1) であるため、 count() 関数を使用するのが賢明な選択です。
empty() 関数の使用が危険である別の例を見てみましょう。 empty() 関数をマジック メソッド __get() と組み合わせて使用する場合も危険です。それぞれテスト属性を持つ 2 つのクラスを定義しましょう。
まず、test 属性を持つ Regular クラスを定義します。
class Regular
{
public $test = 'value' ;
}
|
class Magic
{
private $values = [ 'test' => 'value' ];
public function __get( $key )
{
if (isset( $this ->values[ $key ])) {
return $this ->values[ $key ];
}
}
}
|
$regular = new Regular();
var_dump( $regular ->test); // outputs string(4) "value"
$magic = new Magic();
var_dump( $magic ->test); // outputs string(4) "value"
|
しかし、test 属性で empty() 関数を使用するとどうなるでしょうか?
var_dump( empty ( $regular ->test)); // outputs bool(false)
var_dump( empty ( $magic ->test)); // outputs bool(true)
|
残念ながら、クラスがマジック __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
|