← melang

PHP

melangの各noteで他言語と比較した、PHPの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。

型

関数の引数・戻り値には型宣言を書ける。

$age = 20;
$age = 20;
$age = "Alice"; // 再代入で型が変わる
function setAge(int $age): void {} // 引数・戻り値には型宣言を書ける

変数自体に型注釈を書く構文はないが、関数の引数・戻り値・プロパティには型宣言をつけられる。`declare(strict_types=1)`を宣言すると、型不一致時の暗黙変換をやめて厳格に検査できる。

型注釈の位置

引数はprefix、戻り値はpostfixという、1つのシグネチャの中で位置が混在する。

function setAge(int $age): void {}
$age = 20; // 変数自体には型注釈を書けない

function setAge(int $age): void { // 引数はprefix(int $age)、戻り値はpostfix(: void)
    // ...
}

変数自体には型注釈の構文がないが、関数の引数は`Type $name`という前置、戻り値は`: Type`という後置と、1つの関数シグネチャの中でprefix/postfixが混在する。引数はC系の伝統を継ぎ、戻り値は後発の`: Type`構文(PHP 7以降)を採用した歴史的経緯の違いがそのまま表れている。

型変換

キャスト構文で変換し、失敗を黙って別の値にする場合がある。

$n = (int) $s;
$n = (int) "42";     // 42
$m = (int) "abc";    // 0(失敗を隠す)
$k = (int) "12abc";  // 12(途中まで読む)

filter_var("abc", FILTER_VALIDATE_INT); // false
filter_var("42", FILTER_VALIDATE_INT);  // 42

`(int)`キャストは変換に失敗しても`0`を返すなど、失敗を通常の値として扱う。失敗を検出したい場合は`filter_var`のような別の関数を使い、失敗は`false`で返る。成功した`0`と失敗を区別するには呼び出し側の注意が必要になる。

暗黙の型変換

数値と数値形式の文字列間で暗黙変換が広く行われる。

$x = 10;
$x = 10;
$x + "5abc"; // 15(文字列が数値へ暗黙変換される)
$x == "10";  // true(==は型を揃えてから比較する)

数値と数値形式の文字列との間でも暗黙変換が広く行われ、`==`のような比較演算子でも型が揃えられてから比較される。`declare(strict_types=1)`や`===`を使うことで暗黙変換を抑制できるが、デフォルトでは緩い。

Enum / Sum Type

backed enumで値を持てるが、matchの網羅漏れは実行時エラー。

enum Status: string { case Pending = 'pending'; case Running = 'running'; }
enum Status: string {
    case Pending = 'pending';
    case Running = 'running';
}

echo match ($status) {
    Status::Pending => 'pending',
    Status::Running => 'running',
}; // 網羅しないと実行時にUnhandledMatchErrorになる

backed enumで文字列/整数値を持てるが、variantごとに異なるフィールドは持てない。`match`式は全caseを網羅しないと、コンパイル時ではなく実行時に`UnhandledMatchError`になる。

値の不変性

変数もオブジェクトも既定は変更可能。`readonly`で個別に制限できる。

public readonly string $name; / readonly class (8.2)
final class User {
    public function __construct(public readonly string $name) {}
    public function withName(string $name): static {
        return new static($name);
    }
}

$a = new User("alice");
// $a->name = "bob";      // Error: readonlyは初期化後に変更できない
$b = $a->withName("bob"); // 新しい値を生成

$arr = [1, 2];
$arr[] = 3;               // OK: 配列は値として扱われるが、変数の中身は変えられる

`readonly`プロパティ(8.1)と`readonly class`(8.2)で、初期化後の変更を禁止できる。配列は代入時に値として扱われる(コピーオンライト)ため、別の変数から見える影響は出ない。一方でオブジェクトは、ハンドルが共有される。不変にするかどうかは、書き手の選択である。

名前付き引数

PHP 8.0で導入。位置引数と併用でき、途中の引数だけ省略することもできる。

function foo(int $width, int $height = 0)
function foo(int $width, int $height = 0) {}
foo(width: 10, height: 20); // 名前付き引数
foo(10, height: 20);        // 位置引数と併用可
foo(height: 20, width: 10); // 名前付きだけなら順序は自由

PHP 8.0で追加された比較的新しい機能。名前付き引数を使うと、デフォルト値を持つ引数の一部だけを省略して呼び出せる。

関数の引数

定義側に&を付けると参照渡し。呼び出し側は印が要らない。

function f(&$x)
function a($n) { $n = 1; }
function b(&$n) { $n = 1; }

$x = 0;
a($x); // $xは0のまま
b($x); // $xが1になる。呼び出し側は見た目が同じ

定義側に`&`を付けると参照渡しになるが、呼び出し側は印が要らない。`a($x)`と`b($x)`が同じ見た目で、変数が書き換わるかは関数の定義を見ないと分からない。かつては呼び出し側に`&`を書ける仕組みもあったが、現在は廃止されている。

if 文/式

`if`はstatement。値の分岐は三項演算子`?:`やmatch式で書く。

if ($cond) { ... } else { ... } / $cond ? $a : $b
$x = $cond ? 1 : 2;

if ($cond) {
    process();
}

`if`はstatementで、値の分岐は三項演算子か、PHP 8.0で加わったmatch式を使う。`else if`と`elseif`の2つの書き方がある。

I/O の明示

I/Oを示す構文がない。

function findUser(UserId $id): User
function findUser(UserId $id): User {
    return $db->find($id);
}

function getUser(UserId $id): User {
    return findUser($id); // 呼べる
}

引数・戻り値の型宣言はできるが、effectを示す仕組みは言語にない。

言語名

"Personal Home Page"の略が起源だが、現在は再帰的頭字語に意味が変わった。

元々"Personal Home Page Tools"の略だったが、言語が本格化した後に"PHP: Hypertext Preprocessor"という自己言及的な(再帰的)頭字語に意味を変えた。名前の綴りを変えずに意味だけを再定義するという、ブランド刷新の一手法。