「PHP」の版間の差分

提供:GNU social JP Wiki
(Topic)
(foreach)
384行目: 384行目:
=== Control Structures ===
=== Control Structures ===
Source: [https://www.php.net/manual/en/language.control-structures.php PHP: Control Structures - Manual].
Source: [https://www.php.net/manual/en/language.control-structures.php PHP: Control Structures - Manual].
==== for/foreach ====
foreachは配列の反復処理のための制御構造。
foreach(iterable_expression as $value)
foreach(iterable_expression as $key => $value)
$keyも使いたい場合、2番目の形式を使う。
ループ中に$valueの要素を直接変更したい場合、&をつけておく。
foreach(iterable_expression as &$value)


==== declare ====
==== declare ====

2024年2月3日 (土) 21:31時点における版

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

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

Variables

Variable scope

出典: PHP: Variable scope - Manual

関数の外で使用するとグローバルスコープになる。ただし、関数内では暗黙にはグローバル変数は使えない。未定義変数扱いになる。

なお、波括弧のブロックスコープは存在しない。

関数内でグローバル変数を参照したければ、関数内でglobalで明示的に使用したいグローバル変数を宣言する必要がある。

あるいは、$GLOBALS配列にグローバル変数が入っているのでこれを使う。

C系言語の感覚だと、波括弧でスコープが作られそうなイメージがあるが、PHPの波括弧はスコープを作らない。あくまで、関数の内部かどうか。

逆にいうと、関数内に定義される関数・クラスも基本グローバル。

子関数に変数を渡したい場合、引数かグローバル変数しかない。他に隠蔽したり、親関数からスコープを引き継ぎたい場合、無名関数を使うしか無い。

Variables From External Sources

Ref:

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

配列渡しはPHP側の仕様。

Control Structures

Source: PHP: Control Structures - Manual.

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);を書いておいたほうがよいだろう。

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()でいいか。

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相当をいろいろ指定する形になる。

Features

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コマンドの引数にコードを指定: php -r 'print_r(get_defined_constants());'
  3. phpコマンドに標準入力で読み込み: php <file.php

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

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

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クラス経由で実現するので、内部実装を意識する必要はない。