「PHP」の版間の差分

提供:senooken JP Wiki
(Migration)
 
(同じ利用者による、間の363版が非表示)
1行目: 1行目:
== Library ==


== CakePHP ==
=== Framework ===


=== Basic ===
* Symfony
* CakePHP
* FuelPHP: 2010年誕生。
* Codeigniter: シンプル、軽量。
* Zend
* Laravel: 2011年誕生。
* Phalcon
* [https://ja.wikipedia.org/wiki/Yii Yii - Wikipedia]


==== Structure ====
=== Template ===


* bin: cakeコマンド類。
* [https://worktoolsmith.com/php-template-engines-matome/ PHPテンプレートエンジンまとめ 一覧と構文例(随時追加中) | WorkToolSmith]
* config: CakePHPの全体設定にかかるファイル群。
* logs
* plugins
* src
* tests
* tmp
* vendor
* webroot: アプリケーションルート。


===== Routing =====
いろいろある。
Ref: [https://book.cakephp.org/3/ja/development/routing.html#index-0 ルーティング - 3.10].
* Blade: Laravel標準。
* DIV: 1ファイルでシンプル。大規模には向かない。
* Smarty: 万能。
* Twig: 拡張はしにくい。
使うとしたら、歴史の長いSmarty。


config/routes.phpでルーティングの基本が設定される。ここのコードで、後述のMVCがある程度自動設定されている。
* [https://hnavi.co.jp/knowledge/blog/smarty/ Smartyとは?基礎知識と具体的なメリットをわかりやすく解説 - システム開発のプロが発注成功を手助けする【発注ラウンジ】]
* [https://w.atwiki.jp/smarty/pages/161.html Smartyとは? - smarty @Wiki - atwiki(アットウィキ)]


ほかに、アプリケーショントップの/アクセス時の処理を行う、重要な設定もここで行う。/はMVCの命名規則になっていないので、ここは手動設定?的なことが必要になる。基本はここがLoginになる。
高速らしい。


Router::scopeとRouter::connectを使って処理する。
そもそもテンプレートエンジンがいるのかどうかという議論がある。


scopeはルーティングのグループ化。connectで個別に紐づける。<syntaxhighlight lang="php">
* [https://www.inworks.jp/archives/103 Smarty って、要らなくない? — INWORKS]
Router::scope('/blog', ['plugin' => 'Blog'], function ($routes) {
* [https://pasela.hatenablog.com/entry/20120906/php_templates 素のPHPはもはやテンプレートエンジンとしては使えない - ぱせらんメモ]
     $routes->connect('/', ['controller' => 'Articles']);
* [https://scrapbox.io/fsubal/%E6%9C%AC%E5%BD%93%E3%81%AB%E5%80%92%E3%81%99%E3%81%B9%E3%81%8D%E3%81%A0%E3%81%A3%E3%81%9F%E3%81%AE%E3%81%AF_jQuery_%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%B3%E3%81%A0%E3%81%A3%E3%81%9F 本当に倒すべきだったのは jQuery ではなくテンプレートエンジンだった - fsubal]
* [https://codezine.jp/article/detail/9594 サーバサイドHTMLテンプレートからの脱却のススメ (1/4)|CodeZine(コードジン)]
 
UI/UXを突き詰めると、JavaScriptを使わざるを得ず、サーバーテンプレートエンジンは初回だけなので、いっそのことJSで全部やろうというのが最近の流れの模様。
 
PHP自体が一種のテンプレートエンジンという主張がある。が、関数をあれこれ書く必要があり、可読性が悪い。
 
SmartyよりTwigのほうが性能が上とか。
 
「[https://phalcon-docs-ja.readthedocs.io/ja/stable/reference/volt.html Volt: テンプレートエンジン — Phalcon 3.0.2 ドキュメント (Japanese / 日本語)]」。高速フレームワークのPhalconではVoltを使っている。
 
==== Twig ====
 
* [https://pasela.hatenablog.com/entry/20120906/php_templates 素のPHPはもはやテンプレートエンジンとしては使えない - ぱせらんメモ]
* [http://fabien.potencier.org/templating-engines-in-php.html Templating Engines in PHP |Articles - Fabien Potencier]
* [http://akisi.tabiyaku.net/?p=896 PHPテンプレートエンジンを使おう Twig編 | AkisiのWEB制作日記]
* [https://www.decodednode.com/2013/08/smarty-vs-twig-part-1.html Decoded Node: Smarty vs. Twig (a benchmark done poorly)]
* [https://github.com/AliShareei/smarty-vs-twig-benchmark GitHub - AliShareei/smarty-vs-twig-benchmark: A benchmark of up-to-date versions of Smarty and Twig templating engines]
* [https://github.com/dominics/smarty-twig-benchmark GitHub - dominics/smarty-twig-benchmark: A benchmark of up-to-date versions of Smarty and Twig templating engines]
* [https://medium.com/@gotzmann/the-fastest-template-engine-for-php-b5b461b46296 The fastest template engine for PHP | by Serge Gotsuliak | Medium]
* [https://stackoverflow.com/questions/9363215/pure-php-html-views-vs-template-engines-views Pure PHP/HTML views VS template engines views - Stack Overflow]
 
Twig v3のほうが速いらしいが、Smarty v3のほうが速いというデータもある。
 
==== Smarty ====
 
* [https://github.com/smarty-php/smarty GitHub - smarty-php/smarty: Smarty is a template engine for PHP, facilitating the separation of presentation (HTML/CSS) from application logic.]
* [https://smarty-php.github.io/smarty/stable/ Smarty Documentation]
 
「[https://stackoverflow.com/questions/9363215/pure-php-html-views-vs-template-engines-views Pure PHP/HTML views VS template engines views - Stack Overflow]」が決定的だった。Smartyの開発者がSmartyのほうがTwigより速いと回答していた。2012年。Smartyでいいと思う。
 
=== ORM ===
Ref:
 
* [https://stackoverflow.com/questions/108699/good-php-orm-library database - Good PHP ORM Library? - Stack Overflow]
* [https://codezine.jp/article/detail/5858?p=2&anchor=0 PHPでの「ORMライブラリ」機能比較 (2/5)|CodeZine(コードジン)]
 
いろいろある。Doctrineが有名。
* Doctrine: Symfonyで採用。有名。
* Eloquent
* Propel: Symfony v1.2で採用されていた。
* PHP activerecord
* PHPDAO
* PDO: PHP標準。
* Xyster
ただ、速度を優先する場合、PDOが最速になるらしい。
 
ORMは別になくてもいいか。いや、validateなどでほしいか。<blockquote>ChatGPT: PHPのORMで最高速なのはどれ?
 
 
PHPのORMで「最高速」を目指す場合、目的やデータ量、柔軟性や機能性のニーズによって適切な選択肢が変わります。ただ、以下は一般的に高速性が評価されているORMやアプローチです。
 
### 1. **Cycle ORM**
 
   - **概要**: Cycle ORMは、PHP向けに軽量で高性能なORMとして設計されており、メモリ使用量やクエリ生成において優れた最適化がなされています。また、`DataMapper`パターンを採用しており、柔軟性が高く、複雑なデータ構造にも対応可能です。
 
   - **特徴**:
 
     - メモリ効率が良く、データベースへのアクセスが高速。
 
     - マッピング定義が柔軟で、データベーススキーマの同期などの管理機能も備えている。
 
   - **適合するケース**: 高速性が求められるが、複雑なデータ構造やリレーションも必要な場合。
 
   - **URL**: [Cycle ORM](<nowiki>https://cycle-orm.dev/</nowiki>)
 
### 2. **Doctrine ORM(高度に最適化した場合)**
 
   - **概要**: Doctrineは多機能で、エンタープライズレベルのアプリケーションにも使用されていますが、設定やキャッシュの適切な調整で高速化が可能です。Doctrineは大規模なデータベースや複雑なエンティティ構造を扱うことを得意としており、クエリキャッシュやメタデータキャッシュを活用すると、高速なクエリ処理が実現できます。
 
   - **特徴**:
 
     - キャッシュ(メタデータ、クエリ、結果)を駆使することでパフォーマンスが向上。
 
     - バッチ処理やフェッチモードの調整により、余分なSQLクエリを抑制できる。
 
   - **適合するケース**: 大規模なアプリケーションで、柔軟性や機能性と高速性のバランスが求められる場合。
 
   - **URL**: [Doctrine ORM](<nowiki>https://www.doctrine-project.org/projects/orm.html</nowiki>)
 
### 3. **Eloquent ORM(軽量化する場合)**
 
   - **概要**: Eloquentは、Laravelの標準ORMで、柔軟で書きやすいAPIを提供しています。ただし、Eloquentは`Active Record`パターンを採用しているため、大量のデータや複雑なクエリには向きません。しかし、シンプルなアプリケーションであれば、十分な速度を発揮できます。
 
   - **特徴**:
 
     - データベースアクセスが少ないケースで非常に簡便で高パフォーマンス。
 
     - 適切なスコープやクエリビルダーを利用することで高速なデータアクセスが可能。
 
   - **適合するケース**: シンプルなアプリケーションで、多少の軽量化(不必要なEloquentモデルの削除など)を行い、APIの利便性と速度を両立したい場合。
 
   - **URL**: [Eloquent ORM](<nowiki>https://laravel.com/docs/eloquent</nowiki>)
 
### 4. **直接的なクエリビルダー使用(PDO, Laminas DBなど)**
 
   - **概要**: ORMを使わず、PDOやクエリビルダー(Laminas DBなど)を用いて、データベースと直接やり取りすることも非常に高速な手法です。ORMのオーバーヘッドがないため、特にトランザクション内での多くのデータ操作が求められる場合や、複雑なクエリが必要な場面では、これが最速のアプローチです。
 
   - **特徴**:
 
     - ORMを介さず直接データベースを操作するため、最小限のオーバーヘッドで済む。
 
     - 柔軟で高度なクエリやバッチ操作も容易。
 
   - **適合するケース**: 速度重視の大規模アプリケーションや、複雑なクエリが必要なケース。
 
これらを総合的に考えると、 **「Cycle ORM」** が最もバランスよく高性能を発揮しやすく、ORMの利便性を保持しながらも高速なデータ操作が可能です。しかし、最高速を求めるのであれば、ORMを使わずにクエリビルダーやPDOを直接活用することが理想的です。</blockquote>Cycle ORMが安定して速いらしい。これでいくか。
 
=== Migrate ===
 
* Phinx
* Doctrine Migrations
* cycle/migrations: [https://cycle-orm.dev/docs/database-migrations/current/en Cycle ORM: Databases - Migrations]
「[https://zenn.dev/okranagaimo/articles/fe0427f41296ba PHPで「Doctrine Migrations」を使ってみる]」
 
CakePHPに採用されているPhinxのほうが人気なのでPhinxを使ったほうがよいだろう。
 
ORMとあわせたほうがいいかも。
 
=== Test ===
 
* PHPUnit
 
=== Search ===
検索キーワードをフォームから受信後、DBにSQLで検索をかけて取得結果を返すのが基本。
 
それとは別に、検索用のアプリにリクエストを受け渡しして検索するという方法がある。どちらでもいけるような、ドライバーのライブラリーがある。
 
* [https://laravel.com/docs/9.x/scout Laravel Scout - Laravel 9.x - The PHP Framework For Web Artisans]
* [https://github.com/teamtnt/tntsearch teamtnt/tntsearch: A fully featured full text search engine written in PHP]
* [https://packagist.org/packages/ruflin/elastica ruflin/elastica - Packagist] ([https://www.cloudways.com/blog/php-libraries/ 40 Best PHP Libraries For Web Applications in 2022])
 
検索サービスで有名なのは以下。
 
* ElasticSearch/OpenSearch
* MeiliSearch
* Algolia
* Sphinx Search ([https://sphinxsearch.com/ Sphinx | Open Source Search Engine])
* Apache Solr ([https://solr.apache.org/ Welcome to Apache Solr - Apache Solr])/Apache Lucene ([https://lucene.apache.org/ Apache Lucene - Welcome to Apache Lucene])
Laravel Scoutでtntsearchを使う方法がある ([https://qiita.com/sunny_510/items/ba926dc8d8aa7428f142 Laravel Scout + TNTSearchによる小規模プロジェクトへの全文検索機能の追加 #PHP - Qiita]/[https://helog.jp/laravel/scout-tntsearch/ Laravel ScoutとTNTSearchを使用してサイト全文検索を実装してみる – helog])。
 
「[https://packagist.org/search/?tags=search Packagist]」の検索結果をみても、tntsearchが特に人気の模様。
== About==
 
=== About ===
PHPはプログラミング言語だ。汎用的なプログラミング言語だが、主にウェブサーバー上のソフトウェアで使用される。
 
GNU socialはPHPで記述されている。他にもWordPress・NextCloudなどがPHPで記述されている。これらのPHP製ソフトウェアはVPSだけではなく安価なレンタルサーバーでも動作するため、低コストで運用することができる。
 
PHPの公式リファレンスは日本語版があり、わかりやすくまとまっている。
*[https://www.php.net/manual/ja/langref.php PHP: 言語リファレンス - Manual]
*[https://museum.php.net/ PHP: Release Archives (museum)]: PHP1からのソースコードの保管場所。
ウェブ上にはPHPに関するTipsが多く公開されており、大抵の疑問はウェブ検索で解決できる。
 
=== Version ===
PHPは言語の版数が上がる際、過去の版と互換性の無い破壊的変更がなされることがある。
 
開発者はこのリスクを軽減するために、非推奨の言語機能を避け、実行時の警告 (warning) を適切に処理するべきだ。
 
GNU socialは現在PHP 7系で動作する様に記述されており、PHP 8系への対応は作業途中だ。
 
==== PHP v8 ====
PHP v8になっていろいろ更新が入った。特にPHP v7.4からv8に更新する際のポイントがあるので整理する ([https://web.gnusocial.jp/post/2023/12/08/9535/ 行事: 「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 ===
 
* [https://creatopia.jp/media/12102 PHPをブラウザで実行、動作確認できるおすすめツール4つ|Creatopia Media]
 
PHPをWebブラウザーで実行、動作確認のツールがいくつかある。
 
* paiza.IO: [https://paiza.io/ja/projects/new Online PHP Editor | ブラウザでプログラミング・実行ができる「オンライン実行環境」| paiza.IO]: パーマリンク。
* phpsansbox.io [https://phpsandbox.io/ Write PHP online from your browser - PHPSandbox]: フレームワークも確認可能。
* OnlinePHP.io: [https://onlinephp.io/ PHP Sandbox - Execute PHP code online through your browser]: 複数バージョンの同時実行できる。
* 3v4l.org: [https://3v4l.org/ Online PHP editor | Test code in 250+ PHP versions]: パーマリンク。
 
3v4l.orgがパーマリンクがあって、複数バージョンの動作確認できるので、これがいいと思う。
=== Guide ===
*[https://notabug.org/gnusocialjp/gnusocial/src/main/DOCUMENTATION/DEVELOPERS/CONTRIBUTING/coding_standards.md DOCUMENTATION/DEVELOPERS/CONTRIBUTING/coding_standards.md]
*[https://www.php-fig.org/psr/psr-12/ PSR-12: Extended Coding Style - PHP-FIG]
PHPのコーディングの推奨規約がある。PSR-12というのがメジャーな模様。GNU socialでも採用されている。
 
「[https://stackoverflow.com/questions/1209720/what-should-i-name-my-php-class-file What should I name my PHP class file? - Stack Overflow]」にあるように、PSRではファイル名には記載がない。PSR-4や「[https://www.php-fig.org/bylaws/psr-naming-conventions/ PSR Naming Conventions - PHP-FIG]」に記載がある程度。
 
ただ、「[https://framework.zend.com/manual/1.12/en/coding-standard.naming-conventions.html Manual - Documentation - Zend Framework]」、「[https://book.cakephp.org/4/en/intro/conventions.html#file-and-class-name-conventions CakePHP Conventions - 4.x]」など、他の規約があり、クラス名と同じになっている。
 
PHPのクラス名は大文字小文字を区別しないが、わかりにくいので大文字小文字で、クラス名と一致させておくとよさそう。
 
ただし、viewなど、表示に直接結びついているものは、小文字でもいいかも。ファイル名とURLパスが同じほうが分かりやすい。
 
==== Naming ====
 
* [https://qiita.com/aaari95/items/8325131ce328e42e078c リーダブルコード・PSRから学ぶ 命名規則 #PHP - Qiita]
* [https://www.php.net/manual/ja/function.disk-free-space.php PHP: disk_free_space - Manual]
* [https://www.php.net/manual/ja/language.oop5.properties.php PHP: プロパティ - Manual]
 
変数名やシンボルの命名規則。
 
* クラス名: CamelCase
* メソッド名: mixedCase
* 定数: UPPER_SNAKE_CASE
* プロパティー: PSRでの規定はない。公式文書だとmixedCase
* 変数名: PSRでの規定はない。公式文書だとlower_snake_case
 
上記で揃えるとよいだろう。
 
=== Performance ===
 
==== About ====
PHPのコーディング時に、速度に影響のある書き方がいろいろある。
 
* [https://thk.kanzae.net/net/itc/t2379/ PHP 高速化に関するメモ書き | Thought is free]
* [https://qiita.com/kapitan/items/47d89449f23bc7f7c9bb PHPを少しでも速く動かしたい #PHP - Qiita]
* [https://qiita.com/Hiraku/items/190443b33ee7a2167ade PHPで高速オシャレな配列操作を求めて #PHP - Qiita]
 
一般論。
 
* 言語構造>組込関数>関数の順に速い。
* 複雑な方法より単純な方法のほうが速い。
* 特に配列関係の関数、呼び出しが頻発する処理などで重要。
 
==== 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でやったほうが速い。可読性などの問題はある。
 
===== in_array/array_search =====
[https://thk.kanzae.net/net/itc/t2379/ PHP 高速化に関するメモ書き | Thought is free]
 
in_array/array_searchは遅いらしい。書き方を変えたほうがいい。
 
PHPの言語構造の中で、isset/emptyが非常に重要。これらで値の有無判定ができる。
 
ただし、もともと単純配列になっているのを、連想配列に変換するくらいならば、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";
 
==Language Reference==
===Types===
====Introduction====
Ref: [https://www.php.net/manual/en/language.types.intro.php PHP: Introduction - Manual].
 
PHPの変数は以下の型のいずれかの値となる。
*null
*bool
*int
*float (floating-point number)
*string
*array
*object
*callable
*resource
C言語のようなlong/doubleのような精度ごとの型はない。
 
==== System ====
[https://www.php.net/manual/ja/language.types.type-system.php PHP: 型システム - Manual]
 
組込型の他に、ユーザー定義の型、aliasなどいくつかの型がある。
 
===== 基本型 =====
言語に統合されていて、ユーザー定義で再現不能。
 
* 組込
** null
** スカラー型: bool/int/float/string
** array
** object
** resource
** never
** void
** クラス内の相対型: self/parent/static
* Value型: false/true
* ユーザー定義型/クラス型
** インターフェイス
** クラス
** 列挙型
* callable
 
===== 複合型 =====
複数の基本型を組み合わせた型。交差型とunion型で作れる。
 
* 交差型: 宣言した複数のクラス型をすべて満たす型。&で表現。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====
*[https://www.php.net/manual/ja/language.types.boolean.php PHP: 論理型 (boolean) - Manual]
*[https://qiita.com/minato-naka/items/50645a45998e91c83e2b PHP trueになるもの・falseになるもの確認 falseを入れた配列はどっちに? #初心者 - Qiita]
条件判定にかかってくるので非常に重要。
 
まずは、下記の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: [https://www.php.net/manual/ja/language.types.string.php PHP: 文字列 - Manual].
 
非常に重要。
=====Literal=====
文字列リテラルとしては4の表現がある。
*Single quote: <nowiki>''</nowiki> 変数展開されない。エスケープシーケンス無視。
*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 =====
{| class="wikitable"
|+エスケープされた文字
!記述
!意味
|-
|<code>\n</code>
|ラインフィード (LF またはアスキーの 0x0A (10))
|-
|<code>\r</code>
|キャリッジリターン (CR またはアスキーの 0x0D (13))
|-
|<code>\t</code>
|水平タブ (HT またはアスキーの 0x09 (9))
|-
|<code>\v</code>
|垂直タブ (VT またはアスキーの 0x0B (11))
|-
|<code>\e</code>
|エスケープ (ESC あるいはアスキーの 0x1B (27))
|-
|<code>\f</code>
|フォームフィード (FF またはアスキーの 0x0C (12))
|-
|<code>\\</code>
|バックスラッシュ
|-
|<code>\$</code>
|ドル記号
|-
|<code>\"</code>
|二重引用符
|-
|<code>\[0-7]{1,3}</code>
|8進数: 正規表現 <code>[0-7]{1,3}</code> にマッチする文字シーケンスは、8 進数表記の 1 文字 (例:. <code>"\101" === "A"</code>) です。 正規表現にマッチする文字シーケンスは、8 進数表記の 1 文字です。 1 バイトに収まらない部分は、何もメッセージを出さずにオーバーフローします (例: <code>"\400" === "\000"</code>) 。
|-
|<code>\x[0-9A-Fa-f]{1,2}</code>
|16進数: 正規表現 <code>[0-9A-Fa-f]{1,2}</code> にマッチする文字シーケンスは、16 進数表記の 1 文字(例: <code>"\x41" === "A"</code>)です。
|-
|<code>\u{[0-9A-Fa-f]+}</code>
|Unicode: 正規表現 <code>[0-9A-Fa-f]+</code> にマッチする文字シーケンスは、Unicode のコードポイントです。 そのコードポイントの UTF-8 表現を文字列として出力します。 シーケンスを波括弧で囲む必要があります。例 <code>"\u{41}" === "A"</code>
|}
繰り返しますが、この他の文字をエスケープしようとした場合には、 バックスラッシュも出力されます!
 
=====Variable expansion=====
二重引用符とヒアドキュメントではエスケープシーケンスが解釈され、変数が展開される。<syntaxhighlight lang="php">
<?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
 
?>
</syntaxhighlight>波括弧はなくてもいいが、文字列が連結するなどして変数名の終端を区別できない場合に必須になる。
 
特に重要な挙動は以下。
 
* ""内だと、連想配列添字の引用符不能。
* ${}内だと、連想配列添字の引用符必要。
 
複雑な形式は{$ ... }がセット。{$ } 部分で変数が式扱いになる。
 
<nowiki>さらに複雑なことができる。{${}}を指定すると、内側の波括弧内で、${}部分が変数評価になる場合にだけ式を指定できる。動きがトリッキーすぎる。フォーマット文字列的なことには使えない。バグのもとになりそうなので使用を控えたほうがよさそう。</nowiki>
=====Format=====
Ref:
*[https://chaika.hatenablog.com/entry/2023/02/14/120000 PHP 文字列中に変数で値を埋め込むやつのメモ - かもメモ]
*[https://qiita.com/iez/items/8a36e5cba8bf6af21f45 python の string.format が便利だからPHPでも使いたい #PHP - Qiita]
PHPにはPythonのformatメソッド相当はない。が似たような目的の関数がある。
*sprintf/vprintf
*strtr
strtrは第2引数にold => newの置換のペアの配列を渡す。やることは同じようなものだけどちょっと違う。
 
vsprintfは置換対象が可変長引数ではなく配列なだけ。
=====sprintf=====
Ref: [https://www.php.net/manual/ja/function.sprintf.php PHP: sprintf - Manual]
 
今後何度も使う。
 
C言語のprintfといろいろ違うところがある。<syntaxhighlight lang="php">
<?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
 
?>
</syntaxhighlight>特徴的なのが`%数$指定子`で引数の番号を選べるところ。Pythonの`{数:指定子}`に似ている。
 
後は埋める文字を指定する際は'を前置。
=====文字列の切り出し=====
いくつか方法がある。
*substr/mb_substr
*strpos/mb_strpos/strrpos/mb_strrpos ([https://www.php.net/manual/ja/function.strpos.php PHP: strpos - Manual])
*split
*preg_match ([https://www.php.net/manual/ja/function.preg-match.php PHP: preg_match - Manual])
preg_matchの自由度が高い。速度を気にしなくていいならこれでいいと思われる。ただ、引数の配列に入ってくるのがいまいち。関数の戻り値でほしい。
 
strposとsubstrを組み合わせると端の文字列を切り出せる。
=====文字列置換=====
*[https://www.php.net/manual/ja/function.str-replace.php PHP: str_replace - Manual]: 日本語不能。記号の置換などで便利。
*[https://www.php.net/manual/ja/function.substr-replace.php 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, <nowiki>''</nowiki>, -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なら「[https://www.php.net/manual/en/function.str-starts-with.php PHP: str_starts_with - Manual]/[https://www.php.net/manual/en/function.str-ends-with.php 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でマルチバイト対応。
 
===== 改行分割 =====
 
* [https://stackoverflow.com/questions/3997336/explode-php-string-by-new-line Explode PHP string by new line - Stack Overflow]
* [https://stackoverflow.com/questions/1483497/split-string-by-new-line-characters php - Split string by new line characters - Stack Overflow]
* [https://stackoverflow.com/questions/7836632/how-to-replace-different-newline-styles-in-php-the-smartest-way How to replace different newline styles in PHP the smartest way? - Stack Overflow]
 
<code>explode('\n', $csv)</code> のようなことをしたくなるが、改行が\nとは限らない。
$array = preg_split('/\R/u', $string);
上記がいい。\Rが\r \n \n\rなどにマッチ。uで入力がUTF-8の場合を考慮。例えば、「腰」がuをつけないと分割されてしまう。
 
===== trim =====
[https://www.php.net/manual/ja/function.trim.php PHP: trim - Manual]
 
文字列の両端のホワイトスペースを除去する。
 
===== 文字列反復 =====
[https://www.php.net/manual/ja/function.str-repeat.php PHP: str_repeat - Manual]
str_repeat(string $string, int $times): string
文字列に対する乗算はstr_repeatで行う。他にarray_fillを使った方法もある。
 
プリペアードステートメントで(?,?)を作るときとかで使う。
function timeit(callable $callback)
{
<nowiki> </nowiki>  $time = 'microtime';
<nowiki> </nowiki>  $nanoFactor = 1000;
<nowiki> </nowiki>  if (function_exists('hrtime')) {
<nowiki> </nowiki>      $time = 'hrtime';
<nowiki> </nowiki>      $nanoFactor = 1;
<nowiki> </nowiki>  }
<nowiki> </nowiki>  $start = $time(true);
<nowiki> </nowiki>  $callback();
<nowiki> </nowiki>  $stop = $time(true);
<nowiki> </nowiki><nowiki>  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</nowiki>
<nowiki>*</nowiki>/
常に速いのはrtim。
 
===== 文字数カウント =====
[https://www.php.net/manual/ja/function.substr-count.php PHP: substr_count - Manual]
 
行数カウントなどで文字列をカウントしたいことがそれなりにある。
 
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の判定 =====
 
* [https://www.fourier.jp/blog/php-read-csv-utf8-with-bom UTF-8BOM有無両対応のCSVファイル読み込み(PHP) | 株式会社フーリエ | Web戦略・システム開発[東京/浜松]]
* [https://qiita.com/tf_okrt/items/be81cd66e38fc38d3aeb php で csv を読み込む上での備忘録 #PHP - Qiita]
* [https://stackoverflow.com/questions/26679980/csv-upload-parsing-with-splfileobject-remove-bom php - CSV upload - parsing with SplFileObject - Remove BOM - Stack Overflow]
 
読み込んだファイルにUTF-8のBOMがあって、データ処理としてはBOMを除外したいことがある。
 
いくつか方法がある。
$header[0] = preg_replace('/^\xEF\xBB\xBF/', <nowiki>''</nowiki>, $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;
        }
こういう
 
====Array====
 
===== About =====
[https://www.php.net/manual/ja/language.types.array.php PHP: 配列 - Manual]
 
PHPの配列は、順序マップ。
array(
    key  => value,
    key2 => value2,
    key3 => value3,
    ...
)
全て連想配列。キーを省略したら、登場したキーの数+1の添え字のキーに自動で採番される。ただし、先頭は0。
 
順序があるので、foreachした場合の順序も追加順で保証される。必要なら明示的にソートする。
 
=====Create=====
 
====== Basic ======
配列の作成方法がいくつかある。
*array()/[]
*explode ([https://www.php.net/manual/ja/function.explode.php PHP: explode - Manual])
*array_merge
*array_map
$arr[キー] = 値;
$arr[] = 値;
// キーは文字列か整数。
$arr = [
  'key1' => 'value1',
];
$arrが存在しないか、null/falseの場合、新しい配列を作成する。ただし、この方法は万が一既存の変数があったら、追加になるのであまり推奨されない。明示的に初期化したほうがいい。
 
2行余分に増えるが、上記の形式が初期化もできるのでいいだろう。
explode(',', '物件コード,オーナーコード,棟数,M数,実戸数,a,b,c')
['物件コード','オーナーコード','棟数','M数','実戸数','a','b','c']
explode(',', '物件コード,オーナーコード,棟数,M数,実戸数)
['物件コード', 'オーナーコード', '棟数', 'M数', '実戸数']
explodeで配列を作ると短いのは、要素数8以上。詰めずに書いたら5以上。
 
ただ、余計な関数呼び出しが発生するから、あまりしないほうがいいかも。
 
====== Serial ======
 
* [https://www.php.net/manual/ja/function.array-fill.php PHP: array_fill - Manual]
* [https://www.php.net/manual/ja/function.array-fill-keys.php PHP: array_fill_keys - Manual]
* [https://www.php.net/manual/ja/function.range.php PHP: range - Manual]
* [https://www.php.net/manual/ja/function.str-repeat.php PHP: str_repeat - Manual]
 
同じ値の複数要素、連番データの作成方法がある。
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: 指定値の指定要素数配列。連想配列で複数キーに同じ値を設定したい場合に使う。
 
===== Read =====
 
====== 末尾要素 ======
[https://hishikiryu.com/php-get-last-array-value/ 【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: 非推奨。
 
====== 指定要素の取得 ======
[https://gen0e0.hatenablog.com/entry/2018/03/16/164714 PHPの連想配列から一部を切り出す話 - あしたにっき]
 
* []:
* 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は添え字がなかったら空配列を返してくれるので、添え字アクセスより安全。
 
====== 連想配列の先頭・末尾 ======
 
* [https://stackoverflow.com/questions/1028668/get-first-key-in-a-possibly-associative-array php - Get first key in a (possibly) associative array? - Stack Overflow]
* [https://www.php.net/manual/ja/function.array-key-first.php PHP: array_key_first - Manual]
* [http://taustation.com/php-head-tail-and-subarray/ PHP – 配列の先頭・末尾・部分配列の取出し(非破壊的) – TauStation]
* [https://bashalog.c-brains.jp/15/03/10-172501.php 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]。これがいい。
 
====== 抽出 ======
 
* [https://www.php.net/manual/ja/function.array-splice.php PHP: array_splice - Manual]
* [https://www.php.net/manual/ja/function.array-pop.php PHP: array_pop - Manual]
* [https://www.php.net/manual/ja/function.array-shift.php PHP: array_shift - Manual]
 
配列の分割などで、重複をなくすために、取得後削除したいことがある。
 
先頭と末尾ならarray_shift/array_pop。それ以外はarray_spliceを使う ([https://chatgpt.com/c/67344cfe-dc20-800b-9cd3-1dee66a4deab 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_column ======
[https://www.php.net/manual/ja/function.array-column.php PHP: array_column - Manual]
 
テーブルの取得結果の整形に非常に便利。
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の単純配列。
 
=====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_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);
 
=====Remove=====
配列要素の削除方法がいくつかある。
*unset($arr[$key]);
*array_shift($arr): 先頭要素を削除。削除済み要素を返す。破壊的な処理。
*array_pop($arr): 末尾要素を削除。削除済み要素を返す。破壊的な処理。
*array_slice ([https://www.php.net/manual/ja/function.array-slice.php 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 =====
 
* [https://fellowtuts.com/php/change-array-key-without-changing-order/ 3 Ways to Change Array Key without Changing the Order in PHP]
* [https://stackoverflow.com/questions/9605143/how-to-rename-sub-array-keys-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=====
[https://stackoverflow.com/questions/6418903/how-to-clone-an-array-of-objects-in-php How to clone an array of objects in PHP? - Stack Overflow]
 
配列変数を代入すると通常はそれでコピーになる。ただし、配列にオブジェクトがあると、そのオブジェクトはシャローコピーになる。
$new = array();
foreach ($old as $k => $v) {
    $new[$k] = clone $v;
}
上記のように配列要素をcloneでコピーして作る必要がある模様 ([https://www.php.net/manual/ja/language.oop5.cloning.php PHP: オブジェクトのクローン作成 - Manual])。
=====Comma=====
*[https://www.php.net/manual/ja/language.types.array.php PHP: 配列 - Manual]
*[https://kinsta.com/jp/blog/php-7-3/#trailing-comma-in-function-calls PHP 7.3の新機能(Kinstaで利用可能)]
PHPでは配列の終端カンマは許容される。
 
他にも、名前空間のグループ指定はPHP7.2以上、関数の引数はPHP7.3以上で可能になった。
=====連想配列判定=====
[https://qiita.com/Hiraku/items/721cc3a385cb2d7daebd 配列か連想配列か判定する #PHP - Qiita]
<?php
if (array_values($arr) === $arr) {
  echo '$arrは配列';
} else {
  echo '$arrは連想配列';
}
これで添え字が、数字かどうかをみるのがいい模様。
=====Convert=====
 
====== 2次元配列→1次元配列 ======
 
* [https://zenn.dev/akido_/articles/833232e489137f PHPで二次元配列を一次元配列に変換する方法]
* [https://www.php.net/manual/ja/function.array-column.php PHP: array_column - Manual]
* [https://qiita.com/harukasan/items/a0773aef27d838852e44 PHPのarray_columnが便利 #PHP - Qiita]
 
$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 = <nowiki>''</nowiki>): array {
<nowiki> </nowiki>   $result = [];
<nowiki> </nowiki>  foreach ($array as $key => $value) {
<nowiki> </nowiki>      $newKey = $prefix === <nowiki>''</nowiki> ? $key : $prefix . '.' . $key;
<nowiki> </nowiki>      if (is_array($value)) {
<nowiki> </nowiki>          // 再帰的に呼び出して配列をフラットにする
<nowiki> </nowiki>          $result += flattenArray($value, $newKey);
<nowiki> </nowiki>      } else {
<nowiki> </nowiki>          // フラット化した結果にキーと値を追加
<nowiki> </nowiki>          $result[$newKey] = $value;
<nowiki> </nowiki>      }
<nowiki> </nowiki>  }
<nowiki> </nowiki>  return $result;
}
// 使用例
$nestedArray = [
<nowiki> </nowiki>  'user' => [
<nowiki> </nowiki>      'name' => 'Alice',
<nowiki> </nowiki>      'details' => [
<nowiki> </nowiki>          'age' => 25,
<nowiki> </nowiki>          'address' => [
<nowiki> </nowiki>              'city' => 'New York',
<nowiki> </nowiki>              'zip' => '10001'
<nowiki> </nowiki>          ]
<nowiki> </nowiki>      ]
<nowiki> </nowiki>  ],
<nowiki> </nowiki>  '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に変換する。
 
[https://stackoverflow.com/questions/15191903/convert-an-associative-array-to-a-simple-array-of-its-values-in-php Convert an associative array to a simple array of its values in php - Stack Overflow]
 
[https://qiita.com/ktz_alias/items/16241bdf11757b556cdb 連想配列をキーと値のペアの配列にするちょっと気のきいた方法(かも) #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',
)
*/
 
======単純配列→連想配列======
[https://www.techiedelight.com/ja/convert-regular-array-to-associative-array-php/ PHP で通常の配列を連想配列に変換する]
 
いくつか方法がある。
#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→連想配列 ======
 
* [https://medium.com/@czmole/php-convert-csv-to-associative-arrays-b82b9b4d4412 PHP convert CSV to associative arrays | by Catalin ZMOLE 👨‍💻 | Medium]
* [https://steindom.com/2012/12/08/shortest-php-code-convert-csv-associative-array Shortest PHP code to convert CSV to associative array | Steindom]
* [https://www.php.net/manual/en/function.str-getcsv.php 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のほうがいい。
 
[https://blog.fenrir-inc.com/jp/2014/07/php-csv.html 【PHP】その CSV 変換、本当に「fgetcsv」でいいの? (フェンリル | デベロッパーズブログ)]
======反転|array_flip======
[https://www.php.net/manual/ja/function.array-flip.php PHP: array_flip - Manual]
 
配列のキーと値を反転した配列を返す。元のarrayの値は有効なキーを必要とする。つまり、intかstring。型が違う場合、警告が出て無視される。
 
また、同じ値が複数ある場合、最後のみが有効になる。
 
====== 分割 ======
 
* [https://stackoverflow.com/questions/29792685/php-split-array-in-subarrays PHP Split array in subarrays - Stack Overflow]
* [https://www.php.net/manual/ja/function.array-chunk.php PHP: array_chunk - Manual]
 
1個の大きな配列をそのまま反復させると大きいので、指定要素数ずつに分割して、処理したいことがある。
 
一括INSERTを分割する場合など。array_chunkで配列を分割できるのでこれを使う。
 
implodeで文字列にマージして、explodeで分割というのもある。
 
====== String ======
[https://www.php.net/manual/ja/function.implode.php PHP: implode - Manual]
implode(array|string $separator = "", ?array $array): string
配列だけ指定した場合、空文字で結合する。
var_dump(implode(['a', 'b', 'c'])); // string(3) "abc"
 
=====Search=====
[https://www.sejuku.net/blog/22098 【PHP入門】配列の値を検索するarray_searchと他4つの関数 | 侍エンジニアブログ]
 
いくつか方法がある。
 
基本はin_array。複雑な検索はarray_filter/array_intersect。
======array_key_exists/キー確認======
[https://www.php.net/manual/ja/function.array-key-exists.php 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を回避しながら手短にかける。
======empty======
 
* [https://qiita.com/miriwo/items/c4760cbb2807ee84ef2d PHP 配列が空かどうかを判定する #初心者 - Qiita]
 
* [https://www.php.net/manual/ja/function.count.php PHP: count - Manual]
* [https://www.php.net/manual/ja/function.array-filter.php PHP: array_filter - Manual]
 
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([[/wiki.gnusocial.jp//www.php.net/manual/ja/language.types.mixed.php|mixed]] <code>$needle</code>, [[/wiki.gnusocial.jp//www.php.net/manual/ja/language.types.array.php|array]] <code>$haystack</code>, [[/wiki.gnusocial.jp//www.php.net/manual/ja/language.types.boolean.php|bool]] <code>$strict</code> = <code>[[/wiki.gnusocial.jp//www.php.net/manual/ja/reserved.constants.php#constant.false|false]]</code>): [[/wiki.gnusocial.jp//www.php.net/manual/ja/language.types.boolean.php|bool]]
<code>haystack</code> 内の <code>needle</code> を検索します。 <code>strict</code> が設定されていない限りは型の比較は行いません。
 
基本は$strict=trueで指定したほうがいい。完全一致検索。
======any/all/some/every======
[https://stackoverflow.com/questions/39875691/is-there-a-php-equivalent-of-javascripts-array-prototype-some-function 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で途中で終わるほうが速い模様。
======配列同士の包含・交差判定======
*[https://stackoverflow.com/questions/9655687/check-if-an-array-contains-all-array-values-from-another-array php - Check if an array contains all array values from another array - Stack Overflow]
*[https://stackoverflow.com/questions/523796/checking-if-any-of-an-arrays-elements-are-in-another-array php - Checking if ANY of an array's elements are in another array - Stack Overflow]
1個でも入っているかを見たければ、array_intersect ([https://www.php.net/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 ([https://www.php.net/manual/ja/function.array-diff.php 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より少し早い ([https://zenn.dev/umeso/articles/280e268e196390 PHPの配列から重複を削除するにはarray_unique()よりarray_keys(array_flip())が速いのか])。
 
array_uniqueはデフォルトではvalueだけで判断する。
 
array_uniqueはデフォルトで文字列として比較する。配列などの場合はSORT_REGULARのフラグを指定する ([https://stackoverflow.com/questions/13857775/remove-duplicated-elements-of-associative-array-in-php Remove duplicated elements of associative array in PHP - Stack Overflow])。
 
ただし、型混在など複雑な場合は比較が失敗することがあるので、自前で行ったほうがいいらしい ([https://qiita.com/y-encore/items/40ba694a8899ad1e9416 PHP: array_uniqueについて #PHP - Qiita])。
 
array_unique重複は最初の要素を残す。最後の要素を残したければ、array_reverseを2併用する ([https://stackoverflow.com/questions/16777363/keep-unique-values-of-array-preserving-order-retaining-last-occurrence-of-each 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回は遅い。
 
[https://vijayasankarn.wordpress.com/2017/02/20/array_unique-for-multidimensional-array/ 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で交差を取得。
 
====== 列の一致判定 ======
重複削除判定時などで、複数配列の同じ列・キーで処理したいことがある。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になる。
ややこしいが、コールバック内を!==にして、結果が空になったら完全一致とみなす。そうしないと、元の要素数の余計な判定が必要になる。
これを応用して行列の一致判定をする場合。<syntaxhighlight lang="php">
$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];});
});
});
</syntaxhighlight>例えば、上記は/blogへのアクセスをArticlesController.index()に紐づける。


connectの第二引数にいろいろパラメーターを指定できて、actionのメソッドを指定できたりする。
</syntaxhighlight>外側に行ループ用の配列をわつぃて、その要素を内側で使うだけ。
=====Array Functions=====
======compact======
Ref: [https://www.php.net/manual/ja/function.compact.php PHP: compact - Manual].
 
変数名とその値から、配列を作る。extractの逆。
 
MVCのViewに複数の値を渡す場合などによく使う。
======extract======
Ref: [https://www.php.net/manual/en/function.extract.php PHP: extract - Manual].
 
配列のキー・バリューを変数として取り込む。
 
===== 一括操作 =====
配列要素全体に一括処理を行える関数がいくつかある。for/foreach文が不要なのでコンパクト。
 
* array_map: 適用結果の配列を取得。
* array_filter: 適用して絞り込んだ配列を取得。
* array_reduce: 繰り返し適用して1個にまとめる。
* array_walk: 要素に適用するだけ。
 
====== forとの速度比較 ======
単に反復させるだけなら、基本的にはfor/foreachを使ったほうが速い模様。
<nowiki>$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";</nowiki>
 
116170 ns
372829 ns
713770 ns
可読性やコード量、反復しないところでarray_関数は使うのがよさそう。
 
======array_map======
*[https://www.php.net/manual/ja/function.array-map.php PHP: array_map - Manual]
array_map(?callable $callback, array $array, array ...$arrays): array
JavaScriptのmap相当。非常に重要でよく使う。
 
callbackにnullを指定すると、複数の配列のzip (unpack) を行う。
 
ただ、array_mapのコールバックの引数は通常配列の要素が想定されていて、連想配列のキーにはアクセスできない。


==== Controller ====
それをしたかったら、array_reduceを使う。らしい。
AppControllerを継承して定義する。indexメソッドを最低限実装しておく。


XXXControllerのXXXの小文字部分がURLになっていて、これでアクセスするとindexが発動する。
[https://www.danielauener.com/howto-use-array_map-on-associative-arrays-to-change-values-and-keys/ Howto use array_map on associative arrays to change values and keys - Daniel Auener]


メソッド名が、パスになっていて、それで表示される。このパスメソッドを一般的にacitonと呼んでいる。
いや、そういうことをしなくても、array_keysを使えばOK。
$result = array_map(function($k, $v){return ;}, array_keys($arr), $arr);
Ref: [https://stackoverflow.com/questions/25513361/how-can-i-use-array-map-with-keys-and-values-but-return-an-array-with-the-same php - How can I use array_map with keys and values, but return an array with the same indexes (not int)? - Stack Overflow]


==== View ====
array_mapは単純配列を返す。元々が連想配列の場合、キーが数値に置換される。元のキーを維持したければ、array_combineを併用する。
Ref: [https://book.cakephp.org/3/ja/views.html ビュー - 3.10].
$arr =
    [
      "id" => 1,
      "name" => "Fred",
    ];
$result = array_combine(
      array_keys($arr),
      array_map(function($v){ return $v; }, $arr)
);


Controllerで表示もできるものの、Viewで表示部分をメインにすることもできる。
====== array_filter ======
[https://www.php.net/manual/ja/function.array-filter.php PHP: array_filter - Manual]


Viewはビューテンプレートとレイアウトで大きく構成される。
名前通り配列要素をフィルターリングする。
array_filter(array $array, ?callable $callback = null, int $mode = 0): array
$modeを指定しなければcallbackにはvalueのみ渡される。


テンプレートは実際のページ表示用。レイアウトは、テンプレートを含み、ヘッダー、フッターなどをまとめている。
callbackがtrueを返したら、その要素を残す。callbackを指定しなかったら、!empty($v)相当。なので、array_filter($array) で、キーがある場合の配列要素の空判定にもなる。


レイアウトを用意して、その中にビューテンプレートを埋め込んで表示するイメージ。これにより、ページ全体でレイアウトを統一しやすくなる。
他には応用として、操作対象ののキーの配列を渡して、そのキーを使って複数の配列の同じキーの一致・重複判定などできる。
        /** UPSERTのAI増分対策用に重複削除。 */
<nowiki> </nowiki>      if (!empty($unique)) {
<nowiki> </nowiki>          $old_row = <nowiki>''</nowiki>;
<nowiki> </nowiki>          foreach ($records as $row => $line) {
<nowiki> </nowiki>              // unique対象列が全部一致の場合削除。
<nowiki> </nowiki>              if (array_filter($unique, function($v) use ($line, $records, $old_row) {return $line[$v] !== $records[$old_row][$v];})) {
<nowiki> </nowiki>                  unset($records[$old_row]);
<nowiki> </nowiki>              }
<nowiki> </nowiki>              $old_row = $row;
<nowiki> </nowiki>          }
<nowiki> </nowiki>      }


テンプレートファイルのデフォルトの拡張子は.ctp (CakePHP Templateの略)
====== array_reduce ======
[https://www.php.net/manual/ja/function.array-reduce.php PHP: array_reduce - Manual]
array_reduce(array $array, callable $callback, mixed $initial = null): mixed


===== Template =====
callback(mixed $carry, mixed $item): mixed
src/TemplateがViewテンプレート。ここにコントローラー名に対応したディレクトリーを作って、そこにViewテンプレートを作成する。
$carryに前回処理結果。$itemに現在要素。


例えば、src/Template/Helloにindex.ctpを用意する。
配列要素を集計して1要素にまとめる。


Controller側で、public $autoRender = true;があると、テンプレートを使う。命名規則で自動的に。
統計処理したり、結合したりできる。
var_export(array_reduce([0, 1, 2], function($c, $v){return $c.$v;})); // '012'
配列要素の列を結合したいことがある。そういうときにこれを使う。


そして、$this->viewBuilder()->autoLayout(false) でレイアウトの使用可否を設定する。
==== Enum ====
[https://www.php.net/manual/ja/language.types.enumerations.php PHP: 列挙型 / Enum - Manual]


index.ctpはテンプレートなので、ここにPHPコードを記入してもOK。
PHP 8.1.0から導入。長らくなかった。


テンプレート内では変数が使える。この変数は、コントローラーで$this->setで設定されたものになる。
複数の異なる値を1個の集合として取り扱うデータ型。


基本的には以下のようなphpコードで埋め込む。
終了コードなど、意味がある数字を扱う。
<?php echo $variable; ?>
<?= $variable ?>


===== Layout =====
enumがないと、値の下限、上限など、ただの数字だから保証できない。
デフォルトで使用するレイアウトは、src/Template/Layout/default.ctpにある。


ここにファイルを追加することが、レイアウトの作成になる。
===== Implementation =====


レイアウト内で、テンプレートを表示する関数類がある。
* [https://tech.designone.jp/entry/2022/01/12/181158 【PHP 8.1】とうとうPHPにもEnumがやってきた - デザインワン・ジャパン Tech Blog]
* [https://zenn.dev/naopusyu/scraps/798f57c5f3dbf1 php-enumのメモ]
* [https://blog.wh-plus.co.jp/entry/2023/07/25/164637 PHP8.1のEnumと独自実装のEnumを比較して移行できるか検討しました - WHITEPLUS TechBlog]
* [https://stackoverflow.com/questions/1528280/how-to-implement-enum-like-functionality-in-php How to implement Enum like functionality in PHP? - Stack Overflow]


* $this->fetch('content'): ビューテンプレート
長らく言語機能になかったのでクラスやトレイトを使った独自実装が試されている。


Controller側では、$this->viewBuilder()->layout('Hello');でレイアウトを指定できる。
* [https://github.com/BenSampo/laravel-enum GitHub - BenSampo/laravel-enum: Simple, extensible and powerful enumeration implementation for Laravel.]
* [https://github.com/myclabs/php-enum GitHub - myclabs/php-enum: The enum PHP is missing, inspired from SplEnum]


===== Element =====
昔はSplEnumという実験モジュールがあったが、Enumの登場でなくなった。
レイアウトよりも小さいパーツ。ボタンなど、流用するようなものをElementとして用意できる。


$this->element(エレメント名, [キー=>値,...])で表示できる。
PHP 7.4以前との互換性のために、独自のクラスで実装して、その内部実装で上記ライブラリー類を使う感じだろう。


src/Template/Element内に作成する。エレメント名.ctpで作成する。キー=>値の関係で、パラメーターを渡せる。
==== Iterable ====


Controllerのaction内でsetで、layout内で使用する変数を指定できる。これで、layout-element間で変数を渡せる。
* [https://www.php.net/manual/ja/language.types.iterable.php PHP: Iterable - Manual]
* [https://www.php.net/manual/ja/class.traversable.php PHP: Traversable - Manual]
* [https://www.php.net/manual/ja/class.iterator.php PHP: Iterator - Manual]


==== Model ====
array|Traversable型のエイリアス。PHP 7.1.0で導入。foreachで使用可能で、ジェネレーター内のyield fromでも使える。
DBを処理する部分。


具体的には、テーブルクラス (テーブルへのアクセス処理) とエンティティークラス (テーブルから取り出した1行分のデータのための列の表現) で実現する。
Traversableインターフェイス、Iteratorクラスが特に重要。このメソッドはいろんなところで登場するから。


CakePHP内では、モデル名Table/モデル名でそれぞれ命名する。テーブルクラスは複数形、エンティティークラスは単数形で扱う。テーブル自体も複数形。
* current: 現在の要素を返す。
* key:
* next
* rewind
* valid


CakePHPでDBにアクセスする際には、まず設定ファイルにDBの情報を用意する。Config/app.phpのDatasourcesのdefaultに入力する。
特にcurrentが重要。例えば、ヘッダーをこれで取得などできる。


モデル自体は、src/Model内に配置する。EntityとTableディレクトリーでそれぞれ用意する。
====Type declarations/型宣言====
[https://www.php.net/manual/ja/language.types.declarations.php PHP: 型宣言 - Manual]


継承するだけで、基本的な動作は機能する。
関数の引数、戻り値、クラスのプロパティー (PHP 7.4.0以上) に型を宣言できる。これにより、型を保証でき、その型でなければ、TypeErrorをスローする。


Controllerから、これらのモデルにアクセスする。
関数の戻り値だけ、型の指定箇所がやや特殊で、それ以外は原則変数の直前。関数の戻り値の場合、(): の後に指定する。
function name(): type {}


==== Migration ====
<?php
function sum($a, $b): float {
    return $a + $b;
}
// float が返される点に注意
var_dump(sum(1, 2));
?>
nullable な型とシンタックスシュガー
 
nullableの場合、型名の前に?を指定する (PHP 7.1.0以上)。?TとT|nullは同じ意味。
 
単一の基本型を宣言した場合、 型の名前の前にクエスチョンマーク (?) を付けることで、nullable であるという印を付けることができます。 よって、?T と T|null は同じ意味です。
 
注意: この文法は、PHP 7.1.0 以降でサポートされており、 PHP 8.0で一般化された union 型がサポートされる前から存在します。
 
PHP 7.4未満などの場合は、しかたないのでアノテーションで対応する。
====Type juggling====
[https://www.php.net/manual/ja/language.types.type-juggling.php PHP: 型の相互変換 - Manual]
 
型の相互変換。非常に重要。いろいろ方法がある。
 
共通なのはキャスト (cast)。<syntaxhighlight lang="php">
<?php
$foo = 10;  // $foo は整数です
$bar = (bool) $foo;  // $bar は boolean です
$fst = "$foo"; // to string.
+"+40"; // to int
?>
</syntaxhighlight>C言語と同じで (型) を前置する。ただし、少々長い。
 
文字列への変換は二重引用符囲、数値への変換は算術演算子 (+)。まあ、キャストだけ覚えておくのがシンプル。
 
===== 型キャスト =====
変換先の型を波括弧で囲んで、変換対象の変数に前置することで変換する。
 
使用可能なキャストは以下。
 
* <code>(int)</code> - 整数([[/www.php.net/manual/ja/language.types.integer.php|int]]) へのキャスト
* <code>(bool)</code> - 論理値([[/www.php.net/manual/ja/language.types.boolean.php|bool]]) へのキャスト
* <code>(float)</code> - [[/www.php.net/manual/ja/language.types.float.php|float]] へのキャスト
* <code>(string)</code> - 文字列([[/www.php.net/manual/ja/language.types.string.php|string]]) へのキャスト
* <code>(array)</code> - 配列([[/www.php.net/manual/ja/language.types.array.php|array]]) へのキャスト
* <code>(object)</code> - オブジェクト([[/www.php.net/manual/ja/language.types.object.php|object]]) へのキャスト
* <code>(unset)</code> - [[/www.php.net/manual/ja/language.types.null.php|NULL]] へのキャスト PHP 8.0.0で削除。単にNULLを代入する。
 
言語構造なので、関数よりも高速。丸括弧内のスペースは無視される。
 
====Other====
=====型判定=====
[https://www.php.net/manual/ja/function.gettype.php PHP: gettype - Manual]
 
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: [https://www.php.net/manual/en/language.variables.basics.php PHP: Basics - Manual]
 
===== 使用可能な文字 =====
変数名は、PHPの他のラベルと同じルールに従います。 有効な変数名は文字またはアンダースコアから始まり、任意の数の文字、 数字、アンダースコアが続きます。正規表現によれば、これは次の ように表現することができます。
^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$
ASCIIテキストの範囲だと記号類は_以外変数名に使用不能。
=====Undefined variable=====
 
* [https://www.php.net/manual/ja/language.types.array.php PHP: 配列 - Manual]
 
未定義変数 (undefined variable) の値はNULL。
 
未定義変数 (配列の不在キー) にアクセスすると、E_WARNING (PHP 8未満はE_NOTICE) レベルのエラーが生じて、nullを返す。回避したければ、isset()で検知する。要素の追加時のアクセスは問題ない。
 
未定義変数の検知・制御方法がいくつかある。
*isset ([https://www.php.net/manual/en/function.isset.php PHP: isset - Manual])
*empty ([https://www.php.net/manual/en/function.empty.php 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 =====
出典: [https://www.php.net/manual/en/language.variables.scope.php PHP: Variable scope - Manual]。
 
関数の外で使用するとグローバルスコープになる。ただし、関数内では暗黙にはグローバル変数は使えない。未定義変数扱いになる。
 
関数内でグローバル変数を参照したければ、関数内でglobalで明示的に使用したいグローバル変数を宣言する必要がある。
<?php
$a = 1;
$b = 2;
function Sum()
{
    global $a, $b;
    $b = $a + $b;
}
あるいは、$GLOBALS配列にグローバル変数が入っているのでこれを使う。
 
なお、'''波括弧のブロックスコープは存在しない'''。C系言語の感覚だと、波括弧でスコープが作られそうなイメージがあるが、PHPの波括弧はスコープを作らない。あくまで、関数の内部かどうか。
 
逆にいうと、関数内に定義される関数・クラスも基本グローバル。
 
子関数に変数を渡したい場合、引数かグローバル変数しかない。他に隠蔽したり、親関数からスコープを引き継ぎたい場合、無名関数を使うしか無い。
 
===== Super global =====
[https://www.php.net/manual/ja/language.variables.superglobals.php PHP: スーパーグローバル - Manual]
 
全てのスコープで使用可能な組込変数。関数、メソッド内でもglobal $variable;とする必要がない。
 
* $GLOBALS: グローバル変数の連想配列。
* $_SERVER
* $_GET
* $_POST
* $_FILES
* $_COOKIE
* $_SESSION
* $_REQUEST
* $_ENV
 
==== Variable variables/可変変数 ====
 
* [https://www.php.net/manual/ja/language.variables.variable.php PHP: 可変変数 - Manual]
* [https://www.php.net/manual/ja/language.oop5.properties.php PHP: プロパティ - Manual]
 
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====
Ref:
Ref:
*[https://www.php.net/manual/en/language.variables.external.php PHP: Variables From External Sources - Manual]
*[https://www.php.net/manual/en/faq.html.php PHP: PHP and HTML - Manual]
*[https://www.php.net/manual/en/reserved.variables.php PHP: Predefined Variables - Manual]
*[https://www.php.net/manual/en/book.filter.php PHP: Filter - Manual]
PHPとHTMLフォームの関係がある。重要。
配列渡しはPHP側の仕様。
===== HTML Forms (GET and POST) =====
PHPではフォーム変数のドットとスペースはアンダーバーに変換される。
たとえば <code><input name="a.b" /></code> は <code>$_REQUEST["a_b"]</code> となります。
===== 外部変数名のドット =====
PHPの変数名でドットやスペースは無効。その都合で、それらの文字は_に置換される。フォーム変数も似たような考え方。
=== Constants ===
==== About ====
* [https://www.php.net/manual/ja/language.constants.php PHP: 定数 - Manual]
* [https://www.php.net/manual/ja/function.define.php PHP: define - Manual]
定数は値のためのID (名前)。基本的にスクリプト実行中に変更できない。大文字小文字を区別するが、慣習として大文字で表記する。
constキーワードか、define関数で定義できる。constの場合、制約がある。
constで指定可能なのは、スカラー式 (bool/int/float/string) と、スカラー式のみのarray。動的な設定はできない。
変数と異なり、$の前置は不要。
定数の定義判定は、defined()を使う。
定数の変数との違いは以下。
* $不要。
* スコープに関係なく、あらゆる場所からアクセス可能。
* 後から再定義、未定義不能。
* スカラー値と配列のみ。
constはコンパイル時に定義されるため、トップレベル以外、つまりブロック内部 (関数/ループ/if/try) で宣言できない。defineはできる。
==== define/const ====
* [https://qiita.com/schrosis/items/485b984e05b2eb4521b4 PHPの「define」と「const」の違い #定数 - Qiita]
* [https://qiita.com/nishimura/items/a396c999a85fa4cbc4a0 PHPでプログラム全体の設定に使う変数の保持の仕方 #PHP - Qiita]
{| class="wikitable"
|+
!項目
!const
!define
|-
|構文
|予約語 (少し速い)
|関数
|-
|戻り値
|なし
|あり
|-
|定義元
|スカラー値のみ
|変数/関数OK
|-
|クラス定数
|x
| -
|-
|使用箇所
|制御ブロック内部以外
|どこでも
|-
|スコープ
|名前空間
|グローバル
|}
defineはブロック内で使えるので、何らかの条件で定義を変更できるのが利点。例えば、環境を本番とデバッグに変えたりなど。
動的に変更したいならdefine、それ以外は名前空間やクラス定数として使えるconstだろうか。関数内のマジックナンバー的な使い方はできない。そういうのは、普通の変数で取り扱う。
ただ、constはアプリの設定として使うことはない。クラスの固有値の定義。
==== constant ====
[https://www.php.net/manual/ja/function.constant.php PHP: constant - Manual]
定数名の文字列で、定数の値を取得したい場合に使える。
定数の他に、enumのcaseにも使える。
==== class ====
クラス内に定数を定義できる。デフォルトでpublic。staic変数的な扱い。インスタンスではなく、クラスが保有する。
==== predefined ====
* [https://www.php.net/manual/ja/language.constants.predefined.php PHP: 自動的に定義される定数 - Manual]
* [https://www.php.net/manual/ja/reserved.constants.php PHP: 定義済みの定数 - Manual]
言語で定義済みの定数がいろいろある。true/false/nullなど。
==== Magic/マジック定数 ====
使用箇所で値が変化する定数 (マジック定数) が9個ある。C言語のマクロに近い。コンパイル時に解決される。大文字小文字を区別しない。
{| class="wikitable"
|+PHP の "マジック" 定数
!名前
!説明
|-
|<code>[[/www.php.net/manual/ja/language.constants.magic.php#constant.line|__LINE__]]</code>
|ファイル上の現在の行番号。
|-
|<code>[[/www.php.net/manual/ja/language.constants.magic.php#constant.file|__FILE__]]</code>
|ファイルのフルパスとファイル名 (シンボリックリンクを解決した後のもの)。 インクルードされるファイルの中で使用された場合、インクルードされるファイルの名前が返されます。
|-
|<code>[[/www.php.net/manual/ja/language.constants.magic.php#constant.dir|__DIR__]]</code>
|そのファイルの存在するディレクトリ。include の中で使用すると、 インクルードされるファイルの存在するディレクトリを返します。 つまり、これは <code>dirname(__FILE__)</code> と同じ意味です。 ルートディレクトリである場合を除き、ディレクトリ名の末尾にスラッシュはつきません。
|-
|<code>[[/www.php.net/manual/ja/language.constants.magic.php#constant.function|__FUNCTION__]]</code>
|関数名。無名関数の場合は、<code>{closure}</code>
|-
|<code>[[/www.php.net/manual/ja/language.constants.magic.php#constant.class|__CLASS__]]</code>
|クラス名。 クラス名には、そのクラスが宣言されている名前空間も含みます (例 <code>Foo\Bar</code>)。 トレイトのメソッド内で __CLASS__ を使うと、 そのトレイトを use しているクラスの名前を返します。
|-
|<code>[[/www.php.net/manual/ja/language.constants.magic.php#constant.trait|__TRAIT__]]</code>
|トレイト名。 トレイト名には、宣言された名前空間も含みます (例 <code>Foo\Bar</code>)。
|-
|<code>[[/www.php.net/manual/ja/language.constants.magic.php#constant.method|__METHOD__]]</code>
|クラスのメソッド名。
|-
|<code>[[/www.php.net/manual/ja/language.constants.magic.php#constant.namespace|__NAMESPACE__]]</code>
|現在の名前空間の名前。
|-
|<code>ClassName::class</code>
|完全に修飾されたクラス名。
|}
どれもよく使う。
===Operators===
[https://www.php.net/manual/en/language.operators.php PHP: Operators - Manual]
==== precedence/優先順位 ====
[https://www.php.net/manual/ja/language.operators.precedence.php PHP: 演算子の優先順位 - Manual]
丸括弧をつけるかつけないかが変わる。
特によく使うもの、注意が必要なものを整理する。
===== if (!$var = getVar()) =====
* [https://stackoverflow.com/questions/12453968/why-does-negation-happen-last-in-an-assignment-expression-in-php Why does negation happen last in an assignment expression in PHP? - Stack Overflow]
* [https://qiita.com/itsumoonazicode/items/750778a0e97c85b0dd6f if文の中で変数定義 - PHP #PHP - Qiita]
!は=より優先順位が高いが if (!$var = getVar()) のような式は成立して、変数代入結果の否定が評価される。<blockquote>注意: = は他のほとんどの演算子よりも優先順位が低いはずなのにもかかわらず、 PHP は依然として if (!$a = foo()) のような式も許します。この場合は foo() の戻り値が $a に代入されます。</blockquote>これが成立する理由。=の左辺は変数じゃないといけないから。(!$var) には代入がそもそもできない。そのため、PHPができるだけパース仕様として、以下のように代入部分を丸括弧で囲んだ扱いにしてくれる。
!$var = getVar()
!($var = getVar())
だからこれが成立する。関数の処理結果を保存して、判定してその後の流用に短縮できて便利。
====Assignment/代入演算子====
*[https://www.php.net/manual/en/language.operators.assignment.php PHP: Assignment - Manual]
*[https://qiita.com/rana_kualu/items/e15a9b7c12f175380244 【PHP7.4】PHPの新たな演算子??=ってなんぞ? #NULL合体代入演算子 - Qiita]
??=
    // NULL合体代入演算子
    $id ??= getId();
    // これと同じ
    $id = $id ?? getId();
    $id = @$id ?: getId();
    $id = isset($id) ? $id : getId();
NULL合体演算子の代入版。nullの場合の代入が簡単になった。PHP 7.4から使用可能。
====Comparison/比較演算子====
*[https://www.php.net/manual/ja/language.operators.comparison.php PHP: 比較演算子 - Manual]
*[https://www.php.net/manual/ja/migration70.new-features.php PHP: 新機能 - Manual]
=====三項演算子 (条件演算子)=====
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系言語でも真ん中は省略できない。
式 <code>expr1 ?: expr3</code> の結果は、expr1 が <code>[https://www.php.net/manual/ja/reserved.constants.php#constant.true true]</code> と同等の場合は 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合体演算子を組み合わせて、未定義のなどの場合のデフォルト値設定で役立つ。
[https://www.php.net/manual/ja/language.operators.logical.php PHP: 論理演算子 - Manual]
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/エラー制御演算子@====
*[https://www.php.net/manual/ja/language.operators.errorcontrol.php PHP: エラー制御演算子 - Manual]
*[https://toku1.jp/programming-php-error-handle-operator/ 【PHP】エラー制御演算子に正しい使い道はあるのか? | とりあえず、いっとく!]
式の直前に@を前置すると、その式のエラーメッセージを無視する。
基本的に使わないほうがいい。if文などでガードするより処理が遅い。ただし、Viewなどであまり影響ない場合などは記述がシンプルになるという利点もあるかも。
ただ、やっぱり基本は使わないほうがいい。バグの見落としになる。
====Logical/論理演算子====
[https://www.php.net/manual/ja/language.operators.logical.php PHP: 論理演算子 - Manual]
論理積と論理和が、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演算子を使うことはできない。同じ論理型同士なら成立はするが。
デフォルト値扱いにしたければ、短縮条件演算子?:や、ヌル合体演算子??を使う。
==== 配列演算子 ====
[https://www.php.net/manual/ja/language.operators.array.php PHP: 配列演算子 - Manual]
配列に対する演算子は扱いがやや特殊。
{| class="wikitable"
|+Array Operators
!例
!名前
!結果
|-
|$a + $b
|結合
|<var>$a</var> および <var>$b</var> を結合する。
|-
|$a == $b
|同等
|<var>$a</var> および <var>$b</var> のキー/値のペアが等しい場合に <code>[[/www.php.net/manual/ja/reserved.constants.php#constant.true|true]]</code>。
|-
|$a === $b
|同一
|<var>$a</var> および <var>$b</var> のキー/値のペアが等しく、その並び順が等しく、 かつデータ型も等しい場合に <code>[[/www.php.net/manual/ja/reserved.constants.php#constant.true|true]]</code>。
|-
|$a != $b
|等しくない
|<var>$a</var> が <var>$b</var> と等しくない場合に <code>[[/www.php.net/manual/ja/reserved.constants.php#constant.true|true]]</code>。
|-
|$a <> $b
|等しくない
|<var>$a</var> が <var>$b</var> と等しくない場合に <code>[[/www.php.net/manual/ja/reserved.constants.php#constant.true|true]]</code>。
|-
|$a !== $b
|同一でない
|<var>$a</var> が <var>$b</var> と同一でない場合に <code>[[/www.php.net/manual/ja/reserved.constants.php#constant.true|true]]</code>。
|}
配列の等価演算子はキーと値の両方を比較する。これが重要。後は結合の+。
==== ...演算子/スプレッド演算子 ====
* 関数
** 可変長引数リストと関数呼出 [https://www.php.net/manual/ja/functions.arguments.php#functions.variable-arg-list PHP: 関数の引数 - Manual]。PHP 5.6で導入 ([https://www.php.net/manual/ja/migration56.new-features.php PHP: 新機能 - Manual])。名前付き引数に対応したPHP 8.0から連想配列も対応。PHP 8.1から可変長引数と名前付き引数の同時使用 ([https://www.php.net/manual/ja/migration81.new-features.php#migration81.new-features.core.named-arg-after-unpack PHP: 新機能 - Manual])。アンパックと通常名前付き引数がある場合、後のやつで上書き不能で実行時エラー。
** 第一級callableを生成する記法: PHP 8.1.0で導入 ([https://www.php.net/manual/ja/functions.first_class_callable_syntax.php#functions.first_class_callable_syntax PHP: 第一級callableを生成する記法 - Manual])。Closure::fromCallableの無名関数作成時の別の方法。
* 配列のアンパック: [https://www.php.net/manual/ja/language.types.array.php#language.types.array.unpacking PHP: 配列 - Manual]。array_mergeの代替記法。
** 数値キー・単純配列はPHP 7.4で導入 ([https://www.php.net/manual/ja/migration74.new-features.php#migration74.new-features.core.unpack-inside-array PHP: 新機能 - Manual])。
** 文字キー・連想配列はPHP 8.1 ([https://www.php.net/manual/ja/migration81.new-features.php#migration81.new-features.core.unpacking-string-keys PHP: 新機能 - Manual]、[https://qiita.com/shigakin/items/5e4a2784bbd6b227f4d3 【PHP8.1】あなたはどっち? array_merge VS unpacking(スプレッド演算子) #PHP - Qiita])。
** アンパックと通常名前付き引数がある場合、後のやつで上書き。
** 配列のアンパックは、性能面で問題があり、連想配列がPHP 8.1以上というのがあり、array_mergeを使ったほうがよさそう。
* [https://qiita.com/mpyw/items/835050cbb5ad8a4c0710 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;
}
[https://qiita.com/shigakin/items/5e4a2784bbd6b227f4d3 【PHP8.1】あなたはどっち? array_merge VS unpacking(スプレッド演算子) #PHP - Qiita]
なお、配列のアンパックに関しては、array_mergeのほうが速くてメモリーも少ないとのこと。
==== in演算子 ====
[https://stackoverflow.com/questions/33182976/php-equivalent-of-mysql-in-operator php equivalent of mysql "IN" operator? - Stack Overflow]
PHPにin演算子はない。代わりに、in_arrayで包含判定できる。
ある値が、いずれかのどれかであるかの判定はそれなりにある。
in_array($target, [], true);
例えば、この比較対象が長い場合、(a===b||a===c|a===d) で何回も書かなくて済む。
==== Name ====
誰かに言葉で説明する際に、演算子の名前がほしい。意外と覚えていない。根拠とともに整理する。
{| class="wikitable"
|+
!演算子
!名前
!name
!URL
!説明
|-
|$this->property
|オブジェクト演算子
|object operator
|[https://www.php.net/manual/ja/language.oop5.properties.php PHP: プロパティ - Manual]
|インスタンスのプロパティーとメソッドにアクセスする。
|-
|
|
|
|
|
|-
|
|
|
|
|
|}
===Control Structures===
Source: [https://www.php.net/manual/en/language.control-structures.php PHP: Control Structures - Manual].
====制御構造に関する別の構文====
[https://www.php.net/manual/ja/control-structures.alternative-syntax.php PHP: 制御構造に関する別の構文 - Manual]
if、 while、for、 foreach、switch に関する別の構文がある。開き波括弧部分を:に、閉じ波括弧部分をendif;,endwhile;, endfor;,endforeach;, endswitch;などにできる。else:とelseif:に注意。
この構文は存在だけ知っておくだけでいいと思われる。
====elseif/else if====
[https://www.php.net/manual/ja/control-structures.elseif.php PHP: elseif/else if - Manual]
1単語で書ける。結果は同じだが、文法的な意味が異なる。
====foreach====
===== About =====
[https://www.php.net/manual/ja/control-structures.foreach.php PHP: foreach - Manual]
foreachは配列の反復処理のための制御構造。
foreach (iterable_expression as $value)
foreach (iterable_expression as $key => $value)
$keyも使いたい場合、2番目の形式を使う。
ループ中に$valueの要素を直接変更したい場合、&をつけておく。
foreach (iterable_expression as &$value)
===== Name =====
foreachで使う変数の命名。
foreach (table as $row => $line)
DBからSELECT結果などがkeyに行番号、valueにレコードが入ってくる。こういう場合、rowがややこしい。行番号の意味でrowをキーにしておくといい。
value部分をどうするかだが、valueやitemだと少々わかりにくい。recordやline。SplFileObjectを扱うこともあるからlineがいいと思う。
===== Rewind =====
* [https://github.com/php/php-src/issues/13916 SplFileObject: foreach doesn't start from the correct line after `SplFileObject::seek()` · Issue #13916 · php/php-src · GitHub]
* [https://www.php.net/manual/ja/iterator.rewind.php PHP: Iterator::rewind - Manual]
* [https://www.php.net/manual/ja/class.norewinditerator.php 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ありにしていないと機能しないようなので注意する ([https://stackoverflow.com/questions/1504927/splfileobject-next-behavior php - SPLFileObject next() behavior - Stack Overflow])。
ただ、READ_AHEADにしても、初回がnext()2回呼ばないと2行目にcurrent()でならないので動きがわかりにくい。seek(1)でよい。
===== first/last =====
[https://stackoverflow.com/questions/1070244/php-how-to-determine-the-first-and-last-iteration-in-a-foreach-loop 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でいい。
配列だったら、反復外部で簡単に判定できる。余計な処理を反復内に含めないほうがいい。
===== 反復削除 =====
* [https://iww.hateblo.jp/entry/20220603/array PHPで、foreachでぐるぐる回ってる最中に要素を削除する - 揮発性のメモ2]
* [https://www.techiedelight.com/ja/remove-array-element-in-foreach-loop-php/ PHP の foreach ループで配列要素を削除する]
キーが維持されるので、逆順反復などしなくても、影響ない。unsetすればいい。
===== reverse =====
逆順反復の方法がいくつかある。
[https://stackoverflow.com/questions/10777597/reverse-order-of-foreach-list-items php - Reverse order of foreach list items - Stack Overflow]
$fruits = ['bananas', 'apples', 'pears'];
for($i = count($fruits)-1; $i >= 0; $i--) {
    echo $fruits[$i] . '<nowiki><br></nowiki>';
}
foreach ( array_reverse($accounts) as $account ) {
  echo sprintf("<nowiki><li>%s</li></nowiki>", $account);
}
なお、連想配列は無理。やるとしたら、array_reverse、逆順のキーを取得してそれを使う。
[https://stackoverflow.com/questions/32613036/how-to-reverse-foreach-key-value-php arrays - How to reverse foreach $key value PHP? - Stack Overflow]
====declare====
Source: [https://www.php.net/manual/en/control-structures.declare.php PHP: declare - Manual].
PHPUnitのサンプルコード ([https://phpunit.de/getting-started/phpunit-9.html 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ファイルの冒頭に<code>declare(strict_types=1);</code>を書いておいたほうがよいだろう。
==== return ====
[https://www.php.net/manual/ja/function.return.php PHP: return - Manual]
関数を終了させて、結果を呼び出し元に返すというのは他の言語同様の動きだが、いくつか注意すべき挙動・使用方法がある。
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';
}
===== require/include =====
requireはincludeとほぼ同じ。違いは、失敗時にE_COMPILE_ERRORが発生して処理を中断する点。includeはE_WARNINGで処理は継続する。
使い分けとして、変数読込などで読み込めなくても処理を進めて問題ない場合に、include。
関数定義など、絶対必要なものはrequireなど。
===== _once =====
読込済みなら、再読込しない点がinclude/requireとの決定的な違い。関数の複数定義のエラーを回避できたりする。
読み込めたらtrueを返す。
===== config.php =====
* [https://stackoverflow.com/questions/14752470/creating-a-config-file-in-php Creating a config file in PHP - Stack Overflow]
* [https://blog.websandbag.com/entry/2018/12/04/160218 【PHP】静的なconfigファイルの書き方 - websandbag ブログ]
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をアプリ内で作りたい場合、「[https://laracasts.com/discuss/channels/laravel/how-to-create-dynamically-create-configcustomphp-config-file 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をどう用意するかは議論がある。
* [https://stackoverflow.com/questions/10987703/is-it-right-to-set-a-config-php-class-to-keep-project-settings Is it right to set a config PHP class to keep project settings? - Stack Overflow]
* [https://docs.php.earth/security/configuration/ Configuration in PHP applications | PHP.earth]
* [https://qiita.com/nishimura/items/a396c999a85fa4cbc4a0 PHPでプログラム全体の設定に使う変数の保持の仕方 #PHP - Qiita]
* [https://php-archive.net/php/config-class/ [PHP]コンフィグファイルから設定情報を読み込むためのConfigクラス | PHP Archive]
* [https://qiita.com/satorunooshie/items/ca41f7c824c7ea747708 PHPでconfigファイルをオートロードで呼び出す方法 #Config - Qiita]
* [https://stackoverflow.com/questions/14659769/using-a-config-file-from-within-a-php-class Using a config file from within a php class - Stack Overflow]
* [https://stackoverflow.com/questions/33742740/php-oop-config-class 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;
    }
===Function===
==== User defined ====
[https://www.php.net/manual/ja/functions.user-defined.php PHP: ユーザー定義関数 - Manual]
関数は以下のような構文で定義する。
<?php
function foo($arg_1, $arg_2, /* ..., */ $arg_n)
{
    echo "関数の例\n";
    return $retval;
}
?>
関数内では、他の関数やクラス定義を含む、PHPのあらゆるコードを使用可能。関数内で関数を定義できないC言語とは異なる。
PHPでは、変数と異なり、関数やクラスは全てグローバルスコープ。関数内で定義した関数も外部から呼び出し可能。スコープが欲しければ、無名関数を使う。
また、関数のオーバーロードもできない。関数をunsetしたり、再定義も不能。
可変引数と、デフォルト引数もある。
==== Argument ====
[https://www.php.net/manual/ja/functions.arguments.php PHP: 関数の引数 - Manual]
===== Comma =====
[https://www.php.net/manual/ja/migration73.new-features.php PHP: 新機能 - Manual]
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記法でインスタンスも指定できる。
デフォルト引数は、デフォルト値のない引数の右側の必要がある。そうでない場合、省略できず、指定する意味がなくなく。
「[https://stackoverflow.com/questions/55587939/default-callable-in-function-definition-in-php-7 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);
...の前に型宣言も付与できるが、その場合配列要素が全部その型が必要になる。
===== 名前付き引数 =====
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: [https://www.php.net/manual/ja/functions.returning-values.php PHP: 戻り値 - Manual].
関数はreturn文で値を返せる。そこで処理を終了する。
returnを省略した場合、nullを返す。
==== Variable Functions/可変関数/Callable/コールバック ====
* [https://www.php.net/manual/ja/functions.variable-functions.php PHP: 可変関数 - Manual]
* [https://www.php.net/manual/ja/language.types.callable.php PHP: コールバック / Callable - Manual]
* [https://www.ycomps.co.jp/staffblog/11934 【PHP】コールバック関数サンプル3つをまとめる - ウェブ集客で企業を成功に導くホームページ制作会社|(株)ワイコム・パブリッシングシステムズ(福岡)]
* [https://pisuke-code.com/php-call-func-class-from-string/ PHPで関数やクラスを文字列から呼び出しする方法まとめ | PisukeCode - Web開発まとめ]
* [https://begien.com/article/29/view 【小ネタ】phpで変数でメソッドを実行する | BeginnerEngineerBlog]
* [https://zenn.dev/tanomu/articles/d92721f597e87f call_user_func() と $function() の動きが違った]
PHPで関数を引数で指定したり、変数として扱う仕組みがある。evalを使う必要はない。
可変関数は、関数を文字列で実行する仕組み。これとは別で、Callableという型がある。
可変関数は、関数名の文字列の変数に丸括弧を追加したら実行できるというもの。インスタンス変数があれば、メソッドもできる。
PHP 7.0から、関数のみ"str"()も可能になった。「[https://www.php.net/manual/ja/migration70.php PHP: PHP 5.6.x から PHP 7.0.x への移行 - Manual]」に記載はないが、パース方法が変わったことが由来の模様。
関数もメソッドも統一的に扱うものとして、Callable型がある。
CallableはPHPで関数を引数として渡したり、関数名の文字列を渡して、動的に関数を実行する仕組み。
callable型で表す。関数だけでなく、メソッドやstaticメソッドも対応できる。方法が2種類ある。
# 関数: 関数名の文字列。
# メソッド: 配列で指定。0番目の要素に、インスakeタンスやオブジェクト。1番目の要素にメソッド名の文字列で指定する。
# staticメソッド:  配列で指定。0番目の要素に、クラス名を指定する。'ClassName::methodName' 形式でも指定可能。
==== anonymous/無名関数 ====
[https://www.php.net/manual/ja/functions.anonymous.php PHP: 無名関数 - Manual]
2009年頃にPHP 5.3で登場したらしい ([https://hnw.hatenablog.com/entry/20090710 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/アロー関数 ====
[https://www.php.net/manual/ja/functions.arrow.php PHP: アロー関数 - Manual]
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";
長くなるなら、無名関数にしたほうがよさそう。
===Classes and Objects===
====The Basics====
[https://www.php.net/manual/ja/language.oop5.basic.php PHP: クラスの基礎 - Manual]
=====class=====
class内には変数 (プロパティー)、定数、関数 (メソッド) を含められる。
class内の関数などで、これらのプロパティー、メソッド類の参照時は、擬似変数$this->経由で参照できる。$thisは呼び出し元オブジェクトが入っている。
C系言語であれば、$this->相当は省略できたが、PHPでは指定が必要なので注意する。
=====::class=====
<className>::classでクラス名の完全修飾子の文字列を取得できる。
例外の試験など、クラス名の情報が必要な時によくみかける。
PHP 8.0.0からオブジェクトに対しても::classを使用でき、元のクラス名を取得できる。その場合、get_class()と同じ。同じならPHP 7で使えないのでget_class()でいいか。
====Property====
Ref: [https://www.php.net/manual/ja/language.oop5.properties.php 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と同じ意味になる ([https://stackoverflow.com/questions/1206105/what-does-php-keyword-var-do What does PHP keyword 'var' do? - Stack Overflow])。
宣言時に初期値を代入もできるが、初期値は定数のみ。関数類は使用不能。
*[https://stackoverflow.com/questions/40827870/constant-expression-contains-invalid-operations php - Constant expression contains invalid operations - Stack Overflow]
*[https://qiita.com/H40831/items/15ebfbf7d9c05001b6df 【PHP】クラスプロパティの値には、動的な値を代入することができないようです。 #error - Qiita]
以下のエラーが出る。
PHP Fatal error: Constant expression contains invalid operations in /ぼくのかんがえたさいきょうのクラス.php on line 5
関数類で動的に代入したい場合、__constructでやる。
クラスメソッドからstaticでないプロパティーにアクセスするには、-> (オブジェクト演算子/object operator) を使う。
====Autoloading Classes====
*[https://www.php.net/manual/en/language.oop5.autoload.php PHP: Autoloading Classes - Manual]
*[https://www.php.net/manual/en/function.spl-autoload-register.php PHP: spl_autoload_register - Manual]
別のファイルのクラスを使う方法の話。
#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択になっている。
基本的な使用方法。<syntaxhighlight lang="php">
<?php
spl_autoload_register(function ($class_name) {
    include $class_name . '.php';
});
$obj  = new MyClass1();
$obj2 = new MyClass2();
?>
</syntaxhighlight>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: <code>callback(string $class): void</code> 。重要。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単体で実装できます。
* 欠点
** '''設定が手動''':名前空間とディレクトリのマッピングを手動で設定する必要があり、大規模なプロジェクトでは管理が複雑になりがちです。
** '''標準規約に対応するのが難しい''':<code>PSR-4</code>のような標準的なオートローディング規約(ファイル構造と名前空間のマッピングルール)を自分で実装する必要があるため、コードが煩雑になる可能性があります。
** '''依存ライブラリの管理が難しい''':外部ライブラリを含める場合は、<code>require</code>や<code>include</code>で個別に読み込む必要があり、依存管理が困難です。
composer
* 利点
** '''自動設定''':<code>PSR-4</code>対応の名前空間とディレクトリのマッピングを自動的に管理するため、設定が簡単です。
** '''依存ライブラリの自動管理''':Composerは依存する外部ライブラリもインストールしてオートロードするため、大規模なプロジェクトで便利です。
** '''メンテナンスが容易''':プロジェクトの規模が大きくなっても、<code>composer.json</code>ファイルで管理できるため、ファイル構成や依存関係の変更に対応しやすいです。
* 欠点
** '''Composer依存''':Composerがインストールされていないと使えません。また、プロジェクトに<code>composer.json</code>の設定が必要です。
** '''追加の学習が必要''':Composerの使い方や設定ファイルの理解が必要ですが、習得すれば特に問題にはなりません。
==== Visibility ====
[https://www.php.net/manual/ja/language.oop5.visibility.php PHP: アクセス権 - Manual]
プロパティー、メソッド、定数 (PHP 7.1.0以上) にはpublic/protected/privateのアクセス権 (visibility) を指定できる。
* public: どこからでもアクセス可能。
* protected: クラス自身、継承クラス、親クラス。
* private: クラス自身。
アクセス権を省略した場合、public扱いになる。
なお、同じ型のオブジェクト間では、同一インスタンスでなくても、protected/privateにもアクセス可能。オブジェクト内ではオブジェクト実装が既知だから。
==== スコープ定義演算子 (::) ====
* [https://www.php.net/manual/ja/language.oop5.paamayim-nekudotayim.php PHP: スコープ定義演算子 (::) - Manual]
* [https://www.php.net/manual/ja/language.oop5.late-static-bindings.php PHP: 遅延静的束縛 (Late Static Bindings) - Manual]
* [https://qiita.com/kouki_o9/items/5fd652ce6c7322480089 [PHP]staticメソッドと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::でよいと思う。
====Traits====
[https://www.php.net/manual/ja/language.oop5.traits.php PHP: トレイト - Manual]
コード再利用のための仕組み。単一継承言語で、コードを再利用するための仕組み。関数クラス (デリゲート) 的なもの。クラスに関数クラスのメソッドを取り込める。インスタンス生成などはできず、関数を水平方向で構成可能にする。継承しなくても、メンバーに追加できる。
<?php
trait ezcReflectionReturnInfo {
    function getReturnType() { /*1*/ }
    function getReturnDescription() { /*2*/ }
}
class ezcReflectionMethod extends ReflectionMethod {
    use ezcReflectionReturnInfo;
    /* ... */
}
class ezcReflectionFunction extends ReflectionFunction {
    use ezcReflectionReturnInfo;
    /* ... */
}
?>
==== Magic/マジックメソッド ====
[https://www.php.net/manual/ja/language.oop5.magic.php PHP: マジックメソッド - Manual]
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====
=====クラス名の取得=====
*[https://stackoverflow.com/questions/19901850/how-do-i-get-an-objects-unqualified-short-class-name php - How do I get an object's unqualified (short) class name? - Stack Overflow]
*[https://www.php.net/manual/ja/function.get-class.php PHP: get_class - Manual]
*[https://qiita.com/miriwo/items/7972261a710e79dd1fd5 PHP クラス名::classはどういう処理?? #初心者 - Qiita]
*[https://www.php.net/manual/ja/language.oop5.basic.php#language.oop5.basic.class.class PHP: クラスの基礎 - Manual]
get_class($object);
クラス名::class
$object::class // PHP 8.0以上 (get_class相当)
(new \ReflectionClass($obj))->getShortName(); // クラス名
基本は名前空間付きのフルパスでの取得。クラス名だけだとgetShortName()。クラスがなければnewで例外。
=== Generator ===
* [https://www.php.net/manual/ja/language.generators.php PHP: ジェネレータ - Manual]
* [https://www.php.net/manual/ja/class.generator.php PHP: Generator - Manual]
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。
なお、ジェネレーターの値は前に進むことしかできない。
[https://qiita.com/Hiraku/items/190443b33ee7a2167ade 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)))のようにする。きれい。
=== Namespace ===
[https://www.php.net/manual/ja/language.namespaces.php PHP: 名前空間 - Manual]
PHPの名前空間は、以下の2の問題の解決用の仕組み。
# 自作の関数や変数類の名前がPHPの組込と衝突。
# 名前衝突回避のために長い名前が必要。
なお、requie_onceによる別ファイルの読込か、オートロードで他のファイルなどのシンボルにアクセスできることが、前提になっている。
==== definition ====
[https://www.php.net/manual/ja/language.namespaces.definition.php PHP: 名前空間 - Manual]
名前空間の影響を受けるのは、以下。
* クラス
* インターフェイス
* 関数
* 定数
以下の構文でファイル先頭で宣言する。
namespace [Name];
namespace [Name]\[Sub];
ただし、declareは例外でnamespaceの前にも書ける。ただ、それ以外だとPHPコード以外も含めて記述不能。
同じ名前空間を複数のファイルで定義することも可能。これにより、ファイルをまたいで名前空間を共有できる。
また、名前空間は階層を持つことができる。バックスラッシュで区切る。
===== Multiple =====
[https://www.php.net/manual/ja/language.namespaces.definitionmultiple.php 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 =====
[https://www.php.net/manual/ja/language.namespaces.importing.php PHP: エイリアス/インポート - Manual]
外部の完全修飾名をエイリアスで参照できる。use演算子を使う。namespaceで同じ名前空間に以内なら、useか完全修飾名を使う必要がある。
// これは 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};
===== Global =====
[https://www.php.net/manual/ja/language.namespaces.global.php PHP: グローバル空間 - Manual]
名前の先頭に\をつけるとグローバル空間の名前を指定できる。
$f = \fopen(...)
=== Reserved ===
==== keywords ====
[https://www.php.net/manual/ja/reserved.keywords.php PHP: キーワードのリスト - Manual]
式や関数ではなく、定数、クラス名、関数名として使えず、PHPで予約されている特別なキーワードがいくつかある。
statement/文に近い扱い。言語構文の一部扱い。
{| class="wikitable"
|+PHP のキーワード
|[[/www.php.net/manual/ja/function.halt-compiler.php|__halt_compiler()]]
|[[/www.php.net/manual/ja/language.oop5.abstract.php|abstract]]
|[[/www.php.net/manual/ja/language.operators.logical.php|and]]
|[[/www.php.net/manual/ja/function.array.php|array()]]
|[[/www.php.net/manual/ja/control-structures.foreach.php|as]]
|-
|[[/www.php.net/manual/ja/control-structures.break.php|break]]
|[[/www.php.net/manual/ja/language.types.callable.php|callable]]
|[[/www.php.net/manual/ja/control-structures.switch.php|case]]
|[[/www.php.net/manual/ja/language.exceptions.php|catch]]
|[[/www.php.net/manual/ja/language.oop5.basic.php#language.oop5.basic.class|class]]
|-
|[[/www.php.net/manual/ja/language.oop5.cloning.php|clone]]
|[[/www.php.net/manual/ja/language.oop5.constants.php|const]]
|[[/www.php.net/manual/ja/control-structures.continue.php|continue]]
|[[/www.php.net/manual/ja/control-structures.declare.php|declare]]
|[[/www.php.net/manual/ja/control-structures.switch.php|default]]
|-
|[[/www.php.net/manual/ja/function.die.php|die()]]
|[[/www.php.net/manual/ja/control-structures.do.while.php|do]]
|[[/www.php.net/manual/ja/function.echo.php|echo]]
|[[/www.php.net/manual/ja/control-structures.else.php|else]]
|[[/www.php.net/manual/ja/control-structures.elseif.php|elseif]]
|-
|[[/www.php.net/manual/ja/function.empty.php|empty()]]
|[[/www.php.net/manual/ja/control-structures.declare.php|enddeclare]]
|[[/www.php.net/manual/ja/control-structures.alternative-syntax.php|endfor]]
|[[/www.php.net/manual/ja/control-structures.alternative-syntax.php|endforeach]]
|[[/www.php.net/manual/ja/control-structures.alternative-syntax.php|endif]]
|-
|[[/www.php.net/manual/ja/control-structures.alternative-syntax.php|endswitch]]
|[[/www.php.net/manual/ja/control-structures.alternative-syntax.php|endwhile]]
|[[/www.php.net/manual/ja/function.eval.php|eval()]]
|[[/www.php.net/manual/ja/function.exit.php|exit()]]
|[[/www.php.net/manual/ja/language.oop5.basic.php#language.oop5.basic.extends|extends]]
|-
|[[/www.php.net/manual/ja/language.oop5.final.php|final]]
|[[/www.php.net/manual/ja/language.exceptions.php|finally]]
|[[/www.php.net/manual/ja/functions.arrow.php|fn]] (PHP 7.4 以降)
|[[/www.php.net/manual/ja/control-structures.for.php|for]]
|[[/www.php.net/manual/ja/control-structures.foreach.php|foreach]]
|-
|[[/www.php.net/manual/ja/functions.user-defined.php|function]]
|[[/www.php.net/manual/ja/language.variables.scope.php|global]]
|[[/www.php.net/manual/ja/control-structures.goto.php|goto]]
|[[/www.php.net/manual/ja/control-structures.if.php|if]]
|[[/www.php.net/manual/ja/language.oop5.interfaces.php|implements]]
|-
|[[/www.php.net/manual/ja/function.include.php|include]]
|[[/www.php.net/manual/ja/function.include-once.php|include_once]]
|[[/www.php.net/manual/ja/language.operators.type.php|instanceof]]
|[[/www.php.net/manual/ja/language.oop5.traits.php#language.oop5.traits.conflict|insteadof]]
|[[/www.php.net/manual/ja/language.oop5.interfaces.php|interface]]
|-
|[[/www.php.net/manual/ja/function.isset.php|isset()]]
|[[/www.php.net/manual/ja/function.list.php|list()]]
|[[/www.php.net/manual/ja/control-structures.match.php|match]] (PHP 8.0 以降)
|[[/www.php.net/manual/ja/language.namespaces.php|namespace]]
|[[/www.php.net/manual/ja/language.oop5.basic.php#language.oop5.basic.new|new]]
|-
|[[/www.php.net/manual/ja/language.operators.logical.php|or]]
|[[/www.php.net/manual/ja/function.print.php|print]]
|[[/www.php.net/manual/ja/language.oop5.visibility.php|private]]
|[[/www.php.net/manual/ja/language.oop5.visibility.php|protected]]
|[[/www.php.net/manual/ja/language.oop5.visibility.php|public]]
|-
|[[/www.php.net/manual/ja/language.oop5.properties.php#language.oop5.properties.readonly-properties|readonly]] (PHP 8.1.0 以降) *
|[[/www.php.net/manual/ja/function.require.php|require]]
|[[/www.php.net/manual/ja/function.require-once.php|require_once]]
|[[/www.php.net/manual/ja/function.return.php|return]]
|[[/www.php.net/manual/ja/language.variables.scope.php|static]]
|-
|[[/www.php.net/manual/ja/control-structures.switch.php|switch]]
|[[/www.php.net/manual/ja/language.exceptions.php|throw]]
|[[/www.php.net/manual/ja/language.oop5.traits.php|trait]]
|[[/www.php.net/manual/ja/language.exceptions.php|try]]
|[[/www.php.net/manual/ja/function.unset.php|unset()]]
|-
|[[/www.php.net/manual/ja/language.namespaces.php|use]]
|[[/www.php.net/manual/ja/language.oop5.properties.php|var]]
|[[/www.php.net/manual/ja/control-structures.while.php|while]]
|[[/www.php.net/manual/ja/language.operators.logical.php|xor]]
|[[/www.php.net/manual/ja/language.generators.php|yield]]
|-
|[[/www.php.net/manual/ja/language.generators.syntax.php#control-structures.yield.from|yield from]]
|
|
|
|
|}
<nowiki>*</nowiki> <code>readonly</code> は、関数名として使用できます。
{| class="wikitable"
|+コンパイル時の定数
|[[/www.php.net/manual/ja/language.constants.predefined.php|__CLASS__]]
|[[/www.php.net/manual/ja/language.constants.predefined.php|__DIR__]]
|[[/www.php.net/manual/ja/language.constants.predefined.php|__FILE__]]
|[[/www.php.net/manual/ja/language.constants.predefined.php|__FUNCTION__]]
|[[/www.php.net/manual/ja/language.constants.predefined.php|__LINE__]]
|[[/www.php.net/manual/ja/language.constants.predefined.php|__METHOD__]]
|-
|[[/www.php.net/manual/ja/language.namespaces.nsconstants.php|__NAMESPACE__]]
|[[/www.php.net/manual/ja/language.constants.predefined.php|__TRAIT__]]
|
|
|
|
|}
==== Interfaces ====
[https://www.php.net/manual/ja/reserved.interfaces.php PHP: 定義済みのインターフェイスとクラス - Manual]
===== stdClass =====
[https://www.php.net/manual/ja/class.stdclass.php PHP: stdClass - Manual]
動的なプロパティーが使える、汎用的な空クラス。このクラス自体は、メソッドやプロパティーを持たない。
json_decodeなど一部の関数がこのインスタンスを返す。
// 型変換での作成。連想配列を(object)にキャストすると作れる。
(object) array('foo' => 'bar');
データベースの取得結果が、連想配列の他に、stdClassになっていることがある。
匿名オブジェクトや、動的プロパティーなどが主な利用方法。
データホルダーとして使う場合、連想配列のキーのほうが、自由度が高いので、そちらのほうが便利だと思われる。たくさんある配列関数も使えるし。
==== Constants ====
[https://www.php.net/manual/ja/reserved.constants.php PHP: 定義済みの定数 - Manual]
いくつか重要なものがある。
* PHP_EOL: プラットフォームの行末文字。使う場面は限られている。基本は"\n"でよい。
* PHP_BINARY: 現在実行中のphpコマンドのフルパス。PHP内部から外部のPHPスクリプトコマンドの実行時などで使う。PHP 5.4から使用可能らしい ([https://stackoverflow.com/questions/2372624/how-can-i-get-the-current-php-executable-from-within-a-script How can I get the current PHP executable from within a script? - Stack Overflow])。
=== Wrappers ===
[https://www.php.net/manual/ja/wrappers.php PHP: サポートするプロトコル/ラッパー - Manual]
URL風のプロトコル (ラッパー/wrapper) で、ファイルシステム関数で使用できる。stream_wrapper_registerで自作もできる。
==== php:// ====
[https://www.php.net/manual/ja/wrappers.php.php PHP: php:// - Manual]
===== php://memory php::temp =====
[https://stackoverflow.com/questions/17626532/php-is-it-possible-to-create-a-splfileobject-object-from-file-contents-string 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: [https://www.php.net/manual/en/features.php PHP: Features - Manual].
=== PHP による HTTP 認証 ===
* [https://www.php.net/manual/en/features.http-auth.php PHP: PHP による HTTP 認証 - マニュアル]
* [https://qiita.com/mpyw/items/dc2cb3632370389d700e PHPを利用したBasic認証の仕組み #Apache - Qiita]
* [https://qiita.com/mpyw/items/bb8305ba196f5105be15 PHPによる簡単なログイン認証いろいろ #Security - Qiita]
PHPでのBasic認証の話。
Basic認証が発動すると、$_SERVERのPHP_AUTH_USER/PHP_AUTH_PWに値が入る。
AUTH_TYPEは実際は使っていない模様。以下で確認するとよい。
isset($_SERVER['PHP_AUTH_USER']);
==== HTTP_AUTHORIZATION ====
* [https://zenn.dev/teramoh/articles/fe17486e15194a WordPress:すべては.htaccessでredirectされている]
この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を代入する。
気になるので調べる。
「[https://web.archive.org/web/20140215202107/http://www.php.net/manual/en/features.http-auth.php PHP: HTTP authentication with PHP - Manual]」の2014年の昔の同じマニュアルには以下の記載があった。<blockquote>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)));</blockquote>$_SERVER['HTTP_AUTHORIZATION'] にBasic dGVzdDp0ZXN0のような値が入っており、右側がuser:passのbase64エンコードデータなので、それをデコードして使っていた模様。
上記の記述は以下の2コミットで削除された。
* [https://github.com/php/doc-en/commit/dd3df871f5e3e195f738ac0be1de8d9f5d64cb7c Not supporting HTTP authentication using CGI is legacy and so old tha… · php/doc-en@dd3df87]
* [https://github.com/php/doc-en/commit/2bb07c8c43f028c665a33bfc08a22639e9e35dc6#diff-0799fb69c99862cabc3cb703458e5d33c6695004b5c4f87ed451d756233c70a6R227 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は「[https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.1 RFC 3875 - The Common Gateway Interface (CGI) Version 1.1]」にも登場しているが、ほぼ言及はない。
===Handling file uploads===
Ref: [https://www.php.net/manual/en/features.file-upload.php PHP: Handling file uploads - Manual].
input type="file"などのアップロードファイルのPHPでの処理方法・作法がある<syntaxhighlight lang="html">
<!-- データのエンコード方式である 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>
</syntaxhighlight>PHP側では$_FILES['userfile']に必要な情報が格納される。
*;<var>[[/wiki.gnusocial.jp//www.php.net/manual/ja/reserved.variables.files.php|$_FILES['userfile']['name']]]</var>
*:クライアントマシンの元のファイル名。
*;<var>[[/wiki.gnusocial.jp//www.php.net/manual/ja/reserved.variables.files.php|$_FILES['userfile']['type']]]</var>
*:ファイルの MIME 型。ただし、ブラウザがこの情報を提供する場合。 例えば、<code>"image/gif"</code> のようになります。 この MIME 型は PHP 側ではチェックされません。そのため、 この値は信用できません。
*;<var>[[/wiki.gnusocial.jp//www.php.net/manual/ja/reserved.variables.files.php|$_FILES['userfile']['size']]]</var>
*:アップロードされたファイルのバイト単位のサイズ。
*;<var>[[/wiki.gnusocial.jp//www.php.net/manual/ja/reserved.variables.files.php|$_FILES['userfile']['tmp_name']]]</var>
*:アップロードされたファイルがサーバー上で保存されているテンポラ リファイルの名前。
*;<var>[[/wiki.gnusocial.jp//www.php.net/manual/ja/reserved.variables.files.php|$_FILES['userfile']['error']]]</var>
*:このファイルアップロードに関する [[/wiki.gnusocial.jp//www.php.net/manual/ja/features.file-upload.errors.php|エラーコード]]
*;<var>[[/wiki.gnusocial.jp//www.php.net/manual/ja/reserved.variables.files.php|$_FILES['userfile']['full_path']]]</var>
*:ブラウザからアップロードされたファイルのフルパス。 この値は実際のディレクトリ構造を反映しているとは必ずしも言えないため、 信用できません。 PHP 8.1.0 以降で利用可能です。
tmp_nameが非常に重要。これをリネームする形で保存する。あとはnameも保存時のファイル名で重要。<syntaxhighlight lang="php">
<?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>";
?>
</syntaxhighlight>上記がイメージ。
DBに保存する場合は「[https://qiita.com/NULLchar/items/7bdc6685be0aa909e8fe PHPとMySQLを利用した画像・動画のアップロード・保存・表示 #PHP - Qiita]」も参考になる。
===Using PHP from the command line===
Ref: [https://www.php.net/manual/en/features.commandline.php PHP: Command line usage - Manual].
==== About ====
簡単なコードの確認などでPHPをコマンドラインなどから簡単に実行したいことがよくある。いくつか方法がある ([https://www.php.net/manual/en/features.commandline.usage.php PHP: Usage - Manual])。
#phpコマンドの引数にファイルを指定: <code>php file.php</code>/<code>php -f file.php</code>
#phpコマンドの引数にコードを指定: <code>php -r 'print_r(get_defined_constants());'</code>
#phpコマンドに標準入力で読み込み: <code>php <file.php</code>
標準入力が一番使いやすく感じる。
==== __name__ == "__main__" ====
phpファイルを直接実行時とインポート時とで分離する書き方。
同一ファイルでクラスと実行用ファイルとしたい場合などに必要となる。書き方がいくつかある。
「[https://www.serendip.ws/archives/4360 Python の if __name__ == ‘__main__’: を Perl, Ruby, PHP で行う : Serendip – Webデザイン・プログラミング]」
if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
    // do something
}
「[https://bashalog.c-brains.jp/07/08/02-175300.php PHPスクリプトが直接起動されたかどうかで処理を振り分ける | バシャログ。]」
if (realpath($_SERVER["SCRIPT_FILENAME"]) == realpath(__FILE__)){
    /** ここに処理を書いてね */
}
「[https://stackoverflow.com/questions/2413991/php-equivalent-of-pythons-name-main/2414026#2414026 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====
[https://www.php.net/manual/ja/book.errorfunc.php PHP: エラー処理 - Manual]
=====Runtime Configuration=====
PHPのエラー設定を整理する。 PHPのエラー設定は「[https://www.php.net/manual/en/errorfunc.configuration.php PHP: Runtime Configuration - Manual]」で一覧化されている。
xmlrpc_errors, syslog.facility, syslog.ident以外はどこでも設定可能。
特に重要なのが以下の設定。
{| class="wikitable"
!設定
!初期値
!説明
|-
|error_reporting
|<code>E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED</code>
|エラー出力レベルを設定する。開発時には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に設定しておけばよい。<syntaxhighlight lang="ini">
error_reporting = E_ALL
display_startup_errors = on
log_errors = on
; For file
display_errors = stderr
</syntaxhighlight>htpd.conf/.htaccessの場合は以下。<syntaxhighlight lang="http">
php_value error_reporting -1
php_flag display_startup_errors on
php_flag log_errors on
# For file
php_value display_errors stderr
</syntaxhighlight>
===== logrotate =====
* ライブラリー
** [https://github.com/cesargb/php-log-rotation cesargb/php-log-rotation: PHP Class to rotate log files]
* 自前
** [https://kuranabe.hatenablog.com/entry/2018/02/24/163500 【PHP】ログローテート対応のログ出力関数を実装する - kuranabeの開発備忘録]
* logrotate
** [https://stackoverflow.com/questions/14145812/how-to-configure-logrotate-with-php-logs linux - How to configure logrotate with php logs - Stack Overflow]
** [https://trueman-developer.blogspot.com/2020/02/php.html PHPのログロ-テーション設定]
** [https://qiita.com/shotets/items/e13e1d1739eaea7b1ff9 ログローテートソフトウエア logrotate についてまとめ #Linux - Qiita]
** [https://groworks.jp/blog/5763 PHPで効果的なログファイル管理を実現する方法 | 新潟のホームページ制作|Web制作会社 グローワークス]
出力したログファイルがストレージを圧迫しないように、一定サイズ・期間でリネームして、最大保持数を維持したりする。
いくつか方法がある。
* 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);
}
もう少しいい実装方法はありそう。
==== PHP Options/Info ====
[https://www.php.net/manual/ja/book.info.php PHP: PHP Options/Info - Manual]
PHP事態に関する情報の取得関数群。assert/phpinfo/extension_loadedなどいくつか重要な関数が存在する。
===== assert =====
* [https://www.php.net/manual/ja/function.assert.php PHP: assert - Manual]
* [https://www.infiniteloop.co.jp/tech-blog/2016/12/php-assertion-expectation/ PHP7 の assert による簡易テストはいいぞ。|技術ブログ|北海道札幌市・宮城県仙台市のVR・ゲーム・システム開発 インフィニットループ]
===== memory =====
* [https://su-kun1899.hatenablog.com/entry/2021/11/17/080000 PHP でメモリが足りなくなったら #php - ジムには乗りたい]
* [https://e-garakuta.net/techinfo/doku.php/linux/php-memory PHPのメモリ回りの情報 [がらくたネット]]
* [https://www.php.net/manual/ja/function.memory-get-peak-usage.php PHP: memory_get_peak_usage - Manual]
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 =====
[https://www.php.net/manual/ja/function.phpinfo.php PHP: phpinfo - Manual]
PHPに関する情報をHTMLで表示する。重要。
phpcgi -i
php -r "phpinfo();"
php -m
CLIモードだとHTMLではなくプレーンテキスト。
=== Database ===
* [https://www.php.net/manual/ja/refs.database.php PHP: データベース関連 - Manual]
* [https://www.php.net/manual/ja/refs.database.abstract.php PHP: 抽象化レイヤー - Manual]
PHPでのDBの操作方法が大きく2種類ある。抽象化レイヤーと、DB固有のモジュール。今は抽象化レイヤーの内、PDOというPHP独自の仕組みが主流。
ChatGPTで調べたところ以下の違いがある。
* DBA: Berkeley DB/GDBM/QDBMなど。扱うDBの種類が特殊。ファイルベースのDBで、キー・バリュー形式で設定ファイルなど。一部の設定ファイル・キャッシュ向け。軽量なファイルベースのデータ保存向け。
* ODBC: Microsoft作成。他の言語でも使える。PDOより対応可能なDBの幅が広い。ODBCのライブラリーを使う形。ただし、汎用的なのでPDOよりも重いし汎用的だから設定が複雑。レガシーシステム向け。
* PDO: PHP独自。性能も申し分ないし基本的にはこれを使うのがいい。
==== PDO ====
* [https://www.php.net/manual/ja/book.pdo.php PHP: PDO - Manual]
* [https://qiita.com/mpyw/items/b00b72c5c95aac573b71 PHPでデータベースに接続するときのまとめ #MySQL - Qiita]
===== 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 ======
* [https://www.php.net/manual/ja/pdo.construct.php PHP: PDO::__construct - Manual]
* [https://www.php.net/manual/ja/pdo.setattribute.php PHP: PDO::setAttribute - Manual]
* [https://www.php.net/manual/ja/pdo.constants.php PHP: 定義済み定数 - Manual]
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 =====
* [https://qiita.com/mpyw/items/b00b72c5c95aac573b71#charset PHPでデータベースに接続するときのまとめ #MySQL - Qiita]
* [https://www.php.net/manual/ja/ref.pdo-mysql.connection.php PHP: PDO_MYSQL DSN - Manual]
* [https://www.php.net/manual/ja/mysqlinfo.concepts.charset.php PHP: 文字セット - Manual]
* [https://stackoverflow.com/questions/52128507/php-what-is-the-default-charset-for-pdo-mysql PHP What is the default charset for pdo mysql - Stack Overflow]
* [https://stackoverflow.com/questions/134099/are-pdo-prepared-statements-sufficient-to-prevent-sql-injection 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メソッドがいくつかある。
* [https://www.php.net/manual/ja/pdo.exec.php PHP: PDO::exec - Manual]: 引数のSQLを実行して、影響のある行数を返す。結果のいらないSQLやSELECT以外のINSERT/UPDATEなどで使う。
* [https://www.php.net/manual/ja/pdo.query.php PHP: PDO::query - Manual]: 引数のSQLを実行して、実行結果をPDOStatementで取得する。ユーザー入力を伴わない固定SQLで使う。似たようなSQLをループなどで複数回実行するならprepareのほうが性能が向上する。
* [https://www.php.net/manual/ja/pdo.prepare.php PHP: PDO::prepare - Manual]: [https://www.php.net/manual/ja/pdostatement.execute.php PHP: PDOStatement::execute - Manual]用のSQL文を用意する。プレースホルダーを用意する。プリペアードステートメントというSQLの機能を使う。ユーザー入力をエスケープしたり、一部分だけ異なるようなSQLがキャッシュされて効率が上がる。
===== prepared statement =====
[https://www.php.net/manual/ja/pdo.prepared-statements.php PHP: プリペアドステートメントおよびストアドプロシージャ - Manual]
重要。パラメーターマーク/プレースホルダーを配置したSQL文を用意して、後でプレースホルダーに変数をバインドして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。
* [https://stackoverflow.com/questions/5809951/pdo-valid-characters-for-placeholders php - PDO valid characters for placeholders - Stack Overflow]
* [https://github.com/php/php-src/blob/5638fafa665899523861a12966b42a9c609a1664/ext/pdo/pdo_sql_parser.re#L48 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 ======
[https://www.php.net/manual/ja/pdostatement.execute.php 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実行時に変数が評価される。
なお、似た名前のメソッドに [[https://www.php.net/manual/ja/pdostatement.bindcolumn.php 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も役立つ。
[http://hono-wp.seesaa.net/article/411183815.html 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 ======
[https://kinocolog.com/pdo_prepare_insert/ 【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 = [];
<nowiki> </nowiki>      $this->file->seek(1);
<nowiki> </nowiki>      foreach (new \NoRewindIterator($this->file) as $row) {
<nowiki> </nowiki>          /** @var array $row map_raku_numに存在する列番号の場合、列番号を列名に置換。 */
<nowiki> </nowiki>          $row = array_combine(array_map(function($k) use($map_raku_num) {
<nowiki> </nowiki>              return in_array($k, $map_raku_num) ? array_flip($map_raku_num)[$k] : $k;
<nowiki> </nowiki>          }, array_keys($row)), $row);
<nowiki> </nowiki>          foreach ($map_base_raku as $base => $raku) {
<nowiki> </nowiki>              if (!array_key_exists($raku, $row)) {
<nowiki> </nowiki>                  $row[$raku] = null;
<nowiki> </nowiki>              };
<nowiki> </nowiki>              $formatter = $formatter ?: function($v, $k, $r) {return $v;};
<nowiki> </nowiki>              $row[$raku] = ($row[$raku] === <nowiki>''</nowiki> || $row[$raku] === null) ? null : $row[$raku];
<nowiki> </nowiki>              $record[$base] = $formatter($row[$raku], $base, $row);
<nowiki> </nowiki>          }
<nowiki> </nowiki>          /** 全部空なら除外する。 */
<nowiki> </nowiki>          if (!empty(array_filter($record))) $records[] = $record;
<nowiki> </nowiki>      }
<nowiki> </nowiki>      /** UPSERTのAI増分対策用に重複削除。 */
<nowiki> </nowiki>      if (!empty($unique)) {
<nowiki> </nowiki>          $old_row = <nowiki>''</nowiki>;
<nowiki> </nowiki>          foreach ($records as $row => $line) {
<nowiki> </nowiki>              // unique対象列が全部一致の場合削除。
<nowiki> </nowiki>              if (array_filter($unique, function($v) use ($line, $records, $old_row) {return $line[$v] !== $records[$old_row][$v];})) {
<nowiki> </nowiki>                  unset($records[$old_row]);
<nowiki> </nowiki>              }
<nowiki> </nowiki>              $old_row = $row;
<nowiki> </nowiki>          }
<nowiki> </nowiki>      }
<nowiki> </nowiki>      /** @var array $map_base_holder PDOのプレースホルダーには英数字しか使えないので%エンコーディング。 */
<nowiki> </nowiki>      $map_base_holder = array_combine(array_keys($map_base_raku), array_map(function($v) {
<nowiki> </nowiki>          return preg_replace('~..~', '_$0', strtoupper(unpack('H*', $v)[1]));
<nowiki> </nowiki>      }, array_keys($map_base_raku)));
<nowiki> </nowiki>      try {
<nowiki> </nowiki>          $dbh = new PDO(Raku2Config::getDSN(), Raku2Config::get('CONFIG_DBUSER'), Raku2Config::get('CONFIG_DBPASS'),
<nowiki> </nowiki>              [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
<nowiki> </nowiki>          Raku2Config::writeLog("SQL execute table_name=$table_name");
<nowiki> </nowiki>          $table_name = str_replace(["\0", "`"], ["", "``"], $table_name);
<nowiki> </nowiki>          /** @var string $sql_prefix SQL前半カラム名 */
<nowiki> </nowiki>          $sql_prefix = "INSERT INTO $table_name (" . implode(',', array_keys($map_base_raku)) . ') VALUES ';
<nowiki> </nowiki>          /** @var string $sql_postfix SQL後半カラム名 カラム1 = VALUES(カラム1), カラム2 = VALUES(カラム2)... */
<nowiki> </nowiki>          $sql_postfix = ' ON DUPLICATE KEY UPDATE '
<nowiki> </nowiki>              . implode(',', array_map(function($e){return "$e=VALUES($e)";}, array_keys($map_base_raku)));
<nowiki> </nowiki>          /** @var int $run_per_block 全データを一括INSERTすると、物件テーブルが4388件目でハングするので1000件ずつ分割実行 */
<nowiki> </nowiki>          foreach (array_chunk($records, $run_per_block = 1000) as $sub_records) {
<nowiki> </nowiki>              // [%エンコーディングした列名][行番号]の書式にプレースホルダー配置 (例: 86_8a_a9_943)
<nowiki> </nowiki>              $sql = $sql_prefix
<nowiki> </nowiki>                  . implode(',', array_map(function($row)use($map_base_holder){
<nowiki> </nowiki>                      return '('.implode(',', array_map(function($e)use($row){return ":$e$row";}, $map_base_holder)).')';
<nowiki> </nowiki>                  }, array_keys($sub_records)))
<nowiki> </nowiki>                  . $sql_postfix;
<nowiki> </nowiki>              $sth = $dbh->prepare($sql);
<nowiki> </nowiki>              foreach ($sub_records as $row => $line) {
<nowiki> </nowiki>                  foreach ($map_base_holder as $base => $holder) {
<nowiki> </nowiki>                      $type = in_array($base, array_keys($map_base_type))
<nowiki> </nowiki>                          ? $map_base_type[$base] : PDO::PARAM_STR;
<nowiki> </nowiki>                      $sth->bindValue(":$holder$row", $line[$base], $type);
<nowiki> </nowiki>                  }
<nowiki> </nowiki>              };
<nowiki> </nowiki>              $sth->execute();
<nowiki> </nowiki>              /** @var string $sql ON DUPLICATE KEY UPDATE で更新するとAUTO_INCREMENTが増えるので、最大値でリセット。 */
<nowiki> </nowiki>              $sql = "SET @NEW_AI = (SELECT MAX("
<nowiki> </nowiki>                  . str_replace(["\0", "`"], ["", "``"], array_key_first($map_base_raku)) . ")+1 FROM $table_name)";
<nowiki> </nowiki>              $dbh->exec($sql);
<nowiki> </nowiki>              $dbh->exec("SET @ALTER_SQL = CONCAT('ALTER TABLE $table_name AUTO_INCREMENT =', @NEW_AI)");
<nowiki> </nowiki>              $dbh->exec("PREPARE NEWSQL FROM @ALTER_SQL");
<nowiki> </nowiki>              $dbh->exec("EXECUTE NEWSQL");
<nowiki> </nowiki>          }
<nowiki> </nowiki>      } catch (PDOException $e) {
<nowiki> </nowiki>          Raku2Config::writeLog("table_name=$table_name " . $e->getMessage());
<nowiki> </nowiki>          return Raku2Config::EXIT_EXCEPTION;
<nowiki> </nowiki>      }
分割一括UPSERT例。
====== テーブル名/カラム名には使用不能 ======
「[https://www.php.net/manual/ja/pdo.prepare.php#111977 PHP: PDO::prepare - Manual]」のコメントに記載がある通り、PHPのプリペアードステートメントはテーブル名とカラム名のプレースホルダーは未対応。プレースホルダーは単に文字列置換をしているわけではなく、DBMSにクエリープランの作成を指示するため。これにはテーブル名が分かっていないといけない。
「[https://qiita.com/mpyw/items/b00b72c5c95aac573b71#%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E5%90%8D%E3%82%AB%E3%83%A9%E3%83%A0%E5%90%8D%E3%81%AE%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97%E5%87%A6%E7%90%86 PHPでデータベースに接続するときのまとめ #MySQL - Qiita]」に記載があるような、テーブル名のエスケープを自前で行う必要がある。
* NULLバイトの除外。
* バッククオートの二重化によるエスケープ。
* 全体を囲む。
$sql = sprintf(
    "CREATE TABLE `%s`(id int, name text)",
    str_replace(["\0", "`"], ["", "``"], $table_name)
);
===== Fetch =====
PDOStatementからデータを取得するメソッドが複数ある。
* [https://www.php.net/manual/ja/pdostatement.fetch.php PHP: PDOStatement::fetch - Manual]: 1行。
* [https://www.php.net/manual/ja/pdostatement.fetchall.php PHP: PDOStatement::fetchAll - Manual]: 全行。
* [https://www.php.net/manual/ja/pdostatement.fetchcolumn.php PHP: PDOStatement::fetchColumn - Manual]
* [https://www.php.net/manual/ja/pdostatement.fetchobject.php PHP: PDOStatement::fetchObject - Manual]
「[http://blog.tojiru.net/article/277021312.html 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 ======
* [https://www.php.net/manual/ja/pdo.setattribute.php PHP: PDO::setAttribute - Manual]
* [https://www.php.net/manual/ja/pdostatement.fetch.php PHP: PDOStatement::fetch - Manual]
* [https://www.php.net/manual/ja/pdo.constants.php PHP: 定義済み定数 - Manual]
PDOのコンストラクターのオプション、setAttribute、fetchの引数で取得条件を指定できる。
* PDO::FETCH_BOTH: 規定。カラム名と0開始の添え字の配列で返す。
* PDO::FETCH_NAMED: 同名のカラムが複数ある場合、値の配列を返す。
* PDO::FETCH_NUM: 0開始のカラム番号の配列で返す。
同名カラムがある場合、PDO::FETCH_NUM/FETCH_NAMEDじゃないと取れない。
===== UPSERT =====
いくつかポイントがある。1回のUPSERTで同じレコードの更新を試みると、PKがAIで増分するので事前に固有にしておく。<syntaxhighlight lang="php" line="1">
    /**
    * 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;
    }
</syntaxhighlight>


* [https://book.cakephp.org/migrations/3/ja/index.html Migrations - 3.x]
* [https://qiita.com/ozawan/items/8144e02ca70519f3dcaf CakePHP3のマイグレーションまとめ #cakephp3 - Qiita]


PHPファイルでDBのスキーマ変更を行うための仕組み。VCSでDB設定を管理でき、コマンドでDBの設定などを行える利点がある。
===== Other =====


CakePHPでは、Phinxをマイグレーションに使っており、コマンド類はPhinxのラッパーになっている。細かいことはPhinxにあたる必要がある。
====== テーブルの有無確認 ======
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;
}


config/Migrationsディレクトリーに、マイグレーションファイルを配置し、以下のmigrationsコマンドの実行でDBにテーブルを作成できる。
=== Calendar ===
bin/cake migrations migrate
[https://www.php.net/manual/ja/refs.calendar.php PHP: 日付および時刻関連 - Manual]
マイグレーションファイルは、config/Migrationディレクトリーで、YYYYmmddHHMMSS_MigrationName.phpというように、作成日を入れて用意する。


自分で手作業でマイグレーションファイルを作成できるが、bakeコマンドでひな形を用意できる。これを使ったほうがおそらくいい。
==== time/速度計測 ====


===== マイグレーションファイル =====
* [https://qiita.com/HidakaRintaro/items/8893f1a36709eee3582f PHPの処理速度の計測方法 #PHP7 - Qiita]
* [https://www.php.net/manual/ja/function.hrtime.php PHP: hrtime - Manual]
* [https://www.php.net/manual/ja/function.microtime.php PHP: microtime - Manual]
* [https://www.php.net/manual/ja/intro.hrtime.php PHP: はじめに - Manual]


====== 既存のテーブルにカラムを追加 ======
PHPで処理速度などを計測したいことがある。基本は処理前後のタイムスタンプの差分で、どの言語でも共通の論理だが、いくつか方法がある。
以下のコマンドでカラム追加を含むコードを作成できる。
bin/cake bake migration AddPriceToProducts price:decimal


microtimeとhrtimeの2個の関数をタイムスタンプの取得で使える。hrtimeはPHP 7.3.0以上で使用可能。単位ナノ秒。問題なければ、こちらが推奨されている。HRTime (High Resolution Time) の拡張のモジュールと関係する関数とのこと。
両方とも、引数にtrueを指定して、floatで取得するのが基本。
  <?php
  <?php
  use Migrations\AbstractMigration;
  $start = hrtime(true); // 計測開始時間
// 計測したい処理
$end = hrtime(true); // 計測終了時間
   
   
  class AddPriceToProducts extends AbstractMigration
  // 終了時間から開始時間を引くと処理時間になる
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)
  {
  {
     public function change()
     $time = 'microtime';
     {
     $nanoFactor = 1000;
        $table = $this->table('products');
    if (function_exists('hrtime')) {
         $table->addColumn('price', 'decimal')
         $time = 'hrtime';
              ->update();
        $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 =====
 
* [https://www.php.net/manual/ja/datetime.examples-arithmetic.php PHP: 日付/時刻 の算術 - Manual]
* [https://qiita.com/aminevsky/items/a222845ddbdd2536da56 PHPの日付・時刻関連をまとめてみた #PHP7.2 - Qiita]
* [https://qiita.com/mpyw/items/55df438d12ef8e94dbc5 日付時刻関連で使われる書式の調査 #PHP - Qiita]
* [https://qiita.com/mpyw/items/e42fa173e5a26220a785 日付時刻関連のクラスを活用しよう #PHP - Qiita]
 
PHPの日時処理はdate関数DateTimeクラス、DateTimeImuutableクラス、Carbonなどいろいろある。
 
公式の説明などを見る限り、DateTimeImuutableを推奨しているように見える。
 
単純な日時文字列が欲しいだけなら、date関数系API。それ以外の本格的な日付計算が必要ならDateTimeImmutableを使うといい。プロジェクトでは念のため自前の日時クラスでラップしておく。
 
===== Carbon =====
 
* [https://zenn.dev/yskn_sid25/articles/ae1830480d4d72 なぜPHP標準関数のdateではなくCarbonを使うのか?]
* [https://www.reddit.com/r/PHP/comments/317mkb/eli5_why_is_carbon_better_than_core_php_date/ ELI5: Why is Carbon better than core PHP date classes? : r/PHP]
* [https://carbon.nesbot.com/docs/ Carbon - A simple PHP API extension for DateTime.]
* [https://carstenwindler.de/green-it/why-phps-carbon-is-bad-for-the-climate/ Why PHPs Carbon is bad for the climate - Carsten Windler]
 
PHPの日時処理にCarbonという拡張クラスが人気らしい。人気の理由はテストしやすいからとか。
 
たしかに、標準のdateなどの関数系APIは扱いにくいかもしれない。
 
DateTimeImuutableなら問題ない気がする。
 
===== datetime =====
[https://www.php.net/manual/ja/ref.datetime.php PHP: 日付・時刻 関数 - Manual]
 
関数系API群。
 
* date
* strtotime
* time
 
===== format =====
 
* [https://www.php.net/manual/ja/datetime.formats.php PHP: サポートする日付と時刻の書式 - Manual]
* [https://www.php.net/manual/ja/datetime.format.php PHP: DateTimeInterface::format - Manual]
* [https://www.php.net/manual/ja/class.datetimeinterface.php PHP: DateTimeInterface - Manual]
* [https://www.php.net/manual/ja/datetime.constants.php#constant.date-atom PHP: 定義済み定数 - Manual]
 
日時の書式が2種類ある。DatetImeInterface::formatを見ておくとよい。
 
バックスラッシュでエスケープできる。
 
特に重要なものを抜粋する。
{| class="wikitable"
|+
!種類
!文字
!定数
!例
!説明
|-
|全ての日付/時刻
|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(['-', ':'], <nowiki>''</nowiki>, date('c')); // 20241007T000319+0000
 
===File system===
Ref: [https://www.php.net/manual/ja/refs.fileprocess.file.php PHP: ファイルシステム - Manual].
====File system====
=====file=====
*[https://www.php.net/manual/ja/function.file-get-contents.php file_get_contents] — ファイルの内容を全て文字列に読み込む
*[https://www.php.net/manual/ja/function.file-put-contents.php file_put_contents]: データをファイルに書き込む。戻り値に書き込みバイト数を返す。失敗したらfalse。成否は完全一致===falseで。
*[https://www.php.net/manual/ja/function.file.php PHP: file - Manual]: ファイル全体を読み込んで改行区切りで配列にする。
*readfile: ファイル全体を読み込んで標準出力に出力する。
上記2個の非常に重要な入出力関数がある。
 
バイナリーやHTTP GETに対応している。アップロードされたファイルの読み込みなどでお世話になる。
 
file_get_contents/file_put_contentsはfopen/fwrite/fcloseの一連のファイル処理を含んでいるので非常に簡単。
 
* [https://gustavostraube.wordpress.com/2015/04/21/splfileobject-is-faster-than-fopenfgets/ SplFileObject is faster than fopen/fgets – Gustavo Straube]
* [https://fsen.ru/speed/php-open-file file vs file_get_contents vs fopen vs SplFileObject php - Тесты производительности php - Fast Site Engine]
 
SplFileObjectとどちらが速いかは不明。
 
===== directory =====
 
* [https://www.php.net/manual/ja/function.mkdir.php PHP: mkdir - Manual]:
* rmdir
// ディレクトリー不在なら作成。
$dir = '/path/to/dir'
if (!file_exists($dir)) mkdir($dir, 0777, true);
 
=====check=====
*[https://www.php.net/manual/ja/function.file-exists.php PHP: file_exists - Manual]
*[https://www.php.net/manual/ja/function.is-readable.php PHP: is_readable - Manual]
*[https://www.php.net/manual/ja/function.is-writable.php PHP: is_writable - Manual]
入出力とセットで使うファイルの不在確認の関数群。
 
===== path =====
 
* [https://qiita.com/okdyy75/items/cc9e99025345bb17c37b PHPのディレクトリまとめ #PHP - Qiita]
* [https://www.ajisaba.net/php/pathinfo.html PHP・ファイル名・パスなどの取得]
* [https://zenn.dev/phpbeginners/articles/a237fda03dd970 PHPの便利な関数:ファイルパス関連dirname(), basename(), realpath(), getcwd(), chdir()]
* [https://www.php.net/manual/ja/language.constants.magic.php PHP: マジック定数 - Manual]
* [https://www.php.net/manual/ja/function.basename.php PHP: basename - Manual]: パスからファイル名を取得。
* [https://www.php.net/manual/ja/function.dirname.php PHP: dirname - Manual]: パスからディレクトリーの取得。
* [https://www.php.net/manual/ja/function.realpath.php PHP: realpath - Manual]: 相対パスを絶対パスに変換。
* [https://www.php.net/manual/ja/function.getcwd.php PHP: getcwd - Manual]: 現在のディレクトリー。__DIR__は実行中ディレクトリー。includeしたときにgetcwdは実行元、__DIR__はinclude先。
* [https://www.php.net/manual/ja/function.chdir.php PHP: chdir - Manual]
* __DIR__: ファイルの存在するディレクトリー。終端スラッシュなし。
パス関係の操作。重要。
 
===== operation =====
 
* [https://www.php.net/manual/ja/function.rename.php PHP: rename - Manual]: 改名と移動の両方。
* [https://www.php.net/manual/ja/function.move-uploaded-file.php PHP: move_uploaded_file - Manual]
 
PHPでファイルの移動や改名をする場合、rename。基本的にこれ1個だけ。
rename("/tmp/tmp_file.txt", "/home/user/login/docs/my_file.txt");
 
===== CSV =====
 
* [https://www.php.net/manual/ja/function.fgetcsv.php PHP: fgetcsv - Manual]
* [https://www.php.net/manual/ja/function.str-getcsv.php PHP: str_getcsv - Manual]
* [https://www.php.net/manual/ja/class.splfileobject.php PHP: SplFileObject - Manual]
* [https://blog.fenrir-inc.com/jp/2014/07/php-csv.html 【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 ======
 
* [https://www.php.net/manual/ja/function.fgetcsv.php PHP: fgetcsv - Manual]
* [https://www.php.net/manual/ja/splfileobject.fgetcsv.php PHP: SplFileObject::fgetcsv - Manual]
* [https://www.php.net/manual/ja/function.setlocale.php PHP: setlocale - Manual]
* [https://www.beeete2.com/blog/?p=3070 Windows環境でCSVのパースをするとカラムが正しく分割できない現象の修正方法 - ビー鉄のブログ]
* [https://iamapen.hatenablog.com/entry/2017/08/10/134224 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 ======
 
* [https://www.php.net/manual/ja/function.fputcsv.php PHP: fputcsv - Manual]
* [https://www.php.net/manual/ja/splfileobject.fputcsv.php PHP: SplFileObject::fputcsv - Manual]
* [https://oopsoop.com/how-to-read-and-write-files-in-php/#index_id13 【PHP入門】ファイルの読み書きをする4つの方法]
* [https://www.php.net/manual/ja/splfileobject.fwrite.php 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 =====
 
* [https://www.php.net/manual/en/function.tmpfile.php PHP: tmpfile - Manual]: r+w+bで開く。fcloseなど使用終了時に自動削除。「[https://forums.phpfreaks.com/topic/316092-tmpfile-or-tempnam/ tmpfile() or tempnam()? - PHP Coding Help - PHP Freaks]」自動削除以外はfopen('php://temp', 'w+b')相当。
* [https://www.php.net/manual/ja/function.tempnam.php PHP: tempnam - Manual]: おそらくr+wで開く。
* [https://stackoverflow.com/questions/57589387/using-php-temp-or-tempnam-when-i-write-a-temporary-file-in-order-to-upload Using `php://temp` or `tempnam` when I write a temporary file in order to upload data into 3rd party network storage - Stack Overflow]
* [https://qiita.com/mpyw/items/f24d3764fe3eedf132ff PHPで一時的なファイルポインタを扱う方法 #PHP - Qiita]
 
一時ファイルの作成方法がいくつかある。
 
* <code>$fp = fopen('php://memory', 'r+b');</code>
* <code>$fp = fopen('php://temp', 'r+b');</code>
* <code>$fp = fopen("php://temp/maxmemory:{$n}", 'r+b');</code>
* <code>$file = new SplTempFileObject($n);</code>
* <code>$fp = tmpfile();</code>
* 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'からファイルパスを取得できる。
 
データの読込。「[https://qiita.com/danishi/items/c9076aa6fa156e47351d 【PHP】gzip圧縮されたCSVをSplFileObjectで直接処理する #PHP - Qiita]」
<?php
// gzipファイルのダウンロード
$url = '<nowiki>http://localhost/test.csv.gz'</nowiki>;
$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 = '<nowiki>https://localhost'</nowiki>;
$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 =====
[https://www.php.net/manual/ja/dir.constants.php PHP: 定義済み定数 - Manual]
 
* DIRECTORY_SEPARATOR: Windowsなら\、それ以外/らしい。
このDIRECTORY_SEPARATORは基本的には使うことはない。
 
* [https://stackoverflow.com/questions/26881333/when-to-use-directory-separator-in-php-code path separator - When to use DIRECTORY_SEPARATOR in PHP code? - Stack Overflow]
* [https://stackoverflow.com/questions/625332/is-using-the-directory-separator-constant-necessary/7032949#7032949 language agnostic - Is using the directory separator constant necessary? - Stack Overflow]
 
理由として、Windowsがパスの文脈で/も受け入れるから。必要な場面は、OSからパスを受け取る場合に、\が返ってくることがあり、そこでexplodeしたりしたい場合のみ。
 
自分からパスを指定する文脈では、/で問題なく、DIRECTORY_SEPARATORを使う必要はない。
 
===== ディレクトリー以下のファイル一覧 =====
 
* [https://www.php.net/manual/ja/function.glob.php PHP: glob - Manual]
* [https://www.php.net/manual/ja/function.scandir.php PHP: scandir - Manual]
* [https://qiita.com/katsukii/items/ec816b23f68b6dfa0f87 PHPで任意のディレクトリ下にあるファイルを一覧取得する方法 #PHP - Qiita]
* [https://next-code.jp/blog/tech/php/php/ PHP 最強に簡単なファイル一覧取得方法 - 株式会社NextCode - 福山市のHP制作・システム開発]
* [https://stackoverflow.com/questions/18290498/which-is-better-to-read-files-from-a-directory-using-php-glob-or-scandir-o Which is better to read files from a directory using PHP - glob() or scandir() or readdir()? - Stack Overflow]
* [https://laboratory.kazuuu.net/using-phps-glob-function-to-count-the-number-of-files-in-a-folder/ PHPのglob()関数を使用しフォルダ内のファイルを数える | Men of Letters(メン・オブ・レターズ) – 論理的思考/業務改善/プログラミング]
* [https://stackoverflow.com/questions/5483181/php-most-efficient-way-to-list-the-files-in-a-very-large-directory scandir - PHP most efficient way to list the files in a very large directory - Stack Overflow]
* [https://stackoverflow.com/questions/2763290/which-is-faster-glob-or-opendir php - Which is faster: glob() or opendir() - Stack Overflow]
* [https://www.phparch.com/2010/04/putting-glob-to-the-test/ Putting glob() to the test | php[architect]]
* [https://stackoverflow.com/questions/2922954/getting-the-names-of-all-files-in-a-directory-with-php 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===
[https://www.php.net/manual/ja/refs.international.php PHP: 自然言語および文字エンコーディング - Manual]
 
====mbstring====
[https://www.php.net/manual/ja/book.mbstring.php PHP: マルチバイト文字列 - Manual]
 
=====mb_substr=====
Ref:
*[https://www.php.net/manual/ja/function.mb-substr.php PHP: mb_substr - Manual].
*[https://www.php.net/manual/ja/function.substr.php PHP: substr - Manual]
mb_substr(
 
    string $string,
 
    int $start,
 
    ?int $length = null,
 
    ?string $encoding = null
 
): string
 
substr同様、lengthにはマイナス値を指定可能。その場合、末尾からの文字数になる。
 
省略するかnullを指定すると、全文字。0は0文字。
 
===== mb_convert_kana =====
[https://www.php.net/manual/ja/function.mb-convert-kana.php PHP: mb_convert_kana - Manual]
mb_convert_kana(string $string, string $mode = "KV", ?string $encoding = null): string
{| class="wikitable"
|+使用可能な変換オプション
!オプション
!意味
|-
|<code>r</code>
|「全角」英字を「半角」に変換します。
|-
|<code>R</code>
|「半角」英字を「全角」に変換します。
|-
|<code>n</code>
|「全角」数字を「半角」に変換します。
|-
|<code>N</code>
|「半角」数字を「全角」に変換します。
|-
|<code>a</code>
|「全角」英数字を「半角」に変換します。
|-
|<code>A</code>
|「半角」英数字を「全角」に変換します ("a", "A" オプションに含まれる文字は、U+0022, U+0027, U+005C, U+007Eを除く U+0021 - U+007E の範囲です)。
|-
|<code>s</code>
|「全角」スペースを「半角」に変換します(U+3000 -> U+0020)。
|-
|<code>S</code>
|「半角」スペースを「全角」に変換します(U+0020 -> U+3000)。
|-
|<code>k</code>
|「全角カタカナ」を「半角カタカナ」に変換します。
|-
|<code>K</code>
|「半角カタカナ」を「全角カタカナ」に変換します。
|-
|<code>h</code>
|「全角ひらがな」を「半角カタカナ」に変換します。
|-
|<code>H</code>
|「半角カタカナ」を「全角ひらがな」に変換します。
|-
|<code>c</code>
|「全角カタカナ」を「全角ひらがな」に変換します。
|-
|<code>C</code>
|「全角ひらがな」を「全角カタカナ」に変換します。
|-
|<code>V</code>
|濁点付きの文字を一文字に変換します。"K", "H" と共に使用します。
|}
modeはデフォルトのKVで問題なさそう。
 
なお、日本語の促音、小文字の大文字への変換はできない。素直に置換するしかない。
 
[https://tamoc.com/convert_kana_small_to_large/ 【PHP】ひらがな・カタカナの小文字(小書き文字)を大文字にする方法]
function kana_small_to_large($subject) {
    $search = ['ぁ','ぃ','ぅ','ぇ','ぉ','っ','ゃ','ゅ','ょ','ゎ','ァ','ィ','ゥ','ェ','ォ','ッ','ャ','ュ','ョ','ヮ','ヶ'];
    $replace = ['あ','い','う','え','お','つ','や','ゆ','よ','わ','ア','イ','ウ','エ','オ','ツ','ヤ','ユ','ヨ','ワ','ケ'];
    return str_replace($search, $replace, $subject);
}
//使用例
echo kana_small_to_large('カッパ'); //カツパ
 
===Text===
====Strings====
=====strpos=====
Ref: [https://www.php.net/manual/ja/function.strpos.php PHP: strpos - Manual].
strpos([https://www.php.net/manual/ja/language.types.string.php string] <code>$haystack</code>, [[/wiki.gnusocial.jp//www.php.net/manual/ja/language.types.string.php|string]] <code>$needle</code>, [[/wiki.gnusocial.jp//www.php.net/manual/ja/language.types.integer.php|int]] <code>$offset</code> = 0): [[/wiki.gnusocial.jp//www.php.net/manual/ja/language.types.integer.php|int]]|[[/wiki.gnusocial.jp//www.php.net/manual/ja/language.types.value.php|false]]
文字列 <code>haystack</code> の中で、 <code>needle</code> が最初に現れる位置を探します。
 
include相当。よく使う。
 
戻り値に注意。有無の確認時は、strpos() !== falseの厳密一致でチェックする必要がある。
 
===== echo/print/printf =====
 
* [https://www.php.net/manual/ja/function.echo.php PHP: echo - Manual]
* [https://www.php.net/manual/ja/function.print.php PHP: print - Manual]
* [https://www.php.net/manual/ja/function.printf.php PHP: printf - Manual]
* [https://qiita.com/tadsan/items/e09475093bc336881b20 echoとprintの違い #PHP - Qiita]
* [https://qiita.com/chimayu/items/dcc443c3a99f3379b3da PHP : echoとprintの違い #まとめ - Qiita]
* [https://inouelog.com/php-echo-print/ 【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 =====
 
* [https://www.php.net/manual/ja/function.setlocale.php PHP: setlocale - Manual]
* [https://hnw.hatenablog.com/entry/20120501 PHPのロケールに関するまとめ - hnwの日記]
 
テキスト系の関数では、ロケールを考慮するものがいくつかある。その際の設定にこの関数を使う。
 
=== Basic/Vartype/変数・データ型関連 ===
 
==== Variable ====
 
===== print_r/var_export/var_dump =====
 
* [https://qiita.com/yasumodev/items/4b0e5a0d2d1ec3b3177b [PHP] print_r、var_dump、var_export のちがい #PHP - Qiita]
* [https://laracasts.com/discuss/channels/laravel/how-to-create-dynamically-create-configcustomphp-config-file How to create Dynamically create config/custom.php config file]
* [https://www.php.net/manual/ja/function.print-r.php PHP: print_r - Manual]
* [https://www.php.net/manual/ja/function.var-export.php PHP: var_export - Manual]
* [https://www.php.net/manual/ja/function.var-dump.php 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だと思われる。
 
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 '<nowiki><pre>';</nowiki>
        print_r($val);
        echo  '<nowiki></pre></nowiki>';
}
?>
 
===== var_exportの変数取込 =====
var_exportの文字列表現。config.phpに出力して、Includeするほかに、テキストを変数にしたいことがある。
 
* [https://stackoverflow.com/questions/933506/how-to-read-output-of-var-export-into-a-variable-in-php import - how to read output of var_export into a variable in PHP? - Stack Overflow]
* [https://tips.recatnap.info/laboratory/detail/id/575 reCatnap: php var_export()した後の文字列を元に戻す(eval())]
 
素直にevalする。
$dumpStr = var_export($var,true);
eval("$somevar = $dumpStr;");
くれぐれも入力に注意する。
 
=== Process ===
 
==== exec ====
システムプログラムの実行のための関数群がある。Windowsだとcmd.exe経由で実行される。
 
===== exec/shell_exec/system/passthru =====
 
* [https://stackoverflow.com/questions/7093860/php-shell-exec-vs-exec shell - PHP shell_exec() vs exec() - Stack Overflow]
* [https://www.php.net/manual/ja/function.exec.php PHP: exec - Manual]
* [https://www.php.net/manual/ja/function.passthru.php PHP: passthru - Manual]
* [https://www.php.net/manual/ja/function.shell-exec.php PHP: shell_exec - Manual]/[https://www.php.net/manual/ja/language.operators.execution.php PHP: 実行演算子 - Manual]
* [https://www.php.net/manual/ja/function.system.php PHP: system - Manual]
 
外部プログラム実行のための3の関数がある。違いがある。
 
* shell_exec: 標準出力をstringで返す。バッククオート演算子``と同じ。プログラムの成功可否、終了コードは判断不能。
* exec: 既定で標準出力の最後の1行のみ返す。第二引数に配列を指定すれば、行区切りの配列で返すこともできる。
* passthru: Unixコマンドの出力がバイナリーで、ブラウザーに直接バイナリーを返す場合に、exec/systemの代わりに使う。戻り値は?falseで標準出力に直接結果を出力する。
* system: C言語のsystem関数に類似。system()でコマンドを実行して出力する。成功時にコマンド出力の最終行を返す。出力をファイルや別のストリームにリダイレクトしないと、終了までPHPが止まる。
 
{| class="wikitable"
|+
!項目
!exec
!passthru
!shell_exec/``
!system
|-
|出力
| -
|x
| -
|x
|-
|終了コード
|x
|x
| -
|x
|-
|戻り値
|最終行
|null/false
|全行
|最終行
|-
|全行取得
|x
| -
|x
| -
|-
|用途
|外部コマンドの結果文字列取得 (終了チェックあり)
|画像応答
|外部コマンドの結果文字列取得
|文字応答
|}
基本はshell_exec/`` execで十分。
 
===== exec =====
[https://www.php.net/manual/ja/function.exec.php PHP: exec - Manual]
exec(string $command, array &$output = null, int &$result_code = null): string|false
戻り値は最終行。失敗したらfalseを返す。実行コマンドの終了コードは$result_codeに渡される。
 
===== escape =====
 
* [https://www.php.net/manual/ja/function.escapeshellarg.php PHP: escapeshellarg - Manual]
* [https://www.php.net/manual/ja/function.escapeshellcmd.php PHP: escapeshellcmd - Manual]
* [https://stackoverflow.com/questions/1881582/whats-the-difference-between-escapeshellarg-and-escapeshellcmd php - escapeshellarg と escapeshellcmd の違いは何ですか? - Stack Overflow]
 
escapeshellarg/escapeshellcmdはexec/shell_exec/``と併用するエスケープ用関数。
 
* escapeshellarg: 引数の文字列を一重引用符で囲み、既存の一重引用符を苦オートする。これで、引数全体を1個の引数にする。複数の引数の誤り実行を回避できる。
* escapeshellcmd: シェルに特殊な意味のある&#;`|*?~<>^()[]{}$\、\x0A のシェルの特殊文字にバックスラッシュを追加し、'"は対がない場合のみエスケープ。
 
元々、コマンド全体をエスケープする [escapeshellcmd] だけがあった。が、これだとコマンドの引数を追加する攻撃が可能になるので、 [escapeshellarg] が追加されたらしい ([https://blog.tokumaru.org/2012/04/php-escapeshellcmd-chase.html PHPのescapeshellcmdを巡る冒険 | 徳丸浩の日記])。
 
* [https://blog.tokumaru.org/2011/01/php-escapeshellcmd-is-dangerous.html PHPのescapeshellcmdの危険性 | 徳丸浩の日記]
* [https://blog.tokumaru.org/2011/01/escapeshellcmd-dangerous-usage.html#p01 escapeshellcmdの危険な実例 | 徳丸浩の日記]
* [https://blog.tokumaru.org/2016/12/phpescapeshellcmdmailcve-2016-10033.html PHPのescapeshellcmdを巡る冒険はmail関数を経てCVE-2016-10033に至った | 徳丸浩の日記]
* [https://www.docswell.com/s/ockeghem/58W1LZ-phpconf2016#p19 安全なPHPアプリケーションの作り方2016 | ドクセル]
* [https://blog.hamayanhamayan.com/entry/2021/12/14/222235 CTFのWebセキュリティにおけるCommand Injectionまとめ(Linux, Windows, RCE) - はまやんはまやんはまやん]
 
ただ、escapeshellcmdは、パラメーターインジェクションの危険性があるので、使ってはいけないらしい。
 
基本は引数に [escapeshellarg] を使うだけ。
 
「[https://blog.tokumaru.org/2021/12/php.html PHPにはエスケープ関数が何種類もあるけど、できればエスケープしない方法が良い理由 | 徳丸浩の日記]」にあるように、その後PHP 7.4でproc_openが登場した。これはシェル経由じゃないOSコマンド呼び出しで、エスケープ不要なので安全。基本はこれを使うのがいいとのこと。
 
===== escapeshellarg =====
 
* [https://megu-tech.hatenablog.com/entry/2020/03/25/182721 PHPでコマンド実行しようとしたら日本語が消失した - megutech]
* [https://peccu.hatenablog.com/entry/2015/07/12/000000 PHPのescapeshellargが日本語を消し去った - @peccul is peccu]
* [https://hs9587.hatenablog.com/entry/20111209/1323419546 PHPのロケール、又はアパッチの環境変数 LANG - hs9587’s diary]
* [https://stackoverflow.com/questions/12953929/php-escape-shell-arg-unwanted-behaviour PHP escape shell arg unwanted behaviour - Stack Overflow]
* [https://www.php.net/manual/ja/function.escapeshellarg.php PHP: escapeshellarg - Manual]
* [https://mochi227.blog.jp/archives/5050177.html mochigrammer : escapeshellargで日本語が消える!]
* [https://qiita.com/wakisuke/items/57ce6313c506b230879f 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/その他の基本モジュール===
[https://www.php.net/manual/ja/refs.basic.other.php PHP: その他の基本モジュール - Manual]
====JSON====
[https://www.php.net/manual/ja/book.json.php PHP: JSON - Manual]
 
===== About =====
json_encode/json_decodeをよく使う。非常に重要。
 
json_encodeはオブジェクトをJSON文字列表記にできるのでデバッグなどで便利。
 
[https://kohkimakimoto.hatenablog.com/entry/2012/05/17/180738 PHPでUnicodeアンエスケープしたJSONを出力する関数 - オープンソースこねこね]
 
JSON_UNESCAPED_UNICODE をオプションに指定しないと日本語はユニコードエスケープ表記になる。
 
===== 連想配列 =====
json_decode(
    string $json,
    ?bool $associative = null,
    int $depth = 512,
    int $flags = 0
): mixed
json_decodeは第二引数にtrueを指定しないと、object (stdClass) になる。trueにすると、連想配列になる。基本は連想配列でいいと思う。
 
==== Other ====
 
===== die/exit =====
 
* [https://www.php.net/manual/ja/function.die.php PHP: die - Manual]
* [https://www.php.net/manual/ja/function.exit.php PHP: exit - Manual]
 
dieはexitと完全に同等。
exit(string $status = ?): void
exit(int $status): void
メッセージを表示してスクリプトを終了する。関数ではなく、言語構造扱い。
 
statusを指定しない場合、丸括弧不要。status=0指定とみなされる。
 
statusが文字列なら、終了直前に表示する。intの場合、終了ステータス扱い。0-254。255は予約されている。0は正常終了。
 
===== eval =====
[https://www.php.net/manual/ja/function.eval.php PHP: eval - Manual]
 
文字列をPHPコードとして評価する。危険なので、特にユーザーから入力を受け付ける場合は、注意する。できれば使わないほうがいい。
 
evalで評価する文字列内でreturnした結果が返却値となる。ないならnull。戻り値が必要なら、テキスト内で忘れずにreturnする。
==== SPL ====
[https://www.php.net/manual/ja/book.spl.php PHP: SPL - Manual]
 
Standard PHP Library. PHP 5.1.0で登場。標準的な処理への対応のためのライブラリー。
 
PHP 5から登場しただけあって、実装がかなり洗練されている。メモリー使用量、速度面の性能で有利なことが多い。
 
* SplFileObject
 
===== SPLFileObject =====
 
====== Flag ======
[http://taustation.com/php-splfileobjectsetfrags/ 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の派生クラスなので、このメソッドで操作できる。
 
「[https://www.php.net/manual/ja/class.seekableiterator.php 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 ====
[https://www.php.net/manual/ja/book.url.php PHP: URLs - Manual]
 
===== 全文字のパーセントエンコーディング =====
[https://stackoverflow.com/questions/28110062/php-how-to-encode-all-characters-with-rawurlencode 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]));
}
 
プレースホルダーの場合、%を_で置換すれば元データも復元可能。
 
===Other/Service===
 
==== HTTP ====
* [https://qiita.com/okdyy75/items/d21eb95f01b28f945cc6 PHPでPOST送信まとめ #PHP - Qiita]
* [https://qiita.com/shinkuFencer/items/d7546c8cbf3bbe86dab8 APIなどにfile_get_contents()を使うのはオススメしない理由と代替案 #PHP - Qiita]
* [https://www.php.net/manual/ja/function.file-get-contents.php PHP: file_get_contents - Manual]
* [https://www.php.net/manual/ja/book.curl.php PHP: cURL - Manual]
 
PHPでHTTP通信をする方法がいくつかある。
*file_get_contents
*curl
file_get_contentsはPHP標準。curlは外部ライブラリー。
 
「[https://stackoverflow.com/questions/11064980/php-curl-vs-file-get-contents PHP cURL vs file_get_contents - Stack Overflow]」などを見る限り、GET以外はcurlのほうが速くて複雑なことができるらしい。
 
file_get_contentsは元々ローカルや内部ファイルの読み込み用らしい。
 
GNU socialではHTTPClientクラス経由で実現するので、内部実装を意識する必要はない。
 
curlでのリクエスト方法を覚えておくと、汎用性が高い模様。
 
[https://zenn.dev/pixiv/articles/f68d4a79394401 PHPからのHTTPリクエスト (2016年版)]
 
外部ライブラリーを使っていいなら、Guzzleが今は主流とのこと。Guzzleは内部でcurlを使っている。
 
==== cURL ====
 
===== About =====
* [https://www.php.net/manual/ja/book.curl.php PHP: cURL - Manual]
* [https://www.php.net/manual/ja/curl.examples-basic.php PHP: 基本的な curl の使用法 - Manual]
* [https://qiita.com/shinkuFencer/items/d7546c8cbf3bbe86dab8 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("<nowiki>http://www.example.com/</nowiki>");
$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("<nowiki>http://www.google.com/</nowiki>");
/* 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);
[https://www.php.net/manual/ja/function.curl-getinfo.php PHP: curl_getinfo - Manual]
 
curl_getinfoで応答結果の詳細を確認できる。
 
特に以下は重要。
 
* http_code
 
===== CURLOPT =====
[https://www.php.net/manual/ja/curl.constants.php PHP: 定義済み定数 - Manual]
特に重要なオプションがいくつかある。
 
* CURLOPT_POST: trueならHTTP POST。これを指定するとContent-Tpe=application/x-www-form-urlencodedになる。JSONにしたいならこれは上書き必要。
* CURLOPT_HTTPHEADER: ヘッダー指定。<nowiki>[['Content-Type: application/json']]</nowiki> はよく指定する。
* 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にリクエストヘッダーを含める ([https://stackoverflow.com/questions/17092677/how-to-get-info-on-sent-php-curl-request web services - How to get info on sent PHP curl request - Stack Overflow])。デバッグ用。
 
===== CURLOPT_RETURNTRANSFERとCURLOPT_FILEの競合 =====
 
* [https://qiita.com/mpyw/items/d279ea2e971d110059eb CURLOPT_*のデフォルトオプション結合の罠 #PHP - Qiita]
* [https://stackoverflow.com/questions/56522406/php-curl-confilict-curlopt-file-and-curlopt-returntransfer-in-docker php Curl confilict CURLOPT_FILE and CURLOPT_RETURNTRANSFER in docker - Stack Overflow]
* [https://zenn.dev/xpadev/articles/abc5b7192bca58 PHPのcurlでCURLOPT_FILEを使う際の注意点]
 
この2個のオプションは競合する。後から設定したものが優先される。レスポンスボディーが必要ならば、片方に統一して、片方だけからのアクセスにする。
 
バイナリーやCSVファイルのように、データが大きくて高速なパースが必要なら、SplFileObjectを使いたいのでファイル優先でいいと思う。
 
JSONのようにシンプルで短いなら全部テキストでやってもよいだろう。
 
===== JSONデータの送受信 =====
 
* [https://www.smartllc.jp/blog/20150811-how-to-post-json-in-php/ JSON形式のデータをPOST送受信する方法(PHP) | 合同会社スマート]
* [https://stackoverflow.com/questions/11079135/how-to-post-json-data-with-php-curl How to POST JSON Data With PHP cURL? - Stack Overflow]
# 連想配列でリクエストボディーのデータを作って、json_encodeでJSON文字列に変換。
# POST指定: curl_setopt($ch, CURLOPT_POST, true);
# ヘッダーContent-Type指定: <code>curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));</code>
# リクエストボディー指定: 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('<nowiki>http://posttestserver.com/post.php'</nowiki>);
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'];
[[Category:PHP]]

2024年11月22日 (金) 14:52時点における最新版

Library

Framework

  • Symfony
  • CakePHP
  • FuelPHP: 2010年誕生。
  • Codeigniter: シンプル、軽量。
  • Zend
  • Laravel: 2011年誕生。
  • Phalcon
  • Yii - Wikipedia

Template

いろいろある。

  • Blade: Laravel標準。
  • DIV: 1ファイルでシンプル。大規模には向かない。
  • Smarty: 万能。
  • Twig: 拡張はしにくい。

使うとしたら、歴史の長いSmarty。

高速らしい。

そもそもテンプレートエンジンがいるのかどうかという議論がある。

UI/UXを突き詰めると、JavaScriptを使わざるを得ず、サーバーテンプレートエンジンは初回だけなので、いっそのことJSで全部やろうというのが最近の流れの模様。

PHP自体が一種のテンプレートエンジンという主張がある。が、関数をあれこれ書く必要があり、可読性が悪い。

SmartyよりTwigのほうが性能が上とか。

Volt: テンプレートエンジン — Phalcon 3.0.2 ドキュメント (Japanese / 日本語)」。高速フレームワークのPhalconではVoltを使っている。

Twig

Twig v3のほうが速いらしいが、Smarty v3のほうが速いというデータもある。

Smarty

Pure PHP/HTML views VS template engines views - Stack Overflow」が決定的だった。Smartyの開発者がSmartyのほうがTwigより速いと回答していた。2012年。Smartyでいいと思う。

ORM

Ref:

いろいろある。Doctrineが有名。

  • Doctrine: Symfonyで採用。有名。
  • Eloquent
  • Propel: Symfony v1.2で採用されていた。
  • PHP activerecord
  • PHPDAO
  • PDO: PHP標準。
  • Xyster

ただ、速度を優先する場合、PDOが最速になるらしい。

ORMは別になくてもいいか。いや、validateなどでほしいか。

ChatGPT: PHPのORMで最高速なのはどれ?


PHPのORMで「最高速」を目指す場合、目的やデータ量、柔軟性や機能性のニーズによって適切な選択肢が変わります。ただ、以下は一般的に高速性が評価されているORMやアプローチです。

      1. 1. **Cycle ORM**

   - **概要**: Cycle ORMは、PHP向けに軽量で高性能なORMとして設計されており、メモリ使用量やクエリ生成において優れた最適化がなされています。また、`DataMapper`パターンを採用しており、柔軟性が高く、複雑なデータ構造にも対応可能です。

   - **特徴**:

     - メモリ効率が良く、データベースへのアクセスが高速。

     - マッピング定義が柔軟で、データベーススキーマの同期などの管理機能も備えている。

   - **適合するケース**: 高速性が求められるが、複雑なデータ構造やリレーションも必要な場合。

   - **URL**: [Cycle ORM](https://cycle-orm.dev/)

      1. 2. **Doctrine ORM(高度に最適化した場合)**

   - **概要**: Doctrineは多機能で、エンタープライズレベルのアプリケーションにも使用されていますが、設定やキャッシュの適切な調整で高速化が可能です。Doctrineは大規模なデータベースや複雑なエンティティ構造を扱うことを得意としており、クエリキャッシュやメタデータキャッシュを活用すると、高速なクエリ処理が実現できます。

   - **特徴**:

     - キャッシュ(メタデータ、クエリ、結果)を駆使することでパフォーマンスが向上。

     - バッチ処理やフェッチモードの調整により、余分なSQLクエリを抑制できる。

   - **適合するケース**: 大規模なアプリケーションで、柔軟性や機能性と高速性のバランスが求められる場合。

   - **URL**: [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html)

      1. 3. **Eloquent ORM(軽量化する場合)**

   - **概要**: Eloquentは、Laravelの標準ORMで、柔軟で書きやすいAPIを提供しています。ただし、Eloquentは`Active Record`パターンを採用しているため、大量のデータや複雑なクエリには向きません。しかし、シンプルなアプリケーションであれば、十分な速度を発揮できます。

   - **特徴**:

     - データベースアクセスが少ないケースで非常に簡便で高パフォーマンス。

     - 適切なスコープやクエリビルダーを利用することで高速なデータアクセスが可能。

   - **適合するケース**: シンプルなアプリケーションで、多少の軽量化(不必要なEloquentモデルの削除など)を行い、APIの利便性と速度を両立したい場合。

   - **URL**: [Eloquent ORM](https://laravel.com/docs/eloquent)

      1. 4. **直接的なクエリビルダー使用(PDO, Laminas DBなど)**

   - **概要**: ORMを使わず、PDOやクエリビルダー(Laminas DBなど)を用いて、データベースと直接やり取りすることも非常に高速な手法です。ORMのオーバーヘッドがないため、特にトランザクション内での多くのデータ操作が求められる場合や、複雑なクエリが必要な場面では、これが最速のアプローチです。

   - **特徴**:

     - ORMを介さず直接データベースを操作するため、最小限のオーバーヘッドで済む。

     - 柔軟で高度なクエリやバッチ操作も容易。

   - **適合するケース**: 速度重視の大規模アプリケーションや、複雑なクエリが必要なケース。

これらを総合的に考えると、 **「Cycle ORM」** が最もバランスよく高性能を発揮しやすく、ORMの利便性を保持しながらも高速なデータ操作が可能です。しかし、最高速を求めるのであれば、ORMを使わずにクエリビルダーやPDOを直接活用することが理想的です。

Cycle ORMが安定して速いらしい。これでいくか。

Migrate

PHPで「Doctrine Migrations」を使ってみる

CakePHPに採用されているPhinxのほうが人気なのでPhinxを使ったほうがよいだろう。

ORMとあわせたほうがいいかも。

Test

  • PHPUnit

Search

検索キーワードをフォームから受信後、DBにSQLで検索をかけて取得結果を返すのが基本。

それとは別に、検索用のアプリにリクエストを受け渡しして検索するという方法がある。どちらでもいけるような、ドライバーのライブラリーがある。

検索サービスで有名なのは以下。

Laravel Scoutでtntsearchを使う方法がある (Laravel Scout + TNTSearchによる小規模プロジェクトへの全文検索機能の追加 #PHP - Qiita/Laravel ScoutとTNTSearchを使用してサイト全文検索を実装してみる – helog)。

Packagist」の検索結果をみても、tntsearchが特に人気の模様。

About

About

PHPはプログラミング言語だ。汎用的なプログラミング言語だが、主にウェブサーバー上のソフトウェアで使用される。

GNU socialはPHPで記述されている。他にもWordPress・NextCloudなどがPHPで記述されている。これらのPHP製ソフトウェアはVPSだけではなく安価なレンタルサーバーでも動作するため、低コストで運用することができる。

PHPの公式リファレンスは日本語版があり、わかりやすくまとまっている。

ウェブ上には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. エラーレベルの上昇。
  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ブラウザーで実行、動作確認のツールがいくつかある。

3v4l.orgがパーマリンクがあって、複数バージョンの動作確認できるので、これがいいと思う。

Guide

PHPのコーディングの推奨規約がある。PSR-12というのがメジャーな模様。GNU socialでも採用されている。

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パスが同じほうが分かりやすい。

Naming

変数名やシンボルの命名規則。

  • クラス名: CamelCase
  • メソッド名: mixedCase
  • 定数: UPPER_SNAKE_CASE
  • プロパティー: PSRでの規定はない。公式文書だとmixedCase
  • 変数名: PSRでの規定はない。公式文書だとlower_snake_case

上記で揃えるとよいだろう。

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でやったほうが速い。可読性などの問題はある。

in_array/array_search

PHP 高速化に関するメモ書き | Thought is free

in_array/array_searchは遅いらしい。書き方を変えたほうがいい。

PHPの言語構造の中で、isset/emptyが非常に重要。これらで値の有無判定ができる。

ただし、もともと単純配列になっているのを、連想配列に変換するくらいならば、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";

Language Reference

Types

Introduction

Ref: PHP: Introduction - Manual.

PHPの変数は以下の型のいずれかの値となる。

  • null
  • bool
  • int
  • float (floating-point number)
  • string
  • array
  • object
  • callable
  • resource

C言語のようなlong/doubleのような精度ごとの型はない。

System

PHP: 型システム - Manual

組込型の他に、ユーザー定義の型、aliasなどいくつかの型がある。

基本型

言語に統合されていて、ユーザー定義で再現不能。

  • 組込
    • null
    • スカラー型: bool/int/float/string
    • array
    • object
    • resource
    • never
    • void
    • クラス内の相対型: self/parent/static
  • Value型: false/true
  • ユーザー定義型/クラス型
    • インターフェイス
    • クラス
    • 列挙型
  • callable
複合型

複数の基本型を組み合わせた型。交差型とunion型で作れる。

  • 交差型: 宣言した複数のクラス型をすべて満たす型。&で表現。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

Ref: PHP: sprintf - Manual

今後何度も使う。

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の`{数:指定子}`に似ている。

後は埋める文字を指定する際は'を前置。

文字列の切り出し

いくつか方法がある。

preg_matchの自由度が高い。速度を気にしなくていいならこれでいいと思われる。ただ、引数の配列に入ってくるのがいまいち。関数の戻り値でほしい。

strposとsubstrを組み合わせると端の文字列を切り出せる。

文字列置換
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('\n', $csv) のようなことをしたくなるが、改行が\nとは限らない。

$array = preg_split('/\R/u', $string);

上記がいい。\Rが\r \n \n\rなどにマッチ。uで入力がUTF-8の場合を考慮。例えば、「腰」がuをつけないと分割されてしまう。

trim

PHP: trim - Manual

文字列の両端のホワイトスペースを除去する。

文字列反復

PHP: str_repeat - Manual

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。

文字数カウント

PHP: substr_count - Manual

行数カウントなどで文字列をカウントしたいことがそれなりにある。

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-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;
        }
こういう

Array

About

PHP: 配列 - Manual

PHPの配列は、順序マップ。

array(
    key  => value,
    key2 => value2,
    key3 => value3,
    ...
)

全て連想配列。キーを省略したら、登場したキーの数+1の添え字のキーに自動で採番される。ただし、先頭は0。

順序があるので、foreachした場合の順序も追加順で保証される。必要なら明示的にソートする。

Create
Basic

配列の作成方法がいくつかある。

$arr[キー] = 値;
$arr[] = 値;
// キーは文字列か整数。

$arr = [
  'key1' => 'value1',
];

$arrが存在しないか、null/falseの場合、新しい配列を作成する。ただし、この方法は万が一既存の変数があったら、追加になるのであまり推奨されない。明示的に初期化したほうがいい。

2行余分に増えるが、上記の形式が初期化もできるのでいいだろう。

explode(',', '物件コード,オーナーコード,棟数,M数,実戸数,a,b,c')
['物件コード','オーナーコード','棟数','M数','実戸数','a','b','c']
explode(',', '物件コード,オーナーコード,棟数,M数,実戸数)
['物件コード', 'オーナーコード', '棟数', 'M数', '実戸数']

explodeで配列を作ると短いのは、要素数8以上。詰めずに書いたら5以上。

ただ、余計な関数呼び出しが発生するから、あまりしないほうがいいかも。

Serial

同じ値の複数要素、連番データの作成方法がある。

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: 指定値の指定要素数配列。連想配列で複数キーに同じ値を設定したい場合に使う。
Read
末尾要素

【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: 非推奨。
指定要素の取得

PHPの連想配列から一部を切り出す話 - あしたにっき

  • []:
  • 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 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]。これがいい。

抽出

配列の分割などで、重複をなくすために、取得後削除したいことがある。

先頭と末尾なら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_column

PHP: array_column - Manual

テーブルの取得結果の整形に非常に便利。

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の単純配列。

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_merge。

配列ではなく、配列要素の結合は以下が使える。

implode($arr);
array_reduce($arr, function($c, $v){return $c.$v;});

単に文字列結合するならimplodeがシンプル。

指定した要素を全部の行に追加する場合。きれいな方法はない。

  1. foreach
  2. array_map
foreach ($array as &$row) {
    $row[] = $newElement; // 各行の末尾に要素を追加
}
$array = array_map(function($row) use ($newElement) {
    $row[] = $newElement; // 各行の末尾に要素を追加
    return $row;
}, $array);
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

連想配列のキーの置換、キーの更新、キー名の置換、キー名の更新をしたいことがある。

いくつか方法がある。

サブ配列の場合は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 - Qiita

<?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',
)
*/
単純配列→連想配列

PHP で通常の配列を連想配列に変換する

いくつか方法がある。

  1. array_combine
  2. array_fill_keys
  3. foreach
  4. 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→連想配列

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

PHP: array_flip - Manual

配列のキーと値を反転した配列を返す。元のarrayの値は有効なキーを必要とする。つまり、intかstring。型が違う場合、警告が出て無視される。

また、同じ値が複数ある場合、最後のみが有効になる。

分割

1個の大きな配列をそのまま反復させると大きいので、指定要素数ずつに分割して、処理したいことがある。

一括INSERTを分割する場合など。array_chunkで配列を分割できるのでこれを使う。

implodeで文字列にマージして、explodeで分割というのもある。

String

PHP: implode - Manual

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を回避しながら手短にかける。

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で指定したほうがいい。完全一致検索。

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で途中で終わるほうが速い模様。

配列同士の包含・交差判定

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_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で交差を取得。

列の一致判定

重複削除判定時などで、複数配列の同じ列・キーで処理したいことがある。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];});
});

外側に行ループ用の配列をわつぃて、その要素を内側で使うだけ。

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);

Ref: php - How can I use array_map with keys and values, but return an array with the same indexes (not int)? - Stack Overflow

array_mapは単純配列を返す。元々が連想配列の場合、キーが数値に置換される。元のキーを維持したければ、array_combineを併用する。

$arr =
   [
     "id" => 1,
     "name" => "Fred",
   ];
$result = array_combine(
     array_keys($arr), 
     array_map(function($v){ return $v; }, $arr)
);
array_filter

PHP: array_filter - Manual

名前通り配列要素をフィルターリングする。

array_filter(array $array, ?callable $callback = null, int $mode = 0): array

$modeを指定しなければcallbackにはvalueのみ渡される。

callbackがtrueを返したら、その要素を残す。callbackを指定しなかったら、!empty($v)相当。なので、array_filter($array) で、キーがある場合の配列要素の空判定にもなる。

他には応用として、操作対象ののキーの配列を渡して、そのキーを使って複数の配列の同じキーの一致・重複判定などできる。

        /** 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

PHP: array_reduce - Manual

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'

配列要素の列を結合したいことがある。そういうときにこれを使う。

Enum

PHP: 列挙型 / Enum - Manual

PHP 8.1.0から導入。長らくなかった。

複数の異なる値を1個の集合として取り扱うデータ型。

終了コードなど、意味がある数字を扱う。

enumがないと、値の下限、上限など、ただの数字だから保証できない。

Implementation

長らく言語機能になかったのでクラスやトレイトを使った独自実装が試されている。

昔はSplEnumという実験モジュールがあったが、Enumの登場でなくなった。

PHP 7.4以前との互換性のために、独自のクラスで実装して、その内部実装で上記ライブラリー類を使う感じだろう。

Iterable

array|Traversable型のエイリアス。PHP 7.1.0で導入。foreachで使用可能で、ジェネレーター内のyield fromでも使える。

Traversableインターフェイス、Iteratorクラスが特に重要。このメソッドはいろんなところで登場するから。

  • current: 現在の要素を返す。
  • key:
  • next
  • rewind
  • valid

特にcurrentが重要。例えば、ヘッダーをこれで取得などできる。

Type declarations/型宣言

PHP: 型宣言 - Manual

関数の引数、戻り値、クラスのプロパティー (PHP 7.4.0以上) に型を宣言できる。これにより、型を保証でき、その型でなければ、TypeErrorをスローする。

関数の戻り値だけ、型の指定箇所がやや特殊で、それ以外は原則変数の直前。関数の戻り値の場合、(): の後に指定する。

function name(): type {}
<?php
function sum($a, $b): float {
    return $a + $b;
}

// float が返される点に注意
var_dump(sum(1, 2));
?>

nullable な型とシンタックスシュガー

nullableの場合、型名の前に?を指定する (PHP 7.1.0以上)。?TとT|nullは同じ意味。

単一の基本型を宣言した場合、 型の名前の前にクエスチョンマーク (?) を付けることで、nullable であるという印を付けることができます。 よって、?T と T|null は同じ意味です。

注意: この文法は、PHP 7.1.0 以降でサポートされており、 PHP 8.0で一般化された union 型がサポートされる前から存在します。

PHP 7.4未満などの場合は、しかたないのでアノテーションで対応する。

Type juggling

PHP: 型の相互変換 - Manual

型の相互変換。非常に重要。いろいろ方法がある。

共通なのはキャスト (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を代入する。

言語構造なので、関数よりも高速。丸括弧内のスペースは無視される。

Other

型判定

PHP: gettype - Manual

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

PHP: スーパーグローバル - Manual

全てのスコープで使用可能な組込変数。関数、メソッド内でも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

Ref:

PHPとHTMLフォームの関係がある。重要。

配列渡しはPHP側の仕様。

HTML Forms (GET and POST)

PHPではフォーム変数のドットとスペースはアンダーバーに変換される。

たとえば <input name="a.b" />$_REQUEST["a_b"] となります。

外部変数名のドット

PHPの変数名でドットやスペースは無効。その都合で、それらの文字は_に置換される。フォーム変数も似たような考え方。

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

PHP: constant - Manual

定数名の文字列で、定数の値を取得したい場合に使える。

定数の他に、enumのcaseにも使える。

class

クラス内に定数を定義できる。デフォルトでpublic。staic変数的な扱い。インスタンスではなく、クラスが保有する。

predefined

言語で定義済みの定数がいろいろある。true/false/nullなど。

Magic/マジック定数

使用箇所で値が変化する定数 (マジック定数) が9個ある。C言語のマクロに近い。コンパイル時に解決される。大文字小文字を区別しない。

PHP の "マジック" 定数
名前 説明
__LINE__ ファイル上の現在の行番号。
__FILE__ ファイルのフルパスとファイル名 (シンボリックリンクを解決した後のもの)。 インクルードされるファイルの中で使用された場合、インクルードされるファイルの名前が返されます。
__DIR__ そのファイルの存在するディレクトリ。include の中で使用すると、 インクルードされるファイルの存在するディレクトリを返します。 つまり、これは dirname(__FILE__) と同じ意味です。 ルートディレクトリである場合を除き、ディレクトリ名の末尾にスラッシュはつきません。
__FUNCTION__ 関数名。無名関数の場合は、{closure}
__CLASS__ クラス名。 クラス名には、そのクラスが宣言されている名前空間も含みます (例 Foo\Bar)。 トレイトのメソッド内で __CLASS__ を使うと、 そのトレイトを use しているクラスの名前を返します。
__TRAIT__ トレイト名。 トレイト名には、宣言された名前空間も含みます (例 Foo\Bar)。
__METHOD__ クラスのメソッド名。
__NAMESPACE__ 現在の名前空間の名前。
ClassName::class 完全に修飾されたクラス名。

どれもよく使う。

Operators

PHP: Operators - Manual

precedence/優先順位

PHP: 演算子の優先順位 - Manual

丸括弧をつけるかつけないかが変わる。

特によく使うもの、注意が必要なものを整理する。

if (!$var = getVar())


!は=より優先順位が高いが 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合体演算子を組み合わせて、未定義のなどの場合のデフォルト値設定で役立つ。

PHP: 論理演算子 - Manual

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/論理演算子

PHP: 論理演算子 - Manual

論理積と論理和が、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演算子を使うことはできない。同じ論理型同士なら成立はするが。

デフォルト値扱いにしたければ、短縮条件演算子?:や、ヌル合体演算子??を使う。

配列演算子

PHP: 配列演算子 - Manual

配列に対する演算子は扱いがやや特殊。

Array Operators
名前 結果
$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

配列の等価演算子はキーと値の両方を比較する。これが重要。後は結合の+。

...演算子/スプレッド演算子

関数と配列の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

誰かに言葉で説明する際に、演算子の名前がほしい。意外と覚えていない。根拠とともに整理する。

演算子 名前 name URL 説明
$this->property オブジェクト演算子 object operator PHP: プロパティ - Manual インスタンスのプロパティーとメソッドにアクセスする。

Control Structures

Source: PHP: Control Structures - Manual.

制御構造に関する別の構文

PHP: 制御構造に関する別の構文 - Manual

if、 while、for、 foreach、switch に関する別の構文がある。開き波括弧部分を:に、閉じ波括弧部分をendif;,endwhile;, endfor;,endforeach;, endswitch;などにできる。else:とelseif:に注意。

この構文は存在だけ知っておくだけでいいと思われる。

elseif/else if

PHP: elseif/else if - Manual

1単語で書ける。結果は同じだが、文法的な意味が異なる。

foreach

About

PHP: foreach - Manual

foreachは配列の反復処理のための制御構造。

foreach (iterable_expression as $value)
foreach (iterable_expression as $key => $value)

$keyも使いたい場合、2番目の形式を使う。

ループ中に$valueの要素を直接変更したい場合、&をつけておく。

foreach (iterable_expression as &$value)
Name

foreachで使う変数の命名。

foreach (table as $row => $line)

DBからSELECT結果などがkeyに行番号、valueにレコードが入ってくる。こういう場合、rowがややこしい。行番号の意味でrowをキーにしておくといい。

value部分をどうするかだが、valueやitemだと少々わかりにくい。recordやline。SplFileObjectを扱うこともあるからlineがいいと思う。

Rewind

注意の必要な挙動として、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個だ。

  1. ticks
  2. encoding
  3. strict_types: =1の指定でPHPの暗黙の型変換を無効にする (ストリクトモード)。ただし、影響するのはスカラー型のみ。型が違う場合、TypeErrorの例外が発生する。

指令はファイルコンパイル時に処理されるので、リテラル値のみが使用可能で、変数や定数は使用不能。

declareブロックの <statement> は、<directive> の影響を受ける実行部だ。

declare文はグローバルスコープで使われる。登場以後のコードに影響する。ただし、他のファイルからincludeされても、親ファイルには影響しない。だから安心して使える。

型安全にするために、基本的にPHPファイルの冒頭にdeclare(strict_types=1);を書いておいたほうがよいだろう。

return

PHP: return - Manual

関数を終了させて、結果を呼び出し元に返すというのは他の言語同様の動きだが、いくつか注意すべき挙動・使用方法がある。

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';
}
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をどう用意するかは議論がある。

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;
    }

Function

User defined

PHP: ユーザー定義関数 - Manual

関数は以下のような構文で定義する。

<?php
function foo($arg_1, $arg_2, /* ..., */ $arg_n)
{
    echo "関数の例\n";
    return $retval;
}
?>

関数内では、他の関数やクラス定義を含む、PHPのあらゆるコードを使用可能。関数内で関数を定義できないC言語とは異なる。

PHPでは、変数と異なり、関数やクラスは全てグローバルスコープ。関数内で定義した関数も外部から呼び出し可能。スコープが欲しければ、無名関数を使う。

また、関数のオーバーロードもできない。関数をunsetしたり、再定義も不能。

可変引数と、デフォルト引数もある。

Argument

PHP: 関数の引数 - Manual

Comma

PHP: 新機能 - Manual

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);

...の前に型宣言も付与できるが、その場合配列要素が全部その型が必要になる。

名前付き引数

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を返す。

Variable Functions/可変関数/Callable/コールバック

PHPで関数を引数で指定したり、変数として扱う仕組みがある。evalを使う必要はない。

可変関数は、関数を文字列で実行する仕組み。これとは別で、Callableという型がある。

可変関数は、関数名の文字列の変数に丸括弧を追加したら実行できるというもの。インスタンス変数があれば、メソッドもできる。

PHP 7.0から、関数のみ"str"()も可能になった。「PHP: PHP 5.6.x から PHP 7.0.x への移行 - Manual」に記載はないが、パース方法が変わったことが由来の模様。

関数もメソッドも統一的に扱うものとして、Callable型がある。

CallableはPHPで関数を引数として渡したり、関数名の文字列を渡して、動的に関数を実行する仕組み。

callable型で表す。関数だけでなく、メソッドやstaticメソッドも対応できる。方法が2種類ある。

  1. 関数: 関数名の文字列。
  2. メソッド: 配列で指定。0番目の要素に、インスakeタンスやオブジェクト。1番目の要素にメソッド名の文字列で指定する。
  3. staticメソッド: 配列で指定。0番目の要素に、クラス名を指定する。'ClassName::methodName' 形式でも指定可能。

anonymous/無名関数

PHP: 無名関数 - Manual

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: アロー関数 - Manual

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";

長くなるなら、無名関数にしたほうがよさそう。

Classes and Objects

The Basics

PHP: クラスの基礎 - Manual

class

class内には変数 (プロパティー)、定数、関数 (メソッド) を含められる。

class内の関数などで、これらのプロパティー、メソッド類の参照時は、擬似変数$this->経由で参照できる。$thisは呼び出し元オブジェクトが入っている。

C系言語であれば、$this->相当は省略できたが、PHPでは指定が必要なので注意する。

::class

<className>::classでクラス名の完全修飾子の文字列を取得できる。

例外の試験など、クラス名の情報が必要な時によくみかける。

PHP 8.0.0からオブジェクトに対しても::classを使用でき、元のクラス名を取得できる。その場合、get_class()と同じ。同じならPHP 7で使えないのでget_class()でいいか。

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)。

宣言時に初期値を代入もできるが、初期値は定数のみ。関数類は使用不能。

以下のエラーが出る。

PHP Fatal error: Constant expression contains invalid operations in /ぼくのかんがえたさいきょうのクラス.php on line 5

関数類で動的に代入したい場合、__constructでやる。

クラスメソッドからstaticでないプロパティーにアクセスするには、-> (オブジェクト演算子/object operator) を使う。

Autoloading Classes

別のファイルのクラスを使う方法の話。

  1. require_once()/require()/include: シンプルなファイル読み込み。PHP 4から。
  2. __autoload(): 非推奨。PHP 5.0で登場。
  3. spl_autoload_register(): PHP標準。PHP 5.1.0で登場。
  4. 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のような標準的なオートローディング規約(ファイル構造と名前空間のマッピングルール)を自分で実装する必要があるため、コードが煩雑になる可能性があります。
    • 依存ライブラリの管理が難しい:外部ライブラリを含める場合は、requireincludeで個別に読み込む必要があり、依存管理が困難です。

composer

  • 利点
    • 自動設定PSR-4対応の名前空間とディレクトリのマッピングを自動的に管理するため、設定が簡単です。
    • 依存ライブラリの自動管理:Composerは依存する外部ライブラリもインストールしてオートロードするため、大規模なプロジェクトで便利です。
    • メンテナンスが容易:プロジェクトの規模が大きくなっても、composer.jsonファイルで管理できるため、ファイル構成や依存関係の変更に対応しやすいです。
  • 欠点
    • Composer依存:Composerがインストールされていないと使えません。また、プロジェクトにcomposer.jsonの設定が必要です。
    • 追加の学習が必要:Composerの使い方や設定ファイルの理解が必要ですが、習得すれば特に問題にはなりません。

Visibility

PHP: アクセス権 - Manual

プロパティー、メソッド、定数 (PHP 7.1.0以上) にはpublic/protected/privateのアクセス権 (visibility) を指定できる。

  • public: どこからでもアクセス可能。
  • protected: クラス自身、継承クラス、親クラス。
  • private: クラス自身。

アクセス権を省略した場合、public扱いになる。

なお、同じ型のオブジェクト間では、同一インスタンスでなくても、protected/privateにもアクセス可能。オブジェクト内ではオブジェクト実装が既知だから。

スコープ定義演算子 (::)

スコープ定義演算子 (::) はトークンの一つ。定数、staticプロパティー、staticメソッド、親クラスなどにアクセスできる。

[Paamayim Nekudotayim] とも呼ぶ。ダブルコロンを意味するヘブライ語らしい。

staticメソッド/プロパティーは、遅延静的束縛 (Late Static Bindings) でアクセス可能。

  • MyClass::CONST_VALUE/$classname::CONST_VALUE;
  • self::$my_static
  • parent::CONST_VALUE
  • static: 実行時に最初の呼び出しクラスを参照。

staticは少々ややこしい。基本はself::でよいと思う。

Traits

PHP: トレイト - Manual

コード再利用のための仕組み。単一継承言語で、コードを再利用するための仕組み。関数クラス (デリゲート) 的なもの。クラスに関数クラスのメソッドを取り込める。インスタンス生成などはできず、関数を水平方向で構成可能にする。継承しなくても、メンバーに追加できる。

<?php
trait ezcReflectionReturnInfo {
    function getReturnType() { /*1*/ }
    function getReturnDescription() { /*2*/ }
}

class ezcReflectionMethod extends ReflectionMethod {
    use ezcReflectionReturnInfo;
    /* ... */
}

class ezcReflectionFunction extends ReflectionFunction {
    use ezcReflectionReturnInfo;
    /* ... */
}
?>

Magic/マジックメソッド

PHP: マジックメソッド - Manual

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

クラス名の取得
get_class($object);
クラス名::class
$object::class // PHP 8.0以上 (get_class相当)
(new \ReflectionClass($obj))->getShortName(); // クラス名

基本は名前空間付きのフルパスでの取得。クラス名だけだとgetShortName()。クラスがなければnewで例外。

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)))のようにする。きれい。

Namespace

PHP: 名前空間 - Manual

PHPの名前空間は、以下の2の問題の解決用の仕組み。

  1. 自作の関数や変数類の名前がPHPの組込と衝突。
  2. 名前衝突回避のために長い名前が必要。

なお、requie_onceによる別ファイルの読込か、オートロードで他のファイルなどのシンボルにアクセスできることが、前提になっている。

definition

PHP: 名前空間 - Manual

名前空間の影響を受けるのは、以下。

  • クラス
  • インターフェイス
  • 関数
  • 定数

以下の構文でファイル先頭で宣言する。

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

ファイルへのアクセスに、相対パスと絶対パスがあるように、名前空間へのアクセス方法がいくつかある。

  1. $a = new foo(): 名前空間を指定しない場合。現在の名前空間currentnamespaceがあれば、currentnamespace\foo。なければグローバルのfoo。
  2. $a = new subnamespace\foo():
  3. $a = new \currentnamespace\foo(): 完全修飾名。グローバルプレフィクス演算子付きのクラス名。

現在の名前空間に該当シンボルが不在の場合、自動的にグローバル名前空間 (先頭\) も探す。

余計な検索が発生するので、わかっているならグローバルで最初から指定したほうがいいかも。

Importing

PHP: エイリアス/インポート - Manual

外部の完全修飾名をエイリアスで参照できる。use演算子を使う。namespaceで同じ名前空間に以内なら、useか完全修飾名を使う必要がある。

// これは 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};
Global

PHP: グローバル空間 - Manual

名前の先頭に\をつけるとグローバル空間の名前を指定できる。

$f = \fopen(...)

Reserved

keywords

PHP: キーワードのリスト - Manual

式や関数ではなく、定数、クラス名、関数名として使えず、PHPで予約されている特別なキーワードがいくつかある。

statement/文に近い扱い。言語構文の一部扱い。

PHP のキーワード
__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 print 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

PHP: stdClass - Manual

動的なプロパティーが使える、汎用的な空クラス。このクラス自体は、メソッドやプロパティーを持たない。

json_decodeなど一部の関数がこのインスタンスを返す。

// 型変換での作成。連想配列を(object)にキャストすると作れる。
(object) array('foo' => 'bar');

データベースの取得結果が、連想配列の他に、stdClassになっていることがある。

匿名オブジェクトや、動的プロパティーなどが主な利用方法。

データホルダーとして使う場合、連想配列のキーのほうが、自由度が高いので、そちらのほうが便利だと思われる。たくさんある配列関数も使えるし。

Constants

PHP: 定義済みの定数 - Manual

いくつか重要なものがある。

Wrappers

PHP: サポートするプロトコル/ラッパー - Manual

URL風のプロトコル (ラッパー/wrapper) で、ファイルシステム関数で使用できる。stream_wrapper_registerで自作もできる。

php://

PHP: php:// - Manual

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での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コミットで削除された。

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)。

  1. phpコマンドの引数にファイルを指定: php file.php/php -f file.php
  2. phpコマンドの引数にコードを指定: php -r 'print_r(get_defined_constants());'
  3. 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

PHP: エラー処理 - Manual

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
logrotate

出力したログファイルがストレージを圧迫しないように、一定サイズ・期間でリネームして、最大保持数を維持したりする。

いくつか方法がある。

  • GNU/Linuxのlogrotateコマンド
  • logrotateライブラリー
  • 自前実装

やることは決まっているのだから、自前で実装してもいいかもしれない。

  1. ログ出力時
  2. ログ出力ファイルのサイズを確認して、設定サイズより大きければ、循環。
  3. 最古のログファイルを削除して、順番にリネーム。
  4. 最後に出力。

それだけ。

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);
}

もう少しいい実装方法はありそう。

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: phpinfo - Manual

PHPに関する情報をHTMLで表示する。重要。

phpcgi -i
php -r "phpinfo();"
php -m

CLIモードだとHTMLではなくプレーンテキスト。

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

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を実行する。

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。

名前付きパラメーターに使える文字には制限がある。

BINDCHR     = [:][a-zA-Z0-9_]+;

英数字と_のみ。日本語のカラム名をそのまま使うことはできない。

function encode_all($str) {
    return preg_replace('~..~', '_$0', strtoupper(unpack('H*', $str)[1]));
}

上記のような関数で、一度英数字に変換して使う。

PDOStatement::execute

PHP: PDOStatement::execute - Manual

プリペアードステートメントを実行する。実行にあたって、プレースホルダーに値を埋め込む必要がある。

  1. bindValue/bindParamを使用。
  2. 引数で配列で指定。ただし、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からデータを取得するメソッドが複数ある。

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;
}

Calendar

PHP: 日付および時刻関連 - Manual

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の日時処理はdate関数DateTimeクラス、DateTimeImuutableクラス、Carbonなどいろいろある。

公式の説明などを見る限り、DateTimeImuutableを推奨しているように見える。

単純な日時文字列が欲しいだけなら、date関数系API。それ以外の本格的な日付計算が必要ならDateTimeImmutableを使うといい。プロジェクトでは念のため自前の日時クラスでラップしておく。

Carbon

PHPの日時処理にCarbonという拡張クラスが人気らしい。人気の理由はテストしやすいからとか。

たしかに、標準のdateなどの関数系APIは扱いにくいかもしれない。

DateTimeImuutableなら問題ない気がする。

datetime

PHP: 日付・時刻 関数 - Manual

関数系API群。

  • date
  • strtotime
  • time
format

日時の書式が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とどちらが速いかは不明。

directory
// ディレクトリー不在なら作成。
$dir = '/path/to/dir'
if (!file_exists($dir)) mkdir($dir, 0777, true);
check

入出力とセットで使うファイルの不在確認の関数群。

path

パス関係の操作。重要。

operation

PHPでファイルの移動や改名をする場合、rename。基本的にこれ1個だけ。

rename("/tmp/tmp_file.txt", "/home/user/login/docs/my_file.txt");
CSV

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

fgetcsvやSplFileObject::fgetcsvはOSのロケールを考慮する。

WindowsではUTF-8がなく、解釈できないらしい。かつ、PHP7の問題とか。ただ、LC_TYPE=Cにすると回避できる。

if (0 === strpos(PHP_OS, 'WIN')) {
  setlocale(LC_CTYPE, 'C');
}

PHPのバージョンの条件もつけていい気もするが。

fputcsv

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

一時ファイルの作成方法がいくつかある。

  • $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

PHP: 定義済み定数 - Manual

  • DIRECTORY_SEPARATOR: Windowsなら\、それ以外/らしい。

このDIRECTORY_SEPARATORは基本的には使うことはない。

理由として、Windowsがパスの文脈で/も受け入れるから。必要な場面は、OSからパスを受け取る場合に、\が返ってくることがあり、そこでexplodeしたりしたい場合のみ。

自分からパスを指定する文脈では、/で問題なく、DIRECTORY_SEPARATORを使う必要はない。

ディレクトリー以下のファイル一覧

方法がいくつかある。

  • 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

PHP: マルチバイト文字列 - Manual

mb_substr

Ref:

mb_substr(

    string $string,

    int $start,

    ?int $length = null,

    ?string $encoding = null

): string

substr同様、lengthにはマイナス値を指定可能。その場合、末尾からの文字数になる。

省略するかnullを指定すると、全文字。0は0文字。

mb_convert_kana

PHP: mb_convert_kana - Manual

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('カッパ'); //カツパ

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/printは関数ではなく、if/forなどと同じ言語構造、キーワード扱い。丸括弧はなくてもいい。紛らわしいのでないほうがいい。

  • 共通: 末尾に改行は付与されない。自分で"\n"を指定必要。関数ではないので丸括弧は不要。
  • echo: 戻り値void。式ではないので、if returnなどで使えない。戻り値がない分printよりわずかに速い。文字数が短い。コンマ区切りで複数列挙可能。文字列連結するよりコンマ区切りのほうが.演算子の優先順位など扱いが簡単。HTMLでの埋め込みに便利な <?= ?>の短縮表記 (<?php echo ; ?>相当)もあり。
  • print: 戻り値intで常に1を返す。ifや条件演算子の結果部分など、式の文脈で使用可能。

基本はechoでいい。if return/条件演算子など戻り値や式が必要な箇所でだけprintを使う。

printfは関数。書式指定が必要ならこれ。

setlocale

テキスト系の関数では、ロケールを考慮するものがいくつかある。その際の設定にこの関数を使う。

Basic/Vartype/変数・データ型関連

Variable

print_r/var_export/var_dump

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だと思われる。

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するほかに、テキストを変数にしたいことがある。

素直にevalする。

$dumpStr = var_export($var,true);
eval("$somevar = $dumpStr;");

くれぐれも入力に注意する。

Process

exec

システムプログラムの実行のための関数群がある。Windowsだとcmd.exe経由で実行される。

exec/shell_exec/system/passthru

外部プログラム実行のための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

PHP: exec - Manual

exec(string $command, array &$output = null, int &$result_code = null): string|false

戻り値は最終行。失敗したらfalseを返す。実行コマンドの終了コードは$result_codeに渡される。

escape

escapeshellarg/escapeshellcmdはexec/shell_exec/``と併用するエスケープ用関数。

  • escapeshellarg: 引数の文字列を一重引用符で囲み、既存の一重引用符を苦オートする。これで、引数全体を1個の引数にする。複数の引数の誤り実行を回避できる。
  • escapeshellcmd: シェルに特殊な意味のある&#;`|*?~<>^()[]{}$\、\x0A のシェルの特殊文字にバックスラッシュを追加し、'"は対がない場合のみエスケープ。

元々、コマンド全体をエスケープする [escapeshellcmd] だけがあった。が、これだとコマンドの引数を追加する攻撃が可能になるので、 [escapeshellarg] が追加されたらしい (PHPのescapeshellcmdを巡る冒険 | 徳丸浩の日記)。

ただ、escapeshellcmdは、パラメーターインジェクションの危険性があるので、使ってはいけないらしい。

基本は引数に [escapeshellarg] を使うだけ。

PHPにはエスケープ関数が何種類もあるけど、できればエスケープしない方法が良い理由 | 徳丸浩の日記」にあるように、その後PHP 7.4でproc_openが登場した。これはシェル経由じゃないOSコマンド呼び出しで、エスケープ不要なので安全。基本はこれを使うのがいいとのこと。

escapeshellarg

日本語に使うと日本語が消える。ロケールを考慮する模様。setlocaleで使用間際に変更するという手もあるが、そもそもOSの設定を直すほうが筋かもしれない。

単発コマンドで影響ないなら以下のようなコードを直前に記述して対応する。

if (false === setlocale(LC_CTYPE, "ja_JP.UTF-8")) {
  die("skip setlocale() failed\n");
}

PHP 7.4でproc_openを使えばこういう問題もない。

Other/その他の基本モジュール

PHP: その他の基本モジュール - Manual

JSON

PHP: JSON - Manual

About

json_encode/json_decodeをよく使う。非常に重要。

json_encodeはオブジェクトをJSON文字列表記にできるのでデバッグなどで便利。

PHPでUnicodeアンエスケープしたJSONを出力する関数 - オープンソースこねこね

JSON_UNESCAPED_UNICODE をオプションに指定しないと日本語はユニコードエスケープ表記になる。

連想配列
json_decode(
    string $json,
    ?bool $associative = null,
    int $depth = 512,
    int $flags = 0
): mixed

json_decodeは第二引数にtrueを指定しないと、object (stdClass) になる。trueにすると、連想配列になる。基本は連想配列でいいと思う。

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 - Manual

文字列をPHPコードとして評価する。危険なので、特にユーザーから入力を受け付ける場合は、注意する。できれば使わないほうがいい。

evalで評価する文字列内でreturnした結果が返却値となる。ないならnull。戻り値が必要なら、テキスト内で忘れずにreturnする。

SPL

PHP: SPL - Manual

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

PHP: URLs - Manual

全文字のパーセントエンコーディング

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]));
}

プレースホルダーの場合、%を_で置換すれば元データも復元可能。

Other/Service

HTTP

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でのリクエスト方法を覚えておくと、汎用性が高い模様。

PHPからのHTTPリクエスト (2016年版)

外部ライブラリーを使っていいなら、Guzzleが今は主流とのこと。Guzzleは内部でcurlを使っている。

cURL

About

基本的な使用方法。

  1. curl_initでurlを指定してセッション初期化。
  2. curl_setopt/curl_setopt_arrayでオプションを設定。
    1. CURLOPT_POST=true/CURLOPT_POSTFIELDSでPOST関係指定。
    2. CURLOPT_RETURNTRANSFER=trueでcurl_execの応答ボディーをテキストで取得。
    3. CURLOPT_FILEで保存先ファイル指定。
  3. curl_execで転送実行。CURLOPT_RETURNTRANSFER=trueを指定しない場合、true/falseのみ。
  4. 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);

PHP: curl_getinfo - Manual

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_RETURNTRANSFERとCURLOPT_FILEの競合

この2個のオプションは競合する。後から設定したものが優先される。レスポンスボディーが必要ならば、片方に統一して、片方だけからのアクセスにする。

バイナリーやCSVファイルのように、データが大きくて高速なパースが必要なら、SplFileObjectを使いたいのでファイル優先でいいと思う。

JSONのようにシンプルで短いなら全部テキストでやってもよいだろう。

JSONデータの送受信
  1. 連想配列でリクエストボディーのデータを作って、json_encodeでJSON文字列に変換。
  2. POST指定: curl_setopt($ch, CURLOPT_POST, true);
  3. ヘッダーContent-Type指定: curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
  4. リクエストボディー指定: curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json);
  5. 戻り値のデコード: $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'];