「PHP」の版間の差分

提供:senooken JP Wiki
(引数)
1,660行目: 1,660行目:
どれくらいの頻度で参照するか次第。参照頻度が低いなら、getで毎回設定ファイルを読み込む。多いならstaticのクラス変数にもたせる。
どれくらいの頻度で参照するか次第。参照頻度が低いなら、getで毎回設定ファイルを読み込む。多いならstaticのクラス変数にもたせる。
===Function===
===Function===
=====Return value=====
 
==== User defined ====
[https://www.php.net/manual/ja/functions.user-defined.php PHP: ユーザー定義関数 - Manual]
 
関数は以下のような構文で定義する。
<?php
function foo($arg_1, $arg_2, /* ..., */ $arg_n)
{
    echo "関数の例\n";
    return $retval;
}
?>
関数内では、他の関数やクラス定義を含む、PHPのあらゆるコードを使用可能。関数内で関数を定義できないC言語とは異なる。
 
PHPでは、変数と異なり、関数やクラスは全てグローバルスコープ。関数内で定義した関数も外部から呼び出し可能。スコープが欲しければ、無名関数を使う。
 
また、関数のオーバーロードもできない。関数をunsetしたり、再定義も不能。
 
可変引数と、デフォルト引数もある。
 
==== Argument ====
配列同様に、PHP 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";
}
デフォルト値には、定数を指定できる。PHP 8.1.0から、new ClassName記法でインスタンスも指定できる。
 
デフォルト引数は、デフォルト値のない引数の右側の必要がある。そうでない場合、省略できず、指定する意味がなくなく。
 
===== 可変引数 =====
 
===== 名前付き引数 =====
PHP 8.0.0から名前付き引数が導入された。引数の位置、順番ではなく、名前ベースで渡せる。これにより、デフォルト値を持つ引数をスキップできるし、引数の順番を意識しなくてよくなる。
 
引数の名前の後にコロン:をつけたものを値の前につけて指定する。引数の名前には予約語も使える。ただし、変数など動的には指定できない。
 
位置引数との混在もできる。その場合、名前付き引数は最後にする必要がある。
<?php
myFunction(paramName: $value);
array_foobar(array: $value);
PHP 8.1.0では、引数を...で展開した後に、名前付き引数も指定できる。ただし、展開済み引数の上書きはだめ。
function foo($a, $b, $c = 3, $d = 4) {
  return $a + $b + $c + $d;
}
var_dump(foo(...[1, 2], d: 40)); // 46
var_dump(foo(...['b' => 2, 'a' => 1], d: 40)); // 46
var_dump(foo(...[1, 2], b: 20)); // Fatal error. Named parameter $b overwrites previous argument
 
==== Return value ====
Ref: [https://www.php.net/manual/ja/functions.returning-values.php PHP: 戻り値 - Manual].
Ref: [https://www.php.net/manual/ja/functions.returning-values.php PHP: 戻り値 - Manual].



2024年9月6日 (金) 17:28時点における版

Library

Framework

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

Template

いろいろある。

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

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

高速らしい。

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

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

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

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

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

Twig

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

Smarty

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

ORM

Ref:

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

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

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

ORMは別になくてもいいか。

Migrate

  • Phinx
  • Doctrine Migrations

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

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

Test

  • PHPUnit

Search

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

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

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

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

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

About

About

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

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

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

ウェブ上にはPHPに関するTipsが多く公開されており、大抵の疑問はウェブ検索で解決できる。

Version

PHPは言語の版数が上がる際、過去の版と互換性の無い破壊的変更がなされることがある。

開発者はこのリスクを軽減するために、非推奨の言語機能を避け、実行時の警告 (warning) を適切に処理するべきだ。

GNU socialは現在PHP 7系で動作する様に記述されており、PHP 8系への対応は作業途中だ。

PHP v8

PHP v8になっていろいろ更新が入った。特にPHP v7.4からv8に更新する際のポイントがあるので整理する (行事: 「12月にPHP8.3が出るので、PHP8で増えた文法をおさらいしましょうセミナー」参加報告 | PHP8対応の肝は型とエラーレベル | GNU social JP Web)。

大きく以下2点がある。

  1. エラーレベルの上昇。
  2. 型の厳格化。

エラーレベルが1段階上がったため、今までWarningで問題なかったものがFatal Errorになって動作しなくなる。他に、型が厳格になっている。

具体的には、php.ini/.user.iniで以下を指定して、PHP v7.4時点で警告にできるだけ対応しておく。

error_reporting=E_ALL ; -1

続いて、phpソースファイルに以下を記入して型を厳密にしておく。

declare(strict_types=1);

チェックツールがあるのでこれを使うと問題箇所などがわかる。

  • PHP CodeSniffer
  • PHPStan
  • Rector

まず上記2個を試して、おまけでRectorも試すとよい。

Guide

Ref:

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

Tool

PHPをWebブラウザーで実行、動作確認のツールがいくつかある。

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

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

Libraries - Composer

自前のライブラリーを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レベルになる。

DocBlockは3部構成。

  1. Summary=短い説明。改行直前の.か空行で終わり。
  2. Description=長い説明。アルゴリズムの機能や、使用方法、例など。最初のタグか、改行、DocBlockの終端で終わる。
  3. 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)
 {
 }

以下の要素に前置できる。

  • 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の親クラスのときだけ継承される。

よく使う@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: クラスの注釈部で指定する。メンバー変数の説明。

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

基本的な使用方法を整理する。

  1. 基本的にはクラス単位で試験コードを記載。<Class>クラスの試験コードは<Class>Testの命名にする。
  2. <Class>Test はPHPUnit\Framwork\TestCaseを継承させる。
  3. 試験はpublicのtest*メソッドの命名にする。あるいは、@testのアノテーションを付ければ、命名規則に従わなくてもいい。
  4. 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();
    }
}

基本的な作り。

  1. createMock(<class>::class)で該当クラスのモックを作成。
  2. expectsに呼出回数条件のオブジェクトをセット。
  3. methodで対象メソッドを指定。
  4. withで、該当メソッドの引数処理を指定。

デフォルトで模擬実装はnullを返す。戻り値を変更したければ、will($this->returnValue())などを指定する。よく使うので短縮記法もある。

Table 8.1 Stubbing short hands
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:

クラスの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種類ある。

  1. @runInSeparateProcess
  2. phpunit --stderr

1個目のアノテーションをテストメソッドに指定すると別プロセスでの実行になる。ただ、プロセス生成は時間がかかるため、試験が多いと効率が悪い。

2個目のphpunitの出力を標準エラーに出力させる方法がシンプルで効率もいい。phpunit.xmlに stderr="true"を指定するとキー入力を省略できる。こちらで対応しよう。

Test exit

Ref:

header()後のexit()など、exit/dieを使用するコードがある。phpunit内でこれらがあると、テストも強制終了になる。

上記の別プロセスで実行していた場合、以下のエラーになる。

Test was run in child process and ended unexpectedly

対処方法がいくつかある。

  1. exitを使わないコードに変更。
  2. isTestのようなフラグを元コードに入れてテスト可否で分岐してexitを回避。
  3. execで外部プロセスで実行してexitCodeを試験。
  4. 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

PHP: 型システム - Manual

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

基本型

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

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

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

  • 交差型: 宣言した複数のクラス型をすべて満たす型。&で表現。T/U/Vの交差型はT&U&Vと書く。
  • union型: 複数の型を受け入れる型。|で表現。T/U/Vのunion型はT|U|Vと書く。交差型を含む場合、T|(X&U)と丸括弧で囲む必要がある。
alias

PHPはmixedとiterableの2個の型のエイリアスに対応している。

  • mixed=object|resource|array|string|float|int|bool|null: PHP 8.0.0で導入。mixedは型のトップ。他の全部の型はこの型の部分になる。
  • iterable=Traversable|array: PHP 7.1.0で導入。foreachで反復可能でジェネレーター内でyield from可能。

ただし、ユーザー定義のエイリアスは未対応。

Boolean

条件判定にかかってくるので非常に重要。

まずは、下記のfalseになるもの一覧を把握し、それ以外はすべてtrueになるということを把握しておく。

  • booleanのfalse
  • intの0
  • floatの0.0
  • stringの空文字列、"0"
  • 要素数0個のarray
  • null (未初期化変数含む)

stringの"0"と要素0のarrayがfalseになる点が重要。注意する。要素0のarrayは包含判定、検索などでよく使う。

stringの0ははまりどころ。stringは何がくるかわからないなら、strlenで文字数を見たほうが確実。

Strings

Ref: PHP: 文字列 - Manual.

非常に重要。

Literal

文字列リテラルとしては4の表現がある。

  • Single quote: '' 変数展開されない。
  • Double quote: "" 変数展開される。
  • Here document: <<<EOT 二重引用符扱いで変数展開される。
  • Nowdoc: <<<'EOT' 一重引用符扱いで変数展開されない。

引用符内で引用符'を使う場合はバックスラッシュ\でエスケープが必要。バックスラッシュ自体の指定は二重\\。

Here document/Nowdocは終端IDのインデントで行頭を識別しており、インデントに意味があるので注意する。

echo <<<END
      a
     b
    c
\n
END;
echo <<<'EOT'
My name is "$name". I am printing some $foo->foo.
Now, I am printing some {$foo->bar[1]}.
This should not print a capital 'A': \x41
EOT;
Parse

二重引用符とヒアドキュメントではエスケープシーケンスが解釈され、変数が展開される。

<?php
$juice = "apple";

echo "He drank some $juice juice." . PHP_EOL;

// 意図しない動作をします。"s" は、変数名として有効な文字です。よって、変数は $juices を参照しています。$juice ではありません。
echo "He drank some juice made of $juices." . PHP_EOL;

// 参照する変数名を波括弧で囲むことで、変数名の終端を明示的に指定しています。
echo "He drank some juice made of {$juice}s.";

// 
$juices = array("apple", "orange", "koolaid1" => "purple");
echo "He drank some $juices[0] juice.".PHP_EOL;
echo "He drank some $juices[1] juice.".PHP_EOL;
echo "He drank some $juices[koolaid1] juice.".PHP_EOL;

// 複雑な例1

// これが動作しない理由は、文字列の外で $foo[bar]
// が動作しない理由と同じです。
// PHP はまず最初に foo という名前の定数を探し、
// 見つからない場合はエラーをスローします。
// 定数が見つかった場合は、その値('foo' そのものではない)
// を配列のインデックスとして使います。
echo "This is wrong: {$arr[foo][3]}"; 

// 動作します。多次元配列を使用する際は、
// 文字列の中では必ず配列を波括弧で囲むようにします。
echo "This works: {$arr['foo'][3]}";

// 複雑な例。二重展開で変数になる場合だけ式が使える模様。
$var1=9;
echo "{${mb_strtolower('VAR1')}}"; // 9

?>

波括弧はなくてもいいが、文字列が連結するなどして変数名の終端を区別できない場合に必須になる。

特に重要な挙動は以下。

  • ""内だと、連想配列添字の引用符不能。
  • ${}内だと、連想配列添字の引用符必要。

複雑な形式は{$ ... }がセット。{$ } 部分で変数が式扱いになる。

さらに複雑なことができる。{${}}を指定すると、内側の波括弧内で、${}部分が変数評価になる場合にだけ式を指定できる。動きがトリッキーすぎる。フォーマット文字列的なことには使えない。バグのもとになりそうなので使用を控えたほうがよさそう。

Format

Ref:

PHPにはPythonのformatメソッド相当はない。が似たような目的の関数がある。

  • sprintf/vprintf
  • strtr

strtrは第2引数にold => newの置換のペアの配列を渡す。やることは同じようなものだけどちょっと違う。

vsprintfは置換対象が可変長引数ではなく配列なだけ。

sprintf

Ref: PHP: sprintf - Manual

今後何度も使う。

C言語のprintfといろいろ違うところがある。

<?php
$format = 'The %2$s contains %1$d monkeys.
           That\'s a nice %2$s full of %1$d monkeys.';
echo sprintf($format, $num, $location);

echo sprintf("%'.9d\n", 123); // ......123
echo sprintf("%'.09d\n", 123); // 000000123

?>

特徴的なのが`%数$指定子`で引数の番号を選べるところ。Pythonの`{数:指定子}`に似ている。

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

文字列の切り出し

いくつか方法がある。

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

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

文字列置換
  • str_replace
  • substr_replace: 日本語不能。
  • preg_replace
  • explode/implode
substr_replace($text, '', -1);
substr_replace($text, '.', nb_strrpos('_'));

1文字などの置換ならmb_strrposとの組み合わせ。

		$query = $request->query();
		foreach ($query as $key => $value) {
			unset($query[$key]);
			$keys = explode('_', $key);
			$key = implode('_', array_slice($keys, 0, -1)) . '.' . $keys[count($keys)-1];
			$query[$key] = $value;
		}

explode/implodeが無難で確実。

startsWith/endsWith

PHP 8なら「PHP: str_starts_with - Manual/PHP: str_ends_with - Manual」がある。

PHP 8未満なら以下のようなコード。

function startsWith( $haystack, $needle ) {
     $length = strlen( $needle );
     return substr( $haystack, 0, $length ) === $needle;
}
function endsWith( $haystack, $needle ) {
    $length = strlen( $needle );
    if( !$length ) {
        return true;
    }
    return substr( $haystack, -$length ) === $needle;
}

mb_strlen/mb_substrでマルチバイト対応。

改行分割

explode('\n', $csv) のようなことをしたくなるが、改行が\nとは限らない。

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

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

trim

PHP: trim - Manual

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

Array

Create

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

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: 非推奨。
Append

配列要素の追加。

$arr[キー] = 値;
$arr[] = 値;

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のほうが高い。

基本は$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は詰めたりしない。

Copy

How to clone an array of objects in PHP? - Stack Overflow

配列変数を代入すると通常はそれでコピーになる。ただし、配列にオブジェクトがあると、そのオブジェクトはシャローコピーになる。

$new = array();

foreach ($old as $k => $v) {
    $new[$k] = clone $v;
}

上記のように配列要素をcloneでコピーして作る必要がある模様 (PHP: オブジェクトのクローン作成 - Manual)。

Comma

PHPでは配列の終端カンマは許容される。

他にも、名前空間のグループ指定はPHP7.2以上、関数の引数はPHP7.3以上で可能になった。

連想配列判定

配列か連想配列か判定する #PHP - Qiita

<?php
if (array_values($arr) === $arr) {
  echo '$arrは配列';
} else {
  echo '$arrは連想配列';
}

これで添え字が、数字かどうかをみるのがいい模様。

Convert
連想配列→単純配列

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]] 形式。

後者のパターンはそれなりに使う気がする。

単純配列→連想配列

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

いくつか方法がある。

  1. array_combine
  2. array_fill_keys
  3. foreach
  4. array_flip
$ar = ['a', 'b'];
$ar2 = array_combine($ar, $ar);
var_dump($ar2);

array_combineがシンプル。array_fill_keysは0初期化などしたい場合。

CSV→連想配列

CSVを、よくDBの取得結果の形式の、行単位連想配列に変換する。方法がいくつかある。

<?php 
$csv = array_map('str_getcsv', file($file));
if (count($csv) && !count($csv[count($csv)-1])) unset($csv[count($csv)-1]);
array_walk($csv, function(&$a) use ($csv) {$a = array_combine($csv[0], $a);}); 
array_shift($csv); # remove column header 
?>
$rows = array_map('str_getcsv', file('myfile.csv'));
$header = array_shift($rows);
$csv = array();
foreach ($rows as $row) {
  $csv[] = array_combine($header, $row);
}

1番目の方法がシンプル。

Search

【PHP入門】配列の値を検索するarray_searchと他4つの関数 | 侍エンジニアブログ

いくつか方法がある。

基本はin_array。

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

空確認

PHP 配列が空かどうかを判定する #初心者 - Qiita

emptyで確認できる。が、単にnullなどの場合も判定してしまう。null or emptyという意味ならemptyでもOK。

逆に、issetであることと、nullではないことを確認できる。

array_search

array_search() - 指定した値を配列で検索し、見つかった場合に対応する最初のキーを返す

全てのキーが必要なら、array_keysにfilter_valueを指定する。

in_array

in_array — 配列に値があるかチェックする

in_array(mixed $needle, array $haystack, bool $strict = false): bool

haystack 内の needle を検索します。 strict が設定されていない限りは型の比較は行いません。

基本は$strict=trueで指定したほうがいい。完全一致検索。

any/all/some/every

Is there a PHP equivalent of JavaScript's Array.prototype.some() function - Stack Overflow

配列に対する、1個または全部の評価。

JavaScriptのsome/every相当。

PHP 8.4ならarray_any/array_allが存在する。

PHP 8.4未満なら、いくつか方法がある。

function array_any(array $array, callable $fn) {
    foreach ($array as $value) {
        if($fn($value)) {
            return true;
        }
    }
    return false;
}

function array_every(array $array, callable $fn) {
    foreach ($array as $value) {
        if(!$fn($value)) {
            return false;
        }
    }
    return true;
}
function array_some(array $data, callable $callback) {
    $result = array_filter($data, $callback);
    return count($result) > 0;
}

$myarray = [2, 5, 8, 12, 4];
array_some($myarray, function($value) {
    return $value > 10;
}); // true

foreachで途中で終わるほうが速い模様。

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

1個でも入っているかを見たければ、array_intersect (PHP: array_intersect - Manual) がこの目的に合致する。

$peopleContainsCriminal = !empty(array_intersect($people, $criminals));
$peopleContainsCriminal = array_intersect($people, $criminals);

$criminalsの配列に、$peopleの要素のいずれかが入っているかを上記で判断できる。

array_intersectは1個目の配列要素の内、2個目の存在要素を返す (交差)。交差があれば、1個はあるという意味で、any/someになる。

全部の包含判定したい場合、array_diff (PHP: array_diff - Manual) でできる。

$containsAllValues = !array_diff($search_this, $all);

array_diffはarray_intersectと異なり、1個目の配列要素の内、2個目の不在要素を返す (差分)。なので、空なら全包含となる。非空なら非全包含=some。

完全一致なら、===でOK。

ポイントとしては、1個目の要素は要素数が少ない配列を指定したほうが速くなる。判定だけで、速度が重要なら、foreachで見つかったらすぐreturnしたほうが速い。

array_intersectが実行結果とboolが同じ向きなので、これを使うとわかりやすいだろう。

Array Functions
compact

Ref: PHP: compact - Manual.

変数名とその値から、配列を作る。extractの逆。

MVCのViewに複数の値を渡す場合などによく使う。

extract

Ref: PHP: extract - Manual.

配列のキー・バリューを変数として取り込む。

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_flip

PHP: array_flip - Manual

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

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

Type declarations/型宣言

PHP: 型宣言 - Manual

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

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

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

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

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

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

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

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

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

Type juggling

PHP: 型の相互変換 - Manual

型の相互変換。非常に重要。

いろいろ方法がある。

共通なのはキャスト (cast)。

<?php
$foo = 10;   // $foo は整数です
$bar = (bool) $foo;   // $bar は boolean です
$fst = "$foo"; // to string.
?>

C言語と同じで (型) を前置する。ただし、少々長い。

文字列への変換は二重引用符囲などもOK。まあ、キャストだけ覚えておくのがシンプル。

Other

型判定

PHP: gettype - Manual

PHPでの型確認・判定方法がいくつかある。

  • gettype: 変数の型を文字列で返す。boolean/integer/double/string/array/object/resouce/resource (closed) (PHP v7.2.0以上)/NULL/ unknown type
  • get_class: オブジェクトのクラス名
  • get_debug_type: 変数の型名をデバッグしやす形で取得。
  • is_型名: is_array/is_bool/is_callable/is_float/is_int/is_null/is_numeric/is_object/is_resoure/is_scalar/is_string/function_exists/method_exists

基本はis_型名だろう。

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

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

全てのスコープで使用可能な組込変数。関数、メソッド内でもglobal $variable;とする必要がない。

  • $GLOBALS: グローバル変数の連想配列。
  • $_SERVER
  • $_GET
  • $_POST
  • $_FILES
  • $_COOKIE
  • $_SESSION
  • $_REQUEST
  • $_ENV

Variables From External Sources

Ref:

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

PHP: constant - Manual

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

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

class

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

predefined

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

Magic/マジック定数

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

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

どれもよく使う。

Operators

PHP: Operators - Manual

Assignment/代入演算子

??=

    // NULL合体代入演算子
    $id ??= getId();

    // これと同じ
    $id = $id ?? getId();
    $id = @$id ?: getId();
    $id = isset($id) ? $id : getId();

NULL合体演算子の代入版。nullの場合の代入が簡単になった。PHP 7.4から使用可能。

Comparison/比較演算子

三項演算子 (条件演算子)

if/elseの短縮表記。デフォルト値の設定などでよく使う。

<?php
// 三項演算子の使用例
$action = (empty($_POST['action'])) ? 'default' : $_POST['action'];

// 上記は以下の if/else 式と同じです。
if (empty($_POST['action'])) {
  $action = 'default';
} else {
  $action = $_POST['action'];
}
?>

PHP特有事項として、真ん中を省略できる。その場合、1個目がtrueならそれがそのまま戻る。JavaScriptとかC系言語でも真ん中は省略できない。

expr1 ?: expr3 の結果は、expr1 が true と同等の場合は expr1、 それ以外の場合は expr3 となります。 この場合、expr1 は一度だけ評価されます。

条件演算子のネストはわかりにくいので推奨されない。が、条件演算子の省略形は安定している。false以外の最初の引数を評価する。

<?php
echo 0 ?: 1 ?: 2 ?: 3, PHP_EOL; //1
echo 0 ?: 0 ?: 2 ?: 3, PHP_EOL; //2
echo 0 ?: 0 ?: 0 ?: 3, PHP_EOL; //3
echo $undefinedVariable ?? false ?: 'false default';
?>

NULL合体演算子はnullの時のデフォルト値になるが、こちらはfalseの場合のデフォルト値設定。意味が違う。未定義変数アクセスをガードできないが、それ以外であれば条件演算子の短縮表記のほうをよく使う。非常に重要。

Null合体演算子を組み合わせて、未定義のなどの場合のデフォルト値設定で役立つ。

PHP: 論理演算子 - Manual

elvis演算子と呼ばれることもある模様。

It also combines nicely with the ?? operator, which is equivalent to an empty() check (both isset() and `!= false`):

$x->y ?? null ?: 'fallback';

instead of:

empty($x->y) ? $x->y : 'fallback'
Null 合体演算子/Null collapsing operator
<?php
// $_GET['user'] を取得します。もし存在しない場合は
// 'nobody' を用います。
$username = $_GET['user'] ?? 'nobody';
// 上のコードは、次のコードと同じ意味です。
$username = isset($_GET['user']) ? $_GET['user'] : 'nobody';

$var ?? 'value = isset($var) ? $var : 'value';

変数がnullの場合のガードの簡易記法。PHP v7.0.0で追加。非常に便利。

Error Control/エラー制御演算子@

式の直前に@を前置すると、その式のエラーメッセージを無視する。

基本的に使わないほうがいい。if文などでガードするより処理が遅い。ただし、Viewなどであまり影響ない場合などは記述がシンプルになるという利点もあるかも。

ただ、やっぱり基本は使わないほうがいい。バグの見落としになる。

Logical/論理演算子

PHP: 論理演算子 - Manual

論理積と論理和が、and/orと&&/||で2種類存在する。演算子の優先順位が違う。

// $g に代入されるのは、(true && false) の評価結果です
// これは、次の式と同様です: ($g = (true && false))
$g = true && false;

// $h に true を代入してから "and" 演算子を評価します
// これは、次の式と同様です: (($h = true) and false)
$h = true and false;

なお、PHPの論理演算子は、常に論理値 (true/false) を返すので注意する。

$a = $var || 'default';

上記のように、デフォルト値の代入扱いでor演算子を使うことはできない。同じ論理型同士なら成立はするが。

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

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

関数と配列の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のほうが速くてメモリーも少ないとのこと。

Control Structures

Source: PHP: Control Structures - Manual.

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

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

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

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

elseif/else if

PHP: elseif/else if - Manual

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

for/foreach

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

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

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

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

foreach(iterable_expression as &$value)

declare

Source: PHP: declare - Manual.

PHPUnitのサンプルコード (Getting Started with Version 9 of PHPUnit – The PHP Testing Framework) などで冒頭に以下の記述がある。

<?php declare(strict_types=1);

これの意味が分かっていなかったので整理する。

declare文 (construct) は、コードブロックの実行指令となる。以下の構文となる。

declare (<directive>)
  <statement>

<directive> はdeclareブロックの挙動を指示する。指定可能なものは以下3個だ。

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

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

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

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

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

return

PHP: return - Manual

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

returnで引数を省略すると、戻り値はnullになる。

呼び出し方法、場所で挙動が変わる。

  • 関数/eval内: 即座に関数を終了し、引数を関数の値として返却。
  • グローバルスコープ: スクリプト自体を終了。
  • include/require内: 呼び出し元のファイルに制御を戻す。includeの場合、引数はincludeの戻り値になる。

return文は関数ではないので、引数の括弧は不要。紛らわしいのでないほうがいい。

include内で使えるというのがみそ。config.phpでreturnだけした設定一覧を記述しておいて、includeで変数に取り込むというのをよくやる。

require/include/require_once/include_once

Basic

includeは指定したファイルを読み込み評価する。絶対パスで指定しない場合、include_pathの設定を利用する。include_pathにもなければ現在ディレクトリーも探す。

絶対パス、相対パスの前置があると、include_pathは無視する。

ファイルが読み込まれると、ファイル内のコードは、includeが実行された行の変数スコープを継承する。つまり、呼び出し行で利用可能な全変数がファイル内でも使用可能。ファイル内で定義された関数やクラスはすべて、グローバルスコープになる。ただし、includeが関数定義内に配置されたら、コードは関数内で定義されているとみなす。

ファイルの読込時にはHTMLモードになる。そのため、ファイル内でPHPコードを実行するなら、<?php ?>で囲む必要がある。

includeに失敗したらFALSEを返し、E_WARNINGを発生させる。成功したら、戻り値は1。ただし、ファイル内でreturnを実行したら、その値を返す。

includeは特別な言語構造のため、引数に括弧は不要。結果を評価したいならば、全体を括弧で囲む。

// 動作します。
if ((include 'vars.php') == TRUE) {
    echo 'OK';
}
require/include

requireはincludeとほぼ同じ。違いは、失敗時にE_COMPILE_ERRORが発生して処理を中断する点。includeはE_WARNINGで処理は継続する。

使い分けとして、変数読込などで読み込めなくても処理を進めて問題ない場合に、include。

関数定義など、絶対必要なものはrequireなど。

_once

読込済みなら、再読込しない点がinclude/requireとの決定的な違い。関数の複数定義のエラーを回避できたりする。

読み込めたらtrueを返す。

config.php

includeとreturnの組み合わせのconfig.phpの設定ファイルをいろんなアプリで使われている。

<?php
return [
    'name' => 'hoge',
    'value' => 'fuga',
];
?>
<?php

// configファイルを変数に代入
$config = include __DIR__ . '/config.php';

// 呼び出し。
var_dump($config['name']);

?>

こういう形式。このreturnだけの文は、ほぼinclude前提。

編集対象のアプリの設定を、既存コードと分離する際に、いい方法。

config.phpをアプリ内で作りたい場合、「How to create Dynamically create config/custom.php config file」にあるように、var_exportを使うとよい。

// create the array as a php text string
$text = "<?php\n\nreturn " . var_export($myarray, true) . ";";
config class

config.phpをどう用意するかは議論がある。

include/returnではなくて、クラスのconst定数にするという。

  • クラスのconst定数
  • iniファイル/parse_ini_file

他に、configクラスを用意しておいて、シングルトンか、staticメソッドで参照する形。

どれくらいの頻度で参照するか次第。参照頻度が低いなら、getで毎回設定ファイルを読み込む。多いならstaticのクラス変数にもたせる。

Function

User defined

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

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

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

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

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

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

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

Argument

配列同様に、PHP 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";
}

デフォルト値には、定数を指定できる。PHP 8.1.0から、new ClassName記法でインスタンスも指定できる。

デフォルト引数は、デフォルト値のない引数の右側の必要がある。そうでない場合、省略できず、指定する意味がなくなく。

可変引数
名前付き引数

PHP 8.0.0から名前付き引数が導入された。引数の位置、順番ではなく、名前ベースで渡せる。これにより、デフォルト値を持つ引数をスキップできるし、引数の順番を意識しなくてよくなる。

引数の名前の後にコロン:をつけたものを値の前につけて指定する。引数の名前には予約語も使える。ただし、変数など動的には指定できない。

位置引数との混在もできる。その場合、名前付き引数は最後にする必要がある。

<?php
myFunction(paramName: $value);
array_foobar(array: $value);

PHP 8.1.0では、引数を...で展開した後に、名前付き引数も指定できる。ただし、展開済み引数の上書きはだめ。

function foo($a, $b, $c = 3, $d = 4) {
  return $a + $b + $c + $d;
}

var_dump(foo(...[1, 2], d: 40)); // 46
var_dump(foo(...['b' => 2, 'a' => 1], d: 40)); // 46

var_dump(foo(...[1, 2], b: 20)); // Fatal error. Named parameter $b overwrites previous argument

Return value

Ref: PHP: 戻り値 - Manual.

関数はreturn文で値を返せる。そこで処理を終了する。

returnを省略した場合、nullを返す。

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

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

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

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

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

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

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

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

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

callableの型。非常に重要。

// "use" がない場合
$example = function () {
    var_dump($message);
};
$example();

// $message を引き継ぎます
$example = function () use ($message) {
    var_dump($message);
};

useを指定した場合だけ、親のスコープから変数を引き継げる。変数は関数定義時の値。


Classes and Objects

The Basics

Ref: PHP: The Basics - Manual.

class

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

class内の関数などで、これらのプロパティー、メソッド類の参照時は、$this->経由で参照する必要がある。

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

::class

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

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

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

Property

Ref: PHP: プロパティ - Manual.

クラスのメンバー変数のことをプロパティー (property) とPHPでは呼んでいる。

クラス内で、1以上のキーワード (アクセス権、static、PHP 8.1.0以後のみreadonly) のあとに、オプション型宣言 (PHP 7.4以後、readonly以外) の後に変数宣言を続ける。

public $var1
static $var2
var $var3

staticなど、アクセス権を指定しない場合、publicとデフォルトでみなされる。なお、varキーワードを使う方法もある。これはPHP4までのプロパティーの宣言方法。PHP5以後はpublicと同じ意味になる (What does PHP keyword 'var' do? - Stack Overflow)。

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

以下のエラーが出る。

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

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

Autoloading Classes

Ref:

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

  1. require_once()/require()/include: シンプルなファイル読み込み。PHP 4から。
  2. __autoload(): 非推奨。PHP 5.0で登場。
  3. spl_autoload_register(): PHP標準。PHP 5.1.0で登場。
  4. composer autoload: composer。

C系言語であれば、includeなどで外部ファイルをそのまま自分のファイルに読み込む。PHPでもrequire_onceなどで似たようなこともできる。が、PHPではこれをクラスごとに記述するのが煩雑だとして、自動で読み込む仕組みがいくつかある。

GNU socialでも <https://notabug.org/gnusocialjp/gnusocial/src/main/lib/util/framework.php> でspl_autoload_registerを使っている。

基本的にはcomposerのautoloadかPHP標準のspl_autoload_registerの2択になっている。

基本的な使用方法。

<?php
spl_autoload_register(function ($class_name) {
    include $class_name . '.php';
});

$obj  = new MyClass1();
$obj2 = new MyClass2(); 
?>

MyClass1.php MyClass2.phpから該当クラスを自動読み込みする。

該当クラスを使おうとしたときに、spl_autoload_registerに登録した関数が呼ばれる模様。

spl_autoload_registerは、指定した関数を__autoload()の実装として登録する。順番に登録する。

spl_autoload_register(?callable $callback = null, bool $throw = true, bool $prepend = false): bool

callback: callback(string $class): void 。重要。nullを指定するとデフォルトのspl_autload()が登録される。$classにはクラスの完全修飾子が入る。

このcallback内で独自のrequire_once相当をいろいろ指定する形になる。

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

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

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

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

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

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

Traits

PHP: トレイト - Manual

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

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

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

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

Other

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

基本は名前空間付きのフルパスでの取得。クラス名だけだとgetShortName()

Reserved

keywords

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

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

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

* readonly は、関数名として使用できます。

Interfaces

PHP: 定義済みのインターフェイスとクラス - Manual

stdClass

PHP: stdClass - Manual

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

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

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

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

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

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

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.

簡単なコードの確認などでPHPをコマンドラインなどから簡単に実行したいことがよくある。いくつか方法がある (PHP: Usage - Manual)。

  1. phpコマンドの引数にファイルを指定: php file.php/php -f file.php
  2. phpコマンドの引数kにコードを指定: php -r 'print_r(get_defined_constants());'
  3. phpコマンドに標準入力で読み込み: php <file.php

標準入力が一番使いやすく感じる。

Function Reference

Affecting PHP's Behavior

Error Handling

PHP: エラー処理 - Manual

Runtime Configuration

PHPのエラー設定を整理する。 PHPのエラー設定は「PHP: Runtime Configuration - Manual」で一覧化されている。

xmlrpc_errors, syslog.facility, syslog.ident以外はどこでも設定可能。

特に重要なのが以下の設定。

設定 初期値 説明
error_reporting E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED エラー出力レベルを設定する。開発時にはE_ALL (2147483647/-1) にしておくとよい。
display_errors "1" エラーのHTML出力への表示を設定する。"stderr"を指定すると,stderrに送る。デフォルトで有効なのでそのままでいい。
display_startup_errors "0" PHPの起動シーケンス中のエラー表示を設定する。デバッグ時は有効にしておいたほうがいい。
log_errors "0" エラーメッセージのサーバーのエラーログまたはerror_logへの記録を指定する。これを指定しないとログが残らないため,常に指定したほうがいい
error_log NULL スクリプトエラーが記録されるファイル名を指定する。syslogが指定されると,ファイルではなくシステムロガーに送られる。Unixではsyslog(3)で,Windowsではイベントログになる。指定されていない場合,SAPIエラーロガーに送信される。ApacheのエラーログかCLIならstderrになる。

基本的に以下をphp.ini/.user.iniに設定しておけばよい。

error_reporting = E_ALL
display_startup_errors = on
log_errors = on

; For file
display_errors = stderr

htpd.conf/.htaccessの場合は以下。

php_value error_reporting -1
php_flag display_startup_errors on
php_flag log_errors on

# For file
php_value display_errors stderr
logrotate

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

いくつか方法がある。

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

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

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

それだけ。

const LOG_DIRECTORY = '/var/log/logdir/';           // ログディレクトリ
const LOG_FILENAME  = 'logfname.log';               // ログファイル名
const LOG_FILEPATH  = LOG_DIRECTORY.LOG_FILENAME;   // ログのファイルパス
const MAX_LOTATES   = 3;                            // ログファイルを残す世代数
const MAX_LOGSIZE   = 1024*1024;                    // 1ファイルの最大ログサイズ(バイト)

function WriteLog($strlog){

    // 保存先ディレクトリを作成
    if(!file_exists(LOG_DIRECTORY)){
        mkdir(LOG_DIRECTORY);
    }

    // ログのローテート
    if(@filesize(LOG_FILEPATH) > MAX_LOGSIZE){

        // 最古のログを削除
        @unlink(LOG_FILEPATH.strval(MAX_LOTATES));

        // ログをリネーム .log → .log_01
        for ($i = MAX_LOTATES - 1; $i >= 0; $i--) {
            $bufilename = ($i == 0) ? LOG_FILEPATH : LOG_FILEPATH.strval($i);
            @rename($bufilename, LOG_FILEPATH.strval($i+1));
        }
    }

    // ログ出力 
    file_put_contents(LOG_FILEPATH, date('y-m/d-H:i:s ').$strlog."\n", FILE_APPEND | LOCK_EX);
}

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

Calendar

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

time/速度計測

PHPで処理速度などを計測したいことがある。基本は処理前後のタイムスタンプの差分で、どの言語でも共通の論理だが、いくつか方法がある。

microtimeとhrtimeの2個の関数をタイムスタンプの取得で使える。hrtimeはPHP 7.3.0以上で使用可能。単位ナノ秒。問題なければ、こちらが推奨されている。HRTime (High Resolution Time) の拡張のモジュールと関係する関数とのこと。

両方とも、引数にtrueを指定して、floatで取得するのが基本。

<?php
$start = hrtime(true); // 計測開始時間

// 計測したい処理

$end = hrtime(true); // 計測終了時間

// 終了時間から開始時間を引くと処理時間になる
echo '処理時間:'.($end - $start).'ナノ秒' . PHP_EOL; 
?>

PHPのバージョンを気にするのが嫌なので、ラップするとよい。

<?php
/**
 * Time target function.
 * @param callable $callback Target function.
 * @return int|float Run time [ns].
 */
function timeit(callable $callback)
{
    $time = 'microtime';
    $nanoFactor = 1000;
    if (function_exists('hrtime')) {
        $time = 'hrtime';
        $nanoFactor = 1;
    }

    $start = $time(true);
    $callback();
    $stop = $time(true);
    return ($stop - $start) * $nanoFactor;
}

echo timeit(function(){sleep(1)});

// one liner.
(function($c){$s=hrtime(true);$c();return hrtime(true)-$s;})(function(){});

(function($c){$s=hrtime(true);$c();return hrtime(true)-$s;})(function(){sleep(1)});
?>

こんな感じ。

File system

Ref: PHP: ファイルシステム - Manual.

File system

io
  • file_get_contents — ファイルの内容を全て文字列に読み込む
  • file_put_contents: データをファイルに書き込む。戻り値に書き込みバイト数を返す。失敗したらfalse。成否は完全一致===falseで。
  • PHP: file - Manual: ファイル全体を読み込んで改行区切りで配列にする。

上記2個の非常に重要な入出力関数がある。

バイナリーやHTTP GETに対応している。アップロードされたファイルの読み込みなどでお世話になる。

check

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

path

パス関係の操作。重要。

Directory

Constants

PHP: 定義済み定数 - Manual

  • DIRECTORY_SEPARATOR: Windowsなら\、それ以外/らしい。
ディレクトリー以下のファイル一覧

方法がいくつかある。

  • 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

mbstring

mb_substr

Ref:

mb_substr(

    string $string,

    int $start,

    ?int $length = null,

    ?string $encoding = null

): string

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

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

Text

Strings

strpos

Ref: PHP: strpos - Manual.

strpos(string $haystack, string $needle, int $offset = 0): int|false

文字列 haystack の中で、 needle が最初に現れる位置を探します。

include相当。よく使う。

戻り値に注意。有無の確認時は、strpos() !== falseの厳密一致でチェックする必要がある。

echo/print/printf

PHPの出力関数群。よく使うが、扱いが特殊なので整理する。

まず、echo/printは関数ではなく、if/forなどと同じ言語構造、キーワード扱い。丸括弧はなくてもいい。紛らわしいのでないほうがいい。

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

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

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

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

Variable

print_r/var_export/var_dump

PHPの変数の内容を、配列やオブジェクトなどの複雑なデータ型でも表示、出力可能なのがprint_r/var_export/var_dump。微妙に違いがある。

  • var_dump: これのみ型情報がある。
  • var_export: 変数の文字列表現。config.phpなどファイルに出力すればそのままinclude可能。
  • print_r: 基本は閲覧用の文字列。
  • print_r/var_export: 第二引数をtrueにすると、画面出力ではなく、戻り値に返す。
$text = "<?php\n\nreturn " . var_export($myarray, true) . ";";

使うとしたら、var_exportとvar_dumpだと思われる。

var_dumpは型の情報が詳しい。詳細な情報が欲しい場合、var_dump。そうでない、単に見たいだけなら、var_exportで十分。

var_dumpは表示のみ。出力制御関数を使えば、文字列に保存はできるが、基本はデバッグ表示用。

<?php
$data = array(
    "A" => "Apple",
    "B" => "Banana",
    "C" => "Cherry"
);

echo "---print_r---\n";
print_r($data);

echo "---var_export---\n";
var_export($data);

echo "---var_dump---\n";
var_dump($data);
?>
---print_r---
Array
(
    [A] => Apple
    [B] => Banana
    [C] => Cherry
)

---var_export---
array (
  'A' => 'Apple',
  'B' => 'Banana',
  'C' => 'Cherry',
)


---var_dump---
array(3) {
  ["A"]=>
  string(5) "Apple"
  ["B"]=>
  string(6) "Banana"
  ["C"]=>
  string(6) "Cherry"
}

画面出力でデバッグしたいならば、前後でpre要素を出力させる。

<?php
function print_r2($val){
        echo '<pre>';
        print_r($val);
        echo  '</pre>';
}
?>
var_exportの変数取込

var_exportの文字列表現。config.phpに出力して、Includeするほかに、テキストを変数にしたいことがある。

素直にevalする。

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

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

Process

exec

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

exec/shell_exec/system/passthru

外部プログラム実行のための3の関数がある。違いがある。

  • shell_exec: 標準出力をstringで返す。バッククオート演算子``と同じ。プログラムの成功可否、終了コードは判断不能。
  • exec: 既定で標準出力の最後の1行のみ返す。第二引数に配列を指定すれば、行区切りの配列で返すこともできる。
  • passthru: Unixコマンドの出力がバイナリーで、ブラウザーに直接バイナリーを返す場合に、exec/systemの代わりに使う。戻り値は?falseで標準出力に直接結果を出力する。
  • system: C言語のsystem関数に類似。system()でコマンドを実行して出力する。成功時にコマンド出力の最終行を返す。出力をファイルや別のストリームにリダイレクトしないと、終了までPHPが止まる。
項目 exec passthru shell_exec/`` system
出力 - x - x
終了コード x x - x
戻り値 最終行 null/false 全行 最終行
全行取得 x - x -
用途 外部コマンドの結果文字列取得 (終了チェックあり) 画像応答 外部コマンドの結果文字列取得 文字応答

基本はshell_exec/`` execで十分。

exec

PHP: exec - Manual

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

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

escapeshellarg/escapeshellcmd

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

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

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

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

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

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

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

JSON

PHP: JSON - Manual

About

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

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

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

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

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

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

Other

eval

PHP: eval - Manual

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

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

Topic

HTTP

PHPでHTTP通信をする方法がいくつかある。

  • file_get_contents
  • curl

file_get_contentsはPHP標準。curlは外部ライブラリー。

PHP cURL vs file_get_contents - Stack Overflow」などを見る限り、GET以外はcurlのほうが速くて複雑なことができるらしい。

file_get_contentsは元々ローカルや内部ファイルの読み込み用らしい。

GNU socialではHTTPClientクラス経由で実現するので、内部実装を意識する必要はない。

curlでのリクエスト方法を覚えておくと、汎用性が高い模様。

cURL

基本的な使用方法。

  1. curl_initでurlを指定してセッション初期化。
  2. curl_setopt/curl_setopt_arrayでオプションを設定。
    1. CURLOPT_POST=true/CURLOPT_POSTFIELDSでPOST関係指定。
    2. CURLOPT_RETURNTRANSFER=trueでcurl_execの応答ボディーをテキストで取得。
  3. curl_execで転送実行。CURLOPT_RETURNTRANSFER=trueを指定しない場合、true/falseのみ。
  4. curl_closeでセッション終了。
<?php

$ch = curl_init("http://www.example.com/");
$fp = fopen("example_homepage.txt", "w");

curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, 0);
// curl_setopt_array($ch, $options);

curl_exec($ch);
if(curl_error($ch)) {
    fwrite($fp, curl_error($ch));
}

$info    = curl_getinfo($ch);
$errorNo = curl_errno($ch);

curl_close($ch);
fclose($fp);
?>
<?php

/* curlセッションを初期化する */
$ch = curl_init();

/* curlオプションを設定する */
curl_setopt($ch, CURLOPT_URL, "http://www.google.com/");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

/* curlを実行し、その内容を$result変数に保存 */
$result = curl_exec($ch);

/* curlセッションを終了する */
curl_close($ch);

/* result変数に保存した内容を表示 */
echo htmlspecialchars($result);


PHP: 定義済み定数 - Manual

特に重要なオプションがいくつかある。

  • CURLOPT_POST: trueならHTTP POST (application/x-www-form-urlencoded)。
  • CURLOPT_POSTFIELDS: POSTのリクエストボディー。文字列で渡すか、連想配列。連想配列の値が配列の場合、Content-Type: multipart/form-dataになる。ファイル送信はCURLFile (ファイル名) かCURLStringFile (ファイルの中身)を使う。
  • CURLINFO_HEADER_OUT => true: curl_getinfoにリクエストヘッダーを含める (web services - How to get info on sent PHP curl request - Stack Overflow)。

PHP: curl_getinfo - Manual

curl_getinfoで応答結果の詳細を確認できる。

特に以下は重要。

  • http_code
JSONデータの送受信

JSON形式のデータをPOST送受信する方法(PHP) | 合同会社スマート

  1. 連想配列でリクエストボディーのデータを作って、json_encodeでJSON文字列に変換。
  2. ヘッダーContent-Type指定: curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
  3. リクエストボディー指定: curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json);
  4. 戻り値のデコード: $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();
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, 'http://posttestserver.com/post.php');
$result=curl_exec($ch);
echo 'RETURN:'.$result;
curl_close($ch);

$result=curl_exec($ch);
$res_json = json_decode($result , true );
echo $res_json['return1'];