PHP
About
About
PHPはプログラミング言語だ。汎用的なプログラミング言語だが、主にウェブサーバー上のソフトウェアで使用される。
GNU socialはPHPで記述されている。他にもWordPress・NextCloudなどがPHPで記述されている。これらのPHP製ソフトウェアはVPSだけではなく安価なレンタルサーバーでも動作するため、低コストで運用することができる。
PHPの公式リファレンスは日本語版があり、わかりやすくまとまっている。
- PHP: 言語リファレンス - Manual
- PHP: Release Archives (museum): PHP1からのソースコードの保管場所。
ウェブ上にはPHPに関するTipsが多く公開されており、大抵の疑問はウェブ検索で解決できる。
Version
PHPは言語の版数が上がる際、過去の版と互換性の無い破壊的変更がなされることがある。
開発者はこのリスクを軽減するために、非推奨の言語機能を避け、実行時の警告 (warning) を適切に処理するべきだ。
GNU socialは現在PHP 7系で動作する様に記述されており、PHP 8系への対応は作業途中だ。
PHP v8
PHP v8になっていろいろ更新が入った。特にPHP v7.4からv8に更新する際のポイントがあるので整理する (行事: 「12月にPHP8.3が出るので、PHP8で増えた文法をおさらいしましょうセミナー」参加報告 | PHP8対応の肝は型とエラーレベル | GNU social JP Web)。
大きく以下2点がある。
- エラーレベルの上昇。
- 型の厳格化。
エラーレベルが1段階上がったため、今までWarningで問題なかったものがFatal Errorになって動作しなくなる。他に、型が厳格になっている。
具体的には、php.ini/.user.iniで以下を指定して、PHP v7.4時点で警告にできるだけ対応しておく。
error_reporting=E_ALL ; -1
続いて、phpソースファイルに以下を記入して型を厳密にしておく。
declare(strict_types=1);
チェックツールがあるのでこれを使うと問題箇所などがわかる。
- PHP CodeSniffer: コーディング規約の準拠確認ツール。PHPのバージョンアップグレード可否チェックもできる。
- PHPStan: 静的解析ツール。引数の数、型不一致など、潜在的な問題を検出する。
- Rector
まず上記2個を試して、おまけでRectorも試すとよい。
Tool
PHPをWebブラウザーで実行、動作確認のツールがいくつかある。
- paiza.IO: Online PHP Editor | ブラウザでプログラミング・実行ができる「オンライン実行環境」| paiza.IO: パーマリンク。
- phpsansbox.io Write PHP online from your browser - PHPSandbox: フレームワークも確認可能。
- OnlinePHP.io: PHP Sandbox - Execute PHP code online through your browser: 複数バージョンの同時実行できる。
- 3v4l.org: Online PHP editor | Test code in 250+ PHP versions: パーマリンク。
3v4l.orgがパーマリンクがあって、複数バージョンの動作確認できるので、これがいいと思う。
Guide
PHPのコーディングの推奨規約がある。PSR-12というのがメジャーな模様。GNU socialでも採用されている。
Naming
「What should I name my PHP class file? - Stack Overflow」にあるように、PSRではファイル名には記載がない。PSR-4や「PSR Naming Conventions - PHP-FIG」に記載がある程度。
ただ、「Manual - Documentation - Zend Framework」、「CakePHP Conventions - 4.x」など、他の規約があり、クラス名と同じになっている。
PHPのクラス名は大文字小文字を区別しないが、わかりにくいので大文字小文字で、クラス名と一致させておくとよさそう。
ただし、viewなど、表示に直接結びついているものは、小文字でもいいかも。ファイル名とURLパスが同じほうが分かりやすい。
変数名やシンボルの命名規則。
- クラス名: CamelCase
- メソッド名: mixedCase
- プロパティー: PSRでの規定はない。公式文書だとmixedCase (PHP: プロパティ - Manual)
- インスタンス変数名: PSRでの規定なし。公式文書はmixedCase (PHP: オブジェクトのクローン作成 - Manual)。
- 定数: UPPER_SNAKE_CASE
- 変数名: PSRでの規定はない。公式文書だとlower_snake_case (PHP: disk_free_space - Manual)
- 関数名: PSRでの規定はなし。公式文書だとlower_snake_case (PHP: disk_free_space - Manual)
上記で揃えるとよいだろう。
プロパティーもスネークケースでいい気がする。変数系はスネークケースで統一できるから。いや、オブジェクト系はmixedCase、それ以外はsnake_caseにしている可能性が高い。
フレームワークなどで、privateメソッド、変数は先頭に_をつけるというのがあったりする。が、一般規則にはない。通常は無視していいと思う。
PSR
PHP Standards Recommendations - PHP-FIG
PSR=PHP Standards Recommendations。PHPでの標準推奨事項。
0-22まである。特によく登場するのが以下。
- 4 Autoloading Standard
- 12 Extended Coding Style Guide
PSR-1: Basic Coding Standard
PSR-1: Basic Coding Standard - PHP-FIG
ファイル全体の決め事。
- ファイル内では<?phpか<?=のみを使用。
- UTF-8のBOMなし。
- ファイルでは、シンボルの宣言のみか、実行のみで副作用があるものを宣言に混在させるべきでない。例えば、ini_setでの設定変更、echoとかの関数・文の実行。など。
- 名前空間はPSR-4のautoloadingに準拠。
- クラス名はPascalCase
- クラス定数はUPPER_CASE_WITH_UNDERSCORE。
- メソッド名はcamelCase
- プロパティー名については、意図的に言及回避。
PSR-12: Extended Coding Style
PSR-12: Extended Coding Style - PHP-FIG
PSR-2の拡張・置換コーディングスタイルガイド。PSR-1は前提。
2. General
- ファイル
- PSR-1は前提
- PHPファイルの改行はLF
- ファイル終端は非空行。
- ファイル終端の?>は省略。
- 行
- 行の長さに厳しい制限を設けてはダメ。
- 列長は120文字が上限。
- 列長80文字超過は非推奨。超えるなら80文字以下に分割すべき。
- 行末の空白はダメ。
- 可読性向上のために空行を追加しても用意。
- 1行に複数文はダメ。
- インデント: インデントは4文字スペース。タブ文字は禁止。
- キーワードと型
- PHPのキーワード/予約語と型は小文字。
- 将来のPHPの新しい型とキーワードも小文字。
- 型キーワードの短縮形を使わないといけない。
3. Declare Statements, Namespace, and Import Statements
PHPのヘッダーはいくつかのブロックから構成。ブロックごとに1行の空行で区切る。ブロックの順序は以下。
- <?php
- ファイル単位docblock
- 1以上の文の宣言
- ファイルの名前空間宣言
- クラスベースのuse
- 関数ベースのuse
- 定数ベースのuses
- それ以外のコード
インポート文は先頭スラッシュ始まりはだめ。
例は以下。
<?php
/**
* This file contains an example of coding styles.
*/
declare(strict_types=1);
namespace Vendor\Package;
use Vendor\Package\{ClassA as A, ClassB, ClassC as C};
use Vendor\Package\SomeNamespace\ClassD as D;
use Vendor\Package\AnotherNamespace\ClassE as E;
use function Vendor\Package\{functionA, functionB, functionC};
use function Another\Vendor\functionD;
use const Vendor\Package\{CONSTANT_A, CONSTANT_B, CONSTANT_C};
use const Another\Vendor\CONSTANT_D;
/**
* FooBar is an example class.
*/
class FooBar
{
// ... additional PHP code ...
}
長くて飽きた。
PSR-4: Autoloader
ファイルパスベースのクラスのオートローディングの仕様。PSR-0に加えて、他のオートローディング仕様と完全相互運用可能。
- 名前空間区切りはディレクトリー区切りに一致。
- クラス名の終端はファイル名の終端に一致。ファイル名はクラス名.phpに一致すること。
Performance
About
PHPのコーディング時に、速度に影響のある書き方がいろいろある。
一般論。
- 言語構造>組込関数>関数の順に速い。
- 複雑な方法より単純な方法のほうが速い。
- 特に配列関係の関数、呼び出しが頻発する処理などで重要。
Time
速度の話をするにあたって計測方法。
<?php
/**
* Time target function.
* @param callable $callback Target function.
* @return int|float Run time [ns].
*/
function timeit(callable $callback)
{
$time = 'microtime';
$nanoFactor = 1000;
if (function_exists('hrtime')) {
$time = 'hrtime';
$nanoFactor = 1;
}
$start = $time(true);
$callback();
$stop = $time(true);
return ($stop - $start) * $nanoFactor;
}
echo timeit(function(){sleep(1);});
// one liner.
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){;}), " ns\n";
// arrow function
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function(){;}), " ns\n";
(function($c){$s=hrtime(true);$c();return hrtime(true)-$s;})(function(){sleep(1);});
?>
ワンライナーか上記のtimeit関数で計測する。
Array
配列が特に重要。基本的に、array_mapなどの一括操作系関数を使うと遅い。foreachでやったほうが速い。可読性などの問題はある。
Variable
assign
変数のif文での代入パターン。
- 片方ケースだけ代入必要な場合、条件演算子で毎回じゃないほうが早い。
- 両方ケースで代入必要な場合、デフォルト代入のほうが早い。条件演算子を使わない方が早い。
- 条件演算子とif文の場合、trueはif文、falseは条件演算子のほうがわずかに早い。
- 条件演算子は、可読性優先の場合。
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){$var = 0; $var = true ? 1 : 0;}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){$var = 0; if (true) {$var = 0;};}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){$var = 0; $var = true ? 1 : 0;}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){$var = 0; if (true) {$var = 0;};}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){$var = 0; $var = false ? 1 : 0;}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){$var = 0; if (false) {$var = 0;};}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){$var = 0; if (true) $var = 1;}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){if (true) $var = 1; else $var = 0;}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){$var = 0; if (false) $var = 1;}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){if (false) $var = 1; else $var = 0;}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){$var = true ? 1 : 0;}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){$var = false ? 1 : 0;}), " ns\n";
15080 ns 470 ns 270 ns 170 ns 270 ns 190 ns 170 ns 310 ns 260 ns 290 ns 330 ns 270 ns
Install
Mac
homebrewでインストールするのが推奨されている。ただし、この方法だとEOLのバージョンはインストールできない。
- 【Mac】M1 MacにHomebrew経由でPHP7.4を入れる方法
- shivammathur/homebrew-php: :beer: Homebrew tap for PHP 5.6 to 8.5. PHP 8.5 is built nightly.
PHPの古いバージョンのhomebrewを用意してくれている人がいるので、これを使う。
brew tap shivammathur/php
# このコマンドは以下でもいけるかもしれない。 # brew install php@7.4 brew install shivammathur/php/php@7.4 # インストール後はパスを通す #$ echo 'export PATH="/opt/homebrew/opt/php@7.4/bin:$PATH"' >> ~/.zshrc #$ echo 'export PATH="/opt/homebrew/opt/php@7.4/sbin:$PATH"' >> ~/.zshrc #$ source ~/.zshrc
phpコマンドをphp7.4に変更する。--force --overwriteはいらないかもしれない。
brew link --force --overwrite php@7.4
Event
PHPイベントの世界 #PHPカンファレンス - Qiita
上記記事がよくまとまっている。
以下のカンファレンスが主?なもの。
- PHP Conference: 歴史長い。初回2000。秋から冬に開催。
- PHPerKaigi: 2015年から?交流重視。毎年3月。
Zend Engine
PHPのインタプリター。phpコマンドのコア部分。
Language Reference
Basic syntax/基本的な構文
PHP tag
PHPでは<?php ... ?>のタグを探して、この中のPHPコードを実行する。
- <?php ?>
- <?= ?>: <?php echoの短縮系。
命令の分離
PHPはステートメント (文) の区切りに、セミコロン;が必要。ただし、コードブロックの終了タグには自動的にセミコロンがあると認識されるので、?>がある場合、;は不要。
コーディング試験で、文字数を1文字節約でき、重要なテクニックになる。
<?php echo 0; <?php echo 0?>
なお、php -rで実行する場合、先頭の<?phpは不要。終端は;の他?>も認識する模様。
Types
Introduction
Ref: PHP: Introduction - Manual.
PHPの変数は以下の型のいずれかの値となる。
- null
- bool
- int
- float (floating-point number)
- string
- array
- object
- callable
- resource
C言語のようなlong/doubleのような精度ごとの型はない。
System
組込型の他に、ユーザー定義の型、aliasなどいくつかの型がある。
基本型
言語に統合されていて、ユーザー定義で再現不能。
- 組込
- null
- スカラー型: bool/int/float/string
- array
- object
- resource
- never
- void
- クラス内の相対型: self/parent/static
- Value型: false/true
- ユーザー定義型/クラス型
- インターフェイス
- クラス
- 列挙型
- callable
複合型
複数の基本型を組み合わせた型。交差型 (PHP 8.1以上) とunion型 (PHP 8.0以上) で作れる。
- 交差型: 宣言した複数のクラス型をすべて満たす型。&で表現。T/U/Vの交差型はT&U&Vと書く。
- union型: 複数の型を受け入れる型。|で表現。T/U/Vのunion型はT|U|Vと書く。交差型を含む場合、T|(X&U)と丸括弧で囲む必要がある。
alias
PHPはmixedとiterableの2個の型のエイリアスに対応している。
- mixed=object|resource|array|string|float|int|bool|null: PHP 8.0.0で導入。mixedは型のトップ。他の全部の型はこの型の部分になる。
- iterable=Traversable|array: PHP 7.1.0で導入。foreachで反復可能でジェネレーター内でyield from可能。
ただし、ユーザー定義のエイリアスは未対応。
Boolean
条件判定にかかってくるので非常に重要。
まずは、下記のfalseになるもの一覧を把握し、それ以外はすべてtrueになるということを把握しておく。
- booleanのfalse
- intの0
- floatの0.0
- stringの空文字列、"0"
- 要素数0個のarray
- null (未初期化変数含む)
stringの"0"と要素0のarrayがfalseになる点が重要。注意する。要素0のarrayは包含判定、検索などでよく使う。
stringの0ははまりどころ。stringは何がくるかわからないなら、strlenで文字数を見たほうが確実。
Strings
Ref: PHP: 文字列 - Manual.
非常に重要。
Literal
文字列リテラルとしては4の表現がある。
- Single quote: '' 変数展開されない。エスケープシーケンス無視。
- Double quote: "" 変数展開される。エスケープシーケンス解釈。
- Here document: <<<EOT 二重引用符扱いで変数展開される。
- Nowdoc: <<<'EOT' 一重引用符扱いで変数展開されない。
引用符内で引用符'を使う場合はバックスラッシュ\でエスケープが必要。バックスラッシュ自体の指定は二重\\。
Here document/Nowdocは終端IDのインデントで行頭を識別しており、インデントに意味があるので注意する。
echo <<<END
a
b
c
\n
END;
echo <<<'EOT'
My name is "$name". I am printing some $foo->foo.
Now, I am printing some {$foo->bar[1]}.
This should not print a capital 'A': \x41
EOT;
Escape sequence expansion
| 記述 | 意味 |
|---|---|
\n
|
ラインフィード (LF またはアスキーの 0x0A (10)) |
\r
|
キャリッジリターン (CR またはアスキーの 0x0D (13)) |
\t
|
水平タブ (HT またはアスキーの 0x09 (9)) |
\v
|
垂直タブ (VT またはアスキーの 0x0B (11)) |
\e
|
エスケープ (ESC あるいはアスキーの 0x1B (27)) |
\f
|
フォームフィード (FF またはアスキーの 0x0C (12)) |
\\
|
バックスラッシュ |
\$
|
ドル記号 |
\"
|
二重引用符 |
\[0-7]{1,3}
|
8進数: 正規表現 [0-7]{1,3} にマッチする文字シーケンスは、8 進数表記の 1 文字 (例:. "\101" === "A") です。 正規表現にマッチする文字シーケンスは、8 進数表記の 1 文字です。 1 バイトに収まらない部分は、何もメッセージを出さずにオーバーフローします (例: "\400" === "\000") 。
|
\x[0-9A-Fa-f]{1,2}
|
16進数: 正規表現 [0-9A-Fa-f]{1,2} にマッチする文字シーケンスは、16 進数表記の 1 文字(例: "\x41" === "A")です。
|
\u{[0-9A-Fa-f]+}
|
Unicode: 正規表現 [0-9A-Fa-f]+ にマッチする文字シーケンスは、Unicode のコードポイントです。 そのコードポイントの UTF-8 表現を文字列として出力します。 シーケンスを波括弧で囲む必要があります。例 "\u{41}" === "A"
|
繰り返しますが、この他の文字をエスケープしようとした場合には、 バックスラッシュも出力されます!
Variable expansion
二重引用符とヒアドキュメントではエスケープシーケンスが解釈され、変数が展開される。
<?php
$juice = "apple";
echo "He drank some $juice juice." . PHP_EOL;
// 意図しない動作をします。"s" は、変数名として有効な文字です。よって、変数は $juices を参照しています。$juice ではありません。
echo "He drank some juice made of $juices." . PHP_EOL;
// 参照する変数名を波括弧で囲むことで、変数名の終端を明示的に指定しています。
echo "He drank some juice made of {$juice}s.";
//
$juices = array("apple", "orange", "koolaid1" => "purple");
echo "He drank some $juices[0] juice.".PHP_EOL;
echo "He drank some $juices[1] juice.".PHP_EOL;
echo "He drank some $juices[koolaid1] juice.".PHP_EOL;
// 複雑な例1
// これが動作しない理由は、文字列の外で $foo[bar]
// が動作しない理由と同じです。
// PHP はまず最初に foo という名前の定数を探し、
// 見つからない場合はエラーをスローします。
// 定数が見つかった場合は、その値('foo' そのものではない)
// を配列のインデックスとして使います。
echo "This is wrong: {$arr[foo][3]}";
// 動作します。多次元配列を使用する際は、
// 文字列の中では必ず配列を波括弧で囲むようにします。
echo "This works: {$arr['foo'][3]}";
// 複雑な例。二重展開で変数になる場合だけ式が使える模様。
$var1=9;
echo "{${mb_strtolower('VAR1')}}"; // 9
?>
波括弧はなくてもいいが、文字列が連結するなどして変数名の終端を区別できない場合に必須になる。
特に重要な挙動は以下。
- ""内だと、連想配列添字の引用符不能。
- ${}内だと、連想配列添字の引用符必要。
複雑な形式は{$ ... }がセット。{$ } 部分で変数が式扱いになる。
さらに複雑なことができる。{${}}を指定すると、内側の波括弧内で、${}部分が変数評価になる場合にだけ式を指定できる。動きがトリッキーすぎる。フォーマット文字列的なことには使えない。バグのもとになりそうなので使用を控えたほうがよさそう。
Format
Ref:
PHPにはPythonのformatメソッド相当はない。が似たような目的の関数がある。
- sprintf/vprintf
- strtr
strtrは第2引数にold => newの置換のペアの配列を渡す。やることは同じようなものだけどちょっと違う。
vsprintfは置換対象が可変長引数ではなく配列なだけ。
sprintf
今後何度も使う。
C言語のprintfといろいろ違うところがある。
<?php
$format = 'The %2$s contains %1$d monkeys.
That\'s a nice %2$s full of %1$d monkeys.';
echo sprintf($format, $num, $location);
echo sprintf("%'.9d\n", 123); // ......123
echo sprintf("%'.09d\n", 123); // 000000123
?>
特徴的なのが`%数$指定子`で引数の番号を選べるところ。Pythonの`{数:指定子}`に似ている。
後は埋める文字を指定する際は'を前置。
文字列の切り出し
いくつか方法がある。
- substr/mb_substr
- strpos/mb_strpos/strrpos/mb_strrpos (PHP: strpos - Manual)
- split
- preg_match (PHP: preg_match - Manual)
preg_matchの自由度が高い。速度を気にしなくていいならこれでいいと思われる。ただ、引数の配列に入ってくるのがいまいち。関数の戻り値でほしい。
strposとsubstrを組み合わせると端の文字列を切り出せる。
文字列置換
- PHP: str_replace - Manual: 日本語不能。記号の置換などで便利。
- PHP: substr_replace - Manual: 日本語不能。指定した文字数の位置で置換する。
- preg_replace
- explode/implode: 日本語OK。
str_replace(
array|string $search,
array|string $replace,
string|array $subject,
int &$count = null
): string|array
$search/$replaceが配列の場合、それぞれ前から順番に対応する。$searchの要素数が多い場合、$replaceは空文字が適用される。これでまとめて置換できる。
// <body text='black'> となります
$bodytag = str_replace("%body%", "black", "<body text='%body%'>");
substr_replace($text, '', -1); // 末尾1文字の削除。
// substr_replace($text, '.', mb_strrpos('_'));
1文字などの置換ならmb_strrposとの組み合わせ。
$query = $request->query();
foreach ($query as $key => $value) {
unset($query[$key]);
$keys = explode('_', $key);
$key = implode('_', array_slice($keys, 0, -1)) . '.' . $keys[count($keys)-1];
$query[$key] = $value;
}
日本語はexplode/implodeが無難で確実。
startsWith/endsWith
PHP 8なら「PHP: str_starts_with - Manual/PHP: str_ends_with - Manual」がある。
PHP 8未満なら以下のようなコード。
function startsWith( $haystack, $needle ) {
$length = strlen( $needle );
return substr( $haystack, 0, $length ) === $needle;
}
function endsWith( $haystack, $needle ) {
$length = strlen( $needle );
if( !$length ) {
return true;
}
return substr( $haystack, -$length ) === $needle;
}
mb_strlen/mb_substrでマルチバイト対応。
改行分割
- Explode PHP string by new line - Stack Overflow
- php - Split string by new line characters - Stack Overflow
- How to replace different newline styles in PHP the smartest way? - Stack Overflow
explode('\n', $csv) のようなことをしたくなるが、改行が\nとは限らない。
$array = preg_split('/\R/u', $string);
上記がいい。\Rが\r \n \n\rなどにマッチ。uで入力がUTF-8の場合を考慮。例えば、「腰」がuをつけないと分割されてしまう。
trim
文字列の両端のホワイトスペースを除去する。
文字列反復
str_repeat(string $string, int $times): string
文字列に対する乗算はstr_repeatで行う。他にarray_fillを使った方法もある。
プリペアードステートメントで(?,?)を作るときとかで使う。
function timeit(callable $callback)
{
$time = 'microtime';
$nanoFactor = 1000;
if (function_exists('hrtime')) {
$time = 'hrtime';
$nanoFactor = 1;
}
$start = $time(true);
$callback();
$stop = $time(true);
return ($stop - $start) * $nanoFactor;
}
echo timeit(function(){for ($i = 0; $i<10000; ++$i){rtrim(str_repeat('?,', 5),',');}}) . '=rtrim'. PHP_EOL;
echo timeit(function(){for ($i = 0; $i<10000; ++$i){substr(str_repeat('?,', 5), 0, -1);}}) . '=substr' . PHP_EOL;
echo timeit(function(){for ($i = 0; $i<10000; ++$i) {implode(',', array_fill(0, 5, '?'));}}) . '=array' . PHP_EOL;
echo timeit(function(){for ($i = 0; $i<10000; ++$i){rtrim(str_repeat('?,', 10000),',');}}) . '=rtrim'. PHP_EOL;
echo timeit(function(){for ($i = 0; $i<10000; ++$i){substr(str_repeat('?,', 10000), 0, -1);}}) . '=substr' . PHP_EOL;
echo timeit(function(){for ($i = 0; $i<10000; ++$i) {implode(',', array_fill(0, 10000, '?'));}}) . '=array' . PHP_EOL;
echo timeit(function(){for ($i = 0; $i<10000; ++$i){substr(str_repeat('?,', 10000), 0, -1);}}) . '=substr' . PHP_EOL;
echo timeit(function(){for ($i = 0; $i<10000; ++$i){rtrim(str_repeat('?,', 10000),',');}}) . '=rtrim'. PHP_EOL;
echo timeit(function(){for ($i = 0; $i<10000; ++$i) {implode(',', array_fill(0, 10000, '?'));}}) . '=array' . PHP_EOL;
/*
552413=rtrim
565660=substr
997959=array
6853087=rtrim
6411850=substr
755294953=array
6507484=substr
6451837=rtrim
770350600=array
*/
常に速いのはrtim。
文字数カウント
行数カウントなどで文字列をカウントしたいことがそれなりにある。
substr_countでできる。
substr_count(
string $haystack,
string $needle,
int $offset = 0,
?int $length = null
): int
$text = 'This is a test'; echo substr_count($text, 'is'); // 2 substr_count($str,"\n");
BOMの判定
- UTF-8BOM有無両対応のCSVファイル読み込み(PHP) | 株式会社フーリエ | Web戦略・システム開発[東京/浜松]
- php で csv を読み込む上での備忘録 #PHP - Qiita
- php - CSV upload - parsing with SplFileObject - Remove BOM - Stack Overflow
読み込んだファイルにUTF-8のBOMがあって、データ処理としてはBOMを除外したいことがある。
いくつか方法がある。
$header[0] = preg_replace('/^\xEF\xBB\xBF/', '', $header[0]);
if ($file->fread(3) !== pack('C*', 0xEF, 0xBB, 0xBF)) {
$bom = pack('CCC', 0xEF, 0xBB, 0xBF);
$first = true;
foreach ($file as $line) {
if ($first && substr($line, 0, 3) === $bom) {
$line = substr($line, 3);
}
$first = false;
// your lines don't have a BOM, do your stuff
}
最後の方法がよいと思う。
$line = (substr($line, 0, 3) === "\xEF\xBB\xBF") ? trim(substr($line, 3), '"') : $line;
SplFileObjectだと$csvObj->setFlags(SplFileObject::READ_CSV); でCSV扱いにしてしまうと、1列目はBOMつきでセルの解釈をしてしまうので、二重引用符もデータ扱いになる。BOM除去後にそれも除去しておく。
$current = $this->file->current();
if (count($current)) {
$line = $current[0];
$current[0] = (substr($line, 0, 3) === "\xEF\xBB\xBF") ? trim(substr($line, 3), '"') : $line;
}
こういう
列挙表示
echoやprintなどでパラメーター・変数を順番に列挙したいことがよくある。
echo __FILE__ .':'.__FUNCTION__ .':'.__LINE__."\n";
printf("%s:%s:%s\n", __FILE__, __FUNCTION__, __LINE__);
echo implode(":", [__FILE__,__FUNCTION__,__LINE__])."\n";
@array_walk(debug_backtrace(), function($v){error_log(
implode(':', [$v['file'],$v['function'],$v['line']])
);});
う
普通に.で連結するのがいいかもしれない。配列だったらimplodeか。
Array
About
PHPの配列は、順序マップ。
array(
key => value,
key2 => value2,
key3 => value3,
...
)
全て連想配列。キーはint|string。キーを省略したら、登場したキーの数+1の添え字のキーに自動で採番される。ただし、先頭は0。
角括弧構文による配列要素へのアクセス
配列要素へのアクセスには array[key] 構文 (角括弧構文) を使う。
なお、PHP 8.0.0未満では、波括弧 (array{key}) も使えた。が、PHP 7.4.0で非推奨になって、PHP 8.0.0では廃止になった。
Destructuring
listか[] (PHP 7.1以上 (PHP: 新機能 - Manual)) で配列要素を個別の値に分解できる。要素やキーがわかっていて、一括で項目取得時に便利。
PHP 7.1から連想配列も分解可能になった。
['baz' => $three] = $source_array;
意味があるのはforeach。
変数の値の交換でも便利。
$a = 1; $b = 2; [$b, $a] = [$a, $b];
Create
Basic
配列の作成方法がいくつかある。
- array()/[]
- explode (PHP: explode - Manual)
- array_merge
- array_map
$arr[キー] = 値; $arr[] = 値; // キーは文字列か整数。 $arr = [ 'key1' => 'value1', ];
array()の他に[]も使える。[]のほうが短いのでこちらがいいだろう。
$arrが存在しないか、null/falseの場合、新しい配列を作成する。ただし、この方法は万が一既存の変数があったら、追加になるのであまり推奨されない。明示的に初期化したほうがいい。
2行余分に増えるが、上記の形式が初期化もできるのでいいだろう。
explode(',', '物件コード,オーナーコード,棟数,M数,実戸数,a,b,c')
['物件コード','オーナーコード','棟数','M数','実戸数','a','b','c']
explode(',', '物件コード,オーナーコード,棟数,M数,実戸数)
['物件コード', 'オーナーコード', '棟数', 'M数', '実戸数']
explodeで配列を作ると短いのは、要素数8以上。詰めずに書いたら5以上。
ただ、余計な関数呼び出しが発生するから、あまりしないほうがいいかも。
Serial
- PHP: array_fill - Manual
- PHP: array_fill_keys - Manual
- PHP: range - Manual
- PHP: str_repeat - Manual
- https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83
同じ値の複数要素、連番データの作成方法がある。rangeやarray_fillがシンプル。
array_fill(int $start_index, int $count, mixed $value): array
$a = array_fill(5, 6, 'banana'); print_r($a);
Array
(
[5] => banana
[6] => banana
[7] => banana
[8] => banana
[9] => banana
[10] => banana
)
array_fill_keys(array $keys, mixed $value): array
$keys = array('foo', 5, 10, 'bar');
$a = array_fill_keys($keys, 'banana');
array_fill_keys(['a', 'b'], 'ab');
print_r($a);
Array
(
[foo] => banana
[5] => banana
[10] => banana
[bar] => banana
)
range(0, 12)
explode(',', str_repeat(",", 10));
連続データを作成出来たら、array_combine/array_keys/array_valuesなどの組み合わせで、キーと値は調整できる。
- range: 指定要素数配列
- array_fill/array_fill_keys: 指定値の指定要素数配列。連想配列で複数キーに同じ値を設定したい場合に使う。
value部分を連番にしたい場合、array_mapを使う。
$data = array_map(function ($i) {
return ['column1' => 'value' . $i];
}, range(0, 99));
[
['column1' => 'value0'],
['column1' => 'value1'],
...
['column1' => 'value99'],
]
phpunitのテストデータ作成時などに役立つ。
Merge
配列の追加、結合。
$arr[キー] = 値; $arr[] = 値; [0]+[1]; // 右の配列を左の配列に追加したものを返す。同じキーは左優先。 array_push($arr, 'a', 'b'); // array_pushだと一度に複数追加できる。 array_unshift($arr, 'a', 'b'); // 先頭に追加。 array_merge($arr, [0, 1]); // 配列同士の追加。 $arr = [...$arr, ...[0, 1]] // PHP7.4以上。...演算子。性能はarray_mergeのほうが高い。 array_combine(['k1', 'k2'], [0, 1]); // ['k1' => 0, 'k2' => 1]
基本は$arr[キー] $arr[]でいいだろう。キーと値の配列から連想配列を作るには、array_combine。重宝する。同じキーで、値だけ違うようなデータを作りたいときに、キー指定を使いまわせてコードがシンプルになる。
+演算子の結合は注意が必要。同じキーだと追加されない (PHP: 配列演算子 - Manual)。基本はarray_merge。
配列ではなく、配列要素の結合は以下が使える。
implode($arr);
array_reduce($arr, function($c, $v){return $c.$v;});
単に文字列結合するならimplodeがシンプル。
指定した要素を全部の行に追加する場合。きれいな方法はない。
- foreach
- array_map
foreach ($array as &$row) {
$row[] = $newElement; // 各行の末尾に要素を追加
}
$array = array_map(function($row) use ($newElement) {
$row[] = $newElement; // 各行の末尾に要素を追加
return $row;
}, $array);
Read
角括弧構文による配列要素へのアクセス
配列要素へのアクセスはarray[key]構文を使う。
[]を角括弧構文 (square bracket syntax) と呼んでいる (PHP: Arrays - Manual)。
なお、これとは別でstring access operator (文字列アクセス演算子)、インデックス演算子と呼ぶこともあるが、演算子ではないので「PHP: Operators - Manual」に記載はない。
未定義キーへのアクセスは、未定義変数へのアクセスと同じ扱い。つまり、E_WARNING レベルの警告 (PHP 8.0.0 より前のバージョンでは E_NOTICE) が発生し、結果が null。
回避するには、一般的な未定義変数アクセスと同様に、以下の演算子類を使う。
- empty
- isset
- ??
値の比較をしたい場合、以下のようにガードとセットでするといいかもしれない。
($ar['key'] ?? null) === 2:
https://chatgpt.com/c/67a405d2-7614-800b-9f41-811812718dac
なお、配列以外の型に角括弧構文を使った文字列キーアクセスの挙動には注意が必要基本はFatal error (isset/emptyでのガードも不能) になる。
- null: Warning
- object: Fatal error
- int|float: Warning
- string: PHP 7.3以下=OK、PHP 7.4以上=Warning、PHP 8.0以上=Fatal error
stringはキーではなくて、数値アクセスなら許容される。PHP 7.1から負のインデックスも可能。
[]でアクセスしたい場合、事前にarrayの保証が必要。以下のコードなど。
if (!is_array($ar) || !isset($ar['key']))
末尾要素
【PHP】配列の最後(末尾)の要素を取得まとめ array_key_last, count, end関数 | ヒシキリュウ.com
- array_key_last: PHP v7.3.0+ ($arr[array_key_last($arr)];)。
- count: 昔ながら ($arr[count($arr) - 1];)。
- end: 非推奨。
指定要素の取得
- []:
- array_slice: 範囲取得。
- array_intersect/array_intersect_key: 指定したキーの配列だけ取得。
- array_diff/array_diff_key: 指定したキー以外の配列を取得。
- array_filter: 複雑な場合。
連想配列で指定キー/指定キー以外の一括取得でよく使う。
$needles = ['t1', 't2']; $haystack = ['t1' => 1, 't2' => 2]; array_intersect_key($haystack, array_flip($needles)); array_diff_key($haystack, array_flip($needles));
array_sliceは添え字がなかったら空配列を返してくれるので、添え字アクセスより安全。
連想配列の先頭・末尾
- php - Get first key in a (possibly) associative array? - Stack Overflow
- PHP: array_key_first - Manual
- PHP – 配列の先頭・末尾・部分配列の取出し(非破壊的) – TauStation
- PHP で配列の先頭要素の値を取得するきれいな方法を考える | バシャログ。
PHP 7.3からarray_key_firstがある。これを使う。7.3以前はreset。
他に、元配列を破壊していいなら、array_shift/array_popもある。
array_sliceで部分配列を取得して変数に格納して、array_shiftもある。
$t = ['a' => 0, 'b' => 1]; $t2 = array_slice($t, 0, 1); var_export(array_shift($t2));
他にきれいなのはarray_keys/array_values[0]。これがいい。
抽出
- PHP: array_slice - Manual: 範囲取得。
- PHP: array_splice - Manual: 範囲削除また置換後の配列を取得。
- PHP: array_pop - Manual
- PHP: array_shift - Manual
配列の分割などで、重複をなくすために、取得後削除したいことがある。
先頭と末尾ならarray_shift/array_pop。それ以外はarray_spliceを使う (ChatGPT)。
$array = [1, 2, 3, 4, 5]; $index = 2; // 3番目の要素を取得したい (0から始まるインデックス) // 取得と削除を同時に行う $removedElement = array_splice($array, $index, 1); echo $removedElement[0]; // 3 print_r($array); // [1, 2, 4, 5]
ただ、array_spliceは連想配列に使うと、キーが番号になる。
取得後unsetするのが無難。
配列が大きいので、上限を設けたい場合、array_sliceがよさそう。
array_column
テーブルの取得結果の整形に非常に便利。
array_column(array $array, int|string|null $column_key, int|string|null $index_key = null): array
- column_key: 抽出したいカラム。nullにすると全部の列。index_keyを指定しなかったら元の配列と同じ。
- index_key: 取得後の配列のキーにしたいカラム。
index_keyを指定しなければ、column_keyの単純配列。
<?php
// データベースから返ってきたレコードセットの例
$records = array(
array(
'id' => 2135,
'first_name' => 'John',
'last_name' => 'Doe',
),
array(
'id' => 3245,
'first_name' => 'Sally',
'last_name' => 'Smith',
),
array(
'id' => 5342,
'first_name' => 'Jane',
'last_name' => 'Jones',
),
array(
'id' => 5623,
'first_name' => 'Peter',
'last_name' => 'Doe',
)
);
$first_names = array_column($records, 'first_name');
print_r($first_names);
?>
Array
(
[0] => John
[1] => Sally
[2] => Jane
[3] => Peter
)
Remove
配列要素の削除方法がいくつかある。
- unset($arr[$key]);
- array_shift($arr): 先頭要素を削除。削除済み要素を返す。破壊的な処理。
- array_pop($arr): 末尾要素を削除。削除済み要素を返す。破壊的な処理。
- array_slice (PHP: array_slice - Manual): 先頭・末尾の要素を除去した要素を返す。
$ar = [0, 1, 2];
foreach($ar as $e) {
echo $e;
if ($e === 1) {
array_shift($ar);
}
}
print_r($ar);
012Array
(
[0] => 1
[1] => 2
)
途中で削除しても、foreachは詰めたりしない。
Rename
- 3 Ways to Change Array Key without Changing the Order in PHP
- How to rename sub-array keys in PHP? - Stack Overflow
連想配列のキーの置換、キーの更新、キー名の置換、キー名の更新をしたいことがある。
いくつか方法がある。
サブ配列の場合はarray_mapでやればいい。
$tags = array_map(function($tag) {
return array(
'name' => $tag['name'],
'value' => $tag['url']
);
}, $tags);
シンプルな方法は配列で設定してunset
foreach($tags as &$val){
$val['value'] = $val['url'];
unset($val['url']);
}
他にはjsonを経由したり。array_keys/array_combineを使ったり。
Copy
How to clone an array of objects in PHP? - Stack Overflow
配列変数を代入すると通常はそれでコピーになる。ただし、配列にオブジェクトがあると、そのオブジェクトはシャローコピーになる。
$new = array();
foreach ($old as $k => $v) {
$new[$k] = clone $v;
}
上記のように配列要素をcloneでコピーして作る必要がある模様 (PHP: オブジェクトのクローン作成 - Manual)。
Comma
PHPでは配列の終端カンマは許容される。
他にも、名前空間のグループ指定はPHP7.2以上、関数の引数はPHP7.3以上で可能になった。
連想配列判定
<?php
if (array_values($arr) === $arr) {
echo '$arrは配列';
} else {
echo '$arrは連想配列';
}
これで添え字が、数字かどうかをみるのがいい模様。
Convert
2次元配列→1次元配列
$array = [
[1, 2, 3],
[4, 5, 6],
[7, 8]
];
array_reduce($array, 'array_merge', []);
// Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 4 [4] => 5 [5] => 6 [6] => 7 [7] => 8 )
$array = [
[
'staff' => [
'name1',
'name2',
'name3',
],
],
[
'staff' => [
'name4',
'name5',
'name6',
],
],
[
'staff' => [
'name7',
'name8',
'name9',
],
],
[
'staff' => [
'name10',
'name11',
'name12',
],
],
];
array_reduce(array_column($array, 'staff'), 'array_merge', []);
// Array ( [0] => name1 [1] => name2 [2] => name3 [3] => name4 [4] => name5 [5] => name6 [6] => name7 [7] => name8 [8] => name9 [9] => name10 [10] => name11 [11] => name12 )
array_columnが非常に便利。
$rows = [
0 => [ 'id' => 40, 'title' => 'dave', 'comment' => 'Hello, world!'],
1 => [ 'id' => 10, 'title' => 'alice', 'comment' => '你好,世界!'],
];
var_export(array_column($rows, 'title', 'id'));
// =>
// array (
// 40 => 'dave',
// 10 => 'alice',
// )
$rows = [
0 => [ 'id' => 40, 'title' => 'dave', 'comment' => 'Hello, world!'],
1 => [ 'id' => 10, 'title' => 'alice', 'comment' => '你好,世界!'],
];
var_export(array_column($rows, null, 'id'));
// =>
// array (
// 40 =>
// array (
// 'id' => 40,
// 'title' => 'dave',
// 'comment' => 'Hello, world!',
// ),
// 10 =>
// array (
// 'id' => 10,
// 'title' => 'alice',
// 'comment' => '你好,世界!',
// ),
// )
$map = [];
foreach ($table as $row) {
$map[$row['括りオーナーコード']] = $row['オーナーコード'];
}
DBテーブルからの取得結果が2次元の連想配列になっている。ここから、IDをキーにして、特定の値を取得するmapを作ったり、レコード行を取得できる。
自前でfor文で数行のコードでできるが、関数だと楽。
多次元連想配列→一次元連想配列
https://chatgpt.com/c/673fd301-45c4-800b-bec8-02302ad01383
再帰関数で処理する。
function flattenArray(array $array, string $prefix = ''): array {
$result = [];
foreach ($array as $key => $value) {
$newKey = $prefix === '' ? $key : $prefix . '.' . $key;
if (is_array($value)) {
// 再帰的に呼び出して配列をフラットにする
$result += flattenArray($value, $newKey);
} else {
// フラット化した結果にキーと値を追加
$result[$newKey] = $value;
}
}
return $result;
}
// 使用例
$nestedArray = [
'user' => [
'name' => 'Alice',
'details' => [
'age' => 25,
'address' => [
'city' => 'New York',
'zip' => '10001'
]
]
],
'status' => 'active'
];
$flattenedArray = flattenArray($nestedArray);
print_r($flattenedArray);
Array
(
[user.name] => Alice
[user.details.age] => 25
[user.details.address.city] => New York
[user.details.address.zip] => 10001
[status] => active
)
連想配列なのでarray_mergeではなく+=でOK。
連想配列→単純配列
associative arrayをsimple arrayに変換する。
Convert an associative array to a simple array of its values in php - Stack Overflow
連想配列をキーと値のペアの配列にするちょっと気のきいた方法(かも) #PHP - Qiita
- $array = array_values($array);: 値だけを1次元にしたい場合。
- array_map(null, array_keys($a1), array_values($a1));: 連想配列の[[key,value], [key2, valu2]] 形式。
後者のパターンはそれなりに使う気がする。
DBテーブルから結果を取得後、必要なカラムの単純配列が欲しい場合もarray_mapを使う。
$ar = [
['k1' => 'v11', 'k2' => 'v12'],
['k1' => 'v21', 'k2' => 'v22'],
];
var_export(array_map(function($e){return $e['k1'];}, $ar));
/*
array (
0 => 'v11',
1 => 'v21',
)
*/
// mapが欲しければarray_combineを併用する。
var_export(array_combine(array_map(function($e){return $e['k2'];}, $ar), array_map(function($e){return $e['k1'];}, $ar)));
/*
array (
'v12' => 'v11',
'v22' => 'v21',
)
*/
単純配列→連想配列
いくつか方法がある。
- array_combine
- array_fill_keys
- foreach
- array_flip
$ar = ['a', 'b']; $ar2 = array_combine($ar, $ar); var_dump($ar2); /* array ( 'a' => 'a', 'b' => 'b', ) */
array_combineがシンプル。array_fill_keysは0初期化などしたい場合。
CSV→連想配列
- PHP convert CSV to associative arrays | by Catalin ZMOLE 👨💻 | Medium
- Shortest PHP code to convert CSV to associative array | Steindom
- PHP: str_getcsv - Manual
CSVを、よくDBの取得結果の形式の、行単位連想配列に変換する。方法がいくつかある。
<?php
$csv = array_map('str_getcsv', file($file));
if (count($csv) && !count($csv[count($csv)-1])) unset($csv[count($csv)-1]);
array_walk($csv, function(&$a) use ($csv) {$a = array_combine($csv[0], $a);});
array_shift($csv); # remove column header
?>
$rows = array_map('str_getcsv', file('myfile.csv'));
$header = array_shift($rows);
$csv = array();
foreach ($rows as $row) {
$csv[] = array_combine($header, $row);
}
1番目の方法がシンプル。これよりSplFileObjectのほうがいい。
【PHP】その CSV 変換、本当に「fgetcsv」でいいの? (フェンリル | デベロッパーズブログ)
反転|array_flip
配列のキーと値を反転した配列を返す。元のarrayの値は有効なキーを必要とする。つまり、intかstring。型が違う場合、警告が出て無視される。
また、同じ値が複数ある場合、最後のみが有効になる。
分割
1個の大きな配列をそのまま反復させると大きいので、指定要素数ずつに分割して、処理したいことがある。
一括INSERTを分割する場合など。array_chunkで配列を分割できるのでこれを使う。
implodeで文字列にマージして、explodeで分割というのもある。
String
implode(array|string $separator = "", ?array $array): string
配列だけ指定した場合、空文字で結合する。
var_dump(implode(['a', 'b', 'c'])); // string(3) "abc"
Search
【PHP入門】配列の値を検索するarray_searchと他4つの関数 | 侍エンジニアブログ
いくつか方法がある。
基本はin_array。複雑な検索はarray_filter/array_intersect。
array_key_exists/キー確認
PHP: array_key_exists - Manual
配列のキーの存在確認のほうほうがいくつかある。
- array_key_exits: array_key_exists('first', $search_array);
- isset: nullだとfalseになる (isset($search_array['first']))。
- empty: nullだとfalseになる。
- ??: キー不在だとnullになるのでこれでない場合に対応できる。
基本はarray_key_exitsか??。$ar ?? nullでWARNINGを回避しながら手短にかける。
変数が配列か保証できていない場合、キーの確認と同時に行う場合、以下のいずれかになる。
if (!is_array($ar) || $ar['key'] ?? null)
if (!is_array($ar) || !isset($ar['key'])) // 要素がnullの場合もなしあつかい。
if (!is_array($ar) || array_key_exists('key', $ar)) // 要素がnullの場合はありとみなす。
2番目のissetを使うのがいい。
$arがobjectの場合、isset/emptyだと$ar['key']はarray_key_exists含む配列要素アクセス関係でエラーになるので、is_arrayは必須。
empty
emptyで確認できる。が、単に配列変数がnullなどの場合も判定してしまう。null or emptyという意味ならemptyでもOK。
配列変数があって、空かどうかを見たければis_array && empty
逆に、issetであることと、nullではないことを確認できる。
countで配列要素数をカウントできるのでこれでも確認できるが、配列変数自体がnullの場合エラーになるのでis_arrayのチェックが必要。面倒だからemptyでいいだろう。
ただ、配列の要素の値が全部nullで実質空というような場合は工夫が必要。array_filterを使う。コールバックを指定しなかったら、emptyの判定をする。これがスマート。
$a = ['a' => null, 'b' => null]; var_export(array_filter($a)); var_export(empty(array_filter($a)));
空の要素を削除する場合もarray_filterを使う。不要データの削除などでよく使いそう。
array_search
array_search() - 指定した値を配列で検索し、見つかった場合に対応する最初のキーを返す
全てのキーが必要なら、array_keysにfilter_valueを指定する。
in_array
in_array — 配列に値があるかチェックする
in_array(mixed$needle, array$haystack, bool$strict=false): bool
haystack 内の needle を検索します。 strict が設定されていない限りは型の比較は行いません。
基本は$strict=trueで指定したほうがいい。完全一致検索。
in_arrayの他に、配列のキーに検索したい値を用意しておいて、issetで有無判定する方法がある。こちらのほうが早い。が、注意が必要、通常のリストの場合、array_flipで作れる。が、このためにarray_flipするとarray_flipで時間かかる。
in_array/array_search/isset
PHP 高速化に関するメモ書き | Thought is free
in_array/array_searchは遅いらしい。書き方を変えたほうがいい。
PHPの言語構造の中で、isset/emptyが非常に重要。これらで値の有無判定ができる。
検索回数が重要。検索を2回以上するならば、issetでやったほうがいい。
ただし、もともと単純配列になっているのを、連想配列に変換するくらいならば、forなどを使ってもその変換に時間がかかる。
そして、in_arrayじゃなくて、for/ifで比較するくらいなら、in_arrayのほうが速い。実装の段階で工夫してキーに値を入れられるならそちら。そうでなければ、そのままin_arrayでよさそう。特に要素数が多い場合。
$a = range(0, 1000);
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function()use($a){isset(array_flip($a)[500]);}), " ns\n";
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function()use($a){in_array(500, $a);}), " ns\n";
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function()use($a){isset(array_flip($a)[500]);}), " ns\n";
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function()use($a){isset(array_combine($a, range(1,count($a)))[500]);}), " ns\n";
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function()use($a){
foreach ($a as $k => $v){
$a[$v]=$k;
unset($a[$k]);
}
isset($a[500]);
}), " ns\n";
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function()use($a){
foreach ($a as $v){
if ($v === 500) return true;
}
return false;
}), " ns\n";
in_arrayのほうが速い。
array_columnで検索元をキーにする場合。
$records = [];
for ($i = 0; $i < 1000; ++$i) {
$records[] = array(
'id' => $i,
'first_name' => 'John',
'last_name' => 'Doe',
);
}
$a = $records;
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function()use($a){
$a = array_column($a, 'id');
in_array(500, $a);
}), " ns\n";
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function()use($a){
$a = array_column($a, 'id', 'id');
isset($a[500]);
}), " ns\n";
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function()use($a){
$a = array_column($a, 'id');
for ($i = 500; $i < 600; ++$i) {isset($a[$i]);}
}), " ns\n";
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function()use($a){
$a = array_column($a, 'id', 'id');
for ($i = 500; $i < 600; ++$i) {in_array($i, $a);}
}), " ns\n";
27041 ns 33280 ns 27491 ns 118082 ns
k
これもin_arrayのほうが速い。array_columnで列キーで抽出する場合、データ量が2倍になる。そこの生成コストがかかっており、それが支配的。
配列生成が1回だけで、検索をたくさんする場合はどうだろう。
検索をたくさんする場合、1回あたりの検索に時間がかかるからissetのほうがやっぱり速いみたい。配列生成のコストよりも。
any/all/some/every
Is there a PHP equivalent of JavaScript's Array.prototype.some() function - Stack Overflow
配列に対する、1個または全部の評価。
JavaScriptのsome/every相当。
PHP 8.4ならarray_any/array_allが存在する。
PHP 8.4未満なら、いくつか方法がある。
function array_any(array $array, callable $fn) {
foreach ($array as $value) {
if($fn($value)) {
return true;
}
}
return false;
}
function array_every(array $array, callable $fn) {
foreach ($array as $value) {
if(!$fn($value)) {
return false;
}
}
return true;
}
function array_some(array $data, callable $callback) {
$result = array_filter($data, $callback);
return count($result) > 0;
}
$myarray = [2, 5, 8, 12, 4];
array_some($myarray, function($value) {
return $value > 10;
}); // true
foreachで途中で終わるほうが速い模様。
配列同士の包含・交差判定
- php - Check if an array contains all array values from another array - Stack Overflow
- php - Checking if ANY of an array's elements are in another array - Stack Overflow
1個でも入っているかを見たければ、array_intersect (PHP: array_intersect - Manual) がこの目的に合致する。
$peopleContainsCriminal = !empty(array_intersect($people, $criminals)); $peopleContainsCriminal = array_intersect($people, $criminals);
$criminalsの配列に、$peopleの要素のいずれかが入っているかを上記で判断できる。
array_intersectは1個目の配列要素の内、2個目の存在要素を返す (交差)。交差があれば、1個はあるという意味で、any/someになる。
全部の包含判定したい場合、array_diff (PHP: array_diff - Manual) でできる。
$containsAllValues = !array_diff($search_this, $all);
array_diffはarray_intersectと異なり、1個目の配列要素の内、2個目の不在要素を返す (差分)。なので、空なら全包含となる。非空なら非全包含=some。
完全一致なら、===でOK。
ポイントとしては、1個目の要素は要素数が少ない配列を指定したほうが速くなる。判定だけで、速度が重要なら、foreachで見つかったらすぐreturnしたほうが速い。
array_intersectが実行結果とboolが同じ向きなので、これを使うとわかりやすいだろう。
重複削除
いくつか方法がある。
- 連想配列
- array_diff
- array_unique
- array_keys(array_flip()): array_uniqueより少し早い (PHPの配列から重複を削除するにはarray_unique()よりarray_keys(array_flip())が速いのか)。
array_uniqueはデフォルトではvalueだけで判断する。
array_uniqueはデフォルトで文字列として比較する。配列などの場合はSORT_REGULARのフラグを指定する (Remove duplicated elements of associative array in PHP - Stack Overflow)。
ただし、型混在など複雑な場合は比較が失敗することがあるので、自前で行ったほうがいいらしい (PHP: array_uniqueについて #PHP - Qiita)。
array_unique重複は最初の要素を残す。最後の要素を残したければ、array_reverseを2併用する (php - Keep unique values of array, preserving order, retaining last occurrence of each - Stack Overflow)。
array_reverse(array_unique(array_reverse($array)));
但し、配列が大きいとarray_reverseの2回は遅い。
array_unique for multidimensional array – James' Desk
array_uniqueとarray_intersect_keyをうまく使う方法がある。
連想配列であるプロパティー (例: value) だけに固有条件を入れたい場合、
$tempArr = array_unique(array_column($array, 'value')); print_r(array_intersect_key($array, $tempArr));
一度valueだけarray_uniqueで取得して、その後array_intersect_keyで交差を取得。
同値チェック
「PHP」配列の全要素が同じ値かどうかを簡単に調べる方法 - $a=array... - Yahoo!知恵袋
重複削除の応用で、逆に配列要素が全部同じ値かをチェックできる。
count(array_unique($a)) === 1
列の一致判定
重複削除判定時などで、複数配列の同じ列・キーで処理したいことがある。foreach文と判定用変数を使わずに行うには、array_filterを使う。これくらいしか逆に方法がない。
$needle = ['a', 'b'];
$h1 = ['a' => '1', 'b' => 2];
$h2 = ['a' => '1', 'b' => 2, 'c' => 3];
$h3 = ['a' => '0', 'b' => 2, 'c' => 3];
var_dump($same_all = !array_filter($needle, function($n)use($h1, $h2){return $h1[$n] !== $h2[$n];})); // bool(true)
var_dump($same_all = !array_filter($needle, function($n)use($h1, $h3){return $h1[$n] !== $h3[$n];})); // bool(false)
var_dump($same_all = array_filter($needle, function($n)use($h1, $h2){return $h1[$n] !== $h2[$n];})); // []
var_dump($same_all = array_filter($needle, function($n)use($h1, $h3){return $h1[$n] !== $h3[$n];})); // ['a']
var_dump($same_all = array_filter($needle, function($n)use($h1, $h2){return $h1[$n] === $h2[$n];})); // ['a', 'b']
var_dump($same_all = array_filter($needle, function($n)use($h1, $h3){return $h1[$n] === $h3[$n];})); // ['b']
コールバック内の判定を===にすると、1個でもマッチしたらarray_filterの結果型trueになる。 ややこしいが、コールバック内を!==にして、結果が空になったら完全一致とみなす。そうしないと、元の要素数の余計な判定が必要になる。
これを応用して行列の一致判定をする場合。
$records_unique = []; // [0 => ['a' => 0, 'b' => 1], 1 => ['a' => 1, 'b' => 2]]
$unique = ['a', 'b'];
$is_unique = !array_filter($records_unique, function($record_unique)use($needle, $record){
/** @return bool unique対象列の全一致判定 */
return !array_filter($unique, function($v)use($record_unique, $record){return $record_unique[$v] !== $record2[$v];});
});
外側に行ループ用の配列をわつぃて、その要素を内側で使うだけ。
多次元配列の検索
https://chatgpt.com/c/67f61b7f-a670-800b-b9ba-67701272bd4c
- array_filter
- array_search+array_column
foreach+breakがシンプルで早い。
$found = null;
foreach ($users as $user) {
if ($user['name'] === 'Bob') {
$found = $user;
break;
}
}
他に、array_filter
Array Functions
compact
Ref: PHP: compact - Manual.
変数名とその値から、配列を作る。extractの逆。
MVCのViewに複数の値を渡す場合などによく使う。
extract
Ref: PHP: extract - Manual.
配列のキー・バリューを変数として取り込む。
一括操作
配列要素全体に一括処理を行える関数がいくつかある。for/foreach文が不要なのでコンパクト。
- array_map: 適用結果の配列を取得。
- array_filter: 適用して絞り込んだ配列を取得。
- array_reduce: 繰り返し適用して1個にまとめる。
- array_walk: 要素に適用するだけ。
forとの速度比較
単に反復させるだけなら、基本的にはfor/foreachを使ったほうが速い模様。
$a = range(0, 10000);
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function()use($a){foreach($a as $v){$v;}}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function()use($a){array_map(function($v){$a;}, $a);}), " ns\n";
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function()use($a){array_walk($a, function($v){$a;});}), " ns\n";
116170 ns 372829 ns 713770 ns
可読性やコード量、反復しないところでarray_関数は使うのがよさそう。
array_map
array_map(?callable $callback, array $array, array ...$arrays): array
JavaScriptのmap相当。非常に重要でよく使う。
callbackにnullを指定すると、複数の配列のzip (unpack) を行う。
ただ、array_mapのコールバックの引数は通常配列の要素が想定されていて、連想配列のキーにはアクセスできない。
それをしたかったら、array_reduceを使う。らしい。
Howto use array_map on associative arrays to change values and keys - Daniel Auener
いや、そういうことをしなくても、array_keysを使えばOK。
$result = array_map(function($k, $v){return ;}, array_keys($arr), $arr);
array_mapは単純配列を返す。元々が連想配列の場合、キーが数値に置換される。元のキーを維持したければ、array_combineを併用する。
$arr =
[
"id" => 1,
"name" => "Fred",
];
$result = array_combine(
array_keys($arr),
array_map(function($v){ return $v; }, $arr)
);
array_filter
名前通り配列要素をフィルターリングする。
array_filter(array $array, ?callable $callback = null, int $mode = 0): array
$modeを指定しなければcallbackにはvalueのみ渡される。
callbackがtrueを返したら、その要素を残す。callbackを指定しなかったら、!empty($v)相当。なので、array_filter($array) で、キーがある場合の配列要素の空判定にもなる。
元配列のキーは維持されるので、リスト系データのフィルター時には注意する。キーを振り直したければ、array_valuesを使う。
他には応用として、操作対象のキーの配列を渡して、そのキーを使って複数の配列の同じキーの一致・重複判定などできる。
/** UPSERTのAI増分対策用に重複削除。 */
if (!empty($unique)) {
$old_row = '';
foreach ($records as $row => $line) {
// unique対象列が全部一致の場合削除。
if (array_filter($unique, function($v) use ($line, $records, $old_row) {return $line[$v] !== $records[$old_row][$v];})) {
unset($records[$old_row]);
}
$old_row = $row;
}
}
array_reduce
array_reduce(array $array, callable $callback, mixed $initial = null): mixed
callback(mixed $carry, mixed $item): mixed
$carryに前回処理結果。$itemに現在要素。
配列要素を集計して1要素にまとめる。
統計処理したり、結合したりできる。
var_export(array_reduce([0, 1, 2], function($c, $v){return $c.$v;})); // '012'
配列要素の列を結合したいことがある。そういうときにこれを使う。
ソート
ソート関係の関数がいくつかある。
| 関数名 | ソートの基準 | キーと値の相関関係 | ソート順 | 関連する関数 |
|---|---|---|---|---|
| array_multisort() | 値 | 文字列がキーの場合は維持し、数値添字配列の場合は維持しない | 最初の配列、あるいはソートオプション | array_walk() |
| asort() | 値 | 維持する | 昇順 | arsort() |
| arsort() | 値 | 維持する | 降順 | asort() |
| krsort() | キー | 維持する | 降順 | ksort() |
| ksort() | キー | 維持する | 昇順 | krsort() |
| natcasesort() | 値 | 維持する | 大文字小文字を区別しない自然順 | natsort() |
| natsort() | 値 | 維持する | 自然順 | natcasesort() |
| rsort() | 値 | 維持しない | 降順 | sort() |
| shuffle() | 値 | 維持しない | ランダム | array_rand() |
| sort() | 値 | 維持しない | 昇順 | rsort() |
| uasort() | 値 | 維持する | ユーザー定義 | uksort() |
| uksort() | キー | 維持する | ユーザー定義 | uasort() |
| usort() | 値 | 維持しない | ユーザー定義 | uasort() |
重要なの。
- sort/rsort: 値のソート
- ksort/krsort: キーのソート。
- usort/uksort/uasort: ユーザー定義関数でのソート。値が配列で、配列の特定のキーでソートしたい場合、これを使うしかない。
usortでいくつかのキーを優先順位をつけてソートする場合、以下のように宇宙船演算子を使うといい模様。
usort($incentive_list, function ($a, $b) {
return $a['monster_attribute'] <=> $b['monster_attribute'] ?: $a['rank'] <=> $b['rank'];
});
宇宙船演算子はPHP 7.0から登場。ほぼusortのための演算子。$a - $bとほぼ同じだが、オーバーフロー時の動作、オブジェクト同士の場合compareToメソッドを使ってくれるなど、$a-$bよりいいらしい。
Enum
PHP 8.1.0から導入。長らくなかった。
複数の異なる値を1個の集合として取り扱うデータ型。
終了コードなど、意味がある数字を扱う。
enumがないと、値の下限、上限など、ただの数字だから保証できない。
Implementation
- 【PHP 8.1】とうとうPHPにもEnumがやってきた - デザインワン・ジャパン Tech Blog
- php-enumのメモ
- PHP8.1のEnumと独自実装のEnumを比較して移行できるか検討しました - WHITEPLUS TechBlog
- How to implement Enum like functionality in PHP? - Stack Overflow
長らく言語機能になかったのでクラスやトレイトを使った独自実装が試されている。
- GitHub - BenSampo/laravel-enum: Simple, extensible and powerful enumeration implementation for Laravel.
- GitHub - myclabs/php-enum: The enum PHP is missing, inspired from SplEnum
昔はSplEnumという実験モジュールがあったが、Enumの登場でなくなった。
PHP 7.4以前との互換性のために、独自のクラスで実装して、その内部実装で上記ライブラリー類を使う感じだろう。
範囲チェック
https://grok.com/share/c2hhcmQtMw%3D%3D_78f28c02-682d-4e1a-90e8-6e00f291f8ff
数値範囲の場合、普通にif
if (!is_int($value) || $value < 1 || $value > 5) {
throw new InvalidArgumentException("区分値は1〜5の整数でなければなりません。");
}
// 許可された区分値のリスト
$allowedValues = [1, 3, 5]; // または ['active', 'inactive']
// 範囲チェック
if (!in_array($value, $allowedValues, true)) {
throw new InvalidArgumentException("無効な区分値です。許可された値: " . implode(", ", $allowedValues));
}
class Category {
const ACTIVE = 'active';
const INACTIVE = 'inactive';
const DELETED = 'deleted';
public static function getAllowedValues() {
return [
self::ACTIVE,
self::INACTIVE,
self::DELETED,
];
}
}
function insertData($pdo, $value) {
// 範囲チェック
if (!in_array($value, Category::getAllowedValues(), true)) {
throw new InvalidArgumentException("無効な区分値です。許可された値: " . implode(", ", Category::getAllowedValues()));
}
// DBにINSERT
$stmt = $pdo->prepare("INSERT INTO table_name (category) VALUES (?)");
$stmt->execute([$value]);
}
複雑な場合、in_arrayでやるのがたぶんいい。
Value型
型だけでなく、値自体もチェックする型。
- false (PHP 8.0以上)
- true (PHP 8.2以上)
カスタムのValue型の定義はできない。そういうことをしたければ、Enum型を使う。
Iterable
array|Traversable型のエイリアス。PHP 7.1.0で導入。foreachで使用可能で、ジェネレーター内のyield fromでも使える。
Traversableインターフェイス、Iteratorクラスが特に重要。このメソッドはいろんなところで登場するから。
- current: 現在の要素を返す。
- key:
- next
- rewind
- valid
特にcurrentが重要。例えば、ヘッダーをこれで取得などできる。
Type declarations/型宣言
About
関数の引数、戻り値、クラスのプロパティー (PHP 7.4.0以上) に型を宣言できる。これにより、型を保証でき、その型でなければ、TypeErrorをスローする。
関数の戻り値だけ、型の指定箇所がやや特殊で、それ以外は原則変数の直前。関数の戻り値の場合、(): の後に指定する。
function name(): type {}
<?php
function sum($a, $b): float {
return $a + $b;
}
// float が返される点に注意
var_dump(sum(1, 2));
?>
なお、(ローカル) 変数への型宣言はない。代わりに、値を代入した時点で、型が自動で決まる模様。変数に型宣言したければ、phpdocしかない。
PHP 7.3未満など、型宣言に対応していない場合は、PHP 7.4未満などの場合は、しかたないのでPHPDocで対応する。
nullable な型とシンタックスシュガー
nullableの場合、型名の前に?を指定する (PHP 7.1.0以上)。?TとT|nullは同じ意味。
この文法は、PHP 7.1.0 以降でサポートされており、 PHP 8.0で一般化された union 型がサポートされる前から存在します。
?を使えないPHP 7.1未満の場合、デフォルト値にnullを代入することで似たことを実現できていた。
<?php
class C {}
function f(C $c = null) {
var_dump($c);
}
f(new C);
f(null);
?>
ただし、この記法は子クラスでデフォルト値をnull以外にした場合、nullを受け付けられなくなって、型の互換性がなくなり、エラーになる。子クラス側で?intが必要になる。
Fatal error: Declaration of B::find(int $offset = 1): void must be compatible with A::find(?int $offset = null): void in /home/user/scripts/code.php on line 13
m
また、PHP 8.4からint $var = nullはDeprecatedエラーになった (PHP: PHP 8.4.x で推奨されなくなる機能 - Manual、【PHP】引数のデフォルト値がnullかつ型にnullが含まれていない関数やメソッドを nikic/php-parser で見つけて修正する│株式会社シーポイントラボ)。
Deprecated: find(): Implicitly marking parameter $offset as nullable is deprecated, the explicit nullable type must be used instead in /in/bBh0S on line 3 NULL
基本はnullを許容するならば、?は必要。
m
m
<?php
class A
{
function find(int $offset = null): void
{
echo "A " . $offset . "\n";
}
}
class B extends A
{
function find(int $offset = 1): void
{
parent::find($offset);
var_dump($offset);
}
}
function find(int $offset =null)
{
var_dump($offset);
}
function main(): void
{
find();
// $a = new A();
// $a->find();
$b = new B();
$b->find();
}
main();
Type juggling
型の相互変換。非常に重要。いろいろ方法がある。
共通なのはキャスト (cast)。
<?php
$foo = 10; // $foo は整数です
$bar = (bool) $foo; // $bar は boolean です
$fst = "$foo"; // to string.
+"+40"; // to int
?>
C言語と同じで (型) を前置する。ただし、少々長い。
文字列への変換は二重引用符囲、数値への変換は算術演算子 (+)。まあ、キャストだけ覚えておくのがシンプル。
型キャスト
変換先の型を波括弧で囲んで、変換対象の変数に前置することで変換する。
使用可能なキャストは以下。
(int)- 整数(int) へのキャスト(bool)- 論理値(bool) へのキャスト(float)- float へのキャスト(string)- 文字列(string) へのキャスト(array)- 配列(array) へのキャスト(object)- オブジェクト(object) へのキャスト(unset)- NULL へのキャスト PHP 8.0.0で削除。単にNULLを代入する。
言語構造なので、関数よりも高速。丸括弧内のスペースは無視される。
https://chatgpt.com/c/67d90154-5ca8-800b-9e1e-1a5f7a895fd6
配列のbool判定は!emptyでもできるが、emptyは言語構造だが、nullや0などの値の判定も行う都合、(bool) よりわずかに遅い。
Other
型判定
PHPでの型確認・判定方法がいくつかある。
- gettype: 変数の型を文字列で返す。boolean/integer/double/string/array/object/resouce/resource (closed) (PHP v7.2.0以上)/NULL/ unknown type
- get_class: オブジェクトのクラス名
- get_debug_type: 変数の型名をデバッグしやす形で取得。
- is_型名: is_array/is_bool/is_callable/is_float/is_int/is_null/is_numeric/is_object/is_resoure/is_scalar/is_string/function_exists/method_exists
基本はis_型名だろう。
associative array vs stdClass
連想配列とオブジェクトのどちらを使うべきか?
- 総合: 配列のほうが専用関数が多く扱いやすいことが多く無難。
- 型: 意識したい場合、stdClass。IDEの補完もしやすい。
- 性能: 配列のほうが速い。オブジェクトはプロパティーとメソッドの管理が必要でやや重い。
- 再利用を意識するならstdClass
まとめ。
- その場しのぎ、一時的な利用など、基本は連想配列。
- いろんな場所で使う構造データはオブジェクト。
json_decodeもいろんな場所で使わないなら連想配列でよいと思う。
Variables
Basics
Ref: PHP: Basics - Manual
使用可能な文字
変数名は、PHPの他のラベルと同じルールに従います。 有効な変数名は文字またはアンダースコアから始まり、任意の数の文字、 数字、アンダースコアが続きます。正規表現によれば、これは次の ように表現することができます。
^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$
ASCIIテキストの範囲だと記号類は_以外変数名に使用不能。
Undefined variable
未定義変数 (undefined variable) の値はNULL。
未定義変数 (配列の不在キー) にアクセスすると、E_WARNING (PHP 8未満はE_NOTICE) レベルのエラーが生じて、nullを返す。回避したければ、isset()で検知する。要素の追加時のアクセスは問題ない。
未定義変数の検知・制御方法がいくつかある。
- isset (PHP: isset - Manual)
- empty (PHP: empty - Manual)
- ??: Null 合体演算子/Null collapsing operator
- ??=: NULL合体代入演算子 PHP v7.4以上。
- @: エラー制御演算子
- array_key_exists
issetとempty、Null合体演算子あたりをメインで使う。特にempty。
emptyは以下相当を実施してくれる。値そのものの評価もするので、値が0で正常なときなど場合によっては困る場合もある。
!(isset($var) && $var) !isset($var) || $var == false
isset($var) && $varは頻繁に使うことになるだろうから、emptyで短縮できる。
empty($var) ? false : true; $var ?? false;
emptyとissetは関係が逆に似ているがissetは挙動が違う。
「Returns true if var exists and has any value other than null. false otherwise.」なので、変数の値を評価はしない。nullかどうかだけしかみない。emptyとは扱いが違うので注意する。
だから、頻繁に使うだろう。emptyとNull合体演算子の上記の記法はいろんなところで頻繁に使うと思われる。基本重要構文。
ただし、emptyは配列が空の場合もtrueになるので、そこは注意する。配列変数の有無を見たければ、issetを使うしかない。
Null合体演算子はNULLしかカバーしないから、emptyが必要な場面がけっこうある。
emptyの反対は、strlen/countあたり。ただし、未定義変数のチェックをしてくれないので、!emptyしたほうがいい。
Variable scope
About
出典: PHP: Variable scope - Manual。
関数の外で使用するとグローバルスコープになる。ただし、関数内では暗黙にはグローバル変数は使えない。未定義変数扱いになる。
関数内でグローバル変数を参照したければ、関数内でglobalで明示的に使用したいグローバル変数を宣言する必要がある。
<?php
$a = 1;
$b = 2;
function Sum()
{
global $a, $b;
$b = $a + $b;
}
あるいは、$GLOBALS配列にグローバル変数が入っているのでこれを使う。
なお、波括弧のブロックスコープは存在しない。C系言語の感覚だと、波括弧でスコープが作られそうなイメージがあるが、PHPの波括弧はスコープを作らない。あくまで、関数の内部かどうか。
逆にいうと、関数内に定義される関数・クラスも基本グローバル。
子関数に変数を渡したい場合、引数かグローバル変数しかない。他に隠蔽したり、親関数からスコープを引き継ぎたい場合、無名関数を使うしか無い。
Super global
全てのスコープで使用可能な組込変数。関数、メソッド内でもglobal $variable;とする必要がない。
- $GLOBALS: グローバル変数の連想配列。
- $_SERVER
- $_GET
- $_POST
- $_FILES
- $_COOKIE
- $_SESSION
- $_REQUEST
- $_ENV
Variable variables/可変変数
PHP 7.0から対応した機能とのこと。
クラスのプロパティーの可変プロパティーアクセスがる。
$ref->{'ref-type'} = 'Journal Article';
class foo {
var $bar = 'I am bar.';
var $arr = array('I am A.', 'I am B.', 'I am C.');
var $r = 'I am r.';
}
$foo = new foo();
$bar = 'bar';
$baz = array('foo', 'bar', 'baz', 'quux');
echo $foo->$bar . "\n";
echo $foo->{$baz[1]} . "\n";
$start = 'b';
$end = 'ar';
echo $foo->{$start . $end} . "\n";
$arr = 'arr';
echo $foo->{$arr[1]} . "\n";
プロパティー名として無効な文字 (-,.()など) を含む場合もアクセスでき便利。例えば、json_decodeの結果など。
Variables From External Sources+外部から来る変数
- PHP: 外部から来る変数 - Manual
- PHP: PHP and HTML - Manual
- PHP: Predefined Variables - Manual
- PHP: Filter - Manual
PHPとHTMLフォームの関係がある。重要。
配列渡しはPHP側の仕様。
HTML Forms (GET and POST)
<form action="foo.php" method="post">
Name: <input type="text" name="username" /><br />
Email: <input type="text" name="email" /><br />
<input type="submit" name="submit" value="Submit me!" />
</form>
フォームに入力されたパラメーターは、$_GET/$_POST/$_REQUESTでしかアクセス不能。
PHPではフォーム変数のドットとスペースはアンダーバーに変換される。
たとえば <input name="a.b" /> は $_REQUEST["a_b"] となります。
外部変数名のドット
PHPの変数名でドットやスペースは無効。その都合で、それらの文字は_に置換される。フォーム変数も似たような考え方。
HTMLフォームから渡される変数の基本的な扱い
- PHP: $_GET - Manual/PHP: $_POST - Manual
- PHP: 外部から来る変数 - Manual
- PHP: フィルタ - Manual
- https://grok.com/share/c2hhcmQtMw%3D%3D_60740a45-80ab-4a70-9ddb-fc22423520d6
いくつか考え方がある。
- パラメーターの存在チェック (isset/empty)
- 余分なスペースの除去 (trim)
- サニタイズ: filter_input/filter_input_array/filter_varでフィルター指定で危険な文字を除去。1-2も部分的に行ってくれる。
- 検証 (validation): filter_varで検証。
- 使用時エスケープ
- HTML表示時: htmlspecialcharsで特殊文字をエスケープ表示。出力直前に行う。
- DB挿入時: prepared statementでSQLインジェクション防止。
filter_inputを使うと、引数でグローバル変数を参照できて、issetは省略できる。
filter_関数が複雑。filter_*_arrayは第2引数で対象エントリーとフィルターの連想配列。filter_inputを何回も呼ばなくていい。複数のエントリーのチェックが必要ならこれがいい。
filter
About
- filter_input/filter_input_array
- filter_var/filter_var_array
_inputはスーパーグローバル変数を定数で指定、_varは一般変数も対象。
_arrayは第1引数に連想配列を受け取り、複数の対象をまとめてフィルターする。
filter_input(
int $type,
string $var_name,
int $filter = FILTER_DEFAULT,
array|int $options = 0
): mixed
戻り値: フィルター後の値。変数未設定時はfalse、フィルタリング失敗時もfalse。FILTER_NULL_ON_FAILURE指定時のみ失敗時にnull。
_inputを使うと、通常キーのissetのnullチェックが必要なのを一緒にできるので省略できる。
filter定数
いくつか重要なものがある。
- 第1引数INPUT_*系変数。グローバル変数 (POST/GET/COOKIE/ENV/SERVER/REQUESTを間接参照。
- 第3引数
- FILTER_VALIDATE_*:
- FILTER_VALIDATE_EMAIL: RFC822の正しいメールアドレスかを検証。
- FILTER_VALIDATE_INT: 値の範囲もチェック可能。検証成功時にintを返す。
- FILTER_VALIDATE_URL
- FILTER_VALIDATE_BOOLEAN
- FILTER_SANITIZE_*:
- FILTER_SANITIZE_STRING: 非推奨。htmlspecialchars()を代わりに使う。
- FILTER_SANITIZE_EMAIL
- FILTER_SANITIZE_SPECIAL_CHARS
- FILTER_SANITIZE_ENCODED
- FILTER_VALIDATE_*:
- 第4引数FILTER_FLAG
- FILTER_FLAG_STRIP_LOW: 制御文字除去
- FILTER_FLAG_STRIP_HIGH: マルチバイト文字除去
- FILTER_FLAG_ALLOW_FRACTION
- FILTER_FLAG_ALLOW_HEX
- FILTER_FLAG_NULL_ON_FAILURE: 検証失敗時にnull。
filter_var
echo filter_var('1234567a3/..', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^(?=.*[a-zA-Z])(?=.*\d).{9,12}$/']]);
var_dump(filter_var_array(
['id' => '1234567a3/..'],
['id' => [
'filter' => FILTER_VALIDATE_REGEXP,
'options' => ['regexp' => '/^(?=.*[a-zA-Z])(?=.*\d).{9,12}$/'],
],
]));
Constants
About
定数は値のためのID (名前)。基本的にスクリプト実行中に変更できない。大文字小文字を区別するが、慣習として大文字で表記する。
constキーワードか、define関数で定義できる。constの場合、制約がある。
constで指定可能なのは、スカラー式 (bool/int/float/string) と、スカラー式のみのarray。動的な設定はできない。
変数と異なり、$の前置は不要。
定数の定義判定は、defined()を使う。
定数の変数との違いは以下。
- $不要。
- スコープに関係なく、あらゆる場所からアクセス可能。
- 後から再定義、未定義不能。
- スカラー値と配列のみ。
constはコンパイル時に定義されるため、トップレベル以外、つまりブロック内部 (関数/ループ/if/try) で宣言できない。defineはできる。
define/const
| 項目 | const | define |
|---|---|---|
| 構文 | 予約語 (少し速い) | 関数 |
| 戻り値 | なし | あり |
| 定義元 | スカラー値のみ | 変数/関数OK |
| クラス定数 | x | - |
| 使用箇所 | 制御ブロック内部以外 | どこでも |
| スコープ | 名前空間 | グローバル |
defineはブロック内で使えるので、何らかの条件で定義を変更できるのが利点。例えば、環境を本番とデバッグに変えたりなど。
動的に変更したいならdefine、それ以外は名前空間やクラス定数として使えるconstだろうか。関数内のマジックナンバー的な使い方はできない。そういうのは、普通の変数で取り扱う。
ただ、constはアプリの設定として使うことはない。クラスの固有値の定義。
constant
定数名の文字列で、定数の値を取得したい場合に使える。
定数の他に、enumのcaseにも使える。
class
クラス内に定数を定義できる。デフォルトでpublic。staic変数的な扱い。インスタンスではなく、クラスが保有する。
predefined
言語で定義済みの定数がいろいろある。true/false/nullなど。
Magic/マジック定数
使用箇所で値が変化する定数 (マジック定数) が9個ある。C言語のマクロに近い。コンパイル時に解決される。大文字小文字を区別しない。
| 名前 | 説明 |
|---|---|
__LINE__
|
ファイル上の現在の行番号。 |
__FILE__
|
ファイルのフルパスとファイル名 (シンボリックリンクを解決した後のもの)。 インクルードされるファイルの中で使用された場合、インクルードされるファイルの名前が返されます。 |
__DIR__
|
そのファイルの存在するディレクトリ。include の中で使用すると、 インクルードされるファイルの存在するディレクトリを返します。 つまり、これは dirname(__FILE__) と同じ意味です。 ルートディレクトリである場合を除き、ディレクトリ名の末尾にスラッシュはつきません。
|
__FUNCTION__
|
関数名。無名関数の場合は、{closure}
|
__CLASS__
|
クラス名。 クラス名には、そのクラスが宣言されている名前空間も含みます (例 Foo\Bar)。 トレイトのメソッド内で __CLASS__ を使うと、 そのトレイトを use しているクラスの名前を返します。
|
__TRAIT__
|
トレイト名。 トレイト名には、宣言された名前空間も含みます (例 Foo\Bar)。
|
__METHOD__
|
クラスのメソッド名。 |
__NAMESPACE__
|
現在の名前空間の名前。 |
ClassName::class
|
完全に修飾されたクラス名。 |
どれもよく使う。
define
PHP5で配列を除くスカラー値 (int/float/string/bool/null) のみ指定可能。PHP から配列も指定可能。
Operators
Token
「PHP: 演算子の優先順位 - Manual」にコメントがあるように、優先順位のページの演算子は完璧ではない。
こちらのパーサトークンの一覧が完全なリストになる。
precedence/優先順位
丸括弧をつけるかつけないかが変わる。
特によく使うもの、注意が必要なものを整理する。
if (!$var = getVar())
- Why does negation happen last in an assignment expression in PHP? - Stack Overflow
- if文の中で変数定義 - PHP #PHP - Qiita
!は=より優先順位が高いが if (!$var = getVar()) のような式は成立して、変数代入結果の否定が評価される。
注意: = は他のほとんどの演算子よりも優先順位が低いはずなのにもかかわらず、 PHP は依然として if (!$a = foo()) のような式も許します。この場合は foo() の戻り値が $a に代入されます。
これが成立する理由。=の左辺は変数じゃないといけないから。(!$var) には代入がそもそもできない。そのため、PHPができるだけパース仕様として、以下のように代入部分を丸括弧で囲んだ扱いにしてくれる。
!$var = getVar() !($var = getVar())
だからこれが成立する。関数の処理結果を保存して、判定してその後の流用に短縮できて便利。
Assignment/代入演算子
??=
// NULL合体代入演算子
$id ??= getId();
// これと同じ
$id = $id ?? getId();
$id = @$id ?: getId();
$id = isset($id) ? $id : getId();
NULL合体演算子の代入版。nullの場合の代入が簡単になった。PHP 7.4から使用可能。
Comparison/比較演算子
三項演算子 (条件演算子)
if/elseの短縮表記。デフォルト値の設定などでよく使う。
<?php
// 三項演算子の使用例
$action = (empty($_POST['action'])) ? 'default' : $_POST['action'];
// 上記は以下の if/else 式と同じです。
if (empty($_POST['action'])) {
$action = 'default';
} else {
$action = $_POST['action'];
}
?>
PHP特有事項として、真ん中を省略できる。その場合、1個目がtrueならそれがそのまま戻る。JavaScriptとかC系言語でも真ん中は省略できない。
式 expr1 ?: expr3 の結果は、expr1 が true と同等の場合は expr1、 それ以外の場合は expr3 となります。 この場合、expr1 は一度だけ評価されます。
条件演算子のネストはわかりにくいので推奨されない。が、条件演算子の省略形は安定している。false以外の最初の引数を評価する。
<?php echo 0 ?: 1 ?: 2 ?: 3, PHP_EOL; //1 echo 0 ?: 0 ?: 2 ?: 3, PHP_EOL; //2 echo 0 ?: 0 ?: 0 ?: 3, PHP_EOL; //3 echo $undefinedVariable ?? false ?: 'false default'; ?>
NULL合体演算子はnullの時のデフォルト値になるが、こちらはfalseの場合のデフォルト値設定。意味が違う。未定義変数アクセスをガードできないが、それ以外であれば条件演算子の短縮表記のほうをよく使う。非常に重要。
Null合体演算子を組み合わせて、未定義のなどの場合のデフォルト値設定で役立つ。
elvis演算子と呼ばれることもある模様。
It also combines nicely with the ?? operator, which is equivalent to an empty() check (both isset() and `!= false`):
$x->y ?? null ?: 'fallback';
instead of:
empty($x->y) ? $x->y : 'fallback'
Null 合体演算子/Null collapsing operator
<?php // $_GET['user'] を取得します。もし存在しない場合は // 'nobody' を用います。 $username = $_GET['user'] ?? 'nobody'; // 上のコードは、次のコードと同じ意味です。 $username = isset($_GET['user']) ? $_GET['user'] : 'nobody';
$var ?? 'value = isset($var) ? $var : 'value';
変数がnullの場合のガードの簡易記法。PHP v7.0.0で追加。非常に便利。
Error Control/エラー制御演算子@
式の直前に@を前置すると、その式のエラーメッセージを無視する。
基本的に使わないほうがいい。if文などでガードするより処理が遅い。ただし、Viewなどであまり影響ない場合などは記述がシンプルになるという利点もあるかも。
ただ、やっぱり基本は使わないほうがいい。バグの見落としになる。
Logical/論理演算子
論理積と論理和が、and/orと&&/||で2種類存在する。演算子の優先順位が違う。
// $g に代入されるのは、(true && false) の評価結果です // これは、次の式と同様です: ($g = (true && false)) $g = true && false; // $h に true を代入してから "and" 演算子を評価します // これは、次の式と同様です: (($h = true) and false) $h = true and false;
なお、PHPの論理演算子は、常に論理値 (true/false) を返すので注意する。
$a = $var || 'default';
上記のように、デフォルト値の代入扱いでor演算子を使うことはできない。同じ論理型同士なら成立はするが。
デフォルト値扱いにしたければ、短縮条件演算子?:や、ヌル合体演算子??を使う。
配列演算子
配列に対する演算子は扱いがやや特殊。
| 例 | 名前 | 結果 |
|---|---|---|
| $a + $b | 結合 | $a および $b を結合する。 |
| $a == $b | 同等 | $a および $b のキー/値のペアが等しい場合に true。
|
| $a === $b | 同一 | $a および $b のキー/値のペアが等しく、その並び順が等しく、 かつデータ型も等しい場合に true。
|
| $a != $b | 等しくない | $a が $b と等しくない場合に true。
|
| $a <> $b | 等しくない | $a が $b と等しくない場合に true。
|
| $a !== $b | 同一でない | $a が $b と同一でない場合に true。
|
配列の等価演算子はキーと値の両方を比較する。これが重要。後は結合の+。
...演算子/スプレッド演算子
他の言語でいうスプレッド (splat) 演算子。PHPでは...演算子。使うときはアンパックという。
- 関数
- 可変長引数リストと関数呼出 PHP: 関数の引数 - Manual。PHP 5.6で導入 (PHP: 新機能 - Manual)。名前付き引数に対応したPHP 8.0から連想配列も対応。PHP 8.1から可変長引数と名前付き引数の同時使用 (PHP: 新機能 - Manual)。アンパックと通常名前付き引数がある場合、後のやつで上書き不能で実行時エラー。
- 第一級callableを生成する記法: PHP 8.1.0で導入 (PHP: 第一級callableを生成する記法 - Manual)。Closure::fromCallableの無名関数作成時の別の方法。
- 配列のアンパック: PHP: 配列 - Manual。array_mergeの代替記法。extractは似ているようで意味が少々違う。
- 数値キー・単純配列はPHP 7.4で導入 (PHP: 新機能 - Manual)。
- 文字キー・連想配列はPHP 8.1 (PHP: 新機能 - Manual、【PHP8.1】あなたはどっち? array_merge VS unpacking(スプレッド演算子) #PHP - Qiita)。
- アンパックと通常名前付き引数がある場合、後のやつで上書き。
- 配列のアンパックは、性能面で問題があり、連想配列がPHP 8.1以上というのがあり、array_mergeを使ったほうがよさそう。
- PHP の ... (3点ドット, Three Dots) の種類,全部言えるかな? #QiitaEngineerFesta2022 - Qiita
関数と配列の2か所で意味がある。配列の他、Traversableオブジェクトも可能。
配列や配列変数の直前に...を前置する (スペースは任意)。
関数の場合、関数定義時の仮引数と、関数呼出時に使用可能。
関数定義時の仮引数で指定すると、その変数が可変長引数を受け入れることを意味する。型宣言はその左に指定可能。これにより、func_get_args()を使わなくてもよくなった。
関数呼出時に使用すると、引数を展開してくれる。配列のアンパックに近い。
<?php
function sum(...$numbers) {
$acc = 0;
foreach ($numbers as $n) {
$acc += $n;
}
return $acc;
}
echo sum(1, 2, 3, 4);
?>
<?php
function add($a, $b) {
return $a + $b;
}
echo add(...[1, 2])."\n";
$a = [1, 2];
echo add(...$a);
?>
<?php
function total_intervals($unit, DateInterval ...$intervals) {
$time = 0;
foreach ($intervals as $interval) {
$time += $interval->$unit;
}
return $time;
}
【PHP8.1】あなたはどっち? array_merge VS unpacking(スプレッド演算子) #PHP - Qiita
なお、配列のアンパックに関しては、array_mergeのほうが速くてメモリーも少ないとのこと。
in演算子
php equivalent of mysql "IN" operator? - Stack Overflow
PHPにin演算子はない。代わりに、in_arrayで包含判定できる。
ある値が、いずれかのどれかであるかの判定はそれなりにある。
in_array($target, [], true);
例えば、この比較対象が長い場合、(a===b||a===c|a===d) で何回も書かなくて済む。
Name
誰かに言葉で説明する際に、演算子や構文の名前がほしい。意外と覚えていない。根拠とともに整理する。
https://chatgpt.com/c/6743ea1b-4a14-800b-b50b-272dd4dbcde0
| 演算子 | 名前 | name | URL | 説明 |
|---|---|---|---|---|
| $this->property | オブジェクト演算子 | object operator | PHP: プロパティ - Manual | インスタンスのプロパティーとメソッドにアクセスする。 |
| ...[] | ...演算子 | PHP: 新機能 - Manual | 配列を展開する。別名splat演算子。スプレッド演算子。スプレッド演算子を使うことをアンパックという。 | |
| $ar['key']
$str[0] |
角括弧構文
文字列アクセス演算子/インデックス演算子 |
PHP: 配列 - ManualPHP: 文字列 - Manual | 演算子ではなく構文。 | |
|
スコープ定義演算子 | PHP: static キーワード - ManualPHP: スコープ定義演算子 (::) - Manual | ||
| ?: | 三項演算子 | |||
| ?? | Null合体演算子 | |||
| . | 結合演算子 | PHP: 文字列演算子 - Manual |
Control Structures
Source: PHP: Control Structures - Manual.
制御構造に関する別の構文
if、 while、for、 foreach、switch に関する別の構文がある。開き波括弧部分を:に、閉じ波括弧部分をendif;,endwhile;, endfor;,endforeach;, endswitch;などにできる。else:とelseif:に注意。
この構文は存在だけ知っておくだけでいいと思われる。
elseif/else if
1単語で書ける。結果は同じだが、文法的な意味が異なる。
foreach
About
foreachは配列の反復処理のための制御構造。
foreach (iterable_expression as $value) foreach (iterable_expression as $key => $value)
$keyも使いたい場合、2番目の形式を使う。
ループ中に$valueの要素を直接変更したい場合、&をつけておく。
foreach (iterable_expression as &$value)
参照にすると、以下のようにDBテーブルからデータ取得時に、一部のカラムだけ追加するような処理がシンプルになる。
foreach ($mute_list as ['target_user_id' => $target_user_id, 'is_muted' => $is_muted]) {
foreach ($data_list as &$data_value) {
if (!empty($data_value['user_id']) && $data_value['user_id'] === $target_user_id) {
$data_value['is_muted'] = $is_muted;
}
}
}
Unpacking nested arrays
配列の配列は[]/listでアンパックできる。配列の配列で特定のキーだけ必要なことがわかっているなら、foreachを1回省略できるのでシンプルになる。
なお[]はPHP 7.1から使用可能。
<?php
$array = [
[1, 2],
[3, 4],
];
foreach ($array as [$a, $b]) {
echo "A: $a; B: $b\n";
}
foreach ($array as list($a, $b)) {
echo "A: $a; B: $b\n";
}
?>
連想配列の場合、キーを指定して値を受け取る。
$array = [
['foo' => 1, 'bar' => 2, 'baz' => 3],
['foo' => 1, 'bar' => 2, 'baz' => 3],
];
foreach ($array as ['foo' => $foo, 'bar' => $bar]) {
echo "foo: $foo; bar: $bar\n";
}
Name
foreachで使う変数の命名。
foreach (table as $row => $line)
DBからSELECT結果などがkeyに行番号、valueにレコードが入ってくる。こういう場合、rowがややこしい。行番号の意味でrowをキーにしておくといい。
value部分をどうするかだが、valueやitemだと少々わかりにくい。recordやline。SplFileObjectを扱うこともあるからlineがいいと思う。
Rewind
- SplFileObject: foreach doesn't start from the correct line after `SplFileObject::seek()` · Issue #13916 · php/php-src · GitHub
- PHP: Iterator::rewind - Manual
- PHP: NoRewindIterator - Manual
注意の必要な挙動として、foreachは最初にrewindでIteratorのポインターを先頭に毎回戻す。なので、ファイル系Iteratorで先頭を飛ばそうとすると工夫が必要。
// use SplFileObject;
$path = stream_get_meta_data($fp = tmpfile())['uri'];
file_put_contents($path, <<<'EOT'
id,value
0,1
EOT
);
$file = new SplFileObject($path);
$file->setFlags(SplFileObject::READ_CSV|\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY);
$file->seek(1);
foreach(new NoRewindIterator($file) as $row) {
var_dump($row);
}
ほぼこのために存在する、SPLのNoRewindIteratorでラップする。すると、rewindをオーバーライドして巻き戻さないので維持できる。
なお、next()はREAD_AHEADありにしていないと機能しないようなので注意する (php - SPLFileObject next() behavior - Stack Overflow)。
ただ、READ_AHEADにしても、初回がnext()2回呼ばないと2行目にcurrent()でならないので動きがわかりにくい。seek(1)でよい。
first/last
PHP How to determine the first and last iteration in a foreach loop? - Stack Overflow
foreachの中で最初と最後を判定したいことがある。
foreach ($array as $key => $element) {
if ($key === array_key_first($array)) {
echo 'FIRST ELEMENT!';
}
if ($key === array_key_last($array)) {
echo 'LAST ELEMENT!';
}
}
ただ、先頭なら$iterable->current()でいい。末尾ならforeachを抜けた後にcurrentでいい。
配列だったら、反復外部で簡単に判定できる。余計な処理を反復内に含めないほうがいい。
反復削除
キーが維持されるので、逆順反復などしなくても、影響ない。unsetすればいい。
reverse
逆順反復の方法がいくつかある。
php - Reverse order of foreach list items - Stack Overflow
$fruits = ['bananas', 'apples', 'pears'];
for($i = count($fruits)-1; $i >= 0; $i--) {
echo $fruits[$i] . '<br>';
}
foreach ( array_reverse($accounts) as $account ) {
echo sprintf("<li>%s</li>", $account);
}
なお、連想配列は無理。やるとしたら、array_reverse、逆順のキーを取得してそれを使う。
arrays - How to reverse foreach $key value PHP? - Stack Overflow
declare
Source: PHP: declare - Manual.
PHPUnitのサンプルコード (Getting Started with Version 9 of PHPUnit – The PHP Testing Framework) などで冒頭に以下の記述がある。
<?php declare(strict_types=1);
これの意味が分かっていなかったので整理する。
declare文 (construct) は、コードブロックの実行指令となる。以下の構文となる。
declare (<directive>) <statement>
<directive> はdeclareブロックの挙動を指示する。指定可能なものは以下3個だ。
- ticks
- encoding
- strict_types: =1の指定でPHPの暗黙の型変換を無効にする (ストリクトモード)。ただし、影響するのはスカラー型のみ。型が違う場合、TypeErrorの例外が発生する。
指令はファイルコンパイル時に処理されるので、リテラル値のみが使用可能で、変数や定数は使用不能。
declareブロックの <statement> は、<directive> の影響を受ける実行部だ。
declare文はグローバルスコープで使われる。登場以後のコードに影響する。ただし、他のファイルからincludeされても、親ファイルには影響しない。だから安心して使える。
型安全にするために、基本的にPHPファイルの冒頭にdeclare(strict_types=1);を書いておいたほうがよいだろう。
return
関数を終了させて、結果を呼び出し元に返すというのは他の言語同様の動きだが、いくつか注意すべき挙動・使用方法がある。
returnで引数を省略すると、戻り値はnullになる。
呼び出し方法、場所で挙動が変わる。
- 関数/eval内: 即座に関数を終了し、引数を関数の値として返却。
- グローバルスコープ: スクリプト自体を終了。
- include/require内: 呼び出し元のファイルに制御を戻す。includeの場合、引数はincludeの戻り値になる。
return文は関数ではないので、引数の括弧は不要。紛らわしいのでないほうがいい。
include内で使えるというのがみそ。config.phpでreturnだけした設定一覧を記述しておいて、includeで変数に取り込むというのをよくやる。
require/include/require_once/include_once
Basic
includeは指定したファイルを読み込み評価する。絶対パスで指定しない場合、include_pathの設定を利用する。include_pathにもなければ現在ディレクトリーも探す。
絶対パス、相対パスの前置があると、include_pathは無視する。
ファイルが読み込まれると、ファイル内のコードは、includeが実行された行の変数スコープを継承する。つまり、呼び出し行で利用可能な全変数がファイル内でも使用可能。ファイル内で定義された関数やクラスはすべて、グローバルスコープになる。ただし、includeが関数定義内に配置されたら、コードは関数内で定義されているとみなす。
ファイルの読込時にはHTMLモードになる。そのため、ファイル内でPHPコードを実行するなら、<?php ?>で囲む必要がある。
includeに失敗したらFALSEを返し、E_WARNINGを発生させる。成功したら、戻り値は1。ただし、ファイル内でreturnを実行したら、その値を返す。
includeは特別な言語構造のため、引数に括弧は不要。結果を評価したいならば、全体を括弧で囲む。
// 動作します。
if ((include 'vars.php') == TRUE) {
echo 'OK';
}
パス指定
[PHPrequire(require_once)するときは必ず絶対パスを使いましょう · DQNEO日記]
require_once dirname(__FILE__). require_once __DIR__.
現在パスから相対参照したい場合、__DIR__を使う。これを使わないと、読み込み元のファイルがベースになる。あるいはフルパスにする。
絶対パスにしないと検索で時間もかかる。
require/include
requireはincludeとほぼ同じ。違いは、失敗時にE_COMPILE_ERRORが発生して処理を中断する点。includeはE_WARNINGで処理は継続する。
使い分けとして、変数読込などで読み込めなくても処理を進めて問題ない場合に、include。
関数定義など、絶対必要なものはrequireなど。
_once
読込済みなら、再読込しない点がinclude/requireとの決定的な違い。関数の複数定義のエラーを回避できたりする。
読み込めたらtrueを返す。
config.php
includeとreturnの組み合わせのconfig.phpの設定ファイルをいろんなアプリで使われている。
<?php
return [
'name' => 'hoge',
'value' => 'fuga',
];
?>
<?php // configファイルを変数に代入 $config = include __DIR__ . '/config.php'; // 呼び出し。 var_dump($config['name']); ?>
こういう形式。このreturnだけの文は、ほぼinclude前提。
編集対象のアプリの設定を、既存コードと分離する際に、いい方法。
config.phpをアプリ内で作りたい場合、「How to create Dynamically create config/custom.php config file」にあるように、var_exportを使うとよい。
// create the array as a php text string $text = "<?php\n\nreturn " . var_export($myarray, true) . ";";
config class
config.phpをどう用意するかは議論がある。
- Is it right to set a config PHP class to keep project settings? - Stack Overflow
- Configuration in PHP applications | PHP.earth
- PHPでプログラム全体の設定に使う変数の保持の仕方 #PHP - Qiita
- [PHPコンフィグファイルから設定情報を読み込むためのConfigクラス | PHP Archive]
- PHPでconfigファイルをオートロードで呼び出す方法 #Config - Qiita
- Using a config file from within a php class - Stack Overflow
- PHP OOP Config Class - Stack Overflow
include/returnではなくて、クラスのconst定数にするという。
- クラスのconst定数
- iniファイル/parse_ini_file
他に、configクラスを用意しておいて、シングルトンか、staticメソッドで参照する形。
どれくらいの頻度で参照するか次第。参照頻度が低いなら、getで毎回設定ファイルを読み込む。参照頻度が高いなら$configをstaticのクラス変数にもたせる。
/**
* config.phpに記載の設定項目を取得する。
* @param string $key configのキー。
* @return mixed configの値かnull。
*/
public static function get(string $key)
{
return (include __DIR__ . '/config.php')[$key] ?? null;
}
config variable
https://chatgpt.com/c/67c66d7c-7fac-800b-94c6-b7711965d1f2
includeでconfig.phpを読み込むのはお決まりパターン。では、DBカラムなどで文字列としてやり取りする場合どうするか?
var_export/evalもあるが、任意のPHPコードを稼働させる危険性がある。
json_encode/json_decodeが確実。
Function
User defined
関数は以下のような構文で定義する。
<?php
function foo($arg_1, $arg_2, /* ..., */ $arg_n)
{
echo "関数の例\n";
return $retval;
}
?>
関数内では、他の関数やクラス定義を含む、PHPのあらゆるコードを使用可能。関数内で関数を定義できないC言語とは異なる。
また、定義前の関数呼び出しはOK。ただし、関数定義にif文などの条件があるなら、定義後じゃないとだめ。
<?php
$makefoo = true;
/* ここでは関数foo()はまだ定義されていないので
コールすることはできません。
しかし関数 bar()はコールできます。 */
bar();
if ($makefoo) {
function foo()
{
echo "I don't exist until program execution reaches me.\n";
}
}
/* ここでは $makefooがtrueと評価されているため
安全にfoo()をコールすることができます。 */
if ($makefoo) foo();
function bar()
{
echo "I exist immediately upon program start.\n";
}
?>
この性質を生かして、php単独ファイルで機能するタイプのアプリの場合、main関数の呼び出しをファイル冒頭にして、main関数の定義を下のほうでやったりできる。構造をきれいにできる。
PHPでは、変数と異なり、関数やクラスは全てグローバルスコープ。関数内で定義した関数も外部から呼び出し可能。スコープが欲しければ、無名関数を使う。
また、関数のオーバーロードもできない。関数をunsetしたり、再定義も不能。
可変引数と、デフォルト引数もある。
Argument
Comma
PHP 7.3から、関数呼び出し時の終端カンマを許容。
my1(1,); my2(2,); // OK
PHP 8.0.0から、関数定義時の引数リストの最後のカンマが許容される。
<?php
function takes_many_args(
$first_arg,
$second_arg,
$a_very_long_argument_name,
$arg_with_default = 5,
$again = 'a default string', // この最後のカンマは、8.0.0 より前では許されません。
)
{
// ...
}
?>
Reference
引数はデフォルトで値渡しになる。値がコピーされて渡される。関数内部で引数自体を修正したい場合、リファレンス渡しにする。
関数定義で変数の前に&をつけると、リファレンス参照になる。
<?php
function add_some_extra(&$string)
{
$string .= 'and something extra.';
}
$str = 'This is a string, ';
add_some_extra($str);
echo $str; // 出力は 'This is a string, and something extra.' となります
?>
Default
関数定義時に、引数部分で変数に値を代入するようにして、デフォルト値を定義できる。引数が指定されなかった場合に使われる。なお、nullが渡された場合も、デフォルト値の代入はされないので注意する。
function makecoffee($type = "cappuccino")
{
return "Making a cup of $type.\n";
}
デフォルト値には、定数を指定できる。具体的には、スカラー値、配列、null。PHP 8.1.0から、new ClassName記法でインスタンスも指定できる。
デフォルト引数は、デフォルト値のない引数の右側の必要がある。そうでない場合、省略できず、指定する意味がなくなく。
「php 7 - Default callable in function definition in php 7 - Stack Overflow」にあるように、$callableのデフォルト引数に匿名関数を指定したりはできない。デフォルトnullを指定しておいて、以下のような匿名関数で設定するとよいだろう。
$callback = $callback ?: function($e) {return $e};
関数内で、値の有無を確認する必要がある。
可変長引数
引数リストに... (...演算子) を含めることで、可変長の引数を受け取ることを示す。...を前置した変数に配列として入る。
<?php
function sum(...$numbers) {
$acc = 0;
foreach ($numbers as $n) {
$acc += $n;
}
return $acc;
}
echo sum(1, 2, 3, 4);
echo sum(...[1, 2, 3, 4]);
...の前に型宣言も付与できるが、その場合配列要素が全部その型が必要になる。
https://chatgpt.com/share/683fa08f-ab2c-800b-9ec9-c30bb5836d06
可変長引数の他に、配列で可変長を扱うこともできる。...を使うと、引数の型を明示できる。配列にすると、配列要素の型を明示できない。
違いはそれと、関数呼出時の引数指定の違いくらい。型を明示できるので、可変長引数を優先した方がいい。可変長引数は、関数内部では配列として扱える。
なお、可変長引数のint ...$varは、PHP 8.4時点で唯一配列要素に型を言語仕様で明示可能な構文。それ以外だと、PHPDocなどを使うしかない。
名前付き引数
PHP 8.0.0から名前付き引数が導入された。引数の位置、順番ではなく、名前ベースで渡せる。これにより、デフォルト値を持つ引数をスキップできるし、引数の順番を意識しなくてよくなる。
引数の名前の後にコロン:をつけたものを値の前につけて指定する。引数の名前には予約語も使える。ただし、変数など動的には指定できない。
位置引数との混在もできる。その場合、名前付き引数は最後にする必要がある。
<?php myFunction(paramName: $value); array_foobar(array: $value);
PHP 8.1.0では、引数を...で展開した後に、名前付き引数も指定できる。ただし、展開済み引数の上書きはだめ。
function foo($a, $b, $c = 3, $d = 4) {
return $a + $b + $c + $d;
}
var_dump(foo(...[1, 2], d: 40)); // 46
var_dump(foo(...['b' => 2, 'a' => 1], d: 40)); // 46
var_dump(foo(...[1, 2], b: 20)); // Fatal error. Named parameter $b overwrites previous argument
Return value
Ref: PHP: 戻り値 - Manual.
関数はreturn文で値を返せる。そこで処理を終了する。
returnを省略した場合、nullを返す。
https://chatgpt.com/c/67f610d0-5a70-800b-919c-cec5bd0f1905
戻り値に: voidを宣言した場合、return で値があるとエラーになる。メソッドチェーン的なことができない。が、nullを返すならガードいるので意味ないと思う。
常に: voidでいいと思う。
Variable Functions/可変関数/Callable/コールバック
- PHP: 可変関数 - Manual
- PHP: コールバック / Callable - Manual
- 【PHP】コールバック関数サンプル3つをまとめる - ウェブ集客で企業を成功に導くホームページ制作会社|(株)ワイコム・パブリッシングシステムズ(福岡)
- PHPで関数やクラスを文字列から呼び出しする方法まとめ | PisukeCode - Web開発まとめ
- 【小ネタ】phpで変数でメソッドを実行する | BeginnerEngineerBlog
- call_user_func() と $function() の動きが違った
PHPで関数を引数で指定したり、変数として扱う仕組みがある。evalを使う必要はない。
可変関数は、関数を文字列で実行する仕組み。これとは別で、Callableという型がある。
可変関数は、関数名の文字列の変数に丸括弧を追加したら実行できるというもの。インスタンス変数があれば、メソッドもできる。
PHP 7.0から、関数のみ"str"()も可能になった。「PHP: PHP 5.6.x から PHP 7.0.x への移行 - Manual」に記載はないが、パース方法が変わったことが由来の模様。
関数もメソッドも統一的に扱うものとして、Callable型がある。
CallableはPHPで関数を引数として渡したり、関数名の文字列を渡して、動的に関数を実行する仕組み。
callable型で表す。関数だけでなく、メソッドやstaticメソッドも対応できる。方法が2種類ある。
- 関数: 関数名の文字列。
- メソッド: 配列で指定。0番目の要素に、インスタンスやオブジェクト。1番目の要素にメソッド名の文字列で指定する。
- staticメソッド: 配列で指定。0番目の要素に、クラス名を指定する。'ClassName::methodName' 形式でも指定可能。
メソッドの場合、以下のような形式。
/** @var callable $function */ $function = [$this, 'methodName'];
引数付きでバインドしたい場合、無名関数でラップして、引数を指定する。または、useで参照する。
https://chatgpt.com/c/67f4a3fb-bcc8-800b-8da9-e6528705258b
引数を処理する場合、call_user_func_arrayや...で展開する。
anonymous/無名関数
2009年頃にPHP 5.3で登場したらしい (PHP 5.3の無名関数を試してみた - hnwの日記)。
callableの型。非常に重要。
$message = "message";
// "use" がない場合
$example = function () {
// 未定義変数参照扱い
var_dump($message);
};
$example();
// $message を引き継ぎます
$example = function () use ($message) {
var_dump($message);
};
useを指定した場合だけ、親のスコープから変数を引き継げる。変数は関数定義時の値。
クラスのコンテキストの場合、$thisは自動で引き継がれる。
即時関数として使うなら、引数で全部渡せる。が、useを使うと引数に指定しなくていいので短くできる。即時関数なら、useで問題ない。
arrow/アロー関数
PHP 7.4で追加。無名関数の簡易構文。かなり短く記述できる。特に、デフォルトで全部キャプチャーしてくれるのが楽。
fn (argument_list) => expr
親の変数を暗黙でキャプチャー (コピー)。参照でキャプチャーしたい場合は無名関数を使うしかない。
$y = 1;
$fn1 = fn($x) => $x + $y;
// $y を値渡しするのと同じ
$fn2 = function ($x) use ($y) {
return $x + $y;
};
var_export($fn1(3));
また、関数本文部分は式。forなどの文を書けない。
どうしても複数行の処理を書きたいなら、結果を配列にする。
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function(){;}), " ns\n";
長くなるなら、無名関数にしたほうがよさそう。
https://chatgpt.com/share/68146a57-9274-800b-9a5d-afb4c32ac727
- 利点: キャプチャーを省略できて、無名関数よりも短くかけることがある。
- 欠点: 文を書けない。式しか書けず、複数処理をほぼ実装できない。PHP 7.4以上。
基本的に、アロー関数は利点がないので、常に無名関数でいいと思う。せいぜい、array_map/sort/filterのようなワンライナーのときだけ、便利かもしれないというくらい。構文を覚えるのが無駄なので常に無名関数でいい。
即時実行関数
PHP即時関数の完全ガイド:使い方と便利な応用例10選 – Japanシーモア
即時関数と呼ぶこともある。無名関数式、アロー関数式でだけ使える。関数定義後、関数全体を丸括弧で囲んで引数の丸括弧記載で、呼べる。
処理を単にブロック化して、構造をグループ化したい場合に使えるか?単に波括弧でいいかも。関数だと変数にスコープを付けられるのが違い。
関数の反復実行
https://chatgpt.com/c/67da286a-f77c-800b-94dd-9031ad6adaec
関数を呼んで、戻り値をチェックして、エラーなら終了みたいなことをやりたいことがある。その際に、関数の反復実行が必要になった。
基本はcallable型にしてそれを()で呼ぶだけでいい。
$functions = [
[Example::class, 'foo'], // クラスメソッドの `callable` 形式
[Example::class, 'bar'],
];
foreach ($functions as $function) {
$function(); // 関数呼び出し
}
エラー処理もするなら。
protected function _performFunctions(array $functions): string
{
foreach ($functions as $function) {
if (!is_callable($function)) continue;
if ($path = $function()) return $path;
}
return '';
}
こんな感じ。ただし、このやり方で呼べるのは、public/protected。privateは呼べない。privateは自分だけだから当然といえば当然。
Classes and Objects
The Basics
- インナークラスはない
- 多重継承はできない。代わりにtraitで実現する。
class
class内には変数 (プロパティー)、定数、関数 (メソッド) を含められる。
class内の関数などで、これらのプロパティー、メソッド類の参照時は、擬似変数$this->経由で参照できる。$thisは呼び出し元オブジェクトが入っている。
C系言語であれば、$this->相当は省略できたが、PHPでは指定が必要なので注意する。
new
インスタンス生成に使うキーワード。コンストラクターで例外をスローしていない限り、常にオブジェクトを生成する。
$instance = new SimpleClass();
$instance = new SimpleClass; // コンストラクターに引数を渡さない場合、()を省略可能。
$class_name = 'SimpleClass';
$instance = new $class_name; // クラス名の変数も使用可能。
$instance = new ('SimpleClass'); // PHP 8.0.0以上で丸括弧で囲むことで任意の指揮と一緒に使える。
文字列で動的に生成可能な点は重要。
https://chatgpt.com/c/67a992f0-41f0-800b-9d6b-7bafb5ffda5f
new self, new parent, new staticもある。new staticは遅延静的束縛。
new staticで静的束縛する場合、戻り値はstaticにする。ただし、PHP 8.0からしか使えない。その場合、戻り値はなしで、phpdocで@return staticにする。
::class
<className>::classでクラス名の完全修飾子の文字列を取得できる。
例外の試験など、クラス名の情報が必要な時によくみかける。
PHP 8.0.0からオブジェクトに対しても::classを使用でき、元のクラス名を取得できる。その場合、get_class()と同じ。同じならPHP 7で使えないのでget_class()でいいか。
プロパティーとメソッド
名前空間が別なので、同名が許容される。呼び出し方で、プロパティーとメソッドを区別している。
シグネチャの互換性に関するルール
メソッドオーバーライド時には、子クラスのシグネチャーが親クラスと互換性が必要。互換性が壊れると、Fatal errorになる。PHP 8未満はE_WARNINGが発生していた。
- アクセス権: 緩めるのはOK、狭めるのはNG。
ただし、コンストラクターとprivateメソッドに関しては、例外でシグネチャーにミスマッチがあってもエラーにならない。
Property
Ref: PHP: プロパティ - Manual.
クラスのメンバー変数のことをプロパティー (property) とPHPでは呼んでいる。
クラス内で、1以上のキーワード (アクセス権、static、PHP 8.1.0以後のみreadonly) のあとに、オプション型宣言 (PHP 7.4以後、readonly以外) の後に変数宣言を続ける。
public $var1 static $var2 var $var3
staticなど、アクセス権を指定しない場合、publicとデフォルトでみなされる。なお、varキーワードを使う方法もある。これはPHP4までのプロパティーの宣言方法。PHP5以後はpublicと同じ意味になる (What does PHP keyword 'var' do? - Stack Overflow)。
が、[PSR-12: Extended Coding Style - PHP-FIG] などのコーディング規約で禁止されている。古い書き方なのでpublicに書き直せばいいと思う。
宣言時に初期値を代入できるが、初期値は定数のみ。関数類は使用不能。
- php - Constant expression contains invalid operations - Stack Overflow
- 【PHP】クラスプロパティの値には、動的な値を代入することができないようです。 #error - Qiita
以下のエラーが出る。
PHP Fatal error: Constant expression contains invalid operations in /ぼくのかんがえたさいきょうのクラス.php on line 5
関数類で動的に代入したい場合、__constructでやる。
クラスメソッドからstaticでないプロパティーにアクセスするには、-> (オブジェクト演算子/object operator) を使う。
プロパティー内の共有
https://chatgpt.com/c/67a58b69-45a0-800b-a8a0-057756e0e5b7
同じ値をプロパティーの配列などで指定したいことがある。ただし、プロパティーで可能な初期値は定数のみ。いくつか方法がある。
- const: 普遍の場合。
- static: 変更する場合。
- define: グローバルなので非推奨。
基本はcostで宣言した値をself::で流用すればいい。
型宣言
Fatal error: Type of Bar::$a must not be defined (as in class Foo)
PHP 7.4: 基底クラスのプロパティに型が無いと、継承クラスでは型が付けられない #PHP - Qiita
基底クラスのプロパティーに型宣言がない場合、派生クラスで型を変更できない。
そのため、型宣言すると上記のエラーになる。
可変プロパティー
プロパティー名は変数に格納してそれをアクセスすることもできる。
<?php
class foo {
var $bar = 'I am bar.';
var $arr = array('I am A.', 'I am B.', 'I am C.');
var $r = 'I am r.';
}
$foo = new foo();
$bar = 'bar';
$baz = array('foo', 'bar', 'baz', 'quux');
echo $foo->$bar . "\n";
echo $foo->{$baz[1]} . "\n";
変数が配列の場合、変数と全体の要素を区別するため、波括弧が必須になる。波括弧を使うと、通常使用不能な-などもプロパティーに使える。
Autoloading Classes
別のファイルのクラスを使う方法の話。
- require_once()/require()/include: シンプルなファイル読み込み。PHP 4から。
- __autoload(): 非推奨。PHP 5.0で登場。
- spl_autoload_register(): PHP標準。PHP 5.1.0で登場。
- composer autoload: composer。
C系言語であれば、includeなどで外部ファイルをそのまま自分のファイルに読み込む。PHPでもrequire_onceなどで似たようなこともできる。が、PHPではこれをクラスごとに記述するのが煩雑だとして、自動で読み込む仕組みがいくつかある。
GNU socialでも <https://notabug.org/gnusocialjp/gnusocial/src/main/lib/util/framework.php> でspl_autoload_registerを使っている。
基本的にはcomposerのautoloadかPHP標準のspl_autoload_registerの2択になっている。
基本的な使用方法。
<?php
spl_autoload_register(function ($class_name) {
include $class_name . '.php';
});
$obj = new MyClass1();
$obj2 = new MyClass2();
?>
MyClass1.php MyClass2.phpから該当クラスを自動読み込みする。
該当クラスを使おうとしたときに、spl_autoload_registerに登録した関数が呼ばれる模様。
spl_autoload_registerは、指定した関数を__autoload()の実装として登録する。順番に登録する。
spl_autoload_register(?callable $callback = null, bool $throw = true, bool $prepend = false): bool
callback: callback(string $class): void 。重要。nullを指定するとデフォルトのspl_autload()が登録される。$classにはクラスの完全修飾子が入る。
このcallback内で独自のrequire_once相当をいろいろ指定する形になる。
ただ、今はcomposerを使うのが主流。
https://chatgpt.com/c/67355ddc-166c-800b-93ee-a25843cb2839
spl_autoloader_registerとcomposerのオートローディングはどちらを使ったほうがいいですか?
spl_autoloader_register
- 利点
- 軽量で柔軟:シンプルなプロジェクトでは、1つの関数で基本的なオートローディングを簡単に設定できます。
- 依存関係がない:Composerのような外部ツールや設定ファイルを必要としないので、PHP単体で実装できます。
- 欠点
- 設定が手動:名前空間とディレクトリのマッピングを手動で設定する必要があり、大規模なプロジェクトでは管理が複雑になりがちです。
- 標準規約に対応するのが難しい:
PSR-4のような標準的なオートローディング規約(ファイル構造と名前空間のマッピングルール)を自分で実装する必要があるため、コードが煩雑になる可能性があります。 - 依存ライブラリの管理が難しい:外部ライブラリを含める場合は、
requireやincludeで個別に読み込む必要があり、依存管理が困難です。
composer
- 利点
- 自動設定:
PSR-4対応の名前空間とディレクトリのマッピングを自動的に管理するため、設定が簡単です。 - 依存ライブラリの自動管理:Composerは依存する外部ライブラリもインストールしてオートロードするため、大規模なプロジェクトで便利です。
- メンテナンスが容易:プロジェクトの規模が大きくなっても、
composer.jsonファイルで管理できるため、ファイル構成や依存関係の変更に対応しやすいです。
- 自動設定:
- 欠点
- Composer依存:Composerがインストールされていないと使えません。また、プロジェクトに
composer.jsonの設定が必要です。 - 追加の学習が必要:Composerの使い方や設定ファイルの理解が必要ですが、習得すれば特に問題にはなりません。
- Composer依存:Composerがインストールされていないと使えません。また、プロジェクトに
コンストラクターとデストラクター
__construct(mixed ...$values = ""): void
コンストラクターメソッド。オブジェクト生成の度に呼ばれる。オブジェクト初期化処理用のメソッド。引数をプロパティーに設定したりする。
子クラスにコンストラクターがある場合、親クラスのコンストラクターはデフォルトでは呼ばれない。必要があるならparent::__construct()の呼び出しが必要。
逆に、子クラスにコンストラクターがない場合、親クラスのコンストラクターがデフォルトで呼ばれる (ただし、praivate宣言されている場合は呼ばれない)。
コンストラクターは扱いが特殊で、他のメソッドのオーバーライドと異なり、シグネチャーが異なっていても問題ない。
古いスタイルのコンストラクター
なお、PHP 7までは、グローバル名前空間のクラスの場合、クラス名と同名のメソッドも古いスタイルのコンストラクターとして解釈される (C++系)。__constructと両方ある場合、__constructが優先される。原則__constructを使うこと。
コンストラクターのプロモーション
PHP 8.0.0以上の機能。コンストラクターの引数をプロパティーに昇格可能。コンストラクターの引数をプロパティーに代入して、それ以外の操作を行わないことがよくある。これの短縮記法。
<?php
class Point {
public function __construct(protected int $x, protected int $y = 0) {
}
}
<?php
class Point {
protected int $x;
protected int $y;
public function __construct(int $x, int $y = 0) {
$this->x = $x;
$this->y = $y;
}
}
コンストラクターの引数に、public/private/protectedのアクセス権がある場合に、プロパティーかつ引数とみなし、該当プロパティーにも最初に代入する。
コンストラクターをスマートに記述できる。できれば使いたい機能。
空の__construct()
https://chatgpt.com/c/67fdac9a-2fac-800b-91bf-460c10fd72f5
たまに空のコンストラクターに出くわす。親クラスのコンストラクターを呼びたくないときに書く。
空のコンストラクターが必要な場面は限られる。
- 親クラスが「全クラスに適用しなくてもいい」処理を __construct() に入れている(=設計の問題とも言える)
- テスト用ダミークラスで、初期化を意図的に無効化したい
- 抽象クラスで abstract public function __construct() を定義しているので、実装側がとりあえず空で書く必要がある
- レガシーコード対応や互換性維持のため、一時的に親の初期化を無効にしたい
ただ、基本は設計の問題。空のコンストラクターはないほうがいい。
Visibility/アクセス権
プロパティー、メソッド、定数 (PHP 7.1.0以上) にはpublic/protected/privateのアクセス権 (visibility) を指定できる。
- public: どこからでもアクセス可能。
- protected: クラス自身、継承クラス、親クラス。
- private: クラス自身。
アクセス権を省略した場合、public扱いになる。
なお、同じ型のオブジェクト間では、同一インスタンスでなくても、protected/privateにもアクセス可能。オブジェクト内ではオブジェクト実装が既知だから。
アクセス権の指定は、PHP 5から導入。PHP 4ではvar指定で全部publicだった。Doxygenや命名規則 (先頭_はprivate) などで区別していた (https://chatgpt.com/c/67a031c2-ce24-800b-b1bf-7d4668b58fb4)。
protectedと自分と血縁関係のあるクラスでは呼べる。親でも呼べるのがミソ。
オブジェクトの継承
オブジェクトの継承を利用することで、共通機能の再実装を省略したりできる。
内部クラスと戻り値の型の互換性
PHP 8.0以下のPHPでは、内部クラスやメソッドの戻り値の型をほぼ宣言していなかった。その結果、継承後にあらゆる戻り値を指定できていた。PHP 8.1以上では戻り値の型をとりあえず宣言するようになった。この場合、継承メソッドの戻り値の型は親と互換性があるものにすべき。そうでないと、非推奨の警告が発生する。注意が必要なのは、戻り値の型の宣言がない場合も警告が出ること。
PHPのバージョン間の互換性の維持で、戻り値の型を宣言できない場合、ReturnTypeWillChangeのアトリビュートを追加することで新しいバージョンでの警告を抑止できる。
<?php
class MyDateTime extends DateTime
{
/**
* @return DateTime|false
*/
#[\ReturnTypeWillChange]
public function modify(string $modifier) { return false; }
}
// PHP 8.1.0 以降では、"Deprecated: Return type of MyDateTime::modify(string $modifier) should either be compatible with DateTime::modify(string $modifier): DateTime|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice"
// 警告は発生しません。
?>
ただ、このアトリビュートは子クラスの方に指定が必要。子クラス側の修正が必要なら、そのまま型をつけたらいいので意味がない。
親クラス側は型をつけないで、phpdocにだけにしておいて、子クラス側で型をつける。子クラス側で厳密にするのは許容される。LSPの原則にしたがっている (PHP: クラスの基礎 - Manual)。
スコープ定義演算子 (::)
- PHP: スコープ定義演算子 (::) - Manual
- PHP: 遅延静的束縛 (Late Static Bindings) - Manual
- [PHPstaticメソッドとstatic::に関するメモ #初心者 - Qiita]
スコープ定義演算子 (::) はトークンの一つ。定数、staticプロパティー、staticメソッド、親クラスなどにアクセスできる。
[Paamayim Nekudotayim] とも呼ぶ。ダブルコロンを意味するヘブライ語らしい。
staticメソッド/プロパティーは、遅延静的束縛 (Late Static Bindings) でアクセス可能。
- MyClass::CONST_VALUE/$classname::CONST_VALUE;
- self::$my_static
- parent::CONST_VALUE
- static: 実行時に最初の呼び出しクラスを参照。
staticは少々ややこしい。基本はself::でよいと思う。
staticキーワード
プロパティーやメソッドをstaticで宣言でき、インスタンス化せずにアクセス可能にできる。ユーティリティー系のメソッドなどの使用に使ったりする。
オブジェクト内部でもself::<method>などで使える。
なお、他の言語だとstaticをつけると基本はpublicを想定になると思うが、PHPは別にprotectedやprivateのアクセス権にもできる。
https://chatgpt.com/share/684253f1-5f08-800b-9180-e1c60db97d23
アクセス権とstaticキーワードの記述順序は、アクセス権→staticになる。これはPSR-12で推奨されているのが理由。
public static method(): void {}
なお、abstractやfinalはアクセス権・可視性より先に書く。
遅延静的束縛 (Late Static Bindings)
PHP: 遅延静的束縛 (Late Static Bindings) - Manual
staticと関連する機能。親クラスと子クラスとでどちらのクラスのスコープでstaticにするかを区別する機能。
static::でメソッドやプロパティーを呼ぶこと。
https://chatgpt.com/c/6800595c-e6fc-800b-aa0d-101df0b3fbbc
例えば、親クラスのstaticメソッドにstatic変数があるとする。この親クラスを継承した子クラスで、このstatic変数は全体で共有になるのか、子クラスで共有になるかの違いが重要。
子クラス名::methodで呼び出すと、子クラス扱いで、それぞれのクラスで独立になる。
- self:: メソッド定義元 (親クラス) で解決。
- static:: メソッド呼び出し元 (子クラス) で解決。
親クラス内のメソッドでstatic変数参照時に、基本はstatic::で呼んで、子クラスのスコープで扱うのが基本になるだろう。
Traits
About
コード再利用のための仕組み。単一継承言語で、コードを再利用するための仕組み。関数クラス (デリゲート) 的なもの。
PHP 5.4から登場した模様 (PHP5.4 で実装された trait のまとめと実際の利用例|技術ブログ|北海道札幌市・宮城県仙台市のVR・ゲーム・システム開発 インフィニットループ)。
Trait自体の英単語には特性,特色,特徴という意味がある。
クラスに関数クラスのメソッドを取り込める。インスタンス生成などはできず、関数を水平方向で構成可能にする。継承しなくても、メンバーに追加できる。
プロパティーもいける。
<?php
trait ezcReflectionReturnInfo {
function getReturnType() { /*1*/ }
function getReturnDescription() { /*2*/ }
}
class ezcReflectionMethod extends ReflectionMethod {
use ezcReflectionReturnInfo;
/* ... */
}
class ezcReflectionFunction extends ReflectionFunction {
use ezcReflectionReturnInfo;
/* ... */
}
?>
優先順位
規定クラスのメンバーよりも、useで取り込んだトレイトのほうが優先される。
- 現在のメンバー
- トレイト
- 継承メソッド
トレイトを組み合わせたトレイト
クラスでextendsで継承できるように、トレイトも他のトレイトを使える。
<?php
trait Hello {
public function sayHello() {
echo 'Hello ';
}
}
trait World {
public function sayWorld() {
echo 'World!';
}
}
trait HelloWorld {
use Hello, World;
}
class MyHelloWorld {
use HelloWorld;
}
トレイトのメンバーの抽象化
トレイトでは、抽象メソッドを使って、トレイトを使うクラスに実装を強制したりできる。
PHP 7まではpublic/protectedだけ、8からはprivateのアクセス権が可能。
トレイトのstaticメンバー
トレイトにはstaticな変数 (メソッド内)、メソッド、プロパティーを定義できる。
ただし、staticメソッド・プロパティーは、トレイトのを直接使うのではなく、traitを使っているクラスに対して使うことが推奨されている。
プロパティー
トレイトにはプロパティーも定義できる。
<?php
trait PropertiesTrait {
public $x = 1;
}
class PropertiesExample {
use PropertiesTrait;
}
$example = new PropertiesExample;
$example->x;
?>
ただし、プロパティーを定義して、使う場合は条件がある。クラスで、互換性 (アクセス権、型、readonlyの有無、初期値) のない同名のプロパティーは定義できない。定義するなら同じか、定義しないほうがいい。
定数
PHP 8.2.0以降では定数も定義できる。ルールはプロパティーと同じ。互換性の条件にfinalが追加される。
interfaceとtrait
https://chatgpt.com/c/67cea053-9f50-800b-b22f-67aea7ac0dfe
interfaceは型の強制だけ。traitは処理の共通化。
interfaceを継承した中身をtraitで作るというのはあり。併用するのがいい気がする。併用しないなら、abstractだけで十分になる。
interfaceを使う必要があるのは、外部ライブラリー連携。引数で特定のメソッドを持っているクラスを受け取って、そのインスタンスのメソッドを呼びたい場合などに必要になる。デリゲート的な感じ。
Overloading/オーバーロード
一般的には多重定義の意味だが、PHPのオーバーロードはプロパティーやメソッドを動的に作る機能。マジックメソッドで実装される (オーバーロードメソッド)。
オーバーロードメソッドは、未宣言プロパティー・メソッドのアクセス時に発動する。
オーバーロードメソッドはすべてpublicで定義する。
プロパティーのオーバーロード
public __set(string $name, mixed $value): void public __get(string $name): mixed public __isset(string $name): bool public __unset(string $name): void
setは不在プロパティーへのデータ書き込み時に使用。
$instance[$name] = $value;
オブジェクトの反復処理
foreachなどにインスタンスを渡すと、アクセス権限のあるプロパティーを反復処理できる。
Magic/マジックメソッド
PHPのデフォルトの動作を上書きする特別なメソッドをマジックメソッドと呼んでいる。
どらも__ (アンダーバー2個) から始まる。__始まりの全メソッドはPHPで予約されているのでユーザー定義メソッドとしては非推奨。
以下がある。
- __construct
- __destruct
- __call
- __callStatic
- __get
- __set
- __isset
- __unset
- __sleep
- __wakeup
- __serialize
- __unserialize
- __toString
- __invoke
- __set_state
- __clone
- __debugInfo
__construct/__destruct/__clone以外の全マジックメソッドはpublic必須。E_WARNINGの警告が発生する。
__construct/__desctructは戻り値型を宣言してはいけない。
Other
クラス名の取得
- php - How do I get an object's unqualified (short) class name? - Stack Overflow
- PHP: get_class - Manual
- PHP クラス名::classはどういう処理?? #初心者 - Qiita
- PHP: クラスの基礎 - Manual
get_class($object); クラス名::class $object::class // PHP 8.0以上 (get_class相当) (new \ReflectionClass($obj))->getShortName(); // クラス名
基本は名前空間付きのフルパスでの取得。クラス名だけだとgetShortName()。クラスがなければnewで例外。
親クラスの取得
get_parent_classで親のクラス名を取得できる。
親プロパティーへのアクセス
https://chatgpt.com/c/67ce36dc-ce70-800b-b559-ad5ac75c3ee1
- get_parent_classでクラス名を取得してインスタンス化。
- ReflectionClass
- $this->{property}
- parent::$property/static::$property: 静的プロパティーの場合のみ。
Namespace
PHPの名前空間は、以下の2の問題の解決用の仕組み。
- 自作の関数や変数類の名前がPHPの組込と衝突。
- 名前衝突回避のために長い名前が必要。
なお、requie_onceによる別ファイルの読込か、オートロードで他のファイルなどのシンボルにアクセスできることが、前提になっている。
definition
名前空間の影響を受けるのは、以下。
- クラス
- インターフェイス
- 関数
- 定数
以下の構文でファイル先頭で宣言する。
namespace [Name]; namespace [Name]\[Sub];
ただし、declareは例外でnamespaceの前にも書ける。ただ、それ以外だとPHPコード以外も含めて記述不能。
同じ名前空間を複数のファイルで定義することも可能。これにより、ファイルをまたいで名前空間を共有できる。
また、名前空間は階層を持つことができる。バックスラッシュで区切る。
Multiple
PHP: 同一ファイル内での複数の名前空間の定義 - Manual
1ファイルで複数の名前空間の定義が可能。
namespace MyProject {
const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */ }
}
namespace AnotherProject {
const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */ }
}
namespace { // global code
session_start();
$a = MyProject\connect();
echo MyProject\Connection::start();
}
名前空間とグローバルを分ける場合、グローバルを名前を指定しないnamespaceで囲む。
Basic
ファイルへのアクセスに、相対パスと絶対パスがあるように、名前空間へのアクセス方法がいくつかある。
- $a = new foo(): 名前空間を指定しない場合。現在の名前空間currentnamespaceがあれば、currentnamespace\foo。なければグローバルのfoo。
- $a = new subnamespace\foo():
- $a = new \currentnamespace\foo(): 完全修飾名。グローバルプレフィクス演算子付きのクラス名。
現在の名前空間に該当シンボルが不在の場合、自動的にグローバル名前空間 (先頭\) も探す。
余計な検索が発生するので、わかっているならグローバルで最初から指定したほうがいいかも。
Importing
About
外部の完全修飾名をエイリアスで参照できる。use演算子を使う。namespaceで同じ名前空間にいないなら、useか完全修飾名を使う必要がある。
PHPでは、定数、関数、クラス、インターフェイス、トレイト、列挙型、名前空間のインポートに対応している。以下が全ケース。
<?php namespace foo; use My\Full\Classname as Another; // これは use My\Full\NSname as NSname と同じです use My\Full\NSname; // グローバルクラスをインポートします use ArrayObject; // 関数をインポートします use function My\Full\functionName; // 関数のエイリアスを定義します use function My\Full\functionName as func; // 定数をインポートします use const My\Full\CONSTANT; $obj = new namespace\Another; // foo\Another クラスのオブジェクトのインスタンスを作成します $obj = new Another; // My\Full\Classname クラスのオブジェクトのインスタンスを作成します NSname\subns\func(); // My\Full\NSname\subns\func 関数をコールします $a = new ArrayObject(array(1)); // ArrayObject クラスのオブジェクトのインスタンスを作成します // "use ArrayObject" がなければ、foo\ArrayObject クラスのオブジェクトのインスタンスを作成することになります func(); // 関数 My\Full\functionName を呼びます echo CONSTANT; // 定数 My\Full\CONSTANT の値を表示します ?>
// これは use My\Full\NSname as NSname と同じです use My\Full\NSname;
useで指定する際は、完全修飾形式。
use文はグループ化できる。
use some\namespace\ClassA;
use some\namespace\ClassB;
use some\namespace\ClassC as C;
use some\namespace\{ClassA, ClassB, ClassC as C};
インポート時のスコープ規則
useキーワードは、ファイル内の一番外側のスコープ (グローバルスコープ) か、名前空間宣言内で行う必要がある。これは、コンパイル時に行われるため。ブロック内では使えない。
また、インポートはファイル単位で行う。インクルード元の親ファイルのインポート規則は引き継がない。
標準関数のuse
https://chatgpt.com/share/682e97a2-a8a0-800b-bea4-ee3fffd91b5e
PHPUnitのTestCase.phpで、「phpunit/src/Framework/TestCase.php at main · sebastianbergmann/phpunit」標準関数をわざわざuse functionしていた。
現在の名前空間に、同名関数がある場合に、同じ名前でグローバル関数の方を呼びたい場合に必要。
他に、パフォーマンスのわずかな改善が見込めるらしい。
PHPの名前解決に以下のルールがある。
- 関数呼び出し時に、現在の名前空間にその関数を検索。
- なければグローバル名前空間を探す。
現在の名前空間の検索→グローバル名前空間の検索の順番になるため、最初から現在の名前空間にあるほうが、1ステップ名前解決が不要になって、ほんの少しだけ早くなるとのこと。
特に、パフォーマンスが重要なライブラリー・ツール系コードで、使用する全標準関数はuse functionで明示するスタイルが浸透しているらしい。
確かに、以下のコードで100万回実行で、0.03 sわずかだが早い。namespaceがあると顕著。
<?php
namespace Example;
use function strlen;
function main(): void
{
$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
strlen("hello");
}
$end = microtime(true);
echo "No use function: " . ($end - $start) . " seconds\n";
echo "No use function: 0.0078551769256592 seconds\n";
echo "No use function: 0.033313989639282 seconds\n";
}
main();
効果あるから、記載できるときはしたほうがよさそう。
PHP Warning: The use statement with non-compound name
n
d
PHP Warning: The use statement with non-compound nameがでるのはグローバル脳 #PHP - Qiita
さっきの、上記のstrlenの検証コードを試すと、以下のような警告が出る。
Warning: The use statement with non-compound name 'strlen' has no effect in /home/user/scripts/code.php on line 2
これは、現在ファイルがグローバル名前空間なのに、インポートしていて、意味がないという警告。
名前空間を定義したら消える。
Global
名前の先頭に\をつけるとグローバル空間の名前を指定できる。
$f = \fopen(...)
Exception
例外という機能がある。致命的な問題が発生したら、例外がthrowされる。例外がthrowされる可能性のある関数は、tryブロックで囲み、catch/finallyで処理する。または、上位のtryに扱いをゆだねる。上位のtryがない場合、fatal errorで強制終了となる。
https://grok.com/share/c2hhcmQtMw%3D%3D_e8cfa6f5-65e1-4929-8ff6-df98c10e742a
例外が発生したら、そのタイミングで処理が中断される。変数への代入中だと、変数への代入はなされない。なので、後続で使いたければ、例外発生前に初期化しておく必要がある。
Generator
PHP 5.5から導入。
シンプルなイテレーターの実装のための機能。配列と異なり、大量のメモリーの確保が不要になる。
関数でreturnの代わりにyieldで値を返すとジェネレーター関数になる。yieldはGeneratorオブジェクトを返す。
PHPがyieldした時点の状態を記憶しており、次呼ばれたらその続きから処理してくれる。
<?php
function gen_one_to_three() {
for ($i = 1; $i <= 3; $i++) {
// yield を繰り返す間、$i の値が維持されることに注目しましょう
yield $i;
}
}
$generator = gen_one_to_three();
foreach ($generator as $value) {
echo "$value\n";
}
?>
yield時。
- yield $id => $fields;でキーバリューで返す。
- yeildだけだとNULL。
なお、ジェネレーターの値は前に進むことしかできない。
PHPで高速オシャレな配列操作を求めて #PHP - Qiita
真価を発揮するのは、巨大配列の処理や、配列処理の分割。
array_関数で配列を一括処理すると遅い。foreachでやると速いが、foreachの処理内容を分割できない。そういうときに、foreachの処理をyieldにすると、分割できる。後続の処理も、generator (配列) を引数に受け取る想定で作ると、連携できる。
echo (new Collection(range(0, 10000)))
->filter('$_ % 2 === 0')
->map('$_ ** 2')
->filter('$_ > 20')
->sum()
;
// 一部だけ切り出す
function my_special_logic($arr) {
return $arr
->filter('$_ % 2 === 0')
->map('$_ ** 2');
}
// 再利用
echo my_special_logic(new Collection(range(0, 10000)))
->filter('$_ > 20')
->sum();
$mapped = [];
for ($v = 0; $v <= 10000; ++$v) {
if ($v % 2) continue;
$v **= 2;
if ($v <= 20) continue;
$mapped[] = $v;
}
echo array_sum($mapped);
// こんな関数は作れない
function my_special_logic($v) {
if ($v % 2) continue;
$v **= 2;
return $v;
}
function my_special_logic($arr) {
foreach ($arr as $v) {
if ($v % 2) continue;
$v **= 2;
yield $v;
}
}
$sum = 0;
foreach (my_special_logic(range(0, 10000)) as $v) {
if ($v <= 20) continue;
$sum += $v;
}
echo $sum;
後続処理を関数にしたいなら、next_func(my_special_logic(range(0, 10000)))のようにする。きれい。
Attribute
About
PHP 8から登場した機能。PHPDocでやっているアノテーション相当を言語機能で実現できる。
クラス、メソッド、関数、パラメーター、プロパティー、クラス定数に指定可能。アトリビュートで定義済みのメタデータはリフレクションAPIで調査可能。
Syntax
#[<attribute-name>(: )]
#[\Override]
#[]で囲む。角括弧[]内部では、1以上のアトリビュートが並び、カンマ,で区切る。アトリビュート名は名前空間のルールに従う。
アトリビュートの引数は任意だが、丸括弧()で囲んで指定する。指定する場合、リテラルか定数式のみが可能。位置指定と名前付き引数の両方が可能。
PHP 8未満では、#でコメントになるので、無視される。
定義済みアトリビュート
そんなに数がない。
- Attribute
- AllowDynamicProperties
- Deprecated
- Override: PHP 8.3。メソッドに指定。親クラスに同名メソッドがなければエラーになる。メソッド名間違いを防げる。
- ReturnTypeWillChange
- SensitiveParameter
以下の2個が特に重要。
- Attribute
- Override
ひとまずOverrideだけ使う感じでもいいと思う。
Reserved
keywords
式や関数ではなく、定数、クラス名、関数名として使えず、PHPで予約されている特別なキーワードがいくつかある。
statement/文に近い扱い。言語構文の一部扱い。
| __halt_compiler() | abstract | and | array() | as |
| break | callable | case | catch | class |
| clone | const | continue | declare | default |
| die() | do | echo | else | elseif |
| empty() | enddeclare | endfor | endforeach | endif |
| endswitch | endwhile | eval() | exit() | extends |
| final | finally | fn (PHP 7.4 以降) | for | foreach |
| function | global | goto | if | implements |
| include | include_once | instanceof | insteadof | interface |
| isset() | list() | match (PHP 8.0 以降) | namespace | new |
| or | private | protected | public | |
| readonly (PHP 8.1.0 以降) * | require | require_once | return | static |
| switch | throw | trait | try | unset() |
| use | var | while | xor | yield |
| yield from |
* readonly は、関数名として使用できます。
| __CLASS__ | __DIR__ | __FILE__ | __FUNCTION__ | __LINE__ | __METHOD__ |
| __NAMESPACE__ | __TRAIT__ |
Interfaces
PHP: 定義済みのインターフェイスとクラス - Manual
stdClass
動的なプロパティーが使える、汎用的な空クラス。このクラス自体は、メソッドやプロパティーを持たない。
json_decodeなど一部の関数がこのインスタンスを返す。
// 型変換での作成。連想配列を(object)にキャストすると作れる。
(object) array('foo' => 'bar');
データベースの取得結果が、連想配列の他に、stdClassになっていることがある。
匿名オブジェクトや、動的プロパティーなどが主な利用方法。
データホルダーとして使う場合、連想配列のキーのほうが、自由度が高いので、そちらのほうが便利だと思われる。たくさんある配列関数も使えるし。
Constants
いくつか重要なものがある。
- PHP_EOL: プラットフォームの行末文字。使う場面は限られている。基本は"\n"でよい。
- PHP_BINARY: 現在実行中のphpコマンドのフルパス。PHP内部から外部のPHPスクリプトコマンドの実行時などで使う。PHP 5.4から使用可能らしい (How can I get the current PHP executable from within a script? - Stack Overflow)。
Wrappers
PHP: サポートするプロトコル/ラッパー - Manual
URL風のプロトコル (ラッパー/wrapper) で、ファイルシステム関数で使用できる。stream_wrapper_registerで自作もできる。
php://
php://memory php::temp
PHP: Is it possible to create a SplFileObject object from file contents (string)? - Stack Overflow
読み書き可能なストリーム。stringのような一時データをファイルのように保存できる。php://memoryは常にメモリー。php://tempはデフォルト2 MB超過でテンポラリーファイルを使う。php://temp/maxmemory:NNで上限を指定できる。単位はバイト。テンポラリーファイルの格納場所はsys_get_temp_dir。
データをファイルとして扱いたい場合、1回書き込んでから読み込む必要がある。
$contents = 'i am a string'; $file = 'php://memory'; // full memory buffering mode //$file = 'php://temp/maxmemory:1048576'; //partial memory buffering mode. Tries to use memory, but over 1MB will automatically page the excess to a file. $o = new SplFileObject($file, 'w+'); $o->fwrite($contents); // read the value back: $o->rewind(); $o->fread(); // 'i am a string'
Features
Ref: PHP: Features - Manual.
PHP による HTTP 認証
- PHP: PHP による HTTP 認証 - マニュアル
- PHPを利用したBasic認証の仕組み #Apache - Qiita
- PHPによる簡単なログイン認証いろいろ #Security - Qiita
PHPでのBasic認証の話。
Basic認証が発動すると、$_SERVERのPHP_AUTH_USER/PHP_AUTH_PWに値が入る。
AUTH_TYPEは実際は使っていない模様。以下で確認するとよい。
isset($_SERVER['PHP_AUTH_USER']);
HTTP_AUTHORIZATION
このHTTP_AUTHORIZATIONがよくわからない。GNU socialやWordPressに以下の.htaccessがあって気になる。
#RewriteCond %{HTTP:Authorization} ^(.*)
#RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]
CGI/FastCGI版のPHPの場合、HTTP_AUTHORIZATIONヘッダーが自動で設定されない。代わりに、以下を記述して、サーバーで設定する必要があるらしい。
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
E=key:valueで環境変数keyにvalueを代入する。
気になるので調べる。
「PHP: HTTP authentication with PHP - Manual」の2014年の昔の同じマニュアルには以下の記載があった。
Another limitation is if you're using the IIS module (ISAPI) and PHP 4, you may not use the PHP_AUTH_* variables but instead, the variable HTTP_AUTHORIZATION is available. For example, consider the following code: list($user, $pw) = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
$_SERVER['HTTP_AUTHORIZATION'] にBasic dGVzdDp0ZXN0のような値が入っており、右側がuser:passのbase64エンコードデータなので、それをデコードして使っていた模様。
上記の記述は以下の2コミットで削除された。
- Not supporting HTTP authentication using CGI is legacy and so old tha… · php/doc-en@dd3df87
- Remove inline PHP 4 refereces - part 1 · php/doc-en@2bb07c8
IISモジュールとPHP 4では、PHP_AUTH_*変数が使えなかったようで、その対策としてHTTP_AUTHORIZATION変数を使っていた模様。
For HTTP Authentication to work with IIS, the PHP directive cgi.rfc2616_headers must be set to 0 (the default value).
上記の記載通り、IISに関してはデフォルトでPHP指令で有効になっているので、解決している。
今はPHP_AUTH_USER/PHP_AUTH_PWを使えばいいので、これらに置換していけばよい。
なお、HTTP_AUTHORIZATIONは「RFC 3875 - The Common Gateway Interface (CGI) Version 1.1」にも登場しているが、ほぼ言及はない。
Handling file uploads
Ref: PHP: Handling file uploads - Manual.
input type="file"などのアップロードファイルのPHPでの処理方法・作法がある
<!-- データのエンコード方式である enctype は、必ず以下のようにしなければなりません -->
<form enctype="multipart/form-data" action="__URL__" method="POST">
<!-- MAX_FILE_SIZE は、必ず "file" input フィールドより前になければなりません -->
<input type="hidden" name="MAX_FILE_SIZE" value="30000" />
<!-- input 要素の name 属性の値が、$_FILES 配列のキーになります -->
このファイルをアップロード: <input name="userfile" type="file" />
<input type="submit" value="ファイルを送信" />
</form>
PHP側では$_FILES['userfile']に必要な情報が格納される。
- $_FILES['userfile']['name']
- クライアントマシンの元のファイル名。
- $_FILES['userfile']['type']
- ファイルの MIME 型。ただし、ブラウザがこの情報を提供する場合。 例えば、
"image/gif"のようになります。 この MIME 型は PHP 側ではチェックされません。そのため、 この値は信用できません。 - $_FILES['userfile']['size']
- アップロードされたファイルのバイト単位のサイズ。
- $_FILES['userfile']['tmp_name']
- アップロードされたファイルがサーバー上で保存されているテンポラ リファイルの名前。
- $_FILES['userfile']['error']
- このファイルアップロードに関する エラーコード
- $_FILES['userfile']['full_path']
- ブラウザからアップロードされたファイルのフルパス。 この値は実際のディレクトリ構造を反映しているとは必ずしも言えないため、 信用できません。 PHP 8.1.0 以降で利用可能です。
tmp_nameが非常に重要。これをリネームする形で保存する。あとはnameも保存時のファイル名で重要。
<?php
$uploaddir = '/var/www/uploads/';
$uploadfile = $uploaddir . basename($_FILES['userfile']['name']);
echo '<pre>';
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
echo "File is valid, and was successfully uploaded.\n";
} else {
echo "Possible file upload attack!\n";
}
echo 'Here is some more debugging info:';
print_r($_FILES);
print "</pre>";
?>
上記がイメージ。
DBに保存する場合は「PHPとMySQLを利用した画像・動画のアップロード・保存・表示 #PHP - Qiita」も参考になる。
Using PHP from the command line
Ref: PHP: Command line usage - Manual.
About
簡単なコードの確認などでPHPをコマンドラインなどから簡単に実行したいことがよくある。いくつか方法がある (PHP: Usage - Manual)。
- phpコマンドの引数にファイルを指定:
php file.php/php -f file.php - phpコマンドの引数にコードを指定:
php -r 'print_r(get_defined_constants());' - phpコマンドに標準入力で読み込み:
php <file.php
標準入力が一番使いやすく感じる。
__name__ == "__main__"
phpファイルを直接実行時とインポート時とで分離する書き方。 同一ファイルでクラスと実行用ファイルとしたい場合などに必要となる。書き方がいくつかある。
「Python の if __name__ == ‘__main__’: を Perl, Ruby, PHP で行う : Serendip – Webデザイン・プログラミング」
if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
// do something
}
「PHPスクリプトが直接起動されたかどうかで処理を振り分ける | バシャログ。」
if (realpath($_SERVER["SCRIPT_FILENAME"]) == realpath(__FILE__)){
/** ここに処理を書いてね */
}
「PHP equivalent of Python's __name__ == "__main__"? - Stack Overflow」
if (!debug_backtrace()) {
// do useful stuff
}
このdebug_backgrace関数がうまい。ルートになるからバックトレースが空になる。realpathとかbasenameはパスの解析が生じるので遅くなる。
Function Reference
Affecting PHP's Behavior
Error Handling
Runtime Configuration
PHPのエラー設定を整理する。 PHPのエラー設定は「PHP: Runtime Configuration - Manual」で一覧化されている。
xmlrpc_errors, syslog.facility, syslog.ident以外はどこでも設定可能。
特に重要なのが以下の設定。
| 設定 | 初期値 | 説明 |
|---|---|---|
| error_reporting | E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
|
エラー出力レベルを設定する。開発時にはE_ALL (2147483647/-1) にしておくとよい。 |
| display_errors | "1" | エラーのHTML出力への表示を設定する。"stderr"を指定すると,stderrに送る。デフォルトで有効なのでそのままでいい。 |
| display_startup_errors | "0" | PHPの起動シーケンス中のエラー表示を設定する。デバッグ時は有効にしておいたほうがいい。 |
| log_errors | "0" | エラーメッセージのサーバーのエラーログまたはerror_logへの記録を指定する。これを指定しないとログが残らないため,常に指定したほうがいい |
| error_log | NULL | スクリプトエラーが記録されるファイル名を指定する。syslogが指定されると,ファイルではなくシステムロガーに送られる。Unixではsyslog(3)で,Windowsではイベントログになる。指定されていない場合,SAPIエラーロガーに送信される。ApacheのエラーログかCLIならstderrになる。 |
基本的に以下をphp.ini/.user.iniに設定しておけばよい。
error_reporting = E_ALL
display_startup_errors = on
log_errors = on
; For file
display_errors = stderr
htpd.conf/.htaccessの場合は以下。
php_value error_reporting -1
php_flag display_startup_errors on
php_flag log_errors on
# For file
php_value display_errors stderr
error_log
PHP標準のログ出力関数。
error_log(
string $message,
int $message_type = 0,
?string $destination = null,
?string $additional_headers = null
): bool
$messageをWebサーバーのエラーログに送る。
| 0 | message は PHP のシステムロガーに送られ、 設定ディレクティブ error_log の値に応じて、 オペレーティングシステムのシステムログ機構を使って保存されるか、 ファイルに保存されるかが決まります。 これがデフォルトのオプションです。
|
| 1 | message は、destination パラメータで指定されたアドレスに、電子メール により送られます。このメッセージタイプの場合にのみ、 4 番目のパラメータである additional_headers が使われます。
|
| 2 | このオプションは存在しません。 |
| 3 | message は destination で指定されたファイルに追加されます。 明示的に指定しない限り、message の 最後には改行文字は追加されません。
|
| 4 | message は、直接 SAPI のログ出力ハンドラに送信されます。
|
<?php
// データベースに接続できない場合、
// サーバーログを通してエラーを通知する。
if (!Ora_Logon($username, $password)) {
error_log("オラクルのデータベースが使用できません!", 0);
}
// FOO に失敗したら、管理者に email で通知する
if (!($foo = allocate_new_foo())) {
error_log("大変です。FOO に失敗しました!", 1,
"operator@example.com");
}
// これ以外の error_log() のコール方法:
error_log("大変だ!", 3, "/var/tmp/my-errors.log");
?>
error_logのデフォルトの出力先はphp.iniのerror_log。php -i | grep error_logでわかる。
error_log(__FILE__.':'.__CLASS__.':'.__FUNCTION__.':'.__LINE__."\n");
これでファイル・関数・行数を表示できる。
logrotate
- ライブラリー
- 自前
- logrotate
出力したログファイルがストレージを圧迫しないように、一定サイズ・期間でリネームして、最大保持数を維持したりする。
いくつか方法がある。
- GNU/Linuxのlogrotateコマンド
- logrotateライブラリー
- 自前実装
やることは決まっているのだから、自前で実装してもいいかもしれない。
- ログ出力時
- ログ出力ファイルのサイズを確認して、設定サイズより大きければ、循環。
- 最古のログファイルを削除して、順番にリネーム。
- 最後に出力。
それだけ。
const LOG_DIRECTORY = '/var/log/logdir/'; // ログディレクトリ
const LOG_FILENAME = 'logfname.log'; // ログファイル名
const LOG_FILEPATH = LOG_DIRECTORY.LOG_FILENAME; // ログのファイルパス
const MAX_LOTATES = 3; // ログファイルを残す世代数
const MAX_LOGSIZE = 1024*1024; // 1ファイルの最大ログサイズ(バイト)
function WriteLog($strlog){
// 保存先ディレクトリを作成
if(!file_exists(LOG_DIRECTORY)){
mkdir(LOG_DIRECTORY);
}
// ログのローテート
if(@filesize(LOG_FILEPATH) > MAX_LOGSIZE){
// 最古のログを削除
@unlink(LOG_FILEPATH.strval(MAX_LOTATES));
// ログをリネーム .log → .log_01
for ($i = MAX_LOTATES - 1; $i >= 0; $i--) {
$bufilename = ($i == 0) ? LOG_FILEPATH : LOG_FILEPATH.strval($i);
@rename($bufilename, LOG_FILEPATH.strval($i+1));
}
}
// ログ出力
file_put_contents(LOG_FILEPATH, date('y-m/d-H:i:s ').$strlog."\n", FILE_APPEND | LOCK_EX);
}
もう少しいい実装方法はありそう。
プロパティーの変更検知
https://chatgpt.com/c/67ad8e2f-7544-800b-a087-8247b582672e
__set/__getをオーバーライドしてログを仕込む。
class Test {
private array $data = [];
public function __set($name, $value) {
echo "プロパティ '{$name}' が '{$value}' に変更されました。\n";
$this->data[$name] = $value;
}
public function __get($name) {
return $this->data[$name] ?? null;
}
}
debugtrace
PHPのバックトレース (関数コールバックツリー) を表示する方法がいくつかある。
debug_print_backtraceは標準出力に出力するだけ。ログファイルへの出力はしにくい。
debug_backtraceは引数など情報量が多すぎる。以下のように、ファイル名、関数名、行数を抽出して出力するのがいい。
@array_walk(debug_backtrace(), function($v){error_log(
implode(':', [$v['file'],$v['class'],$v['function'],$v['line']])
);});
@array_walk(debug_backtrace(), fn($v) => error_log(
implode(':', [$v['file'],$v['class'],$v['function'],$v['line']])
));
@をつけてエラーを抑止しないと、array_walkは参照を引数にとるから以下のような警告が出る。
Notice: Only variables should be passed by reference in /home/user/scripts/code.php on line 9
出力制御
https://grok.com/share/c2hhcmQtMw%3D%3D_9a6601a9-d5d4-4dad-bf58-1c6208d03b90
header/setcookieには作用せず、echoのような標準出力を使う関数に作用する。
例えば、echoした後にheaderでヘッダーをブラウザーに出力したい場合などに、出力制御を行う。
出力バッファリングはPHPスクリプトの実行中に生成される出力を、バッファーにためておいて、必要に応じて取得・変更・削除する仕組み。
Example
<?php
ob_start();
echo "Hello\n";
setcookie("cookiename", "cookiedata");
ob_end_flush();
?>
この例では、echoからの出力は、ob_end_flushが呼ばれるまで、出力バッファーに保存される。
ob_startでoutputbufferingを開始する。最後に、ob_end_flushで破棄するか、ob_get_flushで取得・出力しながら消去するのが基本。
関数
いろいろ関数がある。
- 開始・終了
- ob_start: obの開始。この関数実行後の出力をバッファーに貯める。
- ob_end_clean: バッファーを終了し、内容を破棄する。
- ob_end_flush: バッファーを終了し、出力する。
- ob_get_clean: 現在のバッファーの内容を取得し、バッファーを削除して、終了する。1回限りの取得で使う。通常はこれで事足りる。
- 取得
- ob_get_contents: 現在のバッファーをstringで取得する。
- ob_get_clean: 現在のバッファーの内容を取得し、バッファーを削除する。1回限りの取得で使う。通常はこれで事足りる。
- 解放
- ob_get_flush: バッファーの内容を出力して、バッファーを終了する。表示しつつ、内容を取得したい場合に使う。
- ob_flush: バッファーの内容を出力するだけ。終了はしない。
- ob_clean: 現在のバッファーを破棄。終了はしない。
- 自余
- ob_get_length: 現在のバッファーの長さ取得。
- ob_get_level: バッファーのネストレベルを取得。
flushの接尾辞は出力しちゃうので注意する。getは文字列で取得する。endは終了。
- ob_start
- echo
- ob_get_clean
基本はこの流れ、複数回取得するならば、ob_get_contentsした後に、ob_end_cleanで明示的に終了する。
obで取得できない出力がある場合、デストラクターで出力されている可能性が高い。その場合、該当変数をobの中でunsetして強制破棄すれば取り込める。
PHP Options/Info
PHP: PHP Options/Info - Manual
PHP事態に関する情報の取得関数群。assert/phpinfo/extension_loadedなどいくつか重要な関数が存在する。
assert
memory
PHPのメモリー使用量の計測関数がある。
memory_get_peak_usage
バイト単位で返す。引数にtrueを指定すると実際に割り当てた大きさ。trueを指定しなければemallocが使用するメモリーのみ。
PHPの使用最大メモリーのチェックでは基本は引数は不要。
echo "memory_get_peak_usage: " . memory_get_peak_usage() / (1024 * 1024) . "MB";
php.iniの設定でmemory_limitというのがあり、デフォルトはだいたい128 MB。
phpinfo
PHPに関する情報をHTMLで表示する。重要。
phpcgi -i php -r "phpinfo();" php -m
CLIモードだとHTMLではなくプレーンテキスト。
環境変数
- getenv
- putenv: サーバーの環境変数に追加。現在のリクエスト実行中のみ存在。
php.ini
- get_cfg_var
- ini_get
- ini_get_all
- ini_restore
- ini_set
暗号
パスワードのハッシュ
パスワードハッシュAPI。ユーザー入力したパスワード類を安全にDBに保存するために、平文を暗号化する。
https://grok.com/share/c2hhcmQtMw%3D%3D_eb2f184e-0b13-48ef-abdb-e547f239b045
Crypt_Blowfishというアルゴリズムがあって、これの実装がいくつかある。
その1個にPEAR::Crypt_Blowfishというのがある。が、今はPHP標準のpassword_hashが推奨の模様。
PEAR Crypt BlowfishをOpenSSLにリプレースする #PHP - Qiita
opensslのほうが推奨らしい。
Database
PHPでのDBの操作方法が大きく2種類ある。抽象化レイヤーと、DB固有のモジュール。今は抽象化レイヤーの内、PDOというPHP独自の仕組みが主流。
ChatGPTで調べたところ以下の違いがある。
- DBA: Berkeley DB/GDBM/QDBMなど。扱うDBの種類が特殊。ファイルベースのDBで、キー・バリュー形式で設定ファイルなど。一部の設定ファイル・キャッシュ向け。軽量なファイルベースのデータ保存向け。
- ODBC: Microsoft作成。他の言語でも使える。PDOより対応可能なDBの幅が広い。ODBCのライブラリーを使う形。ただし、汎用的なのでPDOよりも重いし汎用的だから設定が複雑。レガシーシステム向け。
- PDO: PHP独自。性能も申し分ないし基本的にはこれを使うのがいい。
PDO
Introduction
PHP Data Objects。PHPからDBへのアクセスの軽量で高性能なインターフェイス。DBの全関数を実行できるわけではない。DBアクセスの抽象化レイヤーを提供する。つまり、DBが何であろうが、同じ関数でSQLの発行、データ取得ができる。
Connections
Open
PDOインスタンスの作成で接続ができる。
<?php
try {
$driver_options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
$dbh = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', $user, $pass, $driver_options);
} catch (PDOException $e) {
// たとえば、タイムアウトしたあとに再接続を試みます
}
?>
引数にDSNと、ユーザー名、パスワードを指定する。
PDOインスタンスが存在する間、接続が継続する。
option
PDOの最後の引数で、ドライバーごとのオプションを設定できる。後からsetAttributeでも指定できる。
- PDO::ATTR_CASE
- PDO::ATTR_ERRMODE: SQL実行時のエラーの扱い。デフォルトPDO::ERRMODE_SILENTで何も報告しない。PDO::ERRMODE_EXCEPTIONを設定するのがいい。
- PDO::ATTR_ORACLE_NULLS
- PDO::ATTR_STRINGIFY_FETCHES
- PDO::ATTR_STATEMENT_CLASS
- PDO::ATTR_TIMEOUT
- PDO::ATTR_AUTOCOMMIT
- PDO::ATTR_EMULATE_PREPARES
- PDO::MYSQL_ATTR_USE_BUFFERED_QUERY
- PDO::ATTR_DEFAULT_FETCH_MODE
Close
接続終了時には、明示的にオブジェクトを破棄する必要がある。具体的には、変数にnullを代入する。
<?php
$dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
// ここで接続を使用します
$sth = $dbh->query('SELECT * FROM foo');
// 使用を終了したので、閉じます
$sth = null;
$dbh = null;
?>
nullにしない場合、script終了時に閉じられる。
ただ、通常はすぐに単体のscriptは終了するので、いちいち書く必要はない。
charset
- PHPでデータベースに接続するときのまとめ #MySQL - Qiita
- PHP: PDO_MYSQL DSN - Manual
- PHP: 文字セット - Manual
- PHP What is the default charset for pdo mysql - Stack Overflow
- php - Are PDO prepared statements sufficient to prevent SQL injection? - Stack Overflow
DSNにcharsetを指定できる。これはDBのクライアントの送受信に使うエンコーディングとのこと。
The default is utf8 in MySQL 5.5, 5.6, and 5.7, and utf8mb4 in 8.0.
指定したほうがいい模様。
Query
SQLを実行するためのPDOメソッドがいくつかある。
- PHP: PDO::exec - Manual: 引数のSQLを実行して、影響のある行数を返す。結果のいらないSQLやSELECT以外のINSERT/UPDATEなどで使う。
- PHP: PDO::query - Manual: 引数のSQLを実行して、実行結果をPDOStatementで取得する。ユーザー入力を伴わない固定SQLで使う。似たようなSQLをループなどで複数回実行するならprepareのほうが性能が向上する。
- PHP: PDO::prepare - Manual: PHP: PDOStatement::execute - Manual用のSQL文を用意する。プレースホルダーを用意する。プリペアードステートメントというSQLの機能を使う。ユーザー入力をエスケープしたり、一部分だけ異なるようなSQLがキャッシュされて効率が上がる。
prepared statement
PHP: プリペアドステートメントおよびストアドプロシージャ - Manual
重要。パラメーターマーク/プレースホルダーを配置したSQL文を用意して、後でプレースホルダーに変数をバインドしてSQLを実行する。
SQL文末にセミコロン;は不要。逆にあると、Oracleなどで [ORA-00933: SQLコマンドが正しく終了されていません] になったりする。
placeholder
プレースホルダーとして、名前付きパラメーターと疑問符パラメーターの2種類がある。
- 名前付きパラメーター: [:name] の形式で配置する。バインド時は名前。数が多い場合。バインド時は先頭の:は省略可能。compact関数で短縮できる。
- 疑問符パラメーター: [?] を配置する。バインド時は0開始の番号。数が少ない場合シンプル。PHP 7.4.0以上で??で?自体をエスケープ。
なお、名前付きと疑問符は混在できない。プレースホルダーには、SQLのデータリテラルのみを配置できる。SQLの文などはだめ。
プレースホルダーはデータリテラル全体に適用が必要。つまり、LIKEの%は値に含める必要がある。代わりに、引用符がいらない。
<?php
/* 値の配列を渡してプリペアドステートメントを実行する */
$sql = 'SELECT name, colour, calories
FROM fruit
WHERE calories < :calories AND colour = :colour';
$sth = $dbh->prepare($sql, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]);
$sth->execute(['calories' => 150, 'colour' => 'red']);
$red = $sth->fetchAll();
/* 配列のキーの前にも、コロン ":" を付けることができます(オプション) */
$sth->execute([':calories' => 175, ':colour' => 'yellow']);
$yellow = $sth->fetchAll();
?>
<?php
/* 値の配列を渡してプリペアドステートメントを実行する */
$sth = $dbh->prepare('SELECT name, colour, calories
FROM fruit
WHERE calories < ? AND colour = ?');
$sth->execute([150, 'red']);
$red = $sth->fetchAll();
$sth->execute([175, 'yellow']);
$yellow = $sth->fetchAll();
?>
$stmt->execute([1 => $gender, 0 => $city]);
疑問符の場合、キーを指定すれば、順番を変更してもOK。
- php - PDO valid characters for placeholders - Stack Overflow
- php-src/ext/pdo/pdo_sql_parser.re at 5638fafa665899523861a12966b42a9c609a1664 · php/php-src · GitHub
名前付きパラメーターに使える文字には制限がある。
BINDCHR = [:][a-zA-Z0-9_]+;
英数字と_のみ。日本語のカラム名をそのまま使うことはできない。
function encode_all($str) {
return preg_replace('~..~', '_$0', strtoupper(unpack('H*', $str)[1]));
}
上記のような関数で、一度英数字に変換して使う。
PDOStatement::execute
PHP: PDOStatement::execute - Manual
プリペアードステートメントを実行する。実行にあたって、プレースホルダーに値を埋め込む必要がある。
- bindValue/bindParamを使用。
- 引数で配列で指定。ただし、NULL以外、値は全てPDO::PARAM_STR扱い。既存のbindValue/bindParamを全上書き。要素数はプレースホルダーと同一必要。
型が全部PARAM_STRなら問題ない。それ以外の数値などを含めたいなら、bindValueでしたほうがいい。
<?php
/* 入力値の配列を伴うプリペアドステートメントの実行 */
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories
FROM fruit
WHERE calories < ? AND colour = ?');
$sth->execute(array($calories, $colour));
?>
PDOStatement::bindValue/bindParam
プリペアードステートメント内で、部分的に後からプレースホルダーに値を割り当てる。
- bindValue: 呼び出し時に値で埋め込まれる。
- bindParam: execute実行時に変数が評価される。
なお、似た名前のメソッドに [PHP: PDOStatement::bindColumn - Manual] がある。こちらはSELECTの取得結果のカラムをPHP変数に割り当てるためのもの。
基本はbindValueでよい。が、bindParamも出番がある。
<?php
$stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (?, ?)");
$stmt->bindParam(1, $name);
$stmt->bindParam(2, $value);
// 行を挿入します
$name = 'one';
$value = 1;
$stmt->execute();
// パラメータを変更し、別の行を挿入します
$name = 'two';
$value = 2;
$stmt->execute();
?>
変数だけ変えて、複数回実行する場合。
ただ、この場合、executeの引数で渡してもいい。が、引数だと全部PDO::PARAM_STRになってしまうので、bindParamも役立つ。
PDOで複数回SQLを実行: コツコツ学ぶWordPress、技術メモ
<?php
require("db_info.php");
$dsn = 'mysql:host=localhost;dbname='.$database.';charset=utf8';
try {
$dbh = new PDO($dsn, $username, $password,
array(
PDO::ATTR_EMULATE_PREPARES =>false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'"
)
);
} catch (PDOException $e) {
exit('データベース接続失敗。'.$e->getMessage());
}
date_default_timezone_set('Asia/Tokyo');
$result = array();
$st = $dbh->prepare('select city,TRUNCATE(sum(setai)/3,0) as setai,TRUNCATE(sum(man)/3,0) as man,TRUNCATE(sum(woman)/3,0) as woman,TRUNCATE(sum(total)/3,0) as total from jinko where ym in (:ym1,:ym2,:ym3) group by city');
$ym1 = date('Y/m', strtotime(date('Y-m-1').' -1 month'));
$ym2 = date('Y/m', strtotime(date('Y-m-1').' -2 month'));
$ym3 = date('Y/m', strtotime(date('Y-m-1').' -3 month'));
$st->bindValue(':ym1', $ym1 , PDO::PARAM_STR);
$st->bindValue(':ym2', $ym2 , PDO::PARAM_STR);
$st->bindValue(':ym3', $ym3 , PDO::PARAM_STR);
$st->execute();
$result = $st->fetchAll(PDO::FETCH_ASSOC);
$ym1 = date('Y/m', strtotime(date('Y-m-1').' -1 year -1 month'));
$ym2 = date('Y/m', strtotime(date('Y-m-1').' -1 year -2 month'));
$ym3 = date('Y/m', strtotime(date('Y-m-1').' -1 year -3 month'));
$st->bindValue(':ym1', $ym1 , PDO::PARAM_STR);
$st->bindValue(':ym2', $ym2 , PDO::PARAM_STR);
$st->bindValue(':ym3', $ym3 , PDO::PARAM_STR);
$st->execute();
$result = array_merge($result,$st->fetchAll(PDO::FETCH_ASSOC));
echo json_xencode($result);
?>
ただ、bindValueをまたやってもいい。だったら、bindParamは余計になくてもいいか?
一括UPSERT
【PHP】PDOのprepareで複数行を一括INSERTする方法 | キノコログ
以下のようなプレースホルダーSQLを文字列で作って、bindValueも反復させる。
$sql = "INSERT INTO
doraemon_users
(name,gender,type)
VALUES (:name0,:gender0,:type0)
,(:name1,:gender1,:type1),(:name2,:gender2,:type2),(:name3,:gender3,:type3),(:name4,:gender4,:type4),(:name5,:gender5,:type5)"
ON DUPLICATE KEY UPDATE stat1 = stat1 + VALUES(stat1), stat2 = stat2 + VALUES(stat2), stat3 = stat3 + VALUES(stat3)
;
//配列設定
$aryInsert = [];
$aryInsert[] = ['name' => 'のび太', 'gender' => 'man', 'type' => 'human'];
$aryInsert[] = ['name' => 'ドラえもん', 'gender' => 'man', 'type' => 'robot'];
$aryInsert[] = ['name' => 'ジャイアン', 'gender' => 'man', 'type' => 'human'];
$aryInsert[] = ['name' => 'スネ夫', 'gender' => 'man', 'type' => 'human'];
$aryInsert[] = ['name' => 'しずか', 'gender' => 'woman', 'type' => 'human'];
$aryInsert[] = ['name' => 'ドラミ', 'gender' => 'woman', 'type' => 'robot'];
$aryColumn = array_keys($aryInsert[0]);
//SQL文作成処理
$sql = "INSERT INTO
doraemon_users
(".implode(',', $aryColumn).")
VALUES";
$arySql1 = [];
//行の繰り返し
foreach($aryInsert as $key1 => $val1){
$arySql2 = [];
//列(カラム)の繰り返し
foreach($val1 as $key2 => $val2){
$arySql2[] = ':'.$key2.$key1;
}
$arySql1[] = '('.implode(',', $arySql2).')';
}
$sql .= implode(',', $arySql1);
//bind処理
$sth = $pdo -> prepare($sql);
foreach($aryInsert as $key1 => $val1){
foreach($val1 as $key2 => $val2){
$sth -> bindValue(':'.$key2.$key1, $val2);
}
}
//実行処理
$sth -> execute();
$records = [];
$this->file->seek(1);
foreach (new \NoRewindIterator($this->file) as $row) {
/** @var array $row map_raku_numに存在する列番号の場合、列番号を列名に置換。 */
$row = array_combine(array_map(function($k) use($map_raku_num) {
return in_array($k, $map_raku_num) ? array_flip($map_raku_num)[$k] : $k;
}, array_keys($row)), $row);
foreach ($map_base_raku as $base => $raku) {
if (!array_key_exists($raku, $row)) {
$row[$raku] = null;
};
$formatter = $formatter ?: function($v, $k, $r) {return $v;};
$row[$raku] = ($row[$raku] === '' || $row[$raku] === null) ? null : $row[$raku];
$record[$base] = $formatter($row[$raku], $base, $row);
}
/** 全部空なら除外する。 */
if (!empty(array_filter($record))) $records[] = $record;
}
/** UPSERTのAI増分対策用に重複削除。 */
if (!empty($unique)) {
$old_row = '';
foreach ($records as $row => $line) {
// unique対象列が全部一致の場合削除。
if (array_filter($unique, function($v) use ($line, $records, $old_row) {return $line[$v] !== $records[$old_row][$v];})) {
unset($records[$old_row]);
}
$old_row = $row;
}
}
/** @var array $map_base_holder PDOのプレースホルダーには英数字しか使えないので%エンコーディング。 */
$map_base_holder = array_combine(array_keys($map_base_raku), array_map(function($v) {
return preg_replace('~..~', '_$0', strtoupper(unpack('H*', $v)[1]));
}, array_keys($map_base_raku)));
try {
$dbh = new PDO(Raku2Config::getDSN(), Raku2Config::get('CONFIG_DBUSER'), Raku2Config::get('CONFIG_DBPASS'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
Raku2Config::writeLog("SQL execute table_name=$table_name");
$table_name = str_replace(["\0", "`"], ["", "``"], $table_name);
/** @var string $sql_prefix SQL前半カラム名 */
$sql_prefix = "INSERT INTO $table_name (" . implode(',', array_keys($map_base_raku)) . ') VALUES ';
/** @var string $sql_postfix SQL後半カラム名 カラム1 = VALUES(カラム1), カラム2 = VALUES(カラム2)... */
$sql_postfix = ' ON DUPLICATE KEY UPDATE '
. implode(',', array_map(function($e){return "$e=VALUES($e)";}, array_keys($map_base_raku)));
/** @var int $run_per_block 全データを一括INSERTすると、物件テーブルが4388件目でハングするので1000件ずつ分割実行 */
foreach (array_chunk($records, $run_per_block = 1000) as $sub_records) {
// [%エンコーディングした列名][行番号]の書式にプレースホルダー配置 (例: 86_8a_a9_943)
$sql = $sql_prefix
. implode(',', array_map(function($row)use($map_base_holder){
return '('.implode(',', array_map(function($e)use($row){return ":$e$row";}, $map_base_holder)).')';
}, array_keys($sub_records)))
. $sql_postfix;
$sth = $dbh->prepare($sql);
foreach ($sub_records as $row => $line) {
foreach ($map_base_holder as $base => $holder) {
$type = in_array($base, array_keys($map_base_type))
? $map_base_type[$base] : PDO::PARAM_STR;
$sth->bindValue(":$holder$row", $line[$base], $type);
}
};
$sth->execute();
/** @var string $sql ON DUPLICATE KEY UPDATE で更新するとAUTO_INCREMENTが増えるので、最大値でリセット。 */
$sql = "SET @NEW_AI = (SELECT MAX("
. str_replace(["\0", "`"], ["", "``"], array_key_first($map_base_raku)) . ")+1 FROM $table_name)";
$dbh->exec($sql);
$dbh->exec("SET @ALTER_SQL = CONCAT('ALTER TABLE $table_name AUTO_INCREMENT =', @NEW_AI)");
$dbh->exec("PREPARE NEWSQL FROM @ALTER_SQL");
$dbh->exec("EXECUTE NEWSQL");
}
} catch (PDOException $e) {
Raku2Config::writeLog("table_name=$table_name " . $e->getMessage());
return Raku2Config::EXIT_EXCEPTION;
}
分割一括UPSERT例。
テーブル名/カラム名には使用不能
「PHP: PDO::prepare - Manual」のコメントに記載がある通り、PHPのプリペアードステートメントはテーブル名とカラム名のプレースホルダーは未対応。プレースホルダーは単に文字列置換をしているわけではなく、DBMSにクエリープランの作成を指示するため。これにはテーブル名が分かっていないといけない。
「PHPでデータベースに接続するときのまとめ #MySQL - Qiita」に記載があるような、テーブル名のエスケープを自前で行う必要がある。
- NULLバイトの除外。
- バッククオートの二重化によるエスケープ。
- 全体を囲む。
$sql = sprintf(
"CREATE TABLE `%s`(id int, name text)",
str_replace(["\0", "`"], ["", "``"], $table_name)
);
Fetch
PDOStatementからデータを取得するメソッドが複数ある。
- PHP: PDOStatement::fetch - Manual: 1行。
- PHP: PDOStatement::fetchAll - Manual: 全行。
- PHP: PDOStatement::fetchColumn - Manual
- PHP: PDOStatement::fetchObject - Manual
「PDOの真の力を開放する - PHPでデータベースを扱う(3): Architect Note」
<?php
$stmt = $pdo->query('SELECT * FROM Entry');
foreach ($stmt as $row) {
echo $row['title'], $row['content'];
}
//↑は↓のコードと等価
while ($row = $stmt->fetch()) {
echo $row['title'], $row['content'];
}
fetchしてループで処理するなら、fetchしなくても使える。
FETCH_MODE
PDOのコンストラクターのオプション、setAttribute、fetchの引数で取得条件を指定できる。
- PDO::FETCH_BOTH: 規定。カラム名と0開始の添え字の配列で返す。
- PDO::FETCH_NAMED: 同名のカラムが複数ある場合、値の配列を返す。
- PDO::FETCH_NUM: 0開始のカラム番号の配列で返す。
同名カラムがある場合、PDO::FETCH_NUM/FETCH_NAMEDじゃないと取れない。
UPSERT
いくつかポイントがある。1回のUPSERTで同じレコードの更新を試みると、PKがAIで増分するので事前に固有にしておく。
/**
* UPSERTのAI増分対策用に重複削除済みの配列を返す。
*
* 大文字小文字、半角全角をDBで区別しないので、PHP内で違いを吸収して重複判定。
* 重複削除は重いので、implodeなどでできるだけシンプルに実装。
*
* @param array $records 重複を含む元データ。
* @param array $unique_list ユニークキー名の配列。
* @return array $record_unique_list 重複削除済み固有データ。
*/
private function getUniqueList(array $records, array $unique_list): array
{
if (!$unique_list) return $records;
$record_unique_list = [];
// レコードの全部の行
$unique_list_key = array_flip($unique_list);
/** 重複削除は時間がかかるのでログ出力。 */
Raku2Config::writeLog('Prepare unique records.');
foreach ($records as $record) {
/** @var string $needle 追加対象データの重複判定用マージ文字列 */
$needle = $this->kana_small_to_large(implode(array_intersect_key($record, $unique_list_key)));
/** 固有配列を確認して固有の場合追加。 */
foreach ($record_unique_list as $record_unique) {
/** 重複データならスキップ。 */
if ($needle === $this->kana_small_to_large(implode(array_intersect_key($record_unique, $unique_list_key))))
continue 2;
}
$record_unique_list[] = $record;
}
return $record_unique_list;
}
Other
テーブルの有無確認
information_schemaが使える場合は以下のSQLで判定可能。
$table_name = 'table';
try {
$dbh = new PDO(getDSN(), get('CONFIG_DBUSER'), ('CONFIG_DBPASS'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$sth = $dbh->prepare('SELECT count(*) FROM information_schema.tables WHERE TABLE_NAME = ?');
$sth->execute([$table_name]);
if (empty($sth->fetch()[0])) {
echo "テーブル名=$table_name はありません。";
return;
}
} catch (PDOException $e) {
Raku2Config::writeLog("table_name=$table_name " . $e->getMessage());
return;
}
件数取得
- 【PHP】PDOで件数を取得するrowCountの使い方 | キノコログ
- PHP: PDOStatement::fetchColumn - Manual
- PHP: PDOStatement::rowCount - Manual
レコードの有無を件数で確認したいことがある。
SQLでの取得と、PDOのメソッドでやる方法がある。
$sth = $pdo->query("SELECT count(*) FROM users");
$count = $sth->fetchColumn();
$sth = $pdo->query("SELECT * FROM users");
$count = $sth->rowCount();
単に件数だけ欲しいなら、SQLでcountしたほうがシンプルだと思う。レコードも欲しいならrowCount。
持続的な接続
- https://grok.com/share/c2hhcmQtMw%3D%3D_29a772a5-7bff-4afa-9959-58390fe5c56b
- PHP: 接続、および接続の管理 - Manual
CGIのPHPのように、1プロセス1ファイルでリクエストからレスポンスを返す場合、DB接続も都度行う。
ただし、いくつかの方法で、DB接続を再利用可能。アクセスが多い場合に、性能が向上する。ただし、手間がかかったり欠点もある。
- PDO::ATTR_PERSISTENT: mod_phpやphp-fpmのようにPHPのプロセスが継続する場合に有効。ただし、PDOの実行中にtransactionが終了していない場合、前回の環境が後続の環境に影響あったりする。
- DB側の接続プーリング: PgBouncerやProxySQL。外部ツールが必要。
使うならPDO::ATTR_PERSISTENTだろう。
Calendar
time/速度計測
PHPで処理速度などを計測したいことがある。基本は処理前後のタイムスタンプの差分で、どの言語でも共通の論理だが、いくつか方法がある。
microtimeとhrtimeの2個の関数をタイムスタンプの取得で使える。hrtimeはPHP 7.3.0以上で使用可能。単位ナノ秒。問題なければ、こちらが推奨されている。HRTime (High Resolution Time) の拡張のモジュールと関係する関数とのこと。
両方とも、引数にtrueを指定して、floatで取得するのが基本。
<?php $start = hrtime(true); // 計測開始時間 // 計測したい処理 $end = hrtime(true); // 計測終了時間 // 終了時間から開始時間を引くと処理時間になる echo '処理時間:'.($end - $start).'ナノ秒' . PHP_EOL; ?>
PHPのバージョンを気にするのが嫌なので、ラップするとよい。
<?php
/**
* Time target function.
* @param callable $callback Target function.
* @return int|float Run time [ns].
*/
function timeit(callable $callback)
{
$time = 'microtime';
$nanoFactor = 1000;
if (function_exists('hrtime')) {
$time = 'hrtime';
$nanoFactor = 1;
}
$start = $time(true);
$callback();
$stop = $time(true);
return ($stop - $start) * $nanoFactor;
}
echo timeit(function(){sleep(1);});
// one liner.
echo (function($c){$s=hrtime(1);$c();return hrtime(1)-$s;})(function(){;}), " ns\n";
// arrow function
echo (fn($c)=>[$s=hrtime(1),$c(),hrtime(1)-$s][2])(function(){;}), " ns\n";
(function($c){$s=hrtime(true);$c();return hrtime(true)-$s;})(function(){sleep(1);});
?>
こんな感じ。PHP 7.4のアロー関数でも書けるが、いまいちか。
Date/Time
About
- PHP: 日付/時刻 の算術 - Manual
- PHPの日付・時刻関連をまとめてみた #PHP7.2 - Qiita
- 日付時刻関連で使われる書式の調査 #PHP - Qiita
- 日付時刻関連のクラスを活用しよう #PHP - Qiita
PHPの日時処理はdate関数DateTimeクラス、DateTimeImuutableクラス、Carbonなどいろいろある。
公式の説明などを見る限り、DateTimeImuutableを推奨しているように見える。
単純な日時文字列が欲しいだけなら、date関数系API。それ以外の本格的な日付計算が必要ならDateTimeImmutableを使うといい。プロジェクトでは念のため自前の日時クラスでラップしておく。
Carbon
- なぜPHP標準関数のdateではなくCarbonを使うのか?
- ELI5: Why is Carbon better than core PHP date classes? : r/PHP
- Carbon - A simple PHP API extension for DateTime.
- Why PHPs Carbon is bad for the climate - Carsten Windler
PHPの日時処理にCarbonという拡張クラスが人気らしい。人気の理由はテストしやすいからとか。
たしかに、標準のdateなどの関数系APIは扱いにくいかもしれない。
DateTimeImuutableなら問題ない気がする。
datetime
関数系API群。
- date
- strtotime
- time
strtotime
strtotime(string $datetime, ?int $baseTimestamp = null): int|false
文字列形式の日付をUnixタイムスタンプのintに変換する。日時の比較でお世話になる。
format
- PHP: サポートする日付と時刻の書式 - Manual
- PHP: DateTimeInterface::format - Manual
- PHP: DateTimeInterface - Manual
- PHP: 定義済み定数 - Manual
日時の書式が2種類ある。DatetImeInterface::formatを見ておくとよい。
バックスラッシュでエスケープできる。
特に重要なものを抜粋する。
| 種類 | 文字 | 定数 | 例 | 説明 |
|---|---|---|---|---|
| 全ての日付/時刻 | c | DATE_ATOM | 2004-02-12T15:19:21+00:00 | ISO8601日付。 |
| U | time() | Unix Epoch (1970-01-01T00:00) からの秒数。 | ||
| Y-m-d\TH:i:s | 2024-01-01T00:00:00 |
よくやるパターン。
echo date('c') . PHP_EOL; // 2024-10-07T00:03:19+00:00
echo str_replace(['-', ':'], '', date('c')); // 20241007T000319+0000
File system
Ref: PHP: ファイルシステム - Manual.
File system
file
- file_get_contents — ファイルの内容を全て文字列に読み込む
- file_put_contents: データをファイルに書き込む。戻り値に書き込みバイト数を返す。失敗したらfalse。成否は完全一致===falseで。
- PHP: file - Manual: ファイル全体を読み込んで改行区切りで配列にする。
- readfile: ファイル全体を読み込んで標準出力に出力する。
上記2個の非常に重要な入出力関数がある。
バイナリーやHTTP GETに対応している。アップロードされたファイルの読み込みなどでお世話になる。
file_get_contents/file_put_contentsはfopen/fwrite/fcloseの一連のファイル処理を含んでいるので非常に簡単。
- SplFileObject is faster than fopen/fgets – Gustavo Straube
- file vs file_get_contents vs fopen vs SplFileObject php - Тесты производительности php - Fast Site Engine
SplFileObjectとどちらが速いかは不明。
directory
- PHP: mkdir - Manual:
- rmdir
// ディレクトリー不在なら作成。 $dir = '/path/to/dir' if (!file_exists($dir)) mkdir($dir, 0777, true);
check
入出力とセットで使うファイルの不在確認の関数群。
path
- PHPのディレクトリまとめ #PHP - Qiita
- PHP・ファイル名・パスなどの取得
- PHPの便利な関数:ファイルパス関連dirname(), basename(), realpath(), getcwd(), chdir()
- PHP: マジック定数 - Manual
- PHP: basename - Manual: パスからファイル名を取得。
- PHP: dirname - Manual: パスからディレクトリーの取得。
- PHP: realpath - Manual: 相対パスを絶対パスに変換。
- PHP: getcwd - Manual: 現在のディレクトリー。__DIR__は実行中ディレクトリー。includeしたときにgetcwdは実行元、__DIR__はinclude先。
- PHP: chdir - Manual
- __DIR__: ファイルの存在するディレクトリー。終端スラッシュなし。
パス関係の操作。重要。
operation
PHPでファイルの移動や改名をする場合、rename。基本的にこれ1個だけ。
rename("/tmp/tmp_file.txt", "/home/user/login/docs/my_file.txt");
CSV
- PHP: fgetcsv - Manual
- PHP: str_getcsv - Manual
- PHP: SplFileObject - Manual
- 【PHP】その CSV 変換、本当に「fgetcsv」でいいの? (フェンリル | デベロッパーズブログ)
CSVのパース・読込のための関数がいくつかある。一番いいのは、PHP 5.1.0以上のSplFileObject。これが速くてメモリー使用量も小さい優れた実装。
$file = new SplFileObject($filepath);
$file->setFlags(SplFileObject::READ_CSV|\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY);
if (0 === strpos(PHP_OS, 'WIN')) {
setlocale(LC_CTYPE, 'C');
}
foreach ($file as $row) {
$records[] = $row;
}
// Get header
$file->current();
// Skip header.
$file->seek(1);
foreach (new \NoRewindIterator($file) as $row) {
$records[] = $row;
}
fgetcsv
- PHP: fgetcsv - Manual
- PHP: SplFileObject::fgetcsv - Manual
- PHP: setlocale - Manual
- Windows環境でCSVのパースをするとカラムが正しく分割できない現象の修正方法 - ビー鉄のブログ
- Windows版php-7.1 で UTF8 の CSV をパースする - 脳みそスワップアウト
fgetcsvやSplFileObject::fgetcsvはOSのロケールを考慮する。
WindowsではUTF-8がなく、解釈できないらしい。かつ、PHP7の問題とか。ただ、LC_TYPE=Cにすると回避できる。
if (0 === strpos(PHP_OS, 'WIN')) {
setlocale(LC_CTYPE, 'C');
}
PHPのバージョンの条件もつけていい気もするが。
fputcsv
- PHP: fputcsv - Manual
- PHP: SplFileObject::fputcsv - Manual
- 【PHP入門】ファイルの読み書きをする4つの方法
- PHP: SplFileObject::fwrite - Manual
CSVの書き込み。SplFileObjectのfputcsvメソッドを使う。1行ずつ出力するので反復させる。fputcsv関数もあるが、読込がよかったので、SplFileObjectのほうが品質が高いと思われる。
//書き込むファイルのパス
$filename = './file.csv';
//書き込むデータの配列
$list = array(
array('データ1', 'データ2', 'データ3'),
array('データ4', 'データ5', 'データ6'),
array('データ7', 'データ8', 'データ9')
);
if (!$file = new SplFileObject($filename, 'w')) return false;
/** Write BOM. */
if (false === $file->fwrite("\xef\xbb\xbf")) return false;
/** データを1行ずつ書き込む */
foreach ($list as $fields) {
$file->fputcsv($fields);
}
Tmp
- PHP: tmpfile - Manual: r+w+bで開く。fcloseなど使用終了時に自動削除。「tmpfile() or tempnam()? - PHP Coding Help - PHP Freaks」自動削除以外はfopen('php://temp', 'w+b')相当。
- PHP: tempnam - Manual: おそらくr+wで開く。
- Using `php://temp` or `tempnam` when I write a temporary file in order to upload data into 3rd party network storage - Stack Overflow
- PHPで一時的なファイルポインタを扱う方法 #PHP - Qiita
一時ファイルの作成方法がいくつかある。
$fp = fopen('php://memory', 'r+b');$fp = fopen('php://temp', 'r+b');$fp = fopen("php://temp/maxmemory:{$n}", 'r+b');$file = new SplTempFileObject($n);$fp = tmpfile();- tempnam()
php://tempはアップロード失敗時などで、ファイル名が取れなくて、ファイルが残るらしい。php://tempを使うなら、tmpfileかtempnamのどちらかがいい。
いったん一時的な名前で作った後に、後で保存する場合、tempnam。保存不要ならtmpfile。
$ch = curl_init(); $meta = stream_get_meta_data($fp = tmpfile()); curl_setopt($ch, CURLOPT_COOKIEJAR, $meta['uri']); curl_setopt($ch, CURLOPT_COOKIEFILE, $meta['uri']);
stream_get_meta_dataの取得結果の'uri'からファイルパスを取得できる。
データの読込。「【PHP】gzip圧縮されたCSVをSplFileObjectで直接処理する #PHP - Qiita」
<?php
// gzipファイルのダウンロード
$url = 'http://localhost/test.csv.gz';
$ch = curl_init($url);
$tmp = tmpfile();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_FILE => $tmp,
]);
curl_exec($ch);
$tmp_path = stream_get_meta_data($tmp)['uri'];
// CSVとして読み込み
$file = new SplFileObject('compress.zlib://' . $tmp_path);
$file ->setFlags(SplFileObject::READ_CSV|\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY);
if(0 === strpos(PHP_OS, 'WIN')) {
setlocale(LC_CTYPE, 'C');
}
foreach($file as $row){
var_export($row);
}
curlの取得結果の読込。
$path = stream_get_meta_data($fp = tmpfile())['uri']; $url = 'https://localhost'; $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_FILE => $fp]); $result = curl_exec($ch); $file = new SplFileObject($path); $file->setFlags(SplFileObject::READ_CSV|\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY);
確認用一時ファイル作成
ファイル読み書きの動作確認用コードで、今後頻出する一時ファイル作成と読込例。
$path = stream_get_meta_data($fp = tmpfile())['uri']; file_put_contents($path, <<<'EOT' id,value 0,1 EOT ); $file = new SplFileObject($path);
tempnam
ファイルアップロード後、OKなら正式保存みたいなときに使う。
$path = tempnam(sys_get_temp_dir(), 'FOO'); // good $handle = fopen($tmpfname, "w"); fwrite($handle, "writing to tempfile"); fclose($handle);
第2引数のprefixは一時ファイルのプレフィクスになる。
競合などしないなら、最初から本保存用のファイル名でファイルを作ったほうがよいかも。
Directory
Constants
- DIRECTORY_SEPARATOR: Windowsなら\、それ以外/らしい。
このDIRECTORY_SEPARATORは基本的には使うことはない。
- path separator - When to use DIRECTORY_SEPARATOR in PHP code? - Stack Overflow
- language agnostic - Is using the directory separator constant necessary? - Stack Overflow
理由として、Windowsがパスの文脈で/も受け入れるから。必要な場面は、OSからパスを受け取る場合に、\が返ってくることがあり、そこでexplodeしたりしたい場合のみ。
自分からパスを指定する文脈では、/で問題なく、DIRECTORY_SEPARATORを使う必要はない。
ディレクトリー以下のファイル一覧
- PHP: glob - Manual
- PHP: scandir - Manual
- PHPで任意のディレクトリ下にあるファイルを一覧取得する方法 #PHP - Qiita
- PHP 最強に簡単なファイル一覧取得方法 - 株式会社NextCode - 福山市のHP制作・システム開発
- Which is better to read files from a directory using PHP - glob() or scandir() or readdir()? - Stack Overflow
- PHPのglob()関数を使用しフォルダ内のファイルを数える | Men of Letters(メン・オブ・レターズ) – 論理的思考/業務改善/プログラミング
- scandir - PHP most efficient way to list the files in a very large directory - Stack Overflow
- php - Which is faster: glob() or opendir() - Stack Overflow
- Putting glob() to the test | php[architect]
- Getting the names of all files in a directory with PHP - Stack Overflow
方法がいくつかある。
- glob
- scandir
- SPL
ソートや簡易検索が必要かどうかで、速度や適切な方法が異なる。
ただ、使いやすいのはglob。
glob(string $pattern, int $flags = 0): array|false
マッチする・ファイル、ディレクトリーの配列を返す。マッチしなければ空の配列。失敗はfalse。
patternはlibcのglobのルールでマッチする。
- *: 0以上の任意文字。
- ?: 任意の1文字。
- [...]: グループの1文字。先頭が!の場合、否定。
- \: エスケープ。
flags。特に重要なのは以下。
- GLOB_ONLYDIR: ディレクトリーのみ。
- GLOB_NOSORT: デフォルトでアルファベット順でソートしているのを無効にする。これを指定すると速くなる。
./..以外の全ファイルのマッチ。
array_merge(glob('.[!.]*'), glob('*'));
International
PHP: 自然言語および文字エンコーディング - Manual
mbstring
mb_substr
Ref:
mb_substr(
string $string,
int $start,
?int $length = null,
?string $encoding = null
): string
substr同様、lengthにはマイナス値を指定可能。その場合、末尾からの文字数になる。
省略するかnullを指定すると、全文字。0は0文字。
mb_convert_kana
mb_convert_kana(string $string, string $mode = "KV", ?string $encoding = null): string
| オプション | 意味 |
|---|---|
r
|
「全角」英字を「半角」に変換します。 |
R
|
「半角」英字を「全角」に変換します。 |
n
|
「全角」数字を「半角」に変換します。 |
N
|
「半角」数字を「全角」に変換します。 |
a
|
「全角」英数字を「半角」に変換します。 |
A
|
「半角」英数字を「全角」に変換します ("a", "A" オプションに含まれる文字は、U+0022, U+0027, U+005C, U+007Eを除く U+0021 - U+007E の範囲です)。 |
s
|
「全角」スペースを「半角」に変換します(U+3000 -> U+0020)。 |
S
|
「半角」スペースを「全角」に変換します(U+0020 -> U+3000)。 |
k
|
「全角カタカナ」を「半角カタカナ」に変換します。 |
K
|
「半角カタカナ」を「全角カタカナ」に変換します。 |
h
|
「全角ひらがな」を「半角カタカナ」に変換します。 |
H
|
「半角カタカナ」を「全角ひらがな」に変換します。 |
c
|
「全角カタカナ」を「全角ひらがな」に変換します。 |
C
|
「全角ひらがな」を「全角カタカナ」に変換します。 |
V
|
濁点付きの文字を一文字に変換します。"K", "H" と共に使用します。 |
modeはデフォルトのKVで問題なさそう。
なお、日本語の促音、小文字の大文字への変換はできない。素直に置換するしかない。
【PHP】ひらがな・カタカナの小文字(小書き文字)を大文字にする方法
function kana_small_to_large($subject) {
$search = ['ぁ','ぃ','ぅ','ぇ','ぉ','っ','ゃ','ゅ','ょ','ゎ','ァ','ィ','ゥ','ェ','ォ','ッ','ャ','ュ','ョ','ヮ','ヶ'];
$replace = ['あ','い','う','え','お','つ','や','ゆ','よ','わ','ア','イ','ウ','エ','オ','ツ','ヤ','ユ','ヨ','ワ','ケ'];
return str_replace($search, $replace, $subject);
}
//使用例
echo kana_small_to_large('カッパ'); //カツパ
セッション関連
About
複数回のアクセスで特定のデータを保持する方法。状態のないHTTPに状態を持たせる。HTTP CookieでセッションIDを継続して渡して、そのIDに紐づいたデータを連携する。
session_start()でセッションを開始すると、$_SESSIONにセッションIDに連動したデータが格納される。
サーバーとクライアントのHTTPヘッダーで識別情報を勝手にやってくれるので、コード内だと$_SESSION['key']で使用したいキーだけ意識したらいい。デフォルトだとファイルにセッションデータを保存してくれる。
Security
session_startの他にもいくつか共通ですべき内容があるので整理する。
- session_start
- session_regenerate_id(true): セッションIDの再作成。ログイン時やパスワード更新、権限変更など重要な操作時。セッションハイジャックを防止。
- セッション設定: php.iniでセッション関係の共通設定。
ini_set('session.cookie_secure', 1); // HTTPS限定でクッキー送信 ini_set('session.cookie_httponly', 1); // JavaScriptからクッキーアクセス禁止 ini_set('session.cookie_samesite', 'Strict'); // CSRF対策 ini_set('session.gc_maxlifetime', 1440); // セッション有効期限(秒、例: 24分) ini_set('session.cookie_lifetime', 0); // ブラウザ閉じるとクッキー破棄 ini_set('session.use_strict_mode', 1); // セッションIDの厳格モード(未初期化ID拒否)
- セッション破棄: ログアウト時にsession_destroy()でセッションデータを破棄。他にクッキーも削除。
Example
Basic
DB
https://grok.com/share/c2hhcmQtMw%3D%3D_6316c3b2-d8cf-46fe-8c63-c496888aa35f
デフォルトだとファイルにセッションデータを保存する。小中規模くらいならこれで問題ない。が、大規模で複数のサーバーでセッション情報を共有したい場合、DBにする。
session_set_save_handlerにSessionHandlerInterfaceの派生クラスをセットすればいいだけ。後は自動的にやってくれるから、session_startと$_SESSIONだけでいい。
認証機能
パターンが決まっている。
logout
https://grok.com/share/c2hhcmQtMw%3D%3D_a180174b-b3ff-4eac-9de0-523a2781ed6d
logout.php相当で、セッション・クッキー削除後リダイレクト。これが基本。
<?php
// セッションを開始
session_start();
// セッションのデータをすべて破棄
$_SESSION = []; // セッション変数を空にする
// セッションを完全に破棄
session_destroy();
// セッションクッキーを削除(必要に応じて)
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000, // 過去の時間に設定してクッキーを無効化
$params["path"],
$params["domain"],
$params["secure"],
$params["httponly"]
);
}
// ログインページやホームページにリダイレクト
header("Location: login.php");
exit();
Text/テキスト処理
Strings
strpos
Ref: PHP: strpos - Manual.
strpos(string$haystack, string$needle, int$offset= 0): int|false
文字列 haystack の中で、 needle が最初に現れる位置を探します。
include相当。よく使う。
戻り値に注意。有無の確認時は、strpos() !== falseの厳密一致でチェックする必要がある。
echo/print/printf
- PHP: echo - Manual
- PHP: print - Manual
- PHP: printf - Manual
- echoとprintの違い #PHP - Qiita
- PHP : echoとprintの違い #まとめ - Qiita
- 【PHP】echoとprintの違いとは?どっちを使えば良い?
PHPの出力関数群。よく使うが、扱いが特殊なので整理する。
まず、echo/printは関数ではなく、if/forなどと同じ言語構造、キーワード扱い。丸括弧はなくてもいい。紛らわしいのでないほうがいい。
- 共通: 末尾に改行は付与されない。自分で"\n"を指定必要。関数ではないので丸括弧は不要。
- echo: 戻り値void。式ではないので、if returnなどで使えない。戻り値がない分printよりわずかに速い。文字数が短い。コンマ区切りで複数列挙可能。文字列連結するよりコンマ区切りのほうが.演算子の優先順位など扱いが簡単。HTMLでの埋め込みに便利な <?= ?>の短縮表記 (<?php echo ; ?>相当)もあり。
- print: 戻り値intで常に1を返す。ifや条件演算子の結果部分など、式の文脈で使用可能。
基本はechoでいい。if return/条件演算子など戻り値や式が必要な箇所でだけprintを使う。
printfは関数。書式指定が必要ならこれ。
setlocale
テキスト系の関数では、ロケールを考慮するものがいくつかある。その際の設定にこの関数を使う。
strtolower
ASCII文字を小文字にした文字を返す。
Basic/Vartype/変数・データ型関連
Reflection
About
PHPには完全なリフレクションAPIがある。つまり、クラス、インターフェイス、関数、メソッド、拡張モジュールの型とプロパティーを実行時に検査可能。さらに、ドキュメントコメントも取得できる。
リフレクションの対象ごとに、クラスが用意されている。
- Reflection
- ReflectionClass
- ReflectionClassConstant
- ReflectionEnum
- ReflectionEnumUnitCase
- ReflectionEnumBackedCase
- ReflectionZendExtension
- ReflectionExtension
- ReflectionFunction
- ReflectionFunctionAbstract
- ReflectionMethod
- ReflectionNamedType
- ReflectionObject
- ReflectionParameter
- ReflectionProperty
- ReflectionType
- ReflectionUnionType
- ReflectionGenerator
- ReflectionFiber
- ReflectionIntersectionType
- ReflectionReference
- ReflectionAttribute
- Reflector
- ReflectionException
特に重要なのが、ReflectionClass。これを使うことで、クラスのプライベートなメソッドとか、プロパティーに外部からアクセスできる。
Variable
print_r/var_export/var_dump
- [PHP print_r、var_dump、var_export のちがい #PHP - Qiita]
- How to create Dynamically create config/custom.php config file
- PHP: print_r - Manual
- PHP: var_export - Manual
- PHP: var_dump - Manual
PHPの変数の内容を、配列やオブジェクトなどの複雑なデータ型でも表示、出力可能なのがprint_r/var_export/var_dump。微妙に違いがある。
- var_dump: これのみ型情報がある。
- var_export: 変数の文字列表現。config.phpなどファイルに出力すればそのままinclude可能。
- print_r: 基本は閲覧用の文字列。
- print_r/var_export: 第二引数をtrueにすると、画面出力ではなく、戻り値に返す。
$text = "<?php\n\nreturn " . var_export($myarray, true) . ";";
使うとしたら、var_exportとvar_dumpだと思われる。ログ目的だと、タイプの楽なprint_rも悪くない。print_rは配列が循環参照時に、省略してくれるという利点がある。ログ目的ならprint_r($var, true)。
var_dumpは型の情報が詳しい。詳細な情報が欲しい場合、var_dump。そうでない、単に見たいだけなら、var_exportで十分。
var_dumpは表示のみ。出力制御関数を使えば、文字列に保存はできるが、基本はデバッグ表示用。
<?php
$data = array(
"A" => "Apple",
"B" => "Banana",
"C" => "Cherry"
);
echo "---print_r---\n";
print_r($data);
echo "---var_export---\n";
var_export($data);
echo "---var_dump---\n";
var_dump($data);
?>
---print_r---
Array
(
[A] => Apple
[B] => Banana
[C] => Cherry
)
---var_export---
array (
'A' => 'Apple',
'B' => 'Banana',
'C' => 'Cherry',
)
---var_dump---
array(3) {
["A"]=>
string(5) "Apple"
["B"]=>
string(6) "Banana"
["C"]=>
string(6) "Cherry"
}
画面出力でデバッグしたいならば、前後でpre要素を出力させる。
<?php
function print_r2($val){
echo '<pre>';
print_r($val);
echo '</pre>';
}
?>
var_exportの変数取込
var_exportの文字列表現。config.phpに出力して、Includeするほかに、テキストを変数にしたいことがある。
- import - how to read output of var_export into a variable in PHP? - Stack Overflow
- reCatnap: php var_export()した後の文字列を元に戻す(eval())
素直にevalする。
$dumpStr = var_export($var,true);
eval("$somevar = $dumpStr;");
くれぐれも入力に注意する。
信頼できない場合、json_encode/json_decodeで取り込むのが良い模様。
変数取込
- php - Array as var_export/include vs. (un)serialize vs. json_(en|de)code - Stack Overflow
- json_encodeとserializeの比較 - 仮メモ
PHPの配列などの変数を、文字列やファイル出力して、読み込んだりしたいことがある。いくつか方法があり、速度も異なる。
- var_export/include (require)
- serialize/unserialize
- json_encode/json_decode
結論としては、PHPのAPC/Opcache有効状態のvar_export/includeが最速。初回だけ時間がかかるが、2回目からはバイトコード読み込みになるので高速。
それ以外は、jsonがいいとのこと。
Process
exec
システムプログラムの実行のための関数群がある。Windowsだとcmd.exe経由で実行される。
exec/shell_exec/system/passthru
- shell - PHP shell_exec() vs exec() - Stack Overflow
- PHP: exec - Manual
- PHP: passthru - Manual
- PHP: shell_exec - Manual/PHP: 実行演算子 - Manual
- PHP: system - Manual
外部プログラム実行のための3の関数がある。違いがある。
- shell_exec: 標準出力をstringで返す。バッククオート演算子``と同じ。プログラムの成功可否、終了コードは判断不能。
- exec: 既定で標準出力の最後の1行のみ返す。第二引数に配列を指定すれば、行区切りの配列で返すこともできる。
- passthru: Unixコマンドの出力がバイナリーで、ブラウザーに直接バイナリーを返す場合に、exec/systemの代わりに使う。戻り値は?falseで標準出力に直接結果を出力する。
- system: C言語のsystem関数に類似。system()でコマンドを実行して出力する。成功時にコマンド出力の最終行を返す。出力をファイルや別のストリームにリダイレクトしないと、終了までPHPが止まる。
| 項目 | exec | passthru | shell_exec/`` | system |
|---|---|---|---|---|
| 出力 | - | x | - | x |
| 終了コード | x | x | - | x |
| 戻り値 | 最終行 | null/false | 全行 | 最終行 |
| 全行取得 | x | - | x | - |
| 用途 | 外部コマンドの結果文字列取得 (終了チェックあり) | 画像応答 | 外部コマンドの結果文字列取得 | 文字応答 |
基本はshell_exec/`` execで十分。
exec
exec(string $command, array &$output = null, int &$result_code = null): string|false
戻り値は最終行。失敗したらfalseを返す。実行コマンドの終了コードは$result_codeに渡される。
escape
- PHP: escapeshellarg - Manual
- PHP: escapeshellcmd - Manual
- php - escapeshellarg と escapeshellcmd の違いは何ですか? - Stack Overflow
escapeshellarg/escapeshellcmdはexec/shell_exec/``と併用するエスケープ用関数。
- escapeshellarg: 引数の文字列を一重引用符で囲み、既存の一重引用符を苦オートする。これで、引数全体を1個の引数にする。複数の引数の誤り実行を回避できる。
- escapeshellcmd: シェルに特殊な意味のある&#;`|*?~<>^()[]{}$\、\x0A のシェルの特殊文字にバックスラッシュを追加し、'"は対がない場合のみエスケープ。
元々、コマンド全体をエスケープする [escapeshellcmd] だけがあった。が、これだとコマンドの引数を追加する攻撃が可能になるので、 [escapeshellarg] が追加されたらしい (PHPのescapeshellcmdを巡る冒険 | 徳丸浩の日記)。
- PHPのescapeshellcmdの危険性 | 徳丸浩の日記
- escapeshellcmdの危険な実例 | 徳丸浩の日記
- PHPのescapeshellcmdを巡る冒険はmail関数を経てCVE-2016-10033に至った | 徳丸浩の日記
- 安全なPHPアプリケーションの作り方2016 | ドクセル
- CTFのWebセキュリティにおけるCommand Injectionまとめ(Linux, Windows, RCE) - はまやんはまやんはまやん
ただ、escapeshellcmdは、パラメーターインジェクションの危険性があるので、使ってはいけないらしい。
基本は引数に [escapeshellarg] を使うだけ。
「PHPにはエスケープ関数が何種類もあるけど、できればエスケープしない方法が良い理由 | 徳丸浩の日記」にあるように、その後PHP 7.4でproc_openが登場した。これはシェル経由じゃないOSコマンド呼び出しで、エスケープ不要なので安全。基本はこれを使うのがいいとのこと。
escapeshellarg
- PHPでコマンド実行しようとしたら日本語が消失した - megutech
- PHPのescapeshellargが日本語を消し去った - @peccul is peccu
- PHPのロケール、又はアパッチの環境変数 LANG - hs9587’s diary
- PHP escape shell arg unwanted behaviour - Stack Overflow
- PHP: escapeshellarg - Manual
- mochigrammer : escapeshellargで日本語が消える!
- PHPでexecを走らせると日本語だけ消える #CentOS - Qiita
日本語に使うと日本語が消える。ロケールを考慮する模様。setlocaleで使用間際に変更するという手もあるが、そもそもOSの設定を直すほうが筋かもしれない。
単発コマンドで影響ないなら以下のようなコードを直前に記述して対応する。
if (false === setlocale(LC_CTYPE, "ja_JP.UTF-8")) {
die("skip setlocale() failed\n");
}
PHP 7.4でproc_openを使えばこういう問題もない。
Other/その他の基本モジュール
JSON
https://grok.com/share/c2hhcmQtMw%3D%3D_25a431e4-ce24-4f64-ab55-86dd605bc635
About
json_encode/json_decodeをよく使う。非常に重要。
json_encodeはオブジェクトをJSON文字列表記にできるのでデバッグなどで便利。
PHPでUnicodeアンエスケープしたJSONを出力する関数 - オープンソースこねこね
JSON_UNESCAPED_UNICODE をオプションに指定しないと日本語はユニコードエスケープ表記になる。
json_encode(mixed $value, int $flags = 0, int $depth = 512): string|false
連想配列
json_decode(
string $json,
?bool $associative = null,
int $depth = 512,
int $flags = 0
): mixed
json_decodeは第二引数にtrueを指定しないと、object (stdClass) になる。trueにすると、連想配列になる。基本は連想配列でいいと思う。
連想配列のほうが、配列関数を使えて、処理しやすい。他のライブラリーもこれを想定していることが多い。オブジェクト指向APIとか、特定ケースでデフォルトのstdClassでいい。
JsonException+json_last_error
json_decode/json_encodeの戻り値では、処理の成否しかわからず、失敗の理由がわからない。失敗の理由の取得はjson_last_error関数かJsonExceptionで取得する。
PHP 7.3.0以上だと、json_decode/json_encodeの最後の引数でJSON_THROW_ON_ERRORを指定でき、エラー時に例外にできる。$e->getMessage()でエラー内容を統一的に処理できる。最近のPHPのフレームワークはエラーは例外で処理する方向になっているので、これでいくのがよい模様。
json_decodeの場合、以下のように第3引数が必要になるのがいまいち。
try {
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($data)) {
throw new Exception('データが配列ではありません');
}
// 処理
} catch (JsonException $e) {
error_log('JSONデコードエラー: ' . $e->getMessage());
throw new Exception('無効なJSONデータ');
}
$data = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('JSONデコードエラー: ' . json_last_error_msg());
throw new Exception('無効なJSONデータ');
}
if (!is_array($data)) {
throw new Exception('データが配列ではありません');
}
// 処理
プロジェクト内でjson_decodeのラッパー関数を用意して512を省略するのがよい。
DTO
https://grok.com/share/c2hhcmQtMw%3D%3D_73139de0-1175-46a1-916a-37509379bec4
JSONでデータを受け取って、それの内容を使って画面に表示するということが、けっこう頻繁にある。
json_decodeをそのまま使うと、連想配列かstdClassで型の情報がない。使うところで毎回型チェックが必要で怖い。
PHPのstdClassはIDEでの補完は連想配列よりかはしやすいが、型情報はない。素直に、ファイル内にDTOを定義して使うのがいい。
<?php
// メイン処理(コントローラーや画面ロジック)
function getUserDtoFromJson(string $json): UserDto {
return UserDto::fromJson($json); // 型ヒント付きでDTOを返す
}
// 使用例
try {
$json = '{"id": 1, "name": "Taro", "email": "taro@example.com"}';
$dto = getUserDtoFromJson($json);
// Viewへのassign(例: Twigの場合)
// $twig->render('user_view.twig', ['user' => $dto]);
echo "ID: {$dto->id}, Name: {$dto->name}, Email: {$dto->email}";
} catch (InvalidArgumentException $e) {
echo "Error: {$e->getMessage()}";
}
// ファイル末尾: DTO定義
class UserDto {
public int $id;
public string $name;
public ?string $email;
private function __construct(int $id, string $name, ?string $email)
{
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
public static function fromJson(string $json): self
{
return self::fromArray(json_decode($json, true, 512, JSON_THROW_ON_ERROR));
}
public static function fromArray(array $data): self
{
if (!isset($data['id']) || !is_int($data['id'])) {
throw new InvalidArgumentException('id must be an integer');
}
if (!isset($data['name']) || !is_string($data['name'])) {
throw new InvalidArgumentException('name must be a string');
}
if (isset($data['email']) && !is_string($data['email'])) {
throw new InvalidArgumentException('email must be a string or null');
}
return new self($data['id'], $data['name'], $data['email'] ?? null);
}
}
?>
ポイント
- コンストラクターをprivateにして、staticメソッド経由での検証経由の生成を強制。
- プロパティーはpublicでC++のstruct相当。余計なgetterのオーバーヘッド、記述を回避。読み取り専用として扱う。
UserとかProductとか種別違いのDTOが必要な場合、BaseDTOを作ってそれでやる。
<?php
// メイン処理(例: 画面関数)
function getUserDtoFromJson(string $json): UserDto {
return UserDto::fromJson($json);
}
function getProductDtoFromJson(string $json): ProductDto {
return ProductDto::fromJson($json); // 同じJSONから異なるDTOを生成
}
// 使用例
$json = '{"id": 1, "name": "Taro", "email": "taro@example.com", "product_name": "Widget", "price": 100}'; // 共通JSON
$userDto = getUserDtoFromJson($json);
$productDto = getProductDtoFromJson($json);
echo "User: {$userDto->name}, Product: {$productDto->productName}";
// ファイル末尾: DTO定義
abstract class BaseDto {
private function __construct() {} // privateで直接インスタンス化禁止
public static function fromJson(string $json): static
{
// staticでサブクラスを指す
return static::fromArray(json_decode($json, true, 512, JSON_THROW_ON_ERROR));
}
public static function fromArray(array $data): static {
return static::validateAndCreate($data); // 抽象メソッドを呼び出し
}
abstract protected static function validateAndCreate(array $data): static; // サブクラスで実装
}
class UserDto extends BaseDto {
public int $id;
public string $name;
public ?string $email;
protected static function validateAndCreate(array $data): static {
if (!isset($data['id']) || !is_int($data['id'])) {
throw new InvalidArgumentException('id must be an integer');
}
if (!isset($data['name']) || !is_string($data['name'])) {
throw new InvalidArgumentException('name must be a string');
}
if (isset($data['email']) && !is_string($data['email'])) {
throw new InvalidArgumentException('email must be a string or null');
}
$instance = new static();
$instance->id = $data['id'];
$instance->name = $data['name'];
$instance->email = $data['email'] ?? null;
return $instance;
}
}
class ProductDto extends BaseDto {
public int $id;
public string $productName;
public float $price;
protected static function validateAndCreate(array $data): static {
if (!isset($data['id']) || !is_int($data['id'])) {
throw new InvalidArgumentException('id must be an integer');
}
if (!isset($data['product_name']) || !is_string($data['product_name'])) {
throw new InvalidArgumentException('product_name must be a string');
}
if (!isset($data['price']) || !is_numeric($data['price'])) {
throw new InvalidArgumentException('price must be a number');
}
$instance = new static();
$instance->id = $data['id'];
$instance->productName = $data['product_name'];
$instance->price = (float)$data['price'];
return $instance;
}
}
?>
json_decodeのところはtry/catchの形式に直す。これで型を保証できる。
Other
die/exit
dieはexitと完全に同等。
exit(string $status = ?): void exit(int $status): void
メッセージを表示してスクリプトを終了する。関数ではなく、言語構造扱い。
statusを指定しない場合、丸括弧不要。status=0指定とみなされる。
statusが文字列なら、終了直前に表示する。intの場合、終了ステータス扱い。0-254。255は予約されている。0は正常終了。
eval
文字列をPHPコードとして評価する。危険なので、特にユーザーから入力を受け付ける場合は、注意する。できれば使わないほうがいい。
evalで評価する文字列内でreturnした結果が返却値となる。ないならnull。戻り値が必要なら、テキスト内で忘れずにreturnする。
Random
- https://chatgpt.com/share/68145e5c-9d14-800b-9cca-1cd9e46e2dd6
- https://chatgpt.com/share/682fd227-ca64-800b-871c-cf1e3d6b63a4
PHPでランダム値を生成する方法がいくつかある。
- Randomクラス (PHP 8.2以上): PHP 8.2以上だとこれを使うのがいいらしい。
- random_int (PHP 7.0以上): シード値指定不能。
mtはメルセンス・ツイスターという名前の乱数生成方法らしい。乱数生成時にシード値を指定したかったら、mt_srandで指定する。
基本はrandom_intを使う。シード値の指定で再現性が必要であれば、mt_srand+mt_randを使う。
mt_randはmt_srandのグローバルな値に依存するので、注意する。mt_srandの指定なしでmt_randを使う場面は、random_intを使った方がいい。
SPL
Standard PHP Library. PHP 5.1.0で登場。標準的な処理への対応のためのライブラリー。
PHP 5から登場しただけあって、実装がかなり洗練されている。メモリー使用量、速度面の性能で有利なことが多い。
- SplFileObject
SPLFileObject
Flag
PHP – SplFileObject::setFrags() – TauStation
以下の4のフラグがある。
- public const int DROP_NEW_LINE;
- public const int READ_AHEAD;
- public const int SKIP_EMPTY;
- public const int READ_CSV;
READ_CSV以外は、データに影響あるので、指定しないほうがいいと思う。特に、CSVとして扱う場合、空行もデータのときがあるし、フィールド内に改行を含む。DROP_NEW_LINEするとその改行が削除されてしまう。
$file->setFlags(\SplFileObject::READ_CSV|\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY);
\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTYで、行末の空行を除去できる。これを指定するのがいい。
ヘッダー取得
SplFileObjectはIteratorの派生クラスなので、このメソッドで操作できる。
「PHP: SeekableIterator - Manual」も継承しているのでseekも使える。
current()で取得する。
<?php // Your code here! $path = stream_get_meta_data($fp = tmpfile())['uri']; file_put_contents($path, <<<'EOT' id,value 0,1 EOT ); $file = new SplFileObject($path); var_export($file->current()); ?>
URLs
全文字のパーセントエンコーディング
string - PHP How to encode all characters with rawurlencode - Stack Overflow
PDOのプレースホルダーに、日本語カラム名を使いたい場合など、データを英数字のみで表現したい場合に使う。
function encode_all($str) {
return preg_replace('~..~', '%$0', strtoupper(unpack('H*', $str)[1]));
}
p
プレースホルダーの場合、%を_で置換すれば元データも復元可能。
Other/Service
HTTP
- PHPでPOST送信まとめ #PHP - Qiita
- APIなどにfile_get_contents()を使うのはオススメしない理由と代替案 #PHP - Qiita
- PHP: file_get_contents - Manual
- PHP: cURL - Manual
PHPでHTTP通信をする方法がいくつかある。
- file_get_contents
- curl
file_get_contentsはPHP標準。curlは外部ライブラリー。
「PHP cURL vs file_get_contents - Stack Overflow」などを見る限り、GET以外はcurlのほうが速くて複雑なことができるらしい。
file_get_contentsは元々ローカルや内部ファイルの読み込み用らしい。
GNU socialではHTTPClientクラス経由で実現するので、内部実装を意識する必要はない。
curlでのリクエスト方法を覚えておくと、汎用性が高い模様。
外部ライブラリーを使っていいなら、Guzzleが今は主流とのこと。Guzzleは内部でcurlを使っている。
cURL
About
- PHP: cURL - Manual
- PHP: 基本的な curl の使用法 - Manual
- APIなどにfile_get_contents()を使うのはオススメしない理由と代替案 #PHP - Qiita
基本的な使用方法。
- curl_initでurlを指定してセッション初期化。
- curl_setopt/curl_setopt_arrayでオプションを設定。
- CURLOPT_POST=true/CURLOPT_POSTFIELDSでPOST関係指定。
- CURLOPT_RETURNTRANSFER=trueでcurl_execの応答ボディーをテキストで取得。
- CURLOPT_FILEで保存先ファイル指定。
- curl_execで転送実行。CURLOPT_RETURNTRANSFER=trueを指定しない場合、true/falseのみ。
- curl_closeでセッション終了。
<?php
$ch = curl_init("http://www.example.com/");
$fp = fopen("example_homepage.txt", "w");
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, 0);
// curl_setopt_array($ch, $options);
curl_exec($ch);
if(curl_error($ch)) {
fwrite($fp, curl_error($ch));
}
$info = curl_getinfo($ch);
$errorNo = curl_errno($ch);
curl_close($ch);
fclose($fp);
?>
<?php
/* curlセッションを初期化する */
$ch = curl_init("http://www.google.com/");
/* curlオプションを設定する */
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
/* curlを実行し、その内容を$result変数に保存 */
$result = curl_exec($ch);
$info = curl_getinfo($ch);
if ($info['http_code'] !== 200) {
}
/* curlセッションを終了する */
curl_close($ch);
/* result変数に保存した内容を表示 */
echo htmlspecialchars($result);
curl_getinfoで応答結果の詳細を確認できる。
特に以下は重要。
- http_code
CURLOPT
PHP: 定義済み定数 - Manual 特に重要なオプションがいくつかある。
- CURLOPT_POST: trueならHTTP POST。これを指定するとContent-Tpe=application/x-www-form-urlencodedになる。JSONにしたいならこれは上書き必要。
- CURLOPT_HTTPHEADER: ヘッダー指定。[['Content-Type: application/json']] はよく指定する。
- CURLOPT_POSTFIELDS: POSTのリクエストボディー。文字列で渡すか、連想配列。連想配列の値が配列の場合、Content-Type: multipart/form-dataになる。ファイル送信はCURLFile (ファイル名) かCURLStringFile (ファイルの中身)を使う。
- CURLOPT_RETURNTRANSFER: 初期値false。trueにするとcurl_execの戻り値で、レスポンスボディーをテキストで取得できる。
- CURLOPT_FILE: 初期値STDOUT。書き込み先のファイル。ファイルポインターを指定する。CURLOPT_RETURNTRANSFERと2者択一。
- CURLOPT_TIMEOUT: 初回接続時のタイムアウト秒数。3秒が推奨?
- CURLINFO_HEADER_OUT => true: curl_getinfoにリクエストヘッダーを含める (web services - How to get info on sent PHP curl request - Stack Overflow)。デバッグ用。
- CURLOPT_PROXY: 使用するプロキシーサーバーのホスト名を指定する。自分のサーバーの別パスなど、内部リクエスト時には、明示的に空にする。そうしないと、http_proxy、https_proxyの環境変数の影響を受けて、プロキシーサーバーにリクエストを送信してしまう。
CURLOPT_RETURNTRANSFERとCURLOPT_FILEの競合
- CURLOPT_*のデフォルトオプション結合の罠 #PHP - Qiita
- php Curl confilict CURLOPT_FILE and CURLOPT_RETURNTRANSFER in docker - Stack Overflow
- PHPのcurlでCURLOPT_FILEを使う際の注意点
この2個のオプションは競合する。後から設定したものが優先される。レスポンスボディーが必要ならば、片方に統一して、片方だけからのアクセスにする。
バイナリーやCSVファイルのように、データが大きくて高速なパースが必要なら、SplFileObjectを使いたいのでファイル優先でいいと思う。
JSONのようにシンプルで短いなら全部テキストでやってもよいだろう。
JSONデータの送受信
- 連想配列でリクエストボディーのデータを作って、json_encodeでJSON文字列に変換。
- POST指定: curl_setopt($ch, CURLOPT_POST, true);
- ヘッダーContent-Type指定:
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json')); - リクエストボディー指定: curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json);
- 戻り値のデコード: $res_json = json_decode($result , true );
以上。
$data = array(
'test1'=>'aaa',
'test2'=> array(
array(
'test3'=>'bbb'
)
),
'test4'=> array(
array(
'test5'=>'ccc',
'test6'=>'ddd'
)
)
);
$data_json = json_encode($data);
$ch = curl_init('http://posttestserver.com/post.php');
curl_setopt_array($ch, [
CURLOPT_POST => true, // application/x-www-form-urlencoded になるので上書き。
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => $data_json,
CURLOPT_RETURNTRANSFER => true,
// CURLOPT_FILE => $fp,
]);
$result=curl_exec($ch);
echo 'RETURN:'.$result;
curl_close($ch);
$result=curl_exec($ch);
$res_json = json_decode($result , true );
echo $res_json['return1'];
