PHP
Library
Framework
- Symfony
- CakePHP
- FuelPHP: 2010年誕生。
- Codeigniter: シンプル、軽量。
- Zend
- Laravel: 2011年誕生。
- Phalcon
- Yii - Wikipedia
Template
いろいろある。
- Blade: Laravel標準。
- DIV: 1ファイルでシンプル。大規模には向かない。
- Smarty: 万能。
- Twig: 拡張はしにくい。
使うとしたら、歴史の長いSmarty。
- Smartyとは?基礎知識と具体的なメリットをわかりやすく解説 - システム開発のプロが発注成功を手助けする【発注ラウンジ】
- Smartyとは? - smarty @Wiki - atwiki(アットウィキ)
高速らしい。
そもそもテンプレートエンジンがいるのかどうかという議論がある。
- Smarty って、要らなくない? — INWORKS
- 素のPHPはもはやテンプレートエンジンとしては使えない - ぱせらんメモ
- 本当に倒すべきだったのは jQuery ではなくテンプレートエンジンだった - fsubal
- サーバサイドHTMLテンプレートからの脱却のススメ (1/4)|CodeZine(コードジン)
UI/UXを突き詰めると、JavaScriptを使わざるを得ず、サーバーテンプレートエンジンは初回だけなので、いっそのことJSで全部やろうというのが最近の流れの模様。
PHP自体が一種のテンプレートエンジンという主張がある。が、関数をあれこれ書く必要があり、可読性が悪い。
SmartyよりTwigのほうが性能が上とか。
「Volt: テンプレートエンジン — Phalcon 3.0.2 ドキュメント (Japanese / 日本語)」。高速フレームワークのPhalconではVoltを使っている。
Twig
- 素のPHPはもはやテンプレートエンジンとしては使えない - ぱせらんメモ
- Templating Engines in PHP |Articles - Fabien Potencier
- PHPテンプレートエンジンを使おう Twig編 | AkisiのWEB制作日記
- Decoded Node: Smarty vs. Twig (a benchmark done poorly)
- GitHub - AliShareei/smarty-vs-twig-benchmark: A benchmark of up-to-date versions of Smarty and Twig templating engines
- GitHub - dominics/smarty-twig-benchmark: A benchmark of up-to-date versions of Smarty and Twig templating engines
- The fastest template engine for PHP | by Serge Gotsuliak | Medium
- Pure PHP/HTML views VS template engines views - Stack Overflow
Twig v3のほうが速いらしいが、Smarty v3のほうが速いというデータもある。
Smarty
- GitHub - smarty-php/smarty: Smarty is a template engine for PHP, facilitating the separation of presentation (HTML/CSS) from application logic.
- Smarty Documentation
「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は別になくてもいいか。
Migrate
- Phinx
- Doctrine Migrations
「PHPで「Doctrine Migrations」を使ってみる」
CakePHPに採用されているPhinxのほうが人気なのでPhinxを使ったほうがよいだろう。
Test
- PHPUnit
Search
検索キーワードをフォームから受信後、DBにSQLで検索をかけて取得結果を返すのが基本。
それとは別に、検索用のアプリにリクエストを受け渡しして検索するという方法がある。どちらでもいけるような、ドライバーのライブラリーがある。
- Laravel Scout - Laravel 9.x - The PHP Framework For Web Artisans
- teamtnt/tntsearch: A fully featured full text search engine written in PHP
- ruflin/elastica - Packagist (40 Best PHP Libraries For Web Applications in 2022)
検索サービスで有名なのは以下。
- ElasticSearch/OpenSearch
- MeiliSearch
- Algolia
- Sphinx Search (Sphinx | Open Source Search Engine)
- Apache Solr (Welcome to Apache Solr - Apache Solr)/Apache Lucene (Apache Lucene - Welcome to Apache Lucene)
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: 言語リファレンス - Manual
- PHP: Release Archives (museum): PHP1からのソースコードの保管場所。
ウェブ上にはPHPに関するTipsが多く公開されており、大抵の疑問はウェブ検索で解決できる。
Version
PHPは言語の版数が上がる際、過去の版と互換性の無い破壊的変更がなされることがある。
開発者はこのリスクを軽減するために、非推奨の言語機能を避け、実行時の警告 (warning) を適切に処理するべきだ。
GNU socialは現在PHP 7系で動作する様に記述されており、PHP 8系への対応は作業途中だ。
PHP v8
PHP v8になっていろいろ更新が入った。特にPHP v7.4からv8に更新する際のポイントがあるので整理する (行事: 「12月にPHP8.3が出るので、PHP8で増えた文法をおさらいしましょうセミナー」参加報告 | PHP8対応の肝は型とエラーレベル | GNU social JP Web)。
大きく以下2点がある。
- エラーレベルの上昇。
- 型の厳格化。
エラーレベルが1段階上がったため、今までWarningで問題なかったものがFatal Errorになって動作しなくなる。他に、型が厳格になっている。
具体的には、php.ini/.user.iniで以下を指定して、PHP v7.4時点で警告にできるだけ対応しておく。
error_reporting=E_ALL ; -1
続いて、phpソースファイルに以下を記入して型を厳密にしておく。
declare(strict_types=1);
チェックツールがあるのでこれを使うと問題箇所などがわかる。
- PHP CodeSniffer
- PHPStan
- Rector
まず上記2個を試して、おまけでRectorも試すとよい。
Tool
PHPをWebブラウザーで実行、動作確認のツールがいくつかある。
- paiza.IO: Online PHP Editor | ブラウザでプログラミング・実行ができる「オンライン実行環境」| paiza.IO: パーマリンク。
- phpsansbox.io Write PHP online from your browser - PHPSandbox: フレームワークも確認可能。
- OnlinePHP.io: PHP Sandbox - Execute PHP code online through your browser: 複数バージョンの同時実行できる。
- 3v4l.org: Online PHP editor | Test code in 250+ PHP versions: パーマリンク。
3v4l.orgがパーマリンクがあって、複数バージョンの動作確認できるので、これがいいと思う。
Guide
PHPのコーディングの推奨規約がある。PSR-12というのがメジャーな模様。GNU socialでも採用されている。
「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
上記で揃えるとよいだろう。
Package manager
Composer
PHPのパッケージ管理システム。PHP v5.3.2以上で動作する。
Install
Ref: インストール: Composer | モダンなPHPのパッケージマネージャー – senooken JP.
[ -e installer ] || wget https://getcomposer.org/installer php installer --install-dir="$LOCAL/stow/$PKG-$VER/bin" --filename=$PKG
公式サイトからinstallerをダウンロードしてそれを使って任意の場所に設置する。
Usage
Composerを使う場合,composer.json
ファイルを用意する。このファイルはプロジェクトの依存関係を記載する。VCSで管理すべきファイルだ。
このファイルに使用するライブラリーを以下のように記入する。
<{ "require": { "monolog/monolog": "1.0.*" } }
composer.jsonに指定する最初の項目はrequireキーだ。このキーで依存パッケージをComposerに知らせる。パッケージ名とバージョンを指定する。
新規にパッケージを追加する場合は、以下のコマンドでインストールとcomposer.jsonへの追記を行えます。同時に、composer.lockファイルも作成されます。composer.lockも管理すべきファイル。
composer require "monolog/monolog:1.0.*"
パッケージ名はベンダー名とプロジェクト名から構成される。
1.0.*は1.0の任意のバージョンを示す。
composer.jsonを用意したら,以下のようにcomposerのinstallコマンドを実行する。
php composer.phar install
これにより,vendor
ディレクトリーにパッケージがインストールされる。
プロジェクトにgitを使っている場合,.gitignoreにvendorディレクトリーを追加したほうがいい。
Composerによるインストールが完了すると,composer.lockファイルにダウンロードしたパッケージとバージョンを出力する。composer.lockをプロジェクトリポジトリーに追加して,プロジェクトメンバー全員が同じバージョンのパッケージを使用する。
composer.lockが存在するプロジェクトで上記コマンドを実行する場合,composer.jsonの内容に加えて,composer.lockの内容も参照されて,composer.lockと同じバージョンがインストールされる。
パッケージを最新バージョンに更新したい場合,composer update
コマンドを使う。このコマンドを実行すると,最新バージョンをインストールして,composer.lockも更新する。動作としては,composer.lockを削除後にcomposer install
を実行することと等しい。composer updateは基本的には使わない。
updateやinstallの後にパッケージ名を指定すると,指定したパッケージだけ更新やインストールできる。
composer update monolog/monolog
Composerでインストール可能な主なパッケージは,Packagistリポジトリーに配置されている。
自動読み込み (Autoloading)
ライブラリーの自動読み込みのために,Composerはvendor/autoload.php
ファイルを生成する。以下のように,ライブラリーのクラスを使用するファイルの先頭でこのファイルを読み込めば使えるようになる。
<require __DIR__ . '/vendor/autoload.php'; $log = new Monolog\Logger('name'); $log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING)); $log->addWarning('Foo');
composer.jsonのautoload
欄に指定することで,ライブラリー以外の自分のコードも自動読み込みの対象にできる。
composer.jsonを編集した場合、composer dump-autoload
を実行してvendor/autolaod.php
を必ず更新します。
Libraries
自前のライブラリーをComposerでインストール可能な形式にする方法がある。
Every project is a package
ディレクトリーにcomposer.jsonがあると、そのディレクトリーはパッケージになる。プロジェクトとパッケージの違いは、名前の有無。プロジェクトは名前のないパッケージという扱いになる。
パッケージをインストール可能にするにあたって、composer.jsonに最低限名前が必要。
{ "name": "acme/hello-world", "require": { "monolog/monolog": "1.0.*" } }
acme/hello-worldというプロジェクトになる。acmeはベンダー名で、ベンダー名は必須。
ベンダー名に迷う場合、GitHubのユーザー名が適している。パッケージ名は小文字必須。単語区切りは-にするのが慣例。
Library Versioning
VCSでパッケージを管理している場合、composerはVCSからバージョンを自動で判別する。VCSを使っていない場合だけ、versionプロパティーを追加する。
{ "version": "1.0.0" }
Publishing to a VCS
composer.jsonを用意したらVCSのリモートリポジトリーに公開する。ベンダー名とユーザー名は不一致でも問題ない。
公開したパッケージを取り込む場合、requireで指定する。
{ "name": "acme/blog", "repositories": [ { "type": "vcs", "url": "https://github.com/username/hello-world" } ], "require": { "acme/hello-world": "dev-master" } }
パッケージ名hello-worldに必要なリポジトリーの情報をrepositoriesで指定している。たぶん、末尾のパッケージ名とリポジトリー名は一致が必要。
Publishing to packagist
VCSでの公開のケースは以上。ただ、repositoriesの情報は省略する方法がある。これは、Packagistに登録している場合。composerはpackagitstから同盟パッケージを探す。公開して問題ないなら、Packagistへの登録を検討する。
Light-weight distribution packages
.githubディレクトリーのように、パッケージに不要なファイルがある。
.gitattributesでパッケージやzipに含めないファイルを指定できる。
// .gitattributes /demo export-ignore phpunit.xml.dist export-ignore /.github/ export-ignore
以下のコマンドで確認できる。
git archive branchName --format zip -o file.zip
パッケージに含まれないだけで、Gitリポジトリーには入っている。
Articles
Versions and constraints
Versions and constraints - Composer
composer requireなどで指定するパッケージのバージョンにはいくつか記法がある。このバージョン部分は、composerではversion constraint (バージョン制約) と呼んでいる。このバージョン制約で、チェックアウト対象を判断する。
~/my-library$ git branch v1 v2 my-feature another-feature
~/my-library$ git tag v1.0 v1.0.1 v1.0.2 v1.1-BETA v1.1-RC1 v1.1-RC2 v1.1 v1.1.1 v2.0-BETA v2.0-RC1 v2.0 v2.0.1 v2.0.2
tag
基本的に、composerはタグを扱う。
上記のようなタグの場合、composerは先頭のvなどのプレフィクスを除外して考える。基本的にはこの中で、一番新しいものを優先的に探す。
branch
タグではなく、ブランチのチェックアウトが必要なら、特別なdev-*プレフィクス/サフィックスを指定してブランチを指定する。
上記の例で、my-featureブランチの指定が必要ならば、dev-my-featureを指定する。
ブランチ名がバージョン名と似ている場合、記法が変わる。v1.x-devのように指定する。v1タグではなく、v1ブランチを明示するために、.xは必須。あるいは、タグ名とブランチ名を完全に別の名前 (v1ブランチの代わりにv1.xブランチ) にしておけば、.xは不要。
バージョン名によく似たブランチ名を指定する場合だけ、dev-プレイフィクスではなく、-devサフィックスを指定する。
phpDocumentor
Ref: Home | phpDocumentor.
PHPのソースコードにコメントを残す際に、構文に従って記載すると、ツールで表示したり、文書に出力できたりする。ソースコードリーディングにも役立つので、積極的に記載したほうがよさそう。構文を整理しておく。
特に記法が大事。
ファイル冒頭の<?php の直後あたりに書くと、ファイルレベルのDocBlockになる。逆にclassの直前などに書くと、ファイル冒頭でもclassレベルになる。
以下の要素に前置できる。
- require(_once)
- include(_once)
- class
- interface
- trait
- function (including methods)
- property
- constant
- variables, both local and global scope.
Inheritance
DocBlockはSummary/Descriptionを上書きしたり、拡張できる。@inheritdocを使う。
要素ごとに以下のタグを継承する。
Elements | Inherited tags |
---|---|
Any | @author, @version, @copyright |
Classes and Interfaces | @category, @package, @subpackage |
Methods | @param, @return, @throws |
Properties | @var |
@subpackageタグは同じ@packageの親クラスのときだけ継承される。
DocBlock
DocComments
DocBlockはDocCommentと呼ばれるコメントで囲まれる。DocCommentは/**で始まり、*/で終わる。そして、DocComment内の行の先頭は* で始まるべき。
<?php /** * This is a DocBlock. */ function associatedFunction() { } /** This is a single line DocComment. */
複数行形式と1行形式がある。
変数などの説明には1行形式でいいと思う。
PHPDoc
DocBlockは3部構成。
- Summary=短い説明。改行直前の.か空行で終わり。
- Description=長い説明。アルゴリズムの機能や、使用方法、例など。最初のタグか、改行、DocBlockの終端で終わる。
- Tags/Anntations=要素のメタ情報。新しい行の@から始まる。
具体例。
<?php
/**
* A summary informing the user what the associated element does.
*
* A *description*, that can span multiple lines, to go _in-depth_ into
* the details of this element and to provide some background information
* or textual references.
*
* @param string $myArgument With a *description* of this argument,
* these may also span multiple lines.
*
* @return void
*/
function myFunction($myArgument)
{
}
Summary
/** * This is a summary * * This is a description */
/** * This is a summary. * This is a description */
Tag
よく使う@param/@returnの構文。
<type-expression = 1*(array-of-type-expression|array-of-type|type ["|"]) array-of-type-expression = "(" type-expression ")[]" array-of-type = type "[]" type = class-name|keyword class-name = 1*CHAR keyword = "string"|"integer"|"int"|"boolean"|"bool"|"float" |"double"|"object"|"mixed"|"array"|"resource"|"scalar" |"void"|"null"|"callable"|"false"|"true"|"self"
クラス名以外は全小文字。
基本は @<directive> <Type> <name> <description> の書式。スペース区切り。
- @property: クラスの注釈部で指定する。メンバー変数の説明。
- @var: 変数、プロパティー、定数で使用する。一番よく使う。
/** @var int $int This is a counter. */ $int = 0; // There should be no docblock here. $int++; class Foo { /** * Full docblock with a summary. * * @var int */ const INDENT = 4; /** @var string|null Short docblock, should contain a description. */ protected $description = null; public function setDescription($description) { // There should be no docblock here. $this->description = $description; } }
PHPUnit
PHPUnitを使用すれば自動単体テスト (Unit test) が可能だ。
Version
情報源: Supported Versions of PHPUnit – The PHP Testing Framework。
PHPUnitのバージョンごとに対応しているPHPのバージョンが決まっている。
PHP v7.4に対応してい最後のバージョンはPHPUnit 9なので、当分はこれを使うのが良い。
Basic
出典: 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual。
基本的な使用方法を整理する。
- 基本的にはクラス単位で試験コードを記載。<Class>クラスの試験コードは<Class>Testの命名にする。
- <Class>Test はPHPUnit\Framwork\TestCaseを継承させる。
- 試験はpublicのtest*メソッドの命名にする。あるいは、@testのアノテーションを付ければ、命名規則に従わなくてもいい。
- test*メソッド内で、assertSame() などで、期待値との比較で試験を行う。
例:
<?php declare(strict_types=1); use PHPUnit\Framework\TestCase; final class StackTest extends TestCase { private static $dbh; private $instance; public static function setUpBeforeClass(): void { // DB接続などクラス全体の初期化処理 self::$dbh = new PDO(''); } public static function tearDownAfterClass(): void { self::$dbh = null; } protected function setUp(): void { // 該当インスタンスの生成などメソッド単位の初期化処理。 $instance = new Stack(); } public function testPushAndPop(): void { $stack = []; $this->assertSame(0, count($stack)); array_push($stack, 'foo'); $this->assertSame('foo', $stack[count($stack)-1]); $this->assertSame(1, count($stack)); $this->assertSame('foo', array_pop($stack)); $this->assertSame(0, count($stack)); } }
Depends
前回の試験で準備した結果を利用したい場合、@dependsのアノテーションでテスト関数を指定しておくと、指定したテスト関数のreturnを引数に受け継いだテスト関数を記述できる。
@dependsは複数指定でき、指定順に事前に試験が実行されて引数に渡される。
Data Provider
ある関数に対して、テストケースを用意して、複数の引数の組み合わせを試験したいことがよくある。こういうときのために、テスト関数に渡す引数を生成する関数のデータプロバイダーを指定できる。@dataProviderで関数を指定する。データプロバイダーは引数のリストを配列で返すようにする。
データ数が多い場合、名前付き配列にしておくと、どういうデータ項目で失敗したかがわかりやすい。
Iteratorオブジェクトを返してもいい。
Fixtures
出典: 4. Fixtures — PHPUnit 9.6 Manual。
テストメソッドの実行前に、テスト対象のインスタンスの生成や、DB接続など準備がいろいろある。これをFixturesと呼んでいる。この準備がけっこう手間になる。これを省力できるのがテストフレームワークの利点。
テストメソッド実行前後に共通で行える処理がある。
- setUp/tearDown: テストメソッド単位の前後処理。テスト対象インスタンスの生成など。tearDownは何もしなくてもいいことが多い。
- setUpBeforeClass/tearDownAfterClass: クラス単位の前後処理。 DB接続など。
XML Configuration File
出典:
基本はコマンドでテスト対象クラス・ファイルを指定してテストを実行する。他に、XMLの設定ファイル (phpunit.xml) でもテスト対象を指定できる。
testsディレクトリーの全*Test.phpを対象にする最小限の例は以下。
<phpunit bootstrap="src/autoload.php"> <testsuites> <testsuite name="money"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
以下のように--testsuiteで試験対象を指定して実行する。
phpunit --bootstrap src/autoload.php --testsuite money
Assertions
Ref:
基本はassertSameでテストすればいいのだが、それ以外にも例外とかいろいろ試験したいケースがあるので、メソッドを整理する。
Exception
特に例外の試験がイレギュラー。
<?php declare(strict_types=1); use PHPUnit\Framework\TestCase; final class ExceptionTest extends TestCase { public function testException(): void { $this->expectException(InvalidArgumentException::class); // Run test target code following. } }
上記のようにexpectExceptionを使う。
- expectException:
- expectExceptionCode:
- expectExceptionMessage:
- expectExceptionMessageMatches:
例外が発生する処理の前に記述しておく。
Output
echoなど標準出力を試験する際も専用のメソッドがある。
void expectOutputRegex(string $regularExpression)
void expectOutputString(string $expectedString)
bool setOutputCallback(callable $callback)
string getActualOutput()
expectExceptionと同様に事前にセットしておく。
Command-Line
Ref: 3. The Command-Line Test Runner — PHPUnit 9.6 Manual.
phpunitコマンドでいろいろできる。いくつか重要なオプション、使用方法がある。
- phpunit file.php: 指定したファイルのテストを実行。
- --testsuite <name>: テストを指定。
Test Doubles
Ref: 8. Test Doubles — PHPUnit 9.6 Manual.
テスト時に、依存関係を模擬したもので置換したいことがある。PHPUnitにそういう仕組が用意されている。
stub=親、mock=子。
メソッド内で他のクラス・メソッドを使う場合はmockで対象クラスを模擬させる。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class SubjectTest extends TestCase
{
public function testObserversAreUpdated(): void
{
// Create a mock for the Observer class,
// only mock the update() method.
$observer = $this->createMock(Observer::class);
// Set up the expectation for the update() method
// to be called only once and with the string 'something'
// as its parameter.
$observer->expects($this->once())
->method('update')
->with($this->equalTo('something'));
// Create a Subject object and attach the mocked
// Observer object to it.
$subject = new Subject('My subject');
$subject->attach($observer);
// Call the doSomething() method on the $subject object
// which we expect to call the mocked Observer object's
// update() method with the string 'something'.
$subject->doSomething();
}
}
基本的な作り。
- createMock(<class>::class)で該当クラスのモックを作成。
- expectsに呼出回数条件のオブジェクトをセット。
- methodで対象メソッドを指定。
- withで、該当メソッドの引数処理を指定。
デフォルトで模擬実装はnullを返す。戻り値を変更したければ、will($this->returnValue())などを指定する。よく使うので短縮記法もある。
short hand | longer syntax |
---|---|
willReturn($value)
|
will($this->returnValue($value))
|
willReturnArgument($argumentIndex)
|
will($this->returnArgument($argumentIndex))
|
willReturnCallback($callback)
|
will($this->returnCallback($callback))
|
willReturnMap($valueMap)
|
will($this->returnValueMap($valueMap))
|
willReturnOnConsecutiveCalls($value1, $value2)
|
will($this->onConsecutiveCalls($value1, $value2))
|
willReturnSelf()
|
will($this->returnSelf())
|
willThrowException($exception)
|
will($this->throwException($exception))
|
willReturnCallbackで呼び出し関数をまるごと別のものに置換できる。これが非常に便利。
Topic
Test private/protected
Ref:
- php - Best practices to test protected methods with PHPUnit - Stack Overflow
- PHPUnitでprivateメソッドをテストする
- privateとprotectedメソッドをPHPUnitでテストする方法 #PHP - Qiita
クラスのprivate/protectedメソッドのテストには工夫が必要となる。
/**
* privateメソッドを実行する.
* @param string $methodName privateメソッドの名前
* @param array $param privateメソッドに渡す引数
* @return mixed 実行結果
* @throws \ReflectionException 引数のクラスがない場合に発生.
*/
private function doMethod(string $methodName, array $param)
{
// テスト対象のクラスをnewする.
$controller = $this->instance;
// ReflectionClassをテスト対象のクラスをもとに作る.
$reflection = new \ReflectionClass($controller);
// メソッドを取得する.
$method = $reflection->getMethod($methodName);
// アクセス許可をする.
$method->setAccessible(true);
// メソッドを実行して返却値をそのまま返す.
return $method->invokeArgs($controller, $param);
}
ReflectionClassを使って取得できる。上記の関数のFormController部分を試験対象のクラスに差し替えればOK。$this->instanceを指定しておけばそのまま流用できるか。getProperty/getValueでprivateプロパティーも取得可能。
Test header
Ref: unit testing - Test PHP headers with PHPUnit - Stack Overflow.
header関数を使用する場合、phpunitの標準出力と干渉して以下のエラーが出て試験できない。
Cannot modify header information - headers already sent by (output started at .../vendor/phpunit/phpunit/src/Util/Printer.php:138)
回避方法が2種類ある。
@runInSeparateProcess
- phpunit --stderr
1個目のアノテーションをテストメソッドに指定すると別プロセスでの実行になる。ただ、プロセス生成は時間がかかるため、試験が多いと効率が悪い。
2個目のphpunitの出力を標準エラーに出力させる方法がシンプルで効率もいい。phpunit.xmlに stderr="true"を指定するとキー入力を省略できる。こちらで対応しよう。
Test exit
Ref:
- PHP でテストコードを意識したコーディング #PHPUnit - Qiita
- echo + exit しているPHPコードをユニットテストで保護しながら改善する #PHP - Qiita
- header後にdieするテストのアンチパターン - uzullaがブログ
- php - Ignore exit() and die() with PHPUnit - Stack Overflow
- unit testing - How do you use PHPUnit to test a function if that function is supposed to kill PHP? - Stack Overflow
header()後のexit()など、exit/dieを使用するコードがある。phpunit内でこれらがあると、テストも強制終了になる。
上記の別プロセスで実行していた場合、以下のエラーになる。
Test was run in child process and ended unexpectedly
対処方法がいくつかある。
- exitを使わないコードに変更。
- isTestのようなフラグを元コードに入れてテスト可否で分岐してexitを回避。
- execで外部プロセスで実行してexitCodeを試験。
- exit/die部分だけ別関数に抽出してmockで置換?
<https://notabug.org/gnusocialjp/gnusocial/src/main/actions/apiaccountregister.php> のclientErrorが内部でexitする。
このclientErrorをwillなどで置換すればよさそう?
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
組込型の他に、ユーザー定義の型、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
今後何度も使う。
C言語のprintfといろいろ違うところがある。
<?php
$format = 'The %2$s contains %1$d monkeys.
That\'s a nice %2$s full of %1$d monkeys.';
echo sprintf($format, $num, $location);
echo sprintf("%'.9d\n", 123); // ......123
echo sprintf("%'.09d\n", 123); // 000000123
?>
特徴的なのが`%数$指定子`で引数の番号を選べるところ。Pythonの`{数:指定子}`に似ている。
後は埋める文字を指定する際は'を前置。
文字列の切り出し
いくつか方法がある。
- substr/mb_substr
- strpos/mb_strpos/strrpos/mb_strrpos (PHP: strpos - Manual)
- split
- preg_match (PHP: preg_match - Manual)
preg_matchの自由度が高い。速度を気にしなくていいならこれでいいと思われる。ただ、引数の配列に入ってくるのがいまいち。関数の戻り値でほしい。
strposとsubstrを組み合わせると端の文字列を切り出せる。
文字列置換
- PHP: str_replace - Manual: 日本語不能。記号の置換などで便利。
- PHP: substr_replace - Manual: 日本語不能。指定した文字数の位置で置換する。
- preg_replace
- explode/implode: 日本語OK。
str_replace( array|string $search, array|string $replace, string|array $subject, int &$count = null ): string|array
$search/$replaceが配列の場合、それぞれ前から順番に対応する。$searchの要素数が多い場合、$replaceは空文字が適用される。これでまとめて置換できる。
// <body text='black'> となります $bodytag = str_replace("%body%", "black", "<body text='%body%'>");
substr_replace($text, '', -1); // 末尾1文字の削除。 // substr_replace($text, '.', mb_strrpos('_'));
1文字などの置換ならmb_strrposとの組み合わせ。
$query = $request->query(); foreach ($query as $key => $value) { unset($query[$key]); $keys = explode('_', $key); $key = implode('_', array_slice($keys, 0, -1)) . '.' . $keys[count($keys)-1]; $query[$key] = $value; }
日本語はexplode/implodeが無難で確実。
startsWith/endsWith
PHP 8なら「PHP: str_starts_with - Manual/PHP: str_ends_with - Manual」がある。
PHP 8未満なら以下のようなコード。
function startsWith( $haystack, $needle ) { $length = strlen( $needle ); return substr( $haystack, 0, $length ) === $needle; } function endsWith( $haystack, $needle ) { $length = strlen( $needle ); if( !$length ) { return true; } return substr( $haystack, -$length ) === $needle; }
mb_strlen/mb_substrでマルチバイト対応。
改行分割
- Explode PHP string by new line - Stack Overflow
- php - Split string by new line characters - Stack Overflow
- How to replace different newline styles in PHP the smartest way? - Stack Overflow
explode('\n', $csv)
のようなことをしたくなるが、改行が\nとは限らない。
$array = preg_split('/\R/u', $string);
上記がいい。\Rが\r \n \n\rなどにマッチ。uで入力がUTF-8の場合を考慮。例えば、「腰」がuをつけないと分割されてしまう。
trim
文字列の両端のホワイトスペースを除去する。
文字列反復
str_repeat(string $string, int $times): string
文字列に対する乗算はstr_repeatで行う。他にarray_fillを使った方法もある。
プリペアードステートメントで(?,?)を作るときとかで使う。
function timeit(callable $callback) { $time = 'microtime'; $nanoFactor = 1000; if (function_exists('hrtime')) { $time = 'hrtime'; $nanoFactor = 1; } $start = $time(true); $callback(); $stop = $time(true); return ($stop - $start) * $nanoFactor; } echo timeit(function(){for ($i = 0; $i<10000; ++$i){rtrim(str_repeat('?,', 5),',');}}) . '=rtrim'. PHP_EOL; echo timeit(function(){for ($i = 0; $i<10000; ++$i){substr(str_repeat('?,', 5), 0, -1);}}) . '=substr' . PHP_EOL; echo timeit(function(){for ($i = 0; $i<10000; ++$i) {implode(',', array_fill(0, 5, '?'));}}) . '=array' . PHP_EOL; echo timeit(function(){for ($i = 0; $i<10000; ++$i){rtrim(str_repeat('?,', 10000),',');}}) . '=rtrim'. PHP_EOL; echo timeit(function(){for ($i = 0; $i<10000; ++$i){substr(str_repeat('?,', 10000), 0, -1);}}) . '=substr' . PHP_EOL; echo timeit(function(){for ($i = 0; $i<10000; ++$i) {implode(',', array_fill(0, 10000, '?'));}}) . '=array' . PHP_EOL; echo timeit(function(){for ($i = 0; $i<10000; ++$i){substr(str_repeat('?,', 10000), 0, -1);}}) . '=substr' . PHP_EOL; echo timeit(function(){for ($i = 0; $i<10000; ++$i){rtrim(str_repeat('?,', 10000),',');}}) . '=rtrim'. PHP_EOL; echo timeit(function(){for ($i = 0; $i<10000; ++$i) {implode(',', array_fill(0, 10000, '?'));}}) . '=array' . PHP_EOL; /* 552413=rtrim 565660=substr 997959=array 6853087=rtrim 6411850=substr 755294953=array 6507484=substr 6451837=rtrim 770350600=array */
常に速いのはrtim。
文字数カウント
行数カウントなどで文字列をカウントしたいことがそれなりにある。
substr_countでできる。
substr_count( string $haystack, string $needle, int $offset = 0, ?int $length = null ): int
$text = 'This is a test'; echo substr_count($text, 'is'); // 2 substr_count($str,"\n");
BOMの判定
- UTF-8BOM有無両対応のCSVファイル読み込み(PHP) | 株式会社フーリエ | Web戦略・システム開発[東京/浜松]
- php で csv を読み込む上での備忘録 #PHP - Qiita
- php - CSV upload - parsing with SplFileObject - Remove BOM - Stack Overflow
読み込んだファイルにUTF-8のBOMがあって、データ処理としてはBOMを除外したいことがある。
いくつか方法がある。
$header[0] = preg_replace('/^\xEF\xBB\xBF/', '', $header[0]);
if ($file->fread(3) !== pack('C*', 0xEF, 0xBB, 0xBF)) {
$bom = pack('CCC', 0xEF, 0xBB, 0xBF); $first = true; foreach ($file as $line) { if ($first && substr($line, 0, 3) === $bom) { $line = substr($line, 3); } $first = false; // your lines don't have a BOM, do your stuff }
最後の方法がよいと思う。
$line = (substr($line, 0, 3) === "\xEF\xBB\xBF") ? trim(substr($line, 3), '"') : $line;
SplFileObjectだと$csvObj->setFlags(SplFileObject::READ_CSV); でCSV扱いにしてしまうと、1列目はBOMつきでセルの解釈をしてしまうので、二重引用符もデータ扱いになる。BOM除去後にそれも除去しておく。
$current = $this->file->current(); if (count($current)) { $line = $current[0]; $current[0] = (substr($line, 0, 3) === "\xEF\xBB\xBF") ? trim(substr($line, 3), '"') : $line; } こういう
Array
About
PHPの配列は、順序マップ。
array( key => value, key2 => value2, key3 => value3, ... )
全て連想配列。キーを省略したら、登場したキーの数+1の添え字のキーに自動で採番される。ただし、先頭は0。
順序があるので、foreachした場合の順序も追加順で保証される。必要なら明示的にソートする。
Create
Basic
配列の作成方法がいくつかある。
- array()/[]
- explode (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
同じ値の複数要素、連番データの作成方法がある。
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: 非推奨。
指定要素の取得
- 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));
連想配列の先頭・末尾
- php - Get first key in a (possibly) associative array? - Stack Overflow
- PHP: array_key_first - Manual
- PHP – 配列の先頭・末尾・部分配列の取出し(非破壊的) – TauStation
- PHP で配列の先頭要素の値を取得するきれいな方法を考える | バシャログ。
PHP 7.3からarray_key_firstがある。これを使う。7.3以前はreset。
他に、元配列を破壊していいなら、array_shift/array_popもある。
array_sliceで部分配列を取得して変数に格納して、array_shiftもある。
$t = ['a' => 0, 'b' => 1]; $t2 = array_slice($t, 0, 1); var_export(array_shift($t2));
他にきれいなのはarray_keys/array_values[0]。これがいい。
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[]でいいだろう。
Remove
配列要素の削除方法がいくつかある。
- unset($arr[$key]);
- array_shift($arr): 先頭要素を削除。削除済み要素を返す。破壊的な処理。
- array_pop($arr): 末尾要素を削除。削除済み要素を返す。破壊的な処理。
- array_slice (PHP: array_slice - Manual): 先頭・末尾の要素を除去した要素を返す。
$ar = [0, 1, 2]; foreach($ar as $e) { echo $e; if ($e === 1) { array_shift($ar); } } print_r($ar);
012Array ( [0] => 1 [1] => 2 )
途中で削除しても、foreachは詰めたりしない。
Rename
- 3 Ways to Change Array Key without Changing the Order in PHP
- How to rename sub-array keys in PHP? - Stack Overflow
連想配列のキーの置換、キーの更新、キー名の置換、キー名の更新をしたいことがある。
いくつか方法がある。
サブ配列の場合はarray_mapでやればいい。
$tags = array_map(function($tag) { return array( 'name' => $tag['name'], 'value' => $tag['url'] ); }, $tags);
シンプルな方法は配列で設定してunset
foreach($tags as &$val){ $val['value'] = $val['url']; unset($val['url']); }
他にはjsonを経由したり。array_keys/array_combineを使ったり。
Copy
How to clone an array of objects in PHP? - Stack Overflow
配列変数を代入すると通常はそれでコピーになる。ただし、配列にオブジェクトがあると、そのオブジェクトはシャローコピーになる。
$new = array(); foreach ($old as $k => $v) { $new[$k] = clone $v; }
上記のように配列要素をcloneでコピーして作る必要がある模様 (PHP: オブジェクトのクローン作成 - Manual)。
Comma
PHPでは配列の終端カンマは許容される。
他にも、名前空間のグループ指定はPHP7.2以上、関数の引数はPHP7.3以上で可能になった。
連想配列判定
<?php if (array_values($arr) === $arr) { echo '$arrは配列'; } else { echo '$arrは連想配列'; }
これで添え字が、数字かどうかをみるのがいい模様。
Convert
2次元配列→1次元配列
$array = [ [1, 2, 3], [4, 5, 6], [7, 8] ]; array_reduce($array, 'array_merge', []); // Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 4 [4] => 5 [5] => 6 [6] => 7 [7] => 8 )
$array = [ [ 'staff' => [ 'name1', 'name2', 'name3', ], ], [ 'staff' => [ 'name4', 'name5', 'name6', ], ], [ 'staff' => [ 'name7', 'name8', 'name9', ], ], [ 'staff' => [ 'name10', 'name11', 'name12', ], ], ]; array_reduce(array_column($array, 'staff'), 'array_merge', []); // Array ( [0] => name1 [1] => name2 [2] => name3 [3] => name4 [4] => name5 [5] => name6 [6] => name7 [7] => name8 [8] => name9 [9] => name10 [10] => name11 [11] => name12 )
array_columnが非常に便利。
$rows = [ 0 => [ 'id' => 40, 'title' => 'dave', 'comment' => 'Hello, world!'], 1 => [ 'id' => 10, 'title' => 'alice', 'comment' => '你好,世界!'], ]; var_export(array_column($rows, 'title', 'id')); // => // array ( // 40 => 'dave', // 10 => 'alice', // )
$rows = [ 0 => [ 'id' => 40, 'title' => 'dave', 'comment' => 'Hello, world!'], 1 => [ 'id' => 10, 'title' => 'alice', 'comment' => '你好,世界!'], ]; var_export(array_column($rows, null, 'id')); // => // array ( // 40 => // array ( // 'id' => 40, // 'title' => 'dave', // 'comment' => 'Hello, world!', // ), // 10 => // array ( // 'id' => 10, // 'title' => 'alice', // 'comment' => '你好,世界!', // ), // )
$map = []; foreach ($table as $row) { $map[$row['括りオーナーコード']] = $row['オーナーコード']; }
DBテーブルからの取得結果が2次元の連想配列になっている。ここから、IDをキーにして、特定の値を取得するmapを作ったり、レコード行を取得できる。
自前でfor文で数行のコードでできるが、関数だと楽。
連想配列→単純配列
associative arrayをsimple arrayに変換する。
Convert an associative array to a simple array of its values in php - Stack Overflow
連想配列をキーと値のペアの配列にするちょっと気のきいた方法(かも) #PHP - Qiita
- $array = array_values($array);: 値だけを1次元にしたい場合。
- array_map(null, array_keys($a1), array_values($a1));: 連想配列の[[key,value], [key2, valu2]] 形式。
後者のパターンはそれなりに使う気がする。
DBテーブルから結果を取得後、必要なカラムの単純配列が欲しい場合もarray_mapを使う。
$ar = [ ['k1' => 'v11', 'k2' => 'v12'], ['k1' => 'v21', 'k2' => 'v22'], ]; var_export(array_map(function($e){return $e['k1'];}, $ar)); /* array ( 0 => 'v11', 1 => 'v21', ) */ // mapが欲しければarray_combineを併用する。 var_export(array_combine(array_map(function($e){return $e['k2'];}, $ar), array_map(function($e){return $e['k1'];}, $ar))); /* array ( 'v12' => 'v11', 'v22' => 'v21', ) */
単純配列→連想配列
いくつか方法がある。
- array_combine
- array_fill_keys
- foreach
- array_flip
$ar = ['a', 'b']; $ar2 = array_combine($ar, $ar); var_dump($ar2); /* array ( 'a' => 'a', 'b' => 'b', ) */
array_combineがシンプル。array_fill_keysは0初期化などしたい場合。
CSV→連想配列
- PHP convert CSV to associative arrays | by Catalin ZMOLE 👨💻 | Medium
- Shortest PHP code to convert CSV to associative array | Steindom
- PHP: str_getcsv - Manual
CSVを、よくDBの取得結果の形式の、行単位連想配列に変換する。方法がいくつかある。
<?php $csv = array_map('str_getcsv', file($file)); if (count($csv) && !count($csv[count($csv)-1])) unset($csv[count($csv)-1]); array_walk($csv, function(&$a) use ($csv) {$a = array_combine($csv[0], $a);}); array_shift($csv); # remove column header ?>
$rows = array_map('str_getcsv', file('myfile.csv')); $header = array_shift($rows); $csv = array(); foreach ($rows as $row) { $csv[] = array_combine($header, $row); }
1番目の方法がシンプル。これよりSplFileObjectのほうがいい。
【PHP】その CSV 変換、本当に「fgetcsv」でいいの? (フェンリル | デベロッパーズブログ)
反転|array_flip
配列のキーと値を反転した配列を返す。元のarrayの値は有効なキーを必要とする。つまり、intかstring。型が違う場合、警告が出て無視される。
また、同じ値が複数ある場合、最後のみが有効になる。
分割
1個の大きな配列をそのまま反復させると大きいので、指定要素数ずつに分割して、処理したいことがある。
一括INSERTを分割する場合など。array_chunkで配列を分割できるのでこれを使う。
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で途中で終わるほうが速い模様。
配列同士の包含・交差判定
- php - Check if an array contains all array values from another array - Stack Overflow
- php - Checking if ANY of an array's elements are in another array - Stack Overflow
1個でも入っているかを見たければ、array_intersect (PHP: array_intersect - Manual) がこの目的に合致する。
$peopleContainsCriminal = !empty(array_intersect($people, $criminals)); $peopleContainsCriminal = array_intersect($people, $criminals);
$criminalsの配列に、$peopleの要素のいずれかが入っているかを上記で判断できる。
array_intersectは1個目の配列要素の内、2個目の存在要素を返す (交差)。交差があれば、1個はあるという意味で、any/someになる。
全部の包含判定したい場合、array_diff (PHP: array_diff - Manual) でできる。
$containsAllValues = !array_diff($search_this, $all);
array_diffはarray_intersectと異なり、1個目の配列要素の内、2個目の不在要素を返す (差分)。なので、空なら全包含となる。非空なら非全包含=some。
完全一致なら、===でOK。
ポイントとしては、1個目の要素は要素数が少ない配列を指定したほうが速くなる。判定だけで、速度が重要なら、foreachで見つかったらすぐreturnしたほうが速い。
array_intersectが実行結果とboolが同じ向きなので、これを使うとわかりやすいだろう。
重複削除
いくつか方法がある。
- 連想配列
- array_diff
- array_unique
- array_keys(array_flip()): array_uniqueより少し早い (PHPの配列から重複を削除するにはarray_unique()よりarray_keys(array_flip())が速いのか)。
array_uniqueはデフォルトではvalueだけで判断する。
array_uniqueはデフォルトで文字列として比較する。配列などの場合はSORT_REGULARのフラグを指定する (Remove duplicated elements of associative array in PHP - Stack Overflow)。
ただし、型混在など複雑な場合は比較が失敗することがあるので、自前で行ったほうがいいらしい (PHP: array_uniqueについて #PHP - Qiita)。
array_unique重複は最初の要素を残す。最後の要素を残したければ、array_reverseを2併用する (php - Keep unique values of array, preserving order, retaining last occurrence of each - Stack Overflow)。
array_reverse(array_unique(array_reverse($array)));
但し、配列が大きいとarray_reverseの2回は遅い。
array_unique for multidimensional array – James' Desk
array_uniqueとarray_intersect_keyをうまく使う方法がある。
連想配列であるプロパティー (例: value) だけに固有条件を入れたい場合、
$tempArr = array_unique(array_column($array, 'value')); print_r(array_intersect_key($array, $tempArr));
一度valueだけarray_uniqueで取得して、その後array_intersect_keyで交差を取得。
列の一致判定
重複削除判定時などで、複数配列の同じ列・キーで処理したいことがある。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: 要素に適用するだけ。
array_map
array_map(?callable $callback, array $array, array ...$arrays): array
JavaScriptのmap相当。非常に重要でよく使う。
callbackにnullを指定すると、複数の配列のzip (unpack) を行う。
ただ、array_mapのコールバックの引数は通常配列の要素が想定されていて、連想配列のキーにはアクセスできない。
それをしたかったら、array_reduceを使う。らしい。
Howto use array_map on associative arrays to change values and keys - Daniel Auener
いや、そういうことをしなくても、array_keysを使えばOK。
$result = array_map(function($k, $v){return ;}, array_keys($arr), $arr);
array_mapは単純配列を返す。元々が連想配列の場合、キーが数値に置換される。元のキーを維持したければ、array_combineを併用する。
$arr = [ "id" => 1, "name" => "Fred", ]; $result = array_combine( array_keys($arr), array_map(function($v){ return $v; }, $arr) );
array_filter
名前通り配列要素をフィルターリングする。
array_filter(array $array, ?callable $callback = null, int $mode = 0): array
$modeを指定しなければcallbackにはvalueのみ渡される。
callbackがtrueを返したら、その要素を残す。callbackを指定しなかったら、!empty($v)相当。なので、array_filter($array) で、キーがある場合の配列要素の空判定にもなる。
他には応用として、操作対象ののキーの配列を渡して、そのキーを使って複数の配列の同じキーの一致・重複判定などできる。
/** 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; } }
Enum
PHP 8.1.0から導入。長らくなかった。
複数の異なる値を1個の集合として取り扱うデータ型。
終了コードなど、意味がある数字を扱う。
enumがないと、値の下限、上限など、ただの数字だから保証できない。
Implementation
- 【PHP 8.1】とうとうPHPにもEnumがやってきた - デザインワン・ジャパン Tech Blog
- php-enumのメモ
- PHP8.1のEnumと独自実装のEnumを比較して移行できるか検討しました - WHITEPLUS TechBlog
- How to implement Enum like functionality in PHP? - Stack Overflow
長らく言語機能になかったのでクラスやトレイトを使った独自実装が試されている。
- GitHub - BenSampo/laravel-enum: Simple, extensible and powerful enumeration implementation for Laravel.
- GitHub - myclabs/php-enum: The enum PHP is missing, inspired from SplEnum
昔はSplEnumという実験モジュールがあったが、Enumの登場でなくなった。
PHP 7.4以前との互換性のために、独自のクラスで実装して、その内部実装で上記ライブラリー類を使う感じだろう。
Iterable
array|Traversable型のエイリアス。PHP 7.1.0で導入。foreachで使用可能で、ジェネレーター内のyield fromでも使える。
Traversableインターフェイス、Iteratorクラスが特に重要。このメソッドはいろんなところで登場するから。
- current: 現在の要素を返す。
- key:
- next
- rewind
- valid
特にcurrentが重要。例えば、ヘッダーをこれで取得などできる。
Type declarations/型宣言
関数の引数、戻り値、クラスのプロパティー (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
型の相互変換。非常に重要。いろいろ方法がある。
共通なのはキャスト (cast)。
<?php
$foo = 10; // $foo は整数です
$bar = (bool) $foo; // $bar は boolean です
$fst = "$foo"; // to string.
+"+40"; // to int
?>
C言語と同じで (型) を前置する。ただし、少々長い。
文字列への変換は二重引用符囲、数値への変換は算術演算子 (+)。まあ、キャストだけ覚えておくのがシンプル。
Other
型判定
PHPでの型確認・判定方法がいくつかある。
- gettype: 変数の型を文字列で返す。boolean/integer/double/string/array/object/resouce/resource (closed) (PHP v7.2.0以上)/NULL/ unknown type
- get_class: オブジェクトのクラス名
- get_debug_type: 変数の型名をデバッグしやす形で取得。
- is_型名: is_array/is_bool/is_callable/is_float/is_int/is_null/is_numeric/is_object/is_resoure/is_scalar/is_string/function_exists/method_exists
基本はis_型名だろう。
Variables
Basics
Ref: PHP: Basics - Manual.
Undefined variable
未定義変数 (undefined variable) の値はNULL。
未定義変数 (配列の不在キー) にアクセスすると、E_WARNING (PHP 8未満はE_NOTICE) レベルのエラーが生じて、nullを返す。回避したければ、isset()で検知する。要素の追加時のアクセスは問題ない。
未定義変数の検知・制御方法がいくつかある。
- isset (PHP: isset - Manual)
- empty (PHP: empty - Manual)
- ??: Null 合体演算子/Null collapsing operator
- ??=: NULL合体代入演算子 PHP v7.4以上。
- @: エラー制御演算子
- array_key_exists
issetとempty、Null合体演算子あたりをメインで使う。特にempty。
emptyは以下相当を実施してくれる。値そのものの評価もするので、値が0で正常なときなど場合によっては困る場合もある。
!(isset($var) && $var) !isset($var) || $var == false
isset($var) && $varは頻繁に使うことになるだろうから、emptyで短縮できる。
empty($var) ? false : true; $var ?? false;
emptyとissetは関係が逆に似ているがissetは挙動が違う。
「Returns true if var exists and has any value other than null. false otherwise.」なので、変数の値を評価はしない。nullかどうかだけしかみない。emptyとは扱いが違うので注意する。
だから、頻繁に使うだろう。emptyとNull合体演算子の上記の記法はいろんなところで頻繁に使うと思われる。基本重要構文。
ただし、emptyは配列が空の場合もtrueになるので、そこは注意する。配列変数の有無を見たければ、issetを使うしかない。
Null合体演算子はNULLしかカバーしないから、emptyが必要な場面がけっこうある。
emptyの反対は、strlen/countあたり。ただし、未定義変数のチェックをしてくれないので、!emptyしたほうがいい。
Variable scope
About
出典: PHP: Variable scope - Manual。
関数の外で使用するとグローバルスコープになる。ただし、関数内では暗黙にはグローバル変数は使えない。未定義変数扱いになる。
関数内でグローバル変数を参照したければ、関数内でglobalで明示的に使用したいグローバル変数を宣言する必要がある。
<?php $a = 1; $b = 2; function Sum() { global $a, $b; $b = $a + $b; }
あるいは、$GLOBALS配列にグローバル変数が入っているのでこれを使う。
なお、波括弧のブロックスコープは存在しない。C系言語の感覚だと、波括弧でスコープが作られそうなイメージがあるが、PHPの波括弧はスコープを作らない。あくまで、関数の内部かどうか。
逆にいうと、関数内に定義される関数・クラスも基本グローバル。
子関数に変数を渡したい場合、引数かグローバル変数しかない。他に隠蔽したり、親関数からスコープを引き継ぎたい場合、無名関数を使うしか無い。
Super global
全てのスコープで使用可能な組込変数。関数、メソッド内でもglobal $variable;とする必要がない。
- $GLOBALS: グローバル変数の連想配列。
- $_SERVER
- $_GET
- $_POST
- $_FILES
- $_COOKIE
- $_SESSION
- $_REQUEST
- $_ENV
Variables From External Sources
Ref:
- PHP: Variables From External Sources - Manual
- PHP: PHP and HTML - Manual
- PHP: Predefined Variables - Manual
- PHP: Filter - Manual
PHPとHTMLフォームの関係がある。重要。
配列渡しは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
定数名の文字列で、定数の値を取得したい場合に使える。
定数の他に、enumのcaseにも使える。
class
クラス内に定数を定義できる。デフォルトでpublic。staic変数的な扱い。インスタンスではなく、クラスが保有する。
predefined
言語で定義済みの定数がいろいろある。true/false/nullなど。
Magic/マジック定数
使用箇所で値が変化する定数 (マジック定数) が9個ある。C言語のマクロに近い。コンパイル時に解決される。大文字小文字を区別しない。
名前 | 説明 |
---|---|
__LINE__
|
ファイル上の現在の行番号。 |
__FILE__
|
ファイルのフルパスとファイル名 (シンボリックリンクを解決した後のもの)。 インクルードされるファイルの中で使用された場合、インクルードされるファイルの名前が返されます。 |
__DIR__
|
そのファイルの存在するディレクトリ。include の中で使用すると、 インクルードされるファイルの存在するディレクトリを返します。 つまり、これは dirname(__FILE__) と同じ意味です。 ルートディレクトリである場合を除き、ディレクトリ名の末尾にスラッシュはつきません。
|
__FUNCTION__
|
関数名。無名関数の場合は、{closure}
|
__CLASS__
|
クラス名。 クラス名には、そのクラスが宣言されている名前空間も含みます (例 Foo\Bar )。 トレイトのメソッド内で __CLASS__ を使うと、 そのトレイトを use しているクラスの名前を返します。
|
__TRAIT__
|
トレイト名。 トレイト名には、宣言された名前空間も含みます (例 Foo\Bar )。
|
__METHOD__
|
クラスのメソッド名。 |
__NAMESPACE__
|
現在の名前空間の名前。 |
ClassName::class
|
完全に修飾されたクラス名。 |
どれもよく使う。
Operators
precedence/優先順位
丸括弧をつけるかつけないかが変わる。
特によく使うもの、注意が必要なものを整理する。
if (!$var = getVar())
- Why does negation happen last in an assignment expression in PHP? - Stack Overflow
- if文の中で変数定義 - PHP #PHP - Qiita
!は=より優先順位が高いが if (!$var = getVar()) のような式は成立して、変数代入結果の否定が評価される。
注意: = は他のほとんどの演算子よりも優先順位が低いはずなのにもかかわらず、 PHP は依然として if (!$a = foo()) のような式も許します。この場合は foo() の戻り値が $a に代入されます。
これが成立する理由。=の左辺は変数じゃないといけないから。(!$var) には代入がそもそもできない。そのため、PHPができるだけパース仕様として、以下のように代入部分を丸括弧で囲んだ扱いにしてくれる。
!$var = getVar() !($var = getVar())
だからこれが成立する。関数の処理結果を保存して、判定してその後の流用に短縮できて便利。
Assignment/代入演算子
??=
// NULL合体代入演算子 $id ??= getId(); // これと同じ $id = $id ?? getId(); $id = @$id ?: getId(); $id = isset($id) ? $id : getId();
NULL合体演算子の代入版。nullの場合の代入が簡単になった。PHP 7.4から使用可能。
Comparison/比較演算子
三項演算子 (条件演算子)
if/elseの短縮表記。デフォルト値の設定などでよく使う。
<?php // 三項演算子の使用例 $action = (empty($_POST['action'])) ? 'default' : $_POST['action']; // 上記は以下の if/else 式と同じです。 if (empty($_POST['action'])) { $action = 'default'; } else { $action = $_POST['action']; } ?>
PHP特有事項として、真ん中を省略できる。その場合、1個目がtrueならそれがそのまま戻る。JavaScriptとかC系言語でも真ん中は省略できない。
式 expr1 ?: expr3
の結果は、expr1 が true
と同等の場合は expr1、 それ以外の場合は expr3 となります。 この場合、expr1 は一度だけ評価されます。
条件演算子のネストはわかりにくいので推奨されない。が、条件演算子の省略形は安定している。false以外の最初の引数を評価する。
<?php echo 0 ?: 1 ?: 2 ?: 3, PHP_EOL; //1 echo 0 ?: 0 ?: 2 ?: 3, PHP_EOL; //2 echo 0 ?: 0 ?: 0 ?: 3, PHP_EOL; //3 echo $undefinedVariable ?? false ?: 'false default'; ?>
NULL合体演算子はnullの時のデフォルト値になるが、こちらはfalseの場合のデフォルト値設定。意味が違う。未定義変数アクセスをガードできないが、それ以外であれば条件演算子の短縮表記のほうをよく使う。非常に重要。
Null合体演算子を組み合わせて、未定義のなどの場合のデフォルト値設定で役立つ。
elvis演算子と呼ばれることもある模様。
It also combines nicely with the ?? operator, which is equivalent to an empty() check (both isset() and `!= false`):
$x->y ?? null ?: 'fallback';
instead of:
empty($x->y) ? $x->y : 'fallback'
Null 合体演算子/Null collapsing operator
<?php // $_GET['user'] を取得します。もし存在しない場合は // 'nobody' を用います。 $username = $_GET['user'] ?? 'nobody'; // 上のコードは、次のコードと同じ意味です。 $username = isset($_GET['user']) ? $_GET['user'] : 'nobody';
$var ?? 'value = isset($var) ? $var : 'value';
変数がnullの場合のガードの簡易記法。PHP v7.0.0で追加。非常に便利。
Error Control/エラー制御演算子@
式の直前に@を前置すると、その式のエラーメッセージを無視する。
基本的に使わないほうがいい。if文などでガードするより処理が遅い。ただし、Viewなどであまり影響ない場合などは記述がシンプルになるという利点もあるかも。
ただ、やっぱり基本は使わないほうがいい。バグの見落としになる。
Logical/論理演算子
論理積と論理和が、and/orと&&/||で2種類存在する。演算子の優先順位が違う。
// $g に代入されるのは、(true && false) の評価結果です // これは、次の式と同様です: ($g = (true && false)) $g = true && false; // $h に true を代入してから "and" 演算子を評価します // これは、次の式と同様です: (($h = true) and false) $h = true and false;
なお、PHPの論理演算子は、常に論理値 (true/false) を返すので注意する。
$a = $var || 'default';
上記のように、デフォルト値の代入扱いでor演算子を使うことはできない。同じ論理型同士なら成立はするが。
デフォルト値扱いにしたければ、短縮条件演算子?:や、ヌル合体演算子??を使う。
...演算子/スプレッド演算子
- 関数
- 可変長引数リストと関数呼出 PHP: 関数の引数 - Manual。PHP 5.6で導入 (PHP: 新機能 - Manual)。名前付き引数に対応したPHP 8.0から連想配列も対応。PHP 8.1から可変長引数と名前付き引数の同時使用 (PHP: 新機能 - Manual)。アンパックと通常名前付き引数がある場合、後のやつで上書き不能で実行時エラー。
- 第一級callableを生成する記法: PHP 8.1.0で導入 (PHP: 第一級callableを生成する記法 - Manual)。Closure::fromCallableの無名関数作成時の別の方法。
- 配列のアンパック: PHP: 配列 - Manual。array_mergeの代替記法。
- 数値キー・単純配列はPHP 7.4で導入 (PHP: 新機能 - Manual)。
- 文字キー・連想配列はPHP 8.1 (PHP: 新機能 - Manual、【PHP8.1】あなたはどっち? array_merge VS unpacking(スプレッド演算子) #PHP - Qiita)。
- アンパックと通常名前付き引数がある場合、後のやつで上書き。
- 配列のアンパックは、性能面で問題があり、連想配列がPHP 8.1以上というのがあり、array_mergeを使ったほうがよさそう。
- PHP の ... (3点ドット, Three Dots) の種類,全部言えるかな? #QiitaEngineerFesta2022 - Qiita
関数と配列の2か所で意味がある。配列の他、Traversableオブジェクトも可能。
配列や配列変数の直前に...を前置する (スペースは任意)。
関数の場合、関数定義時の仮引数と、関数呼出時に使用可能。
関数定義時の仮引数で指定すると、その変数が可変長引数を受け入れることを意味する。型宣言はその左に指定可能。これにより、func_get_args()を使わなくてもよくなった。
関数呼出時に使用すると、引数を展開してくれる。配列のアンパックに近い。
<?php function sum(...$numbers) { $acc = 0; foreach ($numbers as $n) { $acc += $n; } return $acc; } echo sum(1, 2, 3, 4); ?>
<?php function add($a, $b) { return $a + $b; } echo add(...[1, 2])."\n"; $a = [1, 2]; echo add(...$a); ?>
<?php function total_intervals($unit, DateInterval ...$intervals) { $time = 0; foreach ($intervals as $interval) { $time += $interval->$unit; } return $time; }
【PHP8.1】あなたはどっち? array_merge VS unpacking(スプレッド演算子) #PHP - Qiita
なお、配列のアンパックに関しては、array_mergeのほうが速くてメモリーも少ないとのこと。
in演算子
php equivalent of mysql "IN" operator? - Stack Overflow
PHPにin演算子はない。代わりに、in_arrayで包含判定できる。
ある値が、いずれかのどれかであるかの判定はそれなりにある。
in_array($target, [], true);
例えば、この比較対象が長い場合、(a===b||a===c|a===d) で何回も書かなくて済む。
Name
誰かに言葉で説明する際に、演算子の名前がほしい。意外と覚えていない。根拠とともに整理する。
演算子 | 名前 | name | URL | 説明 |
---|---|---|---|---|
$this->property | オブジェクト演算子 | object operator | PHP: プロパティ - Manual | インスタンスのプロパティーとメソッドにアクセスする。 |
Control Structures
Source: PHP: Control Structures - Manual.
制御構造に関する別の構文
if、 while、for、 foreach、switch に関する別の構文がある。開き波括弧部分を:に、閉じ波括弧部分をendif;,endwhile;, endfor;,endforeach;, endswitch;などにできる。else:とelseif:に注意。
この構文は存在だけ知っておくだけでいいと思われる。
elseif/else if
1単語で書ける。結果は同じだが、文法的な意味が異なる。
foreach
About
foreachは配列の反復処理のための制御構造。
foreach (iterable_expression as $value) foreach (iterable_expression as $key => $value)
$keyも使いたい場合、2番目の形式を使う。
ループ中に$valueの要素を直接変更したい場合、&をつけておく。
foreach (iterable_expression as &$value)
Name
foreachで使う変数の命名。
foreach (table as $row => $line)
DBからSELECT結果などがkeyに行番号、valueにレコードが入ってくる。こういう場合、rowがややこしい。行番号の意味でrowをキーにしておくといい。
value部分をどうするかだが、valueやitemだと少々わかりにくい。recordやline。SplFileObjectを扱うこともあるからlineがいいと思う。
Rewind
- SplFileObject: foreach doesn't start from the correct line after `SplFileObject::seek()` · Issue #13916 · php/php-src · GitHub
- PHP: Iterator::rewind - Manual
- PHP: NoRewindIterator - Manual
注意の必要な挙動として、foreachは最初にrewindでIteratorのポインターを先頭に毎回戻す。なので、ファイル系Iteratorで先頭を飛ばそうとすると工夫が必要。
// use SplFileObject; $path = stream_get_meta_data($fp = tmpfile())['uri']; file_put_contents($path, <<<'EOT' id,value 0,1 EOT ); $file = new SplFileObject($path); $file->setFlags(SplFileObject::READ_CSV|\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY); $file->seek(1); foreach(new NoRewindIterator($file) as $row) { var_dump($row); }
ほぼこのために存在する、SPLのNoRewindIteratorでラップする。すると、rewindをオーバーライドして巻き戻さないので維持できる。
なお、next()はREAD_AHEADありにしていないと機能しないようなので注意する (php - SPLFileObject next() behavior - Stack Overflow)。
ただ、READ_AHEADにしても、初回がnext()2回呼ばないと2行目にcurrent()でならないので動きがわかりにくい。seek(1)でよい。
first/last
PHP How to determine the first and last iteration in a foreach loop? - Stack Overflow
foreachの中で最初と最後を判定したいことがある。
foreach ($array as $key => $element) { if ($key === array_key_first($array)) { echo 'FIRST ELEMENT!'; } if ($key === array_key_last($array)) { echo 'LAST ELEMENT!'; } }
ただ、先頭なら$iterable->current()でいい。末尾ならforeachを抜けた後にcurrentでいい。
配列だったら、反復外部で簡単に判定できる。余計な処理を反復内に含めないほうがいい。
反復削除
キーが維持されるので、逆順反復などしなくても、影響ない。unsetすればいい。
reverse
逆順反復の方法がいくつかある。
php - Reverse order of foreach list items - Stack Overflow
$fruits = ['bananas', 'apples', 'pears']; for($i = count($fruits)-1; $i >= 0; $i--) { echo $fruits[$i] . '<br>'; }
foreach ( array_reverse($accounts) as $account ) { echo sprintf("<li>%s</li>", $account); }
なお、連想配列は無理。やるとしたら、array_reverse、逆順のキーを取得してそれを使う。
arrays - How to reverse foreach $key value PHP? - Stack Overflow
declare
Source: PHP: declare - Manual.
PHPUnitのサンプルコード (Getting Started with Version 9 of PHPUnit – The PHP Testing Framework) などで冒頭に以下の記述がある。
<?php declare(strict_types=1);
これの意味が分かっていなかったので整理する。
declare文 (construct) は、コードブロックの実行指令となる。以下の構文となる。
declare (<directive>) <statement>
<directive> はdeclareブロックの挙動を指示する。指定可能なものは以下3個だ。
- ticks
- encoding
- strict_types: =1の指定でPHPの暗黙の型変換を無効にする (ストリクトモード)。ただし、影響するのはスカラー型のみ。型が違う場合、TypeErrorの例外が発生する。
指令はファイルコンパイル時に処理されるので、リテラル値のみが使用可能で、変数や定数は使用不能。
declareブロックの <statement> は、<directive> の影響を受ける実行部だ。
declare文はグローバルスコープで使われる。登場以後のコードに影響する。ただし、他のファイルからincludeされても、親ファイルには影響しない。だから安心して使える。
型安全にするために、基本的にPHPファイルの冒頭にdeclare(strict_types=1);
を書いておいたほうがよいだろう。
return
関数を終了させて、結果を呼び出し元に返すというのは他の言語同様の動きだが、いくつか注意すべき挙動・使用方法がある。
returnで引数を省略すると、戻り値はnullになる。
呼び出し方法、場所で挙動が変わる。
- 関数/eval内: 即座に関数を終了し、引数を関数の値として返却。
- グローバルスコープ: スクリプト自体を終了。
- include/require内: 呼び出し元のファイルに制御を戻す。includeの場合、引数はincludeの戻り値になる。
return文は関数ではないので、引数の括弧は不要。紛らわしいのでないほうがいい。
include内で使えるというのがみそ。config.phpでreturnだけした設定一覧を記述しておいて、includeで変数に取り込むというのをよくやる。
require/include/require_once/include_once
Basic
includeは指定したファイルを読み込み評価する。絶対パスで指定しない場合、include_pathの設定を利用する。include_pathにもなければ現在ディレクトリーも探す。
絶対パス、相対パスの前置があると、include_pathは無視する。
ファイルが読み込まれると、ファイル内のコードは、includeが実行された行の変数スコープを継承する。つまり、呼び出し行で利用可能な全変数がファイル内でも使用可能。ファイル内で定義された関数やクラスはすべて、グローバルスコープになる。ただし、includeが関数定義内に配置されたら、コードは関数内で定義されているとみなす。
ファイルの読込時にはHTMLモードになる。そのため、ファイル内でPHPコードを実行するなら、<?php ?>で囲む必要がある。
includeに失敗したらFALSEを返し、E_WARNINGを発生させる。成功したら、戻り値は1。ただし、ファイル内でreturnを実行したら、その値を返す。
includeは特別な言語構造のため、引数に括弧は不要。結果を評価したいならば、全体を括弧で囲む。
// 動作します。 if ((include 'vars.php') == TRUE) { echo 'OK'; }
require/include
requireはincludeとほぼ同じ。違いは、失敗時にE_COMPILE_ERRORが発生して処理を中断する点。includeはE_WARNINGで処理は継続する。
使い分けとして、変数読込などで読み込めなくても処理を進めて問題ない場合に、include。
関数定義など、絶対必要なものはrequireなど。
_once
読込済みなら、再読込しない点がinclude/requireとの決定的な違い。関数の複数定義のエラーを回避できたりする。
読み込めたらtrueを返す。
config.php
includeとreturnの組み合わせのconfig.phpの設定ファイルをいろんなアプリで使われている。
<?php return [ 'name' => 'hoge', 'value' => 'fuga', ]; ?>
<?php // configファイルを変数に代入 $config = include __DIR__ . '/config.php'; // 呼び出し。 var_dump($config['name']); ?>
こういう形式。このreturnだけの文は、ほぼinclude前提。
編集対象のアプリの設定を、既存コードと分離する際に、いい方法。
config.phpをアプリ内で作りたい場合、「How to create Dynamically create config/custom.php config file」にあるように、var_exportを使うとよい。
// create the array as a php text string $text = "<?php\n\nreturn " . var_export($myarray, true) . ";";
config class
config.phpをどう用意するかは議論がある。
- Is it right to set a config PHP class to keep project settings? - Stack Overflow
- Configuration in PHP applications | PHP.earth
- PHPでプログラム全体の設定に使う変数の保持の仕方 #PHP - Qiita
- [PHPコンフィグファイルから設定情報を読み込むためのConfigクラス | PHP Archive]
- PHPでconfigファイルをオートロードで呼び出す方法 #Config - Qiita
- Using a config file from within a php class - Stack Overflow
- PHP OOP Config Class - Stack Overflow
include/returnではなくて、クラスのconst定数にするという。
- クラスのconst定数
- iniファイル/parse_ini_file
他に、configクラスを用意しておいて、シングルトンか、staticメソッドで参照する形。
どれくらいの頻度で参照するか次第。参照頻度が低いなら、getで毎回設定ファイルを読み込む。参照頻度が高いなら$configをstaticのクラス変数にもたせる。
/** * config.phpに記載の設定項目を取得する。 * @param string $key configのキー。 * @return mixed configの値かnull。 */ public static function get(string $key) { return (include __DIR__ . '/config.php')[$key] ?? null; }
Function
User defined
関数は以下のような構文で定義する。
<?php function foo($arg_1, $arg_2, /* ..., */ $arg_n) { echo "関数の例\n"; return $retval; } ?>
関数内では、他の関数やクラス定義を含む、PHPのあらゆるコードを使用可能。関数内で関数を定義できないC言語とは異なる。
PHPでは、変数と異なり、関数やクラスは全てグローバルスコープ。関数内で定義した関数も外部から呼び出し可能。スコープが欲しければ、無名関数を使う。
また、関数のオーバーロードもできない。関数をunsetしたり、再定義も不能。
可変引数と、デフォルト引数もある。
Argument
Comma
PHP 7.3から、関数呼び出し時の終端カンマを許容。
my1(1,); my2(2,); // OK
PHP 8.0.0から、関数定義時の引数リストの最後のカンマが許容される。
<?php function takes_many_args( $first_arg, $second_arg, $a_very_long_argument_name, $arg_with_default = 5, $again = 'a default string', // この最後のカンマは、8.0.0 より前では許されません。 ) { // ... } ?>
Reference
引数はデフォルトで値渡しになる。値がコピーされて渡される。関数内部で引数自体を修正したい場合、リファレンス渡しにする。
関数定義で変数の前に&をつけると、リファレンス参照になる。
<?php function add_some_extra(&$string) { $string .= 'and something extra.'; } $str = 'This is a string, '; add_some_extra($str); echo $str; // 出力は 'This is a string, and something extra.' となります ?>
Default
関数定義時に、引数部分で変数に値を代入するようにして、デフォルト値を定義できる。引数が指定されなかった場合に使われる。なお、nullが渡された場合も、デフォルト値の代入はされないので注意する。
function makecoffee($type = "cappuccino") { return "Making a cup of $type.\n"; }
デフォルト値には、定数を指定できる。具体的には、スカラー値、配列、null。PHP 8.1.0から、new ClassName記法でインスタンスも指定できる。
デフォルト引数は、デフォルト値のない引数の右側の必要がある。そうでない場合、省略できず、指定する意味がなくなく。
「php 7 - Default callable in function definition in php 7 - Stack Overflow」にあるように、$callableのデフォルト引数に匿名関数を指定したりはできない。デフォルトnullを指定しておいて、以下のような匿名関数で設定するとよいだろう。
$callback = $callback ?: function($e) {return $e};
関数内で、値の有無を確認する必要がある。
可変長引数
引数リストに...を含めることで、可変長の引数を受け取ることを示す。...を前置した変数に配列として入る。
<?php function sum(...$numbers) { $acc = 0; foreach ($numbers as $n) { $acc += $n; } return $acc; } echo sum(1, 2, 3, 4);
...の前に型宣言も付与できるが、その場合配列要素が全部その型が必要になる。
名前付き引数
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: 可変関数 - Manual
- PHP: コールバック / Callable - Manual
- 【PHP】コールバック関数サンプル3つをまとめる - ウェブ集客で企業を成功に導くホームページ制作会社|(株)ワイコム・パブリッシングシステムズ(福岡)
- PHPで関数やクラスを文字列から呼び出しする方法まとめ | PisukeCode - Web開発まとめ
- 【小ネタ】phpで変数でメソッドを実行する | BeginnerEngineerBlog
- call_user_func() と $function() の動きが違った
PHPで関数を引数で指定したり、変数として扱う仕組みがある。evalを使う必要はない。
可変関数は、関数を文字列で実行する仕組み。これとは別で、Callableという型がある。
可変関数は、関数名の文字列の変数に丸括弧を追加したら実行できるというもの。インスタンス変数があれば、メソッドもできる。
PHP 7.0から、関数のみ"str"()も可能になった。「PHP: PHP 5.6.x から PHP 7.0.x への移行 - Manual」に記載はないが、パース方法が変わったことが由来の模様。
関数もメソッドも統一的に扱うものとして、Callable型がある。
CallableはPHPで関数を引数として渡したり、関数名の文字列を渡して、動的に関数を実行する仕組み。
callable型で表す。関数だけでなく、メソッドやstaticメソッドも対応できる。方法が2種類ある。
- 関数: 関数名の文字列。
- メソッド: 配列で指定。0番目の要素に、インスakeタンスやオブジェクト。1番目の要素にメソッド名の文字列で指定する。
- staticメソッド: 配列で指定。0番目の要素に、クラス名を指定する。'ClassName::methodName' 形式でも指定可能。
anonymous/無名関数
2009年頃にPHP 5.3で登場したらしい (PHP 5.3の無名関数を試してみた - hnwの日記)。
callableの型。非常に重要。
$message = "message"; // "use" がない場合 $example = function () { // 未定義変数参照扱い var_dump($message); }; $example(); // $message を引き継ぎます $example = function () use ($message) { var_dump($message); };
useを指定した場合だけ、親のスコープから変数を引き継げる。変数は関数定義時の値。
即時関数として使うなら、引数で全部渡せる。が、useを使うと引数に指定しなくていいので短くできる。即時関数なら、useで問題ない。
arrow/アロー関数
PHP 7.4で追加。無名関数の簡易構文。かなり短く記述できる。
fn (argument_list) => expr
親の変数を暗黙でキャプチャー (コピー)。参照でキャプチャーしたい場合は無名関数を使うしかない。
$y = 1; $fn1 = fn($x) => $x + $y; // $y を値渡しするのと同じ $fn2 = function ($x) use ($y) { return $x + $y; }; var_export($fn1(3));
Classes and Objects
The Basics
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 - Constant expression contains invalid operations - Stack Overflow
- 【PHP】クラスプロパティの値には、動的な値を代入することができないようです。 #error - Qiita
以下のエラーが出る。
PHP Fatal error: Constant expression contains invalid operations in /ぼくのかんがえたさいきょうのクラス.php on line 5
関数類で動的に代入したい場合、__constructでやる。
クラスメソッドからstaticでないプロパティーにアクセスするには、-> (オブジェクト演算子/object operator) を使う。
Autoloading Classes
Ref:
別のファイルのクラスを使う方法の話。
- require_once()/require()/include: シンプルなファイル読み込み。PHP 4から。
- __autoload(): 非推奨。PHP 5.0で登場。
- spl_autoload_register(): PHP標準。PHP 5.1.0で登場。
- composer autoload: composer。
C系言語であれば、includeなどで外部ファイルをそのまま自分のファイルに読み込む。PHPでもrequire_onceなどで似たようなこともできる。が、PHPではこれをクラスごとに記述するのが煩雑だとして、自動で読み込む仕組みがいくつかある。
GNU socialでも <https://notabug.org/gnusocialjp/gnusocial/src/main/lib/util/framework.php> でspl_autoload_registerを使っている。
基本的にはcomposerのautoloadかPHP標準のspl_autoload_registerの2択になっている。
基本的な使用方法。
<?php
spl_autoload_register(function ($class_name) {
include $class_name . '.php';
});
$obj = new MyClass1();
$obj2 = new MyClass2();
?>
MyClass1.php MyClass2.phpから該当クラスを自動読み込みする。
該当クラスを使おうとしたときに、spl_autoload_registerに登録した関数が呼ばれる模様。
spl_autoload_registerは、指定した関数を__autoload()の実装として登録する。順番に登録する。
spl_autoload_register(?callable $callback = null, bool $throw = true, bool $prepend = false): bool
callback: callback(string $class): void
。重要。nullを指定するとデフォルトのspl_autload()が登録される。$classにはクラスの完全修飾子が入る。
このcallback内で独自のrequire_once相当をいろいろ指定する形になる。
スコープ定義演算子 (::)
- PHP: スコープ定義演算子 (::) - Manual
- PHP: 遅延静的束縛 (Late Static Bindings) - Manual
- [PHPstaticメソッドとstatic::に関するメモ #初心者 - Qiita]
スコープ定義演算子 (::) はトークンの一つ。定数、staticプロパティー、staticメソッド、親クラスなどにアクセスできる。
[Paamayim Nekudotayim] とも呼ぶ。ダブルコロンを意味するヘブライ語らしい。
staticメソッド/プロパティーは、遅延静的束縛 (Late Static Bindings) でアクセス可能。
- MyClass::CONST_VALUE/$classname::CONST_VALUE;
- self::$my_static
- parent::CONST_VALUE
- static: 実行時に最初の呼び出しクラスを参照。
staticは少々ややこしい。基本はself::でよいと思う。
Traits
コード再利用のための仕組み。単一継承言語で、コードを再利用するための仕組み。関数クラス (デリゲート) 的なもの。クラスに関数クラスのメソッドを取り込める。インスタンス生成などはできず、関数を水平方向で構成可能にする。継承しなくても、メンバーに追加できる。
<?php trait ezcReflectionReturnInfo { function getReturnType() { /*1*/ } function getReturnDescription() { /*2*/ } } class ezcReflectionMethod extends ReflectionMethod { use ezcReflectionReturnInfo; /* ... */ } class ezcReflectionFunction extends ReflectionFunction { use ezcReflectionReturnInfo; /* ... */ } ?>
Magic/マジックメソッド
PHPのデフォルトの動作を上書きする特別なメソッドをマジックメソッドと呼んでいる。
どらも__ (アンダーバー2個) から始まる。__始まりの全メソッドはPHPで予約されているのでユーザー定義メソッドとしては非推奨。
以下がある。
- __construct
- __destruct
- __call
- __callStatic
- __get
- __set
- __isset
- __unset
- __sleep
- __wakeup
- __serialize
- __unserialize
- __toString
- __invoke
- __set_state
- __clone
- __debugInfo
__construct/__destruct/__clone以外の全マジックメソッドはpublic必須。E_WARNINGの警告が発生する。
__construct/__desctructは戻り値型を宣言してはいけない。
Other
クラス名の取得
- php - How do I get an object's unqualified (short) class name? - Stack Overflow
- PHP: get_class - Manual
- PHP クラス名::classはどういう処理?? #初心者 - Qiita
- PHP: クラスの基礎 - Manual
get_class($object); クラス名::class $object::class // PHP 8.0以上 (get_class相当) (new \ReflectionClass($obj))->getShortName();
基本は名前空間付きのフルパスでの取得。クラス名だけだとgetShortName()
Namespace
PHPの名前空間は、以下の2の問題の解決用の仕組み。
- 自作の関数や変数類の名前がPHPの組込と衝突。
- 名前衝突回避のために長い名前が必要。
definition
名前空間の影響を受けるのは、以下。
- クラス
- インターフェイス
- 関数
- 定数
以下の構文でファイル先頭で宣言する。
namespace [Name]; namespace [Name]\[Sub];
ただし、declareは例外でnamespaceの前にも書ける。ただ、それ以外だとPHPコード以外も含めて記述不能。
同じ名前空間を複数のファイルで定義することも可能。これにより、ファイルをまたいで名前空間を共有できる。
また、名前空間は階層を持つことができる。バックスラッシュで区切る。
Multiple
PHP: 同一ファイル内での複数の名前空間の定義 - Manual
1ファイルで複数の名前空間の定義が可能。
namespace MyProject { const CONNECT_OK = 1; class Connection { /* ... */ } function connect() { /* ... */ } } namespace AnotherProject { const CONNECT_OK = 1; class Connection { /* ... */ } function connect() { /* ... */ } } namespace { // global code session_start(); $a = MyProject\connect(); echo MyProject\Connection::start(); }
名前空間とグローバルを分ける場合、グローバルを名前を指定しないnamespaceで囲む。
Basic
ファイルへのアクセスに、相対パスと絶対パスがあるように、名前空間へのアクセス方法がいくつかある。
- $a = new foo(): 現在の名前空間currentnamespaceがあれば、currentnamespace\foo。なければグローバルのfoo。
- $a = new \currentnamespace\foo(): 完全修飾名。グローバルプレフィクス演算子付きのクラス名。
Importing
外部の完全修飾名をエイリアスで参照できる。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
名前の先頭に\をつけるとグローバル空間の名前を指定できる。
$f = \fopen(...)
Reserved
keywords
式や関数ではなく、定数、クラス名、関数名として使えず、PHPで予約されている特別なキーワードがいくつかある。
statement/文に近い扱い。言語構文の一部扱い。
__halt_compiler() | abstract | and | array() | as |
break | callable | case | catch | class |
clone | const | continue | declare | default |
die() | do | echo | else | elseif |
empty() | enddeclare | endfor | endforeach | endif |
endswitch | endwhile | eval() | exit() | extends |
final | finally | fn (PHP 7.4 以降) | for | foreach |
function | global | goto | if | implements |
include | include_once | instanceof | insteadof | interface |
isset() | list() | match (PHP 8.0 以降) | namespace | new |
or | private | protected | public | |
readonly (PHP 8.1.0 以降) * | require | require_once | return | static |
switch | throw | trait | try | unset() |
use | var | while | xor | yield |
yield from |
* readonly
は、関数名として使用できます。
__CLASS__ | __DIR__ | __FILE__ | __FUNCTION__ | __LINE__ | __METHOD__ |
__NAMESPACE__ | __TRAIT__ |
Interfaces
PHP: 定義済みのインターフェイスとクラス - Manual
stdClass
動的なプロパティーが使える、汎用的な空クラス。このクラス自体は、メソッドやプロパティーを持たない。
json_decodeなど一部の関数がこのインスタンスを返す。
// 型変換での作成。連想配列を(object)にキャストすると作れる。 (object) array('foo' => 'bar');
データベースの取得結果が、連想配列の他に、stdClassになっていることがある。
匿名オブジェクトや、動的プロパティーなどが主な利用方法。
データホルダーとして使う場合、連想配列のキーのほうが、自由度が高いので、そちらのほうが便利だと思われる。たくさんある配列関数も使えるし。
Constants
いくつか重要なものがある。
- PHP_EOL: プラットフォームの行末文字。
Wrappers
PHP: サポートするプロトコル/ラッパー - Manual
URL風のプロトコル (ラッパー/wrapper) で、ファイルシステム関数で使用できる。stream_wrapper_registerで自作もできる。
php://
php://memory php::temp
PHP: Is it possible to create a SplFileObject object from file contents (string)? - Stack Overflow
読み書き可能なストリーム。stringのような一時データをファイルのように保存できる。php://memoryは常にメモリー。php://tempはデフォルト2 MB超過でテンポラリーファイルを使う。php://temp/maxmemory:NNで上限を指定できる。単位はバイト。テンポラリーファイルの格納場所はsys_get_temp_dir。
データをファイルとして扱いたい場合、1回書き込んでから読み込む必要がある。
$contents = 'i am a string'; $file = 'php://memory'; // full memory buffering mode //$file = 'php://temp/maxmemory:1048576'; //partial memory buffering mode. Tries to use memory, but over 1MB will automatically page the excess to a file. $o = new SplFileObject($file, 'w+'); $o->fwrite($contents); // read the value back: $o->rewind(); $o->fread(); // 'i am a string'
Features
Ref: PHP: Features - Manual.
Handling file uploads
Ref: PHP: Handling file uploads - Manual.
input type="file"などのアップロードファイルのPHPでの処理方法・作法がある
<!-- データのエンコード方式である enctype は、必ず以下のようにしなければなりません -->
<form enctype="multipart/form-data" action="__URL__" method="POST">
<!-- MAX_FILE_SIZE は、必ず "file" input フィールドより前になければなりません -->
<input type="hidden" name="MAX_FILE_SIZE" value="30000" />
<!-- input 要素の name 属性の値が、$_FILES 配列のキーになります -->
このファイルをアップロード: <input name="userfile" type="file" />
<input type="submit" value="ファイルを送信" />
</form>
PHP側では$_FILES['userfile']に必要な情報が格納される。
- $_FILES['userfile']['name']
- クライアントマシンの元のファイル名。
- $_FILES['userfile']['type']
- ファイルの MIME 型。ただし、ブラウザがこの情報を提供する場合。 例えば、
"image/gif"
のようになります。 この MIME 型は PHP 側ではチェックされません。そのため、 この値は信用できません。 - $_FILES['userfile']['size']
- アップロードされたファイルのバイト単位のサイズ。
- $_FILES['userfile']['tmp_name']
- アップロードされたファイルがサーバー上で保存されているテンポラ リファイルの名前。
- $_FILES['userfile']['error']
- このファイルアップロードに関する エラーコード
- $_FILES['userfile']['full_path']
- ブラウザからアップロードされたファイルのフルパス。 この値は実際のディレクトリ構造を反映しているとは必ずしも言えないため、 信用できません。 PHP 8.1.0 以降で利用可能です。
tmp_nameが非常に重要。これをリネームする形で保存する。あとはnameも保存時のファイル名で重要。
<?php
$uploaddir = '/var/www/uploads/';
$uploadfile = $uploaddir . basename($_FILES['userfile']['name']);
echo '<pre>';
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
echo "File is valid, and was successfully uploaded.\n";
} else {
echo "Possible file upload attack!\n";
}
echo 'Here is some more debugging info:';
print_r($_FILES);
print "</pre>";
?>
上記がイメージ。
DBに保存する場合は「PHPとMySQLを利用した画像・動画のアップロード・保存・表示 #PHP - Qiita」も参考になる。
Using PHP from the command line
Ref: PHP: Command line usage - Manual.
About
簡単なコードの確認などでPHPをコマンドラインなどから簡単に実行したいことがよくある。いくつか方法がある (PHP: Usage - Manual)。
- phpコマンドの引数にファイルを指定:
php file.php
/php -f file.php
- phpコマンドの引数にコードを指定:
php -r 'print_r(get_defined_constants());'
- phpコマンドに標準入力で読み込み:
php <file.php
標準入力が一番使いやすく感じる。
__name__ == "__main__"
phpファイルを直接実行時とインポート時とで分離する書き方。 同一ファイルでクラスと実行用ファイルとしたい場合などに必要となる。書き方がいくつかある。
「Python の if __name__ == ‘__main__’: を Perl, Ruby, PHP で行う : Serendip – Webデザイン・プログラミング」
if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) { // do something }
「PHPスクリプトが直接起動されたかどうかで処理を振り分ける | バシャログ。」
if (realpath($_SERVER["SCRIPT_FILENAME"]) == realpath(__FILE__)){ /** ここに処理を書いてね */ }
「PHP equivalent of Python's __name__ == "__main__"? - Stack Overflow」
if (!debug_backtrace()) { // do useful stuff }
このdebug_backgrace関数がうまい。ルートになるからバックトレースが空になる。realpathとかbasenameはパスの解析が生じるので遅くなる。
Function Reference
Affecting PHP's Behavior
Error Handling
Runtime Configuration
PHPのエラー設定を整理する。 PHPのエラー設定は「PHP: Runtime Configuration - Manual」で一覧化されている。
xmlrpc_errors, syslog.facility, syslog.ident以外はどこでも設定可能。
特に重要なのが以下の設定。
設定 | 初期値 | 説明 |
---|---|---|
error_reporting | E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
|
エラー出力レベルを設定する。開発時にはE_ALL (2147483647/-1) にしておくとよい。 |
display_errors | "1" | エラーのHTML出力への表示を設定する。"stderr"を指定すると,stderrに送る。デフォルトで有効なのでそのままでいい。 |
display_startup_errors | "0" | PHPの起動シーケンス中のエラー表示を設定する。デバッグ時は有効にしておいたほうがいい。 |
log_errors | "0" | エラーメッセージのサーバーのエラーログまたはerror_logへの記録を指定する。これを指定しないとログが残らないため,常に指定したほうがいい |
error_log | NULL | スクリプトエラーが記録されるファイル名を指定する。syslogが指定されると,ファイルではなくシステムロガーに送られる。Unixではsyslog(3)で,Windowsではイベントログになる。指定されていない場合,SAPIエラーロガーに送信される。ApacheのエラーログかCLIならstderrになる。 |
基本的に以下をphp.ini/.user.iniに設定しておけばよい。
error_reporting = E_ALL
display_startup_errors = on
log_errors = on
; For file
display_errors = stderr
htpd.conf/.htaccessの場合は以下。
php_value error_reporting -1
php_flag display_startup_errors on
php_flag log_errors on
# For file
php_value display_errors stderr
logrotate
- ライブラリー
- 自前
- logrotate
出力したログファイルがストレージを圧迫しないように、一定サイズ・期間でリネームして、最大保持数を維持したりする。
いくつか方法がある。
- GNU/Linuxのlogrotateコマンド
- logrotateライブラリー
- 自前実装
やることは決まっているのだから、自前で実装してもいいかもしれない。
- ログ出力時
- ログ出力ファイルのサイズを確認して、設定サイズより大きければ、循環。
- 最古のログファイルを削除して、順番にリネーム。
- 最後に出力。
それだけ。
const LOG_DIRECTORY = '/var/log/logdir/'; // ログディレクトリ const LOG_FILENAME = 'logfname.log'; // ログファイル名 const LOG_FILEPATH = LOG_DIRECTORY.LOG_FILENAME; // ログのファイルパス const MAX_LOTATES = 3; // ログファイルを残す世代数 const MAX_LOGSIZE = 1024*1024; // 1ファイルの最大ログサイズ(バイト) function WriteLog($strlog){ // 保存先ディレクトリを作成 if(!file_exists(LOG_DIRECTORY)){ mkdir(LOG_DIRECTORY); } // ログのローテート if(@filesize(LOG_FILEPATH) > MAX_LOGSIZE){ // 最古のログを削除 @unlink(LOG_FILEPATH.strval(MAX_LOTATES)); // ログをリネーム .log → .log_01 for ($i = MAX_LOTATES - 1; $i >= 0; $i--) { $bufilename = ($i == 0) ? LOG_FILEPATH : LOG_FILEPATH.strval($i); @rename($bufilename, LOG_FILEPATH.strval($i+1)); } } // ログ出力 file_put_contents(LOG_FILEPATH, date('y-m/d-H:i:s ').$strlog."\n", FILE_APPEND | LOCK_EX); }
もう少しいい実装方法はありそう。
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。
Database
PDO
Introduction
PHP Data Objects。PHPからDBへのアクセスの軽量で高性能なインターフェイス。DBの全関数を実行できるわけではない。DBアクセスの抽象化レイヤーを提供する。つまり、DBが何であろうが、同じ関数でSQLの発行、データ取得ができる。
Connections
Open
PDOインスタンスの作成で接続ができる。
<?php try { $driver_options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]; $dbh = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', $user, $pass, $driver_options); } catch (PDOException $e) { // たとえば、タイムアウトしたあとに再接続を試みます } ?>
引数にDSNと、ユーザー名、パスワードを指定する。
PDOインスタンスが存在する間、接続が継続する。
option
PDOの最後の引数で、ドライバーごとのオプションを設定できる。後からsetAttributeでも指定できる。
- PDO::ATTR_CASE
- PDO::ATTR_ERRMODE: SQL実行時のエラーの扱い。デフォルトPDO::ERRMODE_SILENTで何も報告しない。PDO::ERRMODE_EXCEPTIONを設定するのがいい。
- PDO::ATTR_ORACLE_NULLS
- PDO::ATTR_STRINGIFY_FETCHES
- PDO::ATTR_STATEMENT_CLASS
- PDO::ATTR_TIMEOUT
- PDO::ATTR_AUTOCOMMIT
- PDO::ATTR_EMULATE_PREPARES
- PDO::MYSQL_ATTR_USE_BUFFERED_QUERY
- PDO::ATTR_DEFAULT_FETCH_MODE
Close
接続終了時には、明示的にオブジェクトを破棄する必要がある。具体的には、変数にnullを代入する。
<?php $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass); // ここで接続を使用します $sth = $dbh->query('SELECT * FROM foo'); // 使用を終了したので、閉じます $sth = null; $dbh = null; ?>
nullにしない場合、script終了時に閉じられる。
ただ、通常はすぐに単体のscriptは終了するので、いちいち書く必要はない。
charset
- PHPでデータベースに接続するときのまとめ #MySQL - Qiita
- PHP: PDO_MYSQL DSN - Manual
- PHP: 文字セット - Manual
- PHP What is the default charset for pdo mysql - Stack Overflow
- php - Are PDO prepared statements sufficient to prevent SQL injection? - Stack Overflow
DSNにcharsetを指定できる。これはDBのクライアントの送受信に使うエンコーディングとのこと。
The default is utf8 in MySQL 5.5, 5.6, and 5.7, and utf8mb4 in 8.0.
指定したほうがいい模様。
Query
SQLを実行するためのPDOメソッドがいくつかある。
- PHP: PDO::exec - Manual: 引数のSQLを実行して、影響のある行数を返す。結果のいらないSQLやSELECT以外のINSERT/UPDATEなどで使う。
- PHP: PDO::query - Manual: 引数のSQLを実行して、実行結果をPDOStatementで取得する。ユーザー入力を伴わない固定SQLで使う。似たようなSQLをループなどで複数回実行するならprepareのほうが性能が向上する。
- PHP: PDO::prepare - Manual: PHP: PDOStatement::execute - Manual用のSQL文を用意する。プレースホルダーを用意する。プリペアードステートメントというSQLの機能を使う。ユーザー入力をエスケープしたり、一部分だけ異なるようなSQLがキャッシュされて効率が上がる。
prepared statement
PHP: プリペアドステートメントおよびストアドプロシージャ - Manual
重要。パラメーターマーク/プレースホルダーを配置したSQL文を用意して、後でプレースホルダーに変数をバインドしてSQLを実行する。
placeholder
プレースホルダーとして、名前付きパラメーターと疑問符パラメーターの2種類がある。
- 名前付きパラメーター: [:name] の形式で配置する。バインド時は名前。数が多い場合。バインド時は先頭の:は省略可能。compact関数で短縮できる。
- 疑問符パラメーター: [?] を配置する。バインド時は0開始の番号。数が少ない場合シンプル。PHP 7.4.0以上で??で?自体をエスケープ。
なお、名前付きと疑問符は混在できない。プレースホルダーには、SQLのデータリテラルのみを配置できる。SQLの文などはだめ。
プレースホルダーはデータリテラル全体に適用が必要。つまり、LIKEの%は値に含める必要がある。代わりに、引用符がいらない。
<?php /* 値の配列を渡してプリペアドステートメントを実行する */ $sql = 'SELECT name, colour, calories FROM fruit WHERE calories < :calories AND colour = :colour'; $sth = $dbh->prepare($sql, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]); $sth->execute(['calories' => 150, 'colour' => 'red']); $red = $sth->fetchAll(); /* 配列のキーの前にも、コロン ":" を付けることができます(オプション) */ $sth->execute([':calories' => 175, ':colour' => 'yellow']); $yellow = $sth->fetchAll(); ?>
<?php /* 値の配列を渡してプリペアドステートメントを実行する */ $sth = $dbh->prepare('SELECT name, colour, calories FROM fruit WHERE calories < ? AND colour = ?'); $sth->execute([150, 'red']); $red = $sth->fetchAll(); $sth->execute([175, 'yellow']); $yellow = $sth->fetchAll(); ?>
$stmt->execute([1 => $gender, 0 => $city]);
疑問符の場合、キーを指定すれば、順番を変更してもOK。
- php - PDO valid characters for placeholders - Stack Overflow
- php-src/ext/pdo/pdo_sql_parser.re at 5638fafa665899523861a12966b42a9c609a1664 · php/php-src · GitHub
名前付きパラメーターに使える文字には制限がある。
BINDCHR = [:][a-zA-Z0-9_]+;
英数字と_のみ。日本語のカラム名をそのまま使うことはできない。
function encode_all($str) { return preg_replace('~..~', '_$0', strtoupper(unpack('H*', $str)[1])); }
上記のような関数で、一度英数字に変換して使う。
PDOStatement::execute
PHP: PDOStatement::execute - Manual
プリペアードステートメントを実行する。実行にあたって、プレースホルダーに値を埋め込む必要がある。
- bindValue/bindParamを使用。
- 引数で配列で指定。ただし、NULL以外、値は全てPDO::PARAM_STR扱い。既存のbindValue/bindParamを全上書き。要素数はプレースホルダーと同一必要。
型が全部PARAM_STRなら問題ない。それ以外の数値などを含めたいなら、bindValueでしたほうがいい。
PDOStatement::bindValue/bindParam
プリペアードステートメント内で、部分的に後からプレースホルダーに値を割り当てる。
- bindValue: 呼び出し時に値で埋め込まれる。
- bindParam: execute実行時に変数が評価される。
なお、似た名前のメソッドに [PHP: PDOStatement::bindColumn - Manual] がある。こちらはSELECTの取得結果のカラムをPHP変数に割り当てるためのもの。
基本はbindValueでよい。が、bindParamも出番がある。
<?php $stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (?, ?)"); $stmt->bindParam(1, $name); $stmt->bindParam(2, $value); // 行を挿入します $name = 'one'; $value = 1; $stmt->execute(); // パラメータを変更し、別の行を挿入します $name = 'two'; $value = 2; $stmt->execute(); ?>
変数だけ変えて、複数回実行する場合。
ただ、この場合、executeの引数で渡してもいい。が、引数だと全部PDO::PARAM_STRになってしまうので、bindParamも役立つ。
PDOで複数回SQLを実行: コツコツ学ぶWordPress、技術メモ
<?php require("db_info.php"); $dsn = 'mysql:host=localhost;dbname='.$database.';charset=utf8'; try { $dbh = new PDO($dsn, $username, $password, array( PDO::ATTR_EMULATE_PREPARES =>false, PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'" ) ); } catch (PDOException $e) { exit('データベース接続失敗。'.$e->getMessage()); } date_default_timezone_set('Asia/Tokyo'); $result = array(); $st = $dbh->prepare('select city,TRUNCATE(sum(setai)/3,0) as setai,TRUNCATE(sum(man)/3,0) as man,TRUNCATE(sum(woman)/3,0) as woman,TRUNCATE(sum(total)/3,0) as total from jinko where ym in (:ym1,:ym2,:ym3) group by city'); $ym1 = date('Y/m', strtotime(date('Y-m-1').' -1 month')); $ym2 = date('Y/m', strtotime(date('Y-m-1').' -2 month')); $ym3 = date('Y/m', strtotime(date('Y-m-1').' -3 month')); $st->bindValue(':ym1', $ym1 , PDO::PARAM_STR); $st->bindValue(':ym2', $ym2 , PDO::PARAM_STR); $st->bindValue(':ym3', $ym3 , PDO::PARAM_STR); $st->execute(); $result = $st->fetchAll(PDO::FETCH_ASSOC); $ym1 = date('Y/m', strtotime(date('Y-m-1').' -1 year -1 month')); $ym2 = date('Y/m', strtotime(date('Y-m-1').' -1 year -2 month')); $ym3 = date('Y/m', strtotime(date('Y-m-1').' -1 year -3 month')); $st->bindValue(':ym1', $ym1 , PDO::PARAM_STR); $st->bindValue(':ym2', $ym2 , PDO::PARAM_STR); $st->bindValue(':ym3', $ym3 , PDO::PARAM_STR); $st->execute(); $result = array_merge($result,$st->fetchAll(PDO::FETCH_ASSOC)); echo json_xencode($result); ?>
ただ、bindValueをまたやってもいい。だったら、bindParamは余計になくてもいいか?
一括UPSERT
【PHP】PDOのprepareで複数行を一括INSERTする方法 | キノコログ
以下のようなプレースホルダーSQLを文字列で作って、bindValueも反復させる。
$sql = "INSERT INTO doraemon_users (name,gender,type) VALUES (:name0,:gender0,:type0) ,(:name1,:gender1,:type1),(:name2,:gender2,:type2),(:name3,:gender3,:type3),(:name4,:gender4,:type4),(:name5,:gender5,:type5)" ON DUPLICATE KEY UPDATE stat1 = stat1 + VALUES(stat1), stat2 = stat2 + VALUES(stat2), stat3 = stat3 + VALUES(stat3) ;
//配列設定 $aryInsert = []; $aryInsert[] = ['name' => 'のび太', 'gender' => 'man', 'type' => 'human']; $aryInsert[] = ['name' => 'ドラえもん', 'gender' => 'man', 'type' => 'robot']; $aryInsert[] = ['name' => 'ジャイアン', 'gender' => 'man', 'type' => 'human']; $aryInsert[] = ['name' => 'スネ夫', 'gender' => 'man', 'type' => 'human']; $aryInsert[] = ['name' => 'しずか', 'gender' => 'woman', 'type' => 'human']; $aryInsert[] = ['name' => 'ドラミ', 'gender' => 'woman', 'type' => 'robot']; $aryColumn = array_keys($aryInsert[0]); //SQL文作成処理 $sql = "INSERT INTO doraemon_users (".implode(',', $aryColumn).") VALUES"; $arySql1 = []; //行の繰り返し foreach($aryInsert as $key1 => $val1){ $arySql2 = []; //列(カラム)の繰り返し foreach($val1 as $key2 => $val2){ $arySql2[] = ':'.$key2.$key1; } $arySql1[] = '('.implode(',', $arySql2).')'; } $sql .= implode(',', $arySql1); //bind処理 $sth = $pdo -> prepare($sql); foreach($aryInsert as $key1 => $val1){ foreach($val1 as $key2 => $val2){ $sth -> bindValue(':'.$key2.$key1, $val2); } } //実行処理 $sth -> execute();
$records = []; $this->file->seek(1); foreach (new \NoRewindIterator($this->file) as $row) { /** @var array $row map_raku_numに存在する列番号の場合、列番号を列名に置換。 */ $row = array_combine(array_map(function($k) use($map_raku_num) { return in_array($k, $map_raku_num) ? array_flip($map_raku_num)[$k] : $k; }, array_keys($row)), $row); foreach ($map_base_raku as $base => $raku) { if (!array_key_exists($raku, $row)) { $row[$raku] = null; }; $formatter = $formatter ?: function($v, $k, $r) {return $v;}; $row[$raku] = ($row[$raku] === '' || $row[$raku] === null) ? null : $row[$raku]; $record[$base] = $formatter($row[$raku], $base, $row); } /** 全部空なら除外する。 */ if (!empty(array_filter($record))) $records[] = $record; } /** UPSERTのAI増分対策用に重複削除。 */ if (!empty($unique)) { $old_row = ''; foreach ($records as $row => $line) { // unique対象列が全部一致の場合削除。 if (array_filter($unique, function($v) use ($line, $records, $old_row) {return $line[$v] !== $records[$old_row][$v];})) { unset($records[$old_row]); } $old_row = $row; } } /** @var array $map_base_holder PDOのプレースホルダーには英数字しか使えないので%エンコーディング。 */ $map_base_holder = array_combine(array_keys($map_base_raku), array_map(function($v) { return preg_replace('~..~', '_$0', strtoupper(unpack('H*', $v)[1])); }, array_keys($map_base_raku))); try { $dbh = new PDO(Raku2Config::getDSN(), Raku2Config::get('CONFIG_DBUSER'), Raku2Config::get('CONFIG_DBPASS'), [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); Raku2Config::writeLog("SQL execute table_name=$table_name"); $table_name = str_replace(["\0", "`"], ["", "``"], $table_name); /** @var string $sql_prefix SQL前半カラム名 */ $sql_prefix = "INSERT INTO $table_name (" . implode(',', array_keys($map_base_raku)) . ') VALUES '; /** @var string $sql_postfix SQL後半カラム名 カラム1 = VALUES(カラム1), カラム2 = VALUES(カラム2)... */ $sql_postfix = ' ON DUPLICATE KEY UPDATE ' . implode(',', array_map(function($e){return "$e=VALUES($e)";}, array_keys($map_base_raku))); /** @var int $run_per_block 全データを一括INSERTすると、物件テーブルが4388件目でハングするので1000件ずつ分割実行 */ foreach (array_chunk($records, $run_per_block = 1000) as $sub_records) { // [%エンコーディングした列名][行番号]の書式にプレースホルダー配置 (例: 86_8a_a9_943) $sql = $sql_prefix . implode(',', array_map(function($row)use($map_base_holder){ return '('.implode(',', array_map(function($e)use($row){return ":$e$row";}, $map_base_holder)).')'; }, array_keys($sub_records))) . $sql_postfix; $sth = $dbh->prepare($sql); foreach ($sub_records as $row => $line) { foreach ($map_base_holder as $base => $holder) { $type = in_array($base, array_keys($map_base_type)) ? $map_base_type[$base] : PDO::PARAM_STR; $sth->bindValue(":$holder$row", $line[$base], $type); } }; $sth->execute(); /** @var string $sql ON DUPLICATE KEY UPDATE で更新するとAUTO_INCREMENTが増えるので、最大値でリセット。 */ $sql = "SET @NEW_AI = (SELECT MAX(" . str_replace(["\0", "`"], ["", "``"], array_key_first($map_base_raku)) . ")+1 FROM $table_name)"; $dbh->exec($sql); $dbh->exec("SET @ALTER_SQL = CONCAT('ALTER TABLE $table_name AUTO_INCREMENT =', @NEW_AI)"); $dbh->exec("PREPARE NEWSQL FROM @ALTER_SQL"); $dbh->exec("EXECUTE NEWSQL"); } } catch (PDOException $e) { Raku2Config::writeLog("table_name=$table_name " . $e->getMessage()); return Raku2Config::EXIT_EXCEPTION; }
分割一括UPSERT例。
テーブル名/カラム名には使用不能
「PHP: PDO::prepare - Manual」のコメントに記載がある通り、PHPのプリペアードステートメントはテーブル名とカラム名のプレースホルダーは未対応。プレースホルダーは単に文字列置換をしているわけではなく、DBMSにクエリープランの作成を指示するため。これにはテーブル名が分かっていないといけない。
「PHPでデータベースに接続するときのまとめ #MySQL - Qiita」に記載があるような、テーブル名のエスケープを自前で行う必要がある。
- NULLバイトの除外。
- バッククオートの二重化によるエスケープ。
- 全体を囲む。
$sql = sprintf( "CREATE TABLE `%s`(id int, name text)", str_replace(["\0", "`"], ["", "``"], $table_name) );
Fetch
PDOStatementからデータを取得するメソッドが複数ある。
- PHP: PDOStatement::fetch - Manual: 1行。
- PHP: PDOStatement::fetchAll - Manual: 全行。
- PHP: PDOStatement::fetchColumn - Manual
- PHP: PDOStatement::fetchObject - Manual
「PDOの真の力を開放する - PHPでデータベースを扱う(3): Architect Note」
<?php $stmt = $pdo->query('SELECT * FROM Entry'); foreach ($stmt as $row) { echo $row['title'], $row['content']; } //↑は↓のコードと等価 while ($row = $stmt->fetch()) { echo $row['title'], $row['content']; }
fetchしてループで処理するなら、fetchしなくても使える。
FETCH_MODE
PDOのコンストラクターのオプション、setAttribute、fetchの引数で取得条件を指定できる。
- PDO::FETCH_BOTH: 規定。カラム名と0開始の添え字の配列で返す。
- PDO::FETCH_NAMED: 同名のカラムが複数ある場合、値の配列を返す。
- PDO::FETCH_NUM: 0開始のカラム番号の配列で返す。
同名カラムがある場合、PDO::FETCH_NUM/FETCH_NAMEDじゃないと取れない。
Calendar
time/速度計測
PHPで処理速度などを計測したいことがある。基本は処理前後のタイムスタンプの差分で、どの言語でも共通の論理だが、いくつか方法がある。
microtimeとhrtimeの2個の関数をタイムスタンプの取得で使える。hrtimeはPHP 7.3.0以上で使用可能。単位ナノ秒。問題なければ、こちらが推奨されている。HRTime (High Resolution Time) の拡張のモジュールと関係する関数とのこと。
両方とも、引数にtrueを指定して、floatで取得するのが基本。
<?php $start = hrtime(true); // 計測開始時間 // 計測したい処理 $end = hrtime(true); // 計測終了時間 // 終了時間から開始時間を引くと処理時間になる echo '処理時間:'.($end - $start).'ナノ秒' . PHP_EOL; ?>
PHPのバージョンを気にするのが嫌なので、ラップするとよい。
<?php /** * Time target function. * @param callable $callback Target function. * @return int|float Run time [ns]. */ function timeit(callable $callback) { $time = 'microtime'; $nanoFactor = 1000; if (function_exists('hrtime')) { $time = 'hrtime'; $nanoFactor = 1; } $start = $time(true); $callback(); $stop = $time(true); return ($stop - $start) * $nanoFactor; } echo timeit(function(){sleep(1);}); // one liner. (function($c){$s=hrtime(true);$c();return hrtime(true)-$s;})(function(){}); (function($c){$s=hrtime(true);$c();return hrtime(true)-$s;})(function(){sleep(1);}); ?>
こんな感じ。
Date/Time
About
- PHP: 日付/時刻 の算術 - Manual
- PHPの日付・時刻関連をまとめてみた #PHP7.2 - Qiita
- 日付時刻関連で使われる書式の調査 #PHP - Qiita
- 日付時刻関連のクラスを活用しよう #PHP - Qiita
PHPの日時処理はdate関数DateTimeクラス、DateTimeImuutableクラス、Carbonなどいろいろある。
公式の説明などを見る限り、DateTimeImuutableを推奨しているように見える。
単純な日時文字列が欲しいだけなら、date関数系API。それ以外の本格的な日付計算が必要ならDateTimeImmutableを使うといい。プロジェクトでは念のため自前の日時クラスでラップしておく。
Carbon
- なぜPHP標準関数のdateではなくCarbonを使うのか?
- ELI5: Why is Carbon better than core PHP date classes? : r/PHP
- Carbon - A simple PHP API extension for DateTime.
- Why PHPs Carbon is bad for the climate - Carsten Windler
PHPの日時処理にCarbonという拡張クラスが人気らしい。人気の理由はテストしやすいからとか。
たしかに、標準のdateなどの関数系APIは扱いにくいかもしれない。
DateTimeImuutableなら問題ない気がする。
datetime
関数系API群。
- date
- strtotime
- time
format
日時の書式が2種類ある。DatetImeInterface::formatを見ておくとよい。
特に重要なものを抜粋する。
種類 | 文字 | 例 | 説明 |
---|---|---|---|
全ての日付/時刻 | c | 2004-02-12T15:19:21+00:00 | ISO8601日付。 |
U | time() | Unix Epoch (1970-01-01T00:00) からの秒数。 |
よくやるパターン。
echo date('c') . PHP_EOL; // 2024-10-07T00:03:19+00:00 echo str_replace(['-', ':'], '', date('c')); // 20241007T000319+0000
File system
Ref: PHP: ファイルシステム - Manual.
File system
file
- file_get_contents — ファイルの内容を全て文字列に読み込む
- file_put_contents: データをファイルに書き込む。戻り値に書き込みバイト数を返す。失敗したらfalse。成否は完全一致===falseで。
- PHP: file - Manual: ファイル全体を読み込んで改行区切りで配列にする。
- readfile: ファイル全体を読み込んで標準出力に出力する。
上記2個の非常に重要な入出力関数がある。
バイナリーやHTTP GETに対応している。アップロードされたファイルの読み込みなどでお世話になる。
file_get_contents/file_put_contentsはfopen/fwrite/fcloseの一連のファイル処理を含んでいるので非常に簡単。
- SplFileObject is faster than fopen/fgets – Gustavo Straube
- file vs file_get_contents vs fopen vs SplFileObject php - Тесты производительности php - Fast Site Engine
SplFileObjectとどちらが速いかは不明。
directory
- PHP: mkdir - Manual:
- rmdir
// ディレクトリー不在なら作成。 $dir = '/path/to/dir' if (!file_exists($dir)) mkdir($dir, 0777, true);
check
入出力とセットで使うファイルの不在確認の関数群。
path
- PHPのディレクトリまとめ #PHP - Qiita
- PHP・ファイル名・パスなどの取得
- PHPの便利な関数:ファイルパス関連dirname(), basename(), realpath(), getcwd(), chdir()
- PHP: マジック定数 - Manual
- PHP: basename - Manual: パスからファイル名を取得。
- PHP: dirname - Manual: パスからディレクトリーの取得。
- PHP: realpath - Manual: 相対パスを絶対パスに変換。
- PHP: getcwd - Manual: 現在のディレクトリー。__DIR__は実行中ディレクトリー。includeしたときにgetcwdは実行元、__DIR__はinclude先。
- PHP: chdir - Manual
- __DIR__: ファイルの存在するディレクトリー。終端スラッシュなし。
パス関係の操作。重要。
operation
PHPでファイルの移動や改名をする場合、rename。基本的にこれ1個だけ。
rename("/tmp/tmp_file.txt", "/home/user/login/docs/my_file.txt");
CSV
- PHP: fgetcsv - Manual
- PHP: str_getcsv - Manual
- PHP: SplFileObject - Manual
- 【PHP】その CSV 変換、本当に「fgetcsv」でいいの? (フェンリル | デベロッパーズブログ)
CSVのパース・読込のための関数がいくつかある。一番いいのは、PHP 5.1.0以上のSplFileObject。これが速くてメモリー使用量も小さい優れた実装。
$file = new SplFileObject($filepath); $file->setFlags(SplFileObject::READ_CSV|\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY); if (0 === strpos(PHP_OS, 'WIN')) { setlocale(LC_CTYPE, 'C'); } foreach ($file as $row) { $records[] = $row; } // Get header $file->current(); // Skip header. $file->seek(1); foreach (new \NoRewindIterator($file) as $row) { $records[] = $row; }
fgetcsv
- PHP: fgetcsv - Manual
- PHP: SplFileObject::fgetcsv - Manual
- PHP: setlocale - Manual
- Windows環境でCSVのパースをするとカラムが正しく分割できない現象の修正方法 - ビー鉄のブログ
- Windows版php-7.1 で UTF8 の CSV をパースする - 脳みそスワップアウト
fgetcsvやSplFileObject::fgetcsvはOSのロケールを考慮する。
WindowsではUTF-8がなく、解釈できないらしい。かつ、PHP7の問題とか。ただ、LC_TYPE=Cにすると回避できる。
if (0 === strpos(PHP_OS, 'WIN')) { setlocale(LC_CTYPE, 'C'); }
PHPのバージョンの条件もつけていい気もするが。
Tmp
- PHP: tmpfile - Manual: r+w+bで開く。fcloseなど使用終了時に自動削除。「tmpfile() or tempnam()? - PHP Coding Help - PHP Freaks」自動削除以外はfopen('php://temp', 'w+b')相当。
- PHP: tempnam - Manual: おそらくr+wで開く。
- Using `php://temp` or `tempnam` when I write a temporary file in order to upload data into 3rd party network storage - Stack Overflow
- PHPで一時的なファイルポインタを扱う方法 #PHP - Qiita
一時ファイルの作成方法がいくつかある。
$fp = fopen('php://memory', 'r+b');
$fp = fopen('php://temp', 'r+b');
$fp = fopen("php://temp/maxmemory:{$n}", 'r+b');
$file = new SplTempFileObject($n);
$fp = tmpfile();
- tempnam()
php://tempはアップロード失敗時などで、ファイル名が取れなくて、ファイルが残るらしい。php://tempを使うなら、tmpfileかtempnamのどちらかがいい。
いったん一時的な名前で作った後に、後で保存する場合、tempnam。保存不要ならtmpfile。
$ch = curl_init(); $meta = stream_get_meta_data($fp = tmpfile()); curl_setopt($ch, CURLOPT_COOKIEJAR, $meta['uri']); curl_setopt($ch, CURLOPT_COOKIEFILE, $meta['uri']);
stream_get_meta_dataの取得結果の'uri'からファイルパスを取得できる。
データの読込。「【PHP】gzip圧縮されたCSVをSplFileObjectで直接処理する #PHP - Qiita」
<?php // gzipファイルのダウンロード $url = 'http://localhost/test.csv.gz'; $ch = curl_init($url); $tmp = tmpfile(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_FILE => $tmp, ]); curl_exec($ch); $tmp_path = stream_get_meta_data($tmp)['uri']; // CSVとして読み込み $file = new SplFileObject('compress.zlib://' . $tmp_path); $file ->setFlags(SplFileObject::READ_CSV|\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY); if(0 === strpos(PHP_OS, 'WIN')) { setlocale(LC_CTYPE, 'C'); } foreach($file as $row){ var_export($row); }
curlの取得結果の読込。
$path = stream_get_meta_data($fp = tmpfile())['uri']; $url = 'https://localhost'; $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_FILE => $fp]); $result = curl_exec($ch); $file = new SplFileObject($path); $file->setFlags(SplFileObject::READ_CSV|\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY);
確認用一時ファイル作成
ファイル読み書きの動作確認用コードで、今後頻出する一時ファイル作成と読込例。
$path = stream_get_meta_data($fp = tmpfile())['uri']; file_put_contents($path, <<<'EOT' id,value 0,1 EOT ); $file = new SplFileObject($path);
tempnam
ファイルアップロード後、OKなら正式保存みたいなときに使う。
$path = tempnam(sys_get_temp_dir(), 'FOO'); // good $handle = fopen($tmpfname, "w"); fwrite($handle, "writing to tempfile"); fclose($handle);
第2引数のprefixは一時ファイルのプレフィクスになる。
競合などしないなら、最初から本保存用のファイル名でファイルを作ったほうがよいかも。
Directory
Constants
- DIRECTORY_SEPARATOR: Windowsなら\、それ以外/らしい。
このDIRECTORY_SEPARATORは基本的には使うことはない。
- path separator - When to use DIRECTORY_SEPARATOR in PHP code? - Stack Overflow
- language agnostic - Is using the directory separator constant necessary? - Stack Overflow
理由として、Windowsがパスの文脈で/も受け入れるから。必要な場面は、OSからパスを受け取る場合に、\が返ってくることがあり、そこでexplodeしたりしたい場合のみ。
自分からパスを指定する文脈では、/で問題なく、DIRECTORY_SEPARATORを使う必要はない。
ディレクトリー以下のファイル一覧
- PHP: glob - Manual
- PHP: scandir - Manual
- PHPで任意のディレクトリ下にあるファイルを一覧取得する方法 #PHP - Qiita
- PHP 最強に簡単なファイル一覧取得方法 - 株式会社NextCode - 福山市のHP制作・システム開発
- Which is better to read files from a directory using PHP - glob() or scandir() or readdir()? - Stack Overflow
- PHPのglob()関数を使用しフォルダ内のファイルを数える | Men of Letters(メン・オブ・レターズ) – 論理的思考/業務改善/プログラミング
- scandir - PHP most efficient way to list the files in a very large directory - Stack Overflow
- php - Which is faster: glob() or opendir() - Stack Overflow
- Putting glob() to the test | php[architect]
- Getting the names of all files in a directory with PHP - Stack Overflow
方法がいくつかある。
- glob
- scandir
- SPL
ソートや簡易検索が必要かどうかで、速度や適切な方法が異なる。
ただ、使いやすいのはglob。
glob(string $pattern, int $flags = 0): array|false
マッチする・ファイル、ディレクトリーの配列を返す。マッチしなければ空の配列。失敗はfalse。
patternはlibcのglobのルールでマッチする。
- *: 0以上の任意文字。
- ?: 任意の1文字。
- [...]: グループの1文字。先頭が!の場合、否定。
- \: エスケープ。
flags。特に重要なのは以下。
- GLOB_ONLYDIR: ディレクトリーのみ。
- GLOB_NOSORT: デフォルトでアルファベット順でソートしているのを無効にする。これを指定すると速くなる。
./..以外の全ファイルのマッチ。
array_merge(glob('.[!.]*'), glob('*'));
International
PHP: 自然言語および文字エンコーディング - Manual
mbstring
mb_substr
Ref:
mb_substr(
string $string,
int $start,
?int $length = null,
?string $encoding = null
): string
substr同様、lengthにはマイナス値を指定可能。その場合、末尾からの文字数になる。
省略するかnullを指定すると、全文字。0は0文字。
mb_convert_kana
mb_convert_kana(string $string, string $mode = "KV", ?string $encoding = null): string
オプション | 意味 |
---|---|
r
|
「全角」英字を「半角」に変換します。 |
R
|
「半角」英字を「全角」に変換します。 |
n
|
「全角」数字を「半角」に変換します。 |
N
|
「半角」数字を「全角」に変換します。 |
a
|
「全角」英数字を「半角」に変換します。 |
A
|
「半角」英数字を「全角」に変換します ("a", "A" オプションに含まれる文字は、U+0022, U+0027, U+005C, U+007Eを除く U+0021 - U+007E の範囲です)。 |
s
|
「全角」スペースを「半角」に変換します(U+3000 -> U+0020)。 |
S
|
「半角」スペースを「全角」に変換します(U+0020 -> U+3000)。 |
k
|
「全角カタカナ」を「半角カタカナ」に変換します。 |
K
|
「半角カタカナ」を「全角カタカナ」に変換します。 |
h
|
「全角ひらがな」を「半角カタカナ」に変換します。 |
H
|
「半角カタカナ」を「全角ひらがな」に変換します。 |
c
|
「全角カタカナ」を「全角ひらがな」に変換します。 |
C
|
「全角ひらがな」を「全角カタカナ」に変換します。 |
V
|
濁点付きの文字を一文字に変換します。"K", "H" と共に使用します。 |
modeはデフォルトのKVで問題なさそう。
なお、日本語の促音、小文字の大文字への変換はできない。素直に置換するしかない。
【PHP】ひらがな・カタカナの小文字(小書き文字)を大文字にする方法
function kana_small_to_large($subject) { $search = ['ぁ','ぃ','ぅ','ぇ','ぉ','っ','ゃ','ゅ','ょ','ゎ','ァ','ィ','ゥ','ェ','ォ','ッ','ャ','ュ','ョ','ヮ','ヶ']; $replace = ['あ','い','う','え','お','つ','や','ゆ','よ','わ','ア','イ','ウ','エ','オ','ツ','ヤ','ユ','ヨ','ワ','ケ']; return str_replace($search, $replace, $subject); } //使用例 echo kana_small_to_large('カッパ'); //カツパ
Text
Strings
strpos
Ref: PHP: strpos - Manual.
strpos(string $haystack
, string $needle
, int $offset
= 0): int|false
文字列 haystack
の中で、 needle
が最初に現れる位置を探します。
include相当。よく使う。
戻り値に注意。有無の確認時は、strpos() !== falseの厳密一致でチェックする必要がある。
echo/print/printf
- PHP: echo - Manual
- PHP: print - Manual
- PHP: printf - Manual
- echoとprintの違い #PHP - Qiita
- PHP : echoとprintの違い #まとめ - Qiita
- 【PHP】echoとprintの違いとは?どっちを使えば良い?
PHPの出力関数群。よく使うが、扱いが特殊なので整理する。
まず、echo/printは関数ではなく、if/forなどと同じ言語構造、キーワード扱い。丸括弧はなくてもいい。紛らわしいのでないほうがいい。
- 共通: 末尾に改行は付与されない。自分で"\n"を指定必要。関数ではないので丸括弧は不要。
- echo: 戻り値void。式ではないので、if returnなどで使えない。戻り値がない分printよりわずかに速い。文字数が短い。コンマ区切りで複数列挙可能。文字列連結するよりコンマ区切りのほうが.演算子の優先順位など扱いが簡単。HTMLでの埋め込みに便利な <?= ?>の短縮表記 (<?php echo ; ?>相当)もあり。
- print: 戻り値intで常に1を返す。ifや条件演算子の結果部分など、式の文脈で使用可能。
基本はechoでいい。if return/条件演算子など戻り値や式が必要な箇所でだけprintを使う。
printfは関数。書式指定が必要ならこれ。
Basic/Vartype/変数・データ型関連
Variable
print_r/var_export/var_dump
- [PHP print_r、var_dump、var_export のちがい #PHP - Qiita]
- How to create Dynamically create config/custom.php config file
- PHP: print_r - Manual
- PHP: var_export - Manual
- PHP: var_dump - Manual
PHPの変数の内容を、配列やオブジェクトなどの複雑なデータ型でも表示、出力可能なのがprint_r/var_export/var_dump。微妙に違いがある。
- var_dump: これのみ型情報がある。
- var_export: 変数の文字列表現。config.phpなどファイルに出力すればそのままinclude可能。
- print_r: 基本は閲覧用の文字列。
- print_r/var_export: 第二引数をtrueにすると、画面出力ではなく、戻り値に返す。
$text = "<?php\n\nreturn " . var_export($myarray, true) . ";";
使うとしたら、var_exportとvar_dumpだと思われる。
var_dumpは型の情報が詳しい。詳細な情報が欲しい場合、var_dump。そうでない、単に見たいだけなら、var_exportで十分。
var_dumpは表示のみ。出力制御関数を使えば、文字列に保存はできるが、基本はデバッグ表示用。
<?php $data = array( "A" => "Apple", "B" => "Banana", "C" => "Cherry" ); echo "---print_r---\n"; print_r($data); echo "---var_export---\n"; var_export($data); echo "---var_dump---\n"; var_dump($data); ?>
---print_r--- Array ( [A] => Apple [B] => Banana [C] => Cherry ) ---var_export--- array ( 'A' => 'Apple', 'B' => 'Banana', 'C' => 'Cherry', ) ---var_dump--- array(3) { ["A"]=> string(5) "Apple" ["B"]=> string(6) "Banana" ["C"]=> string(6) "Cherry" }
画面出力でデバッグしたいならば、前後でpre要素を出力させる。
<?php function print_r2($val){ echo '<pre>'; print_r($val); echo '</pre>'; } ?>
var_exportの変数取込
var_exportの文字列表現。config.phpに出力して、Includeするほかに、テキストを変数にしたいことがある。
- import - how to read output of var_export into a variable in PHP? - Stack Overflow
- reCatnap: php var_export()した後の文字列を元に戻す(eval())
素直にevalする。
$dumpStr = var_export($var,true); eval("$somevar = $dumpStr;");
くれぐれも入力に注意する。
Process
exec
システムプログラムの実行のための関数群がある。Windowsだとcmd.exe経由で実行される。
exec/shell_exec/system/passthru
- shell - PHP shell_exec() vs exec() - Stack Overflow
- PHP: exec - Manual
- PHP: passthru - Manual
- PHP: shell_exec - Manual/PHP: 実行演算子 - Manual
- PHP: system - Manual
外部プログラム実行のための3の関数がある。違いがある。
- shell_exec: 標準出力をstringで返す。バッククオート演算子``と同じ。プログラムの成功可否、終了コードは判断不能。
- exec: 既定で標準出力の最後の1行のみ返す。第二引数に配列を指定すれば、行区切りの配列で返すこともできる。
- passthru: Unixコマンドの出力がバイナリーで、ブラウザーに直接バイナリーを返す場合に、exec/systemの代わりに使う。戻り値は?falseで標準出力に直接結果を出力する。
- system: C言語のsystem関数に類似。system()でコマンドを実行して出力する。成功時にコマンド出力の最終行を返す。出力をファイルや別のストリームにリダイレクトしないと、終了までPHPが止まる。
項目 | exec | passthru | shell_exec/`` | system |
---|---|---|---|---|
出力 | - | x | - | x |
終了コード | x | x | - | x |
戻り値 | 最終行 | null/false | 全行 | 最終行 |
全行取得 | x | - | x | - |
用途 | 外部コマンドの結果文字列取得 (終了チェックあり) | 画像応答 | 外部コマンドの結果文字列取得 | 文字応答 |
基本はshell_exec/`` execで十分。
exec
exec(string $command, array &$output = null, int &$result_code = null): string|false
戻り値は最終行。失敗したらfalseを返す。実行コマンドの終了コードは$result_codeに渡される。
escapeshellarg/escapeshellcmd
- PHP: escapeshellarg - Manual
- PHP: escapeshellcmd - Manual
- php - escapeshellarg と escapeshellcmd の違いは何ですか? - Stack Overflow
exec/shell_exec/``と併用するエスケープ用関数。
- escapeshellarg: 引数の文字列を一重引用符で囲み、既存の一重引用符を苦オートする。これで、引数全体を1個の引数にする。複数の引数の誤り実行を回避できる。
- escapeshellcmd: シェルに特殊な意味のある&#;`|*?~<>^()[]{}$\、\x0A のシェルの特殊文字にバックスラッシュを追加し、'"は対がない場合のみエスケープ。
元々、コマンド全体をエスケープする [escapeshellcmd] だけがあった。が、これだとコマンドの引数を追加する攻撃が可能になるので、 [escapeshellarg] が追加されたらしい (PHPのescapeshellcmdを巡る冒険 | 徳丸浩の日記)。
- PHPのescapeshellcmdの危険性 | 徳丸浩の日記
- escapeshellcmdの危険な実例 | 徳丸浩の日記
- PHPのescapeshellcmdを巡る冒険はmail関数を経てCVE-2016-10033に至った | 徳丸浩の日記
- 安全なPHPアプリケーションの作り方2016 | ドクセル
- CTFのWebセキュリティにおけるCommand Injectionまとめ(Linux, Windows, RCE) - はまやんはまやんはまやん
ただ、escapeshellcmdは、パラメーターインジェクションの危険性があるので、使ってはいけないらしい。
基本は引数に [escapeshellarg] を使うだけ。
「PHPにはエスケープ関数が何種類もあるけど、できればエスケープしない方法が良い理由 | 徳丸浩の日記」にあるように、その後PHP 7.4でproc_openが登場した。これはシェル経由じゃないOSコマンド呼び出しで、エスケープ不要なので安全。基本はこれを使うのがいいとのこと。
Other/その他の基本モジュール
JSON
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で評価する文字列内でreturnした結果が返却値となる。ないならnull。戻り値が必要なら、テキスト内で忘れずにreturnする。
SPL
Standard PHP Library. PHP 5.1.0で登場。標準的な処理への対応のためのライブラリー。
PHP 5から登場しただけあって、実装がかなり洗練されている。メモリー使用量、速度面の性能で有利なことが多い。
- SplFileObject
SPLFileObject
Flag
PHP – SplFileObject::setFrags() – TauStation
以下の4のフラグがある。
- public const int DROP_NEW_LINE;
- public const int READ_AHEAD;
- public const int SKIP_EMPTY;
- public const int READ_CSV;
READ_CSV以外は、データに影響あるので、指定しないほうがいいと思う。特に、CSVとして扱う場合、空行もデータのときがあるし、フィールド内に改行を含む。DROP_NEW_LINEするとその改行が削除されてしまう。
$file->setFlags(\SplFileObject::READ_CSV|\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY);
\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTYで、行末の空行を除去できる。これを指定するのがいい。
ヘッダー取得
SplFileObjectはIteratorの派生クラスなので、このメソッドで操作できる。
「PHP: SeekableIterator - Manual」も継承しているのでseekも使える。
current()で取得する。
<?php // Your code here! $path = stream_get_meta_data($fp = tmpfile())['uri']; file_put_contents($path, <<<'EOT' id,value 0,1 EOT ); $file = new SplFileObject($path); var_export($file->current()); ?>
URLs
全文字のパーセントエンコーディング
string - PHP How to encode all characters with rawurlencode - Stack Overflow
PDOのプレースホルダーに、日本語カラム名を使いたい場合など、データを英数字のみで表現したい場合に使う。
function encode_all($str) { return preg_replace('~..~', '%$0', strtoupper(unpack('H*', $str)[1])); }
p
プレースホルダーの場合、%を_で置換すれば元データも復元可能。
Other/Service
HTTP
- PHPでPOST送信まとめ #PHP - Qiita
- APIなどにfile_get_contents()を使うのはオススメしない理由と代替案 #PHP - Qiita
- PHP: file_get_contents - Manual
- PHP: cURL - Manual
PHPでHTTP通信をする方法がいくつかある。
- file_get_contents
- curl
file_get_contentsはPHP標準。curlは外部ライブラリー。
「PHP cURL vs file_get_contents - Stack Overflow」などを見る限り、GET以外はcurlのほうが速くて複雑なことができるらしい。
file_get_contentsは元々ローカルや内部ファイルの読み込み用らしい。
GNU socialではHTTPClientクラス経由で実現するので、内部実装を意識する必要はない。
curlでのリクエスト方法を覚えておくと、汎用性が高い模様。
外部ライブラリーを使っていいなら、Guzzleが今は主流とのこと。Guzzleは内部でcurlを使っている。
cURL
About
- PHP: cURL - Manual
- PHP: 基本的な curl の使用法 - Manual
- APIなどにfile_get_contents()を使うのはオススメしない理由と代替案 #PHP - Qiita
基本的な使用方法。
- curl_initでurlを指定してセッション初期化。
- curl_setopt/curl_setopt_arrayでオプションを設定。
- CURLOPT_POST=true/CURLOPT_POSTFIELDSでPOST関係指定。
- CURLOPT_RETURNTRANSFER=trueでcurl_execの応答ボディーをテキストで取得。
- CURLOPT_FILEで保存先ファイル指定。
- curl_execで転送実行。CURLOPT_RETURNTRANSFER=trueを指定しない場合、true/falseのみ。
- curl_closeでセッション終了。
<?php $ch = curl_init("http://www.example.com/"); $fp = fopen("example_homepage.txt", "w"); curl_setopt($ch, CURLOPT_FILE, $fp); curl_setopt($ch, CURLOPT_HEADER, 0); // curl_setopt_array($ch, $options); curl_exec($ch); if(curl_error($ch)) { fwrite($fp, curl_error($ch)); } $info = curl_getinfo($ch); $errorNo = curl_errno($ch); curl_close($ch); fclose($fp); ?> <?php /* curlセッションを初期化する */ $ch = curl_init("http://www.google.com/"); /* curlオプションを設定する */ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); /* curlを実行し、その内容を$result変数に保存 */ $result = curl_exec($ch); $info = curl_getinfo($ch); if ($info['http_code'] !== 200) { } /* curlセッションを終了する */ curl_close($ch); /* result変数に保存した内容を表示 */ echo htmlspecialchars($result);
curl_getinfoで応答結果の詳細を確認できる。
特に以下は重要。
- http_code
CURLOPT
PHP: 定義済み定数 - Manual 特に重要なオプションがいくつかある。
- CURLOPT_POST: trueならHTTP POST。これを指定するとContent-Tpe=application/x-www-form-urlencodedになる。JSONにしたいならこれは上書き必要。
- CURLOPT_HTTPHEADER: ヘッダー指定。[['Content-Type: application/json']] はよく指定する。
- CURLOPT_POSTFIELDS: POSTのリクエストボディー。文字列で渡すか、連想配列。連想配列の値が配列の場合、Content-Type: multipart/form-dataになる。ファイル送信はCURLFile (ファイル名) かCURLStringFile (ファイルの中身)を使う。
- CURLOPT_RETURNTRANSFER: 初期値false。trueにするとcurl_execの戻り値で、レスポンスボディーをテキストで取得できる。
- CURLOPT_FILE: 初期値STDOUT。書き込み先のファイル。ファイルポインターを指定する。CURLOPT_RETURNTRANSFERと2者択一。
- CURLOPT_TIMEOUT: 初回接続時のタイムアウト秒数。3秒が推奨?
- CURLINFO_HEADER_OUT => true: curl_getinfoにリクエストヘッダーを含める (web services - How to get info on sent PHP curl request - Stack Overflow)。デバッグ用。
CURLOPT_RETURNTRANSFERとCURLOPT_FILEの競合
- CURLOPT_*のデフォルトオプション結合の罠 #PHP - Qiita
- php Curl confilict CURLOPT_FILE and CURLOPT_RETURNTRANSFER in docker - Stack Overflow
- PHPのcurlでCURLOPT_FILEを使う際の注意点
この2個のオプションは競合する。後から設定したものが優先される。レスポンスボディーが必要ならば、片方に統一して、片方だけからのアクセスにする。
バイナリーやCSVファイルのように、データが大きくて高速なパースが必要なら、SplFileObjectを使いたいのでファイル優先でいいと思う。
JSONのようにシンプルで短いなら全部テキストでやってもよいだろう。
JSONデータの送受信
- 連想配列でリクエストボディーのデータを作って、json_encodeでJSON文字列に変換。
- POST指定: curl_setopt($ch, CURLOPT_POST, true);
- ヘッダーContent-Type指定:
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
- リクエストボディー指定: curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json);
- 戻り値のデコード: $res_json = json_decode($result , true );
以上。
$data = array( 'test1'=>'aaa', 'test2'=> array( array( 'test3'=>'bbb' ) ), 'test4'=> array( array( 'test5'=>'ccc', 'test6'=>'ddd' ) ) ); $data_json = json_encode($data); $ch = curl_init('http://posttestserver.com/post.php'); curl_setopt_array($ch, [ CURLOPT_POST => true, // application/x-www-form-urlencoded になるので上書き。 CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => $data_json, CURLOPT_RETURNTRANSFER => true, // CURLOPT_FILE => $fp, ]); $result=curl_exec($ch); echo 'RETURN:'.$result; curl_close($ch); $result=curl_exec($ch); $res_json = json_decode($result , true ); echo $res_json['return1'];