🔍

PHP8.0〜8.4で強化された主なポイントをPHP7時代と比較しながらおさらいする

2024/12/18に公開

この記事は、Lancers(ランサーズ) Advent Calendar 2024 の18日目の記事です。

はじめに

ランサーズ株式会社、バックエンドエンジニアの @koji9412 です。

2024年11月21日にPHP 8.4.1がリリースされたので、PHP8から新機能などを振り返りつつ抑えておきたいポイントを紹介します。

PHP7時代の書き方と比較しつつ理解が進められたらと思います。

PHP 8.0

名前付き引数

名前付き引数により、引数の順番に縛られず可読性の高い呼び出しが可能になりました。

以下がPHP7での書き方とPHP8での書き方の比較です。

PHP 7

// PHP 7 の例
function createUser($name, $age, $role) {
    echo "名前: {$name}, 年齢: {$age}, 役割: {$role}\n";
}

createUser("太郎", 25, "管理者");

PHP 8.0

// PHP 8.0 の例 (名前付き引数)
function createUser($name, $age, $role) {
    echo "名前: {$name}, 年齢: {$age}, 役割: {$role}\n";
}

// 引数の順番に依存せず、可読性が向上
createUser(
    age: 25,
    role: "管理者",
    name: "太郎"
);

ユニオン型対応

Union型対応で、関数やメソッドの引数・戻り値に複数の型を柔軟に指定できるようになりました。

PHP 7

// PHP 7 の例
function sum($a) {
    if (is_int($a)) {
        return $a;
    } elseif (is_float($a)) {
        return (int)$a;
    }
    return 0;
}

echo sum(10);   // 10
echo sum(10.5); // 10

PHP 8.0

// PHP 8.0 の例 (ユニオン型対応)
function sum(int|float $a): int|float {
    return $a;
}

echo sum(10);   // 10
echo sum(10.5); // 10.5

match式

match式の導入で、switch文よりもスッキリした条件分岐が実現できるようになりました。

PHP 7

// PHP 7 の例
$status = 2;
$result = '';
switch ($status) {
    case 1:
        $result = '成功';
        break;
    case 2:
        $result = '失敗';
        break;
    default:
        $result = '不明';
}
echo $result; // "失敗"

PHP 8.0

// PHP 8.0 の例 (match式)
$status = 2;
$result = match($status) {
    1 => '成功',
    2 => '失敗',
    default => '不明',
};
echo $result; // "失敗"

コンストラクタ短縮

コンストラクタプロパティプロモーションによって、クラス定義時のプロパティ初期化がより簡潔になりました。

PHP 7

// PHP 7 の例
class User {
    private $name;
    private $age;

    public function __construct($name, $age) {
        $this->name = $name;
        $this->age  = $age;
    }

    public function getInfo() {
        return "名前: {$this->name}, 年齢: {$this->age}";
    }
}

$user = new User("太郎", 30);
echo $user->getInfo(); // "名前: 太郎, 年齢: 30"

PHP 8.0

// PHP 8.0 の例 (コンストラクタ短縮)
class User {
    public function __construct(private string $name, private int $age) {}

    public function getInfo(): string {
        return "名前: {$this->name}, 年齢: {$this->age}";
    }
}

$user = new User(name: "太郎", age: 30);
echo $user->getInfo(); // "名前: 太郎, 年齢: 30"

Null安全

Nullsafe演算子で、メソッドチェーン内でのnull判定が容易になり、コードの煩雑さを軽減できるようになりました。

PHP 7

// PHP 7 の例
class Address {
    public function getCity() {
        return "東京";
    }
}

class Person {
    private $address;
    public function __construct($address = null) {
        $this->address = $address;
    }
    public function getAddress() {
        return $this->address;
    }
}

$personWithAddress = new Person(new Address());
$personWithoutAddress = new Person();

$city1 = $personWithAddress->getAddress() ? $personWithAddress->getAddress()->getCity() : null;
$city2 = $personWithoutAddress->getAddress() ? $personWithoutAddress->getAddress()->getCity() : null;

echo $city1; // "東京"
echo $city2; // null

PHP 8.0

// PHP 8.0 の例 (Null安全)
class Address {
    public function getCity() {
        return "東京";
    }
}

class Person {
    public function __construct(private ?Address $address = null) {}
    public function getAddress(): ?Address {
        return $this->address;
    }
}

$personWithAddress = new Person(address: new Address());
$personWithoutAddress = new Person();

$city1 = $personWithAddress->getAddress()?->getCity();
$city2 = $personWithoutAddress->getAddress()?->getCity();

echo $city1; // "東京"
echo $city2; // null

PHP 8.1

列挙型(Enums)

PHP 8.1では列挙型が追加され、関連する定数をまとめて定義できるようになりました。
これにより、特定の限定された値の集まりを型として扱うことが可能です。

PHP 7

// PHP 7 の例
class UserRole {
    const ADMIN = '管理者';
    const EDITOR = '編集者';
    const VIEWER = '閲覧者';
}

function getUserRole($role) {
    if (!in_array($role, [UserRole::ADMIN, UserRole::EDITOR, UserRole::VIEWER], true)) {
        return '不明な役割';
    }
    return $role;
}

echo getUserRole(UserRole::ADMIN); // "管理者"

PHP 8.1

// PHP 8.1 の例(Enums)
enum UserRole: string {
    case ADMIN = '管理者';
    case EDITOR = '編集者';
    case VIEWER = '閲覧者';
}

function getUserRole(UserRole $role): string {
    return $role->value;
}

echo getUserRole(UserRole::ADMIN); // "管理者"

リードオンリープロパティ(Readonly Properties)

PHP 8.1ではreadonlyで指定されたプロパティは初期化以降に変更できなくなります。

PHP 7

// PHP 7 の例
class User {
    private $name;
    public function __construct($name) {
        $this->name = $name; 
    }
    public function getName() {
        return $this->name;
    }
}

$user = new User("太郎");
echo $user->getName(); // "太郎"
// セッターがなければ実質的に読み取り専用だが、strictには保証されない

PHP 8.1

// PHP 8.1 の例(Readonly Properties)
class User {
    public readonly string $name;
    public function __construct(string $name) {
        $this->name = $name;
    }
}

$user = new User("太郎");
echo $user->name; // "太郎"
// $user->name = "花子"; // エラー: readonlyプロパティは再代入不可

インターセクション型(Intersection Types)

PHP 8.1では&を用いて、複数のインターフェイスを同時に満たす型を定義できます。

PHP 7

// PHP 7 の例
interface CanWalk {
    public function walk();
}

interface CanSpeak {
    public function speak();
}

class Person implements CanWalk, CanSpeak {
    public function walk() { echo "歩く\n"; }
    public function speak() { echo "話す\n"; }
}

function act($obj) {
    // $objが両方のインターフェイスを実装していることを確認する必要がある
    if ($obj instanceof CanWalk && $obj instanceof CanSpeak) {
        $obj->walk();
        $obj->speak();
    }
}

act(new Person()); // "歩く" "話す"

PHP 8.1

// PHP 8.1 の例(インターセクション型)
interface CanWalk {
    public function walk();
}

interface CanSpeak {
    public function speak();
}

class Person implements CanWalk, CanSpeak {
    public function walk() { echo "歩く\n"; }
    public function speak() { echo "話す\n"; }
}

function act(CanWalk&CanSpeak $obj) { // &はスペースでは空けないよ
    // $objは必ずCanWalkかつCanSpeak
    $obj->walk();
    $obj->speak();
}

act(new Person()); // "歩く" "話す"

array_is_list()

array_is_list()は、配列が連続した数値インデックスを持つリスト形式かどうかを簡単に判定できます。

PHP 7

// PHP 7 の例
function isListArray(array $arr): bool {
    $keys = array_keys($arr);
    return $keys === range(0, count($arr) - 1);
}

$list = ["太郎", "花子", "次郎"];
$assoc = ["name" => "太郎", "age" => 30];

var_dump(isListArray($list));  // bool(true)
var_dump(isListArray($assoc)); // bool(false)

PHP 8.1

// PHP 8.1 の例(array_is_list)
$list = ["太郎", "花子", "次郎"];
$assoc = ["name" => "太郎", "age" => 30];

var_dump(array_is_list($list));  // bool(true)
var_dump(array_is_list($assoc)); // bool(false)

never戻り値型(never Return Type)

neverは、関数が正常に戻ることがないことを明示します。
例として、関数が常に例外をスローするか、スクリプトを終了させる場合などに使用します。

PHP 7

// PHP 7 の例
function alwaysExit() {
    exit("終了します\n");
    // 型宣言できないが、ここから戻ることはない
}

alwaysExit();
echo "この行には到達しない"; // 実行されない

PHP 8.1

// PHP 8.1 の例(never戻り値型)
function alwaysExit(): never {
    exit("終了します\n");
}

alwaysExit();
echo "この行には到達しない"; // 実行されない

PHP 8.2

Readonlyクラス(Readonly Classes)

PHP 8.2ではクラス全体をreadonlyとして宣言することが可能になりました。
このクラス内の全てのプロパティは読み取り専用となり、初期化後に変更できなくなります。

PHP 7

// PHP 7 の例
class User {
    private $name;
    public function __construct($name) {
        $this->name = $name; 
    }
    public function getName() {
        return $this->name;
    }
}

$user = new User("太郎");
echo $user->getName(); // "太郎"
// このままではreadonlyを保証できないため、再代入防止を開発者が担保する必要がある

PHP 8.2

// PHP 8.2 の例(Readonly Classes)
readonly class User {
    public function __construct(public string $name) {}
}

$user = new User("太郎");
echo $user->name; // "太郎"
// $user->name = "花子"; // エラー: readonlyクラスのプロパティは再代入不可

Disjunctive Normal Form (DNF)型

PHP 8.2では、型宣言において和集合と積集合を組み合わせた「論理和標準形」(DNF)を記述できます。
(A&B)|Cのような、より複雑な型指定が可能になります。

PHP 7

// PHP 7 の例
interface CanWalk {
    public function walk();
}

interface CanSwim {
    public function swim();
}

interface CanFly {
    public function fly();
}

function action($obj) {
    if (($obj instanceof CanWalk && $obj instanceof CanSwim) || $obj instanceof CanFly) {
        // 条件分岐で複雑に確認する必要がある
        if ($obj instanceof CanWalk && $obj instanceof CanSwim) {
            $obj->walk();
            $obj->swim();
        } else {
            $obj->fly();
        }
    }
}

PHP 8.2

// PHP 8.2 の例(DNF型)
interface CanWalk {
    public function walk();
}

interface CanSwim {
    public function swim();
}

interface CanFly {
    public function fly();
}

// (CanWalk & CanSwim) | CanFly というDNF型で受け取れる
function action((CanWalk&CanSwim)|CanFly $obj) {
    if ($obj instanceof CanFly) {
        $obj->fly();
    } else {
        $obj->walk();
        $obj->swim();
    }
}

true,false,null を型として使用可能

PHP 8.2ではtrue,false,nullを型として明示的に指定できます。
これにより、戻り値が常にfalseであったり、常にnullを返す関数を型で表現できます。

PHP 7

// PHP 7 の例
function alwaysFalse() {
    return false;
}
// 戻り値がfalse固定であることを型で明示できない

PHP 8.2

// PHP 8.2 の例(true/false/null型)
function alwaysFalse(): false {
    return false;
}

// これにより戻り値がfalse以外になりえないことを型で保証できる

トレイトでの定数定義

PHP 8.2ではトレイト内で定数を定義できるようになりました。

PHP 7

// PHP 7 の例
trait MyTrait {
    // 定数を定義できないため、クラス側で定義する必要があった
    public function sayHello() {
        echo "こんにちは\n";
    }
}

class Greeter {
    use MyTrait;
    const GREETING = "こんにちは";
}

$g = new Greeter();
echo Greeter::GREETING; // "こんにちは"

PHP 8.2

// PHP 8.2 の例(トレイトでの定数定義)
trait MyTrait {
    const GREETING = "こんにちは";
    public function sayHello() {
        echo self::GREETING . "\n";
    }
}

class Greeter {
    use MyTrait;
}

$g = new Greeter();
$g->sayHello(); // "こんにちは"

mysqli_execute_query()の追加

PHP 8.2では mysqli_execute_query() 関数および mysqli::execute_query() メソッドが追加され、
クエリの実行がより簡潔になりました。

PHP 7

// PHP 7 の例
$mysqli = new mysqli("localhost", "user", "pass", "test");
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $id); // "i"はint型
$stmt->execute();
$result = $stmt->get_result();

PHP 8.2

// PHP 8.2 の例(mysqli_execute_query)
$mysqli = new mysqli("localhost", "user", "pass", "test");
$result = $mysqli->execute_query("SELECT * FROM users WHERE id = ?", [$id]);

PHP 8.3

json_validate()

PHP 8.3ではjson_validate()関数が追加されました。
この関数は文字列が有効なJSONであるかをチェックしますが、実際にデコードは行わず、妥当性のみを検証します。

PHP 7

// PHP 7 の例
$jsonString = '{"name":"太郎","age":20}';

// 有効なJSONかどうかをチェックするには、json_decodeした結果がnullでないことを確認するなど、迂回的な方法が必要だった
$data = json_decode($jsonString);
if ($data !== null && json_last_error() === JSON_ERROR_NONE) {
    echo "有効なJSONです\n";
} else {
    echo "無効なJSONです\n";
}

PHP 8.3

// PHP 8.3 の例(json_validate)
$jsonString = '{"name":"太郎","age":20}';

if (json_validate($jsonString)) {
    echo "有効なJSONです\n";
} else {
    echo "無効なJSONです\n";
}

ini_parse_quantity()

PHP 8.3ではini_parse_quantity()関数が追加され、"2M"のようなphp.ini形式のサイズ指定を数値(バイト)に変換できるようになりました。

PHP 7

// PHP 7 の例
$sizeStr = "2M";
// この文字列を数値に変換するには手動で単位を判定して計算する必要がある
$units = ['K' => 1024, 'M' => 1024*1024, 'G' => 1024*1024*1024];
$lastChar = strtoupper(substr($sizeStr, -1));
if (isset($units[$lastChar])) {
    $value = (int)$sizeStr * $units[$lastChar];
} else {
    $value = (int)$sizeStr;
}
echo $value; // 2097152

PHP 8.3

// PHP 8.3 の例(ini_parse_quantity)
$sizeStr = "2M";
$value = ini_parse_quantity($sizeStr);
echo $value; // 2097152 (2MBをバイト換算)

PDO_SQLiteに sqlite3_extension_dir 設定が追加

PHP 8.3では、PDO_SQLiteに sqlite3_extension_dir 設定が追加されました。この設定により、SQLite拡張モジュールの場所を指定できるようになりました。

PHP 7

// PHP 7 の例
// PDO_SQLiteで拡張を読み込むには、実質的に組み込みか、PHPの再ビルドが必要
$db = new PDO('sqlite::memory:');

PHP 8.3

// PHP 8.3 の例(PDO_SQLite拡張サポート)
// php.iniなどでsqlite3_extension_dirを指定し、そのディレクトリ内にあるSQLite拡張を読み込める
$db = new PDO('sqlite::memory:');
$db->sqliteCreateFunction("my_custom_func", function($x){ return $x * 2; }, 1);
$result = $db->query("SELECT my_custom_func(10)")->fetchColumn();
echo $result; // 20

curl_upkeep()の追加

curl_upkeep()関数が追加され、アイドル状態の接続を維持するための内部作業を実行できます。
これにより、長時間使用されていない接続も再利用を容易にし、パフォーマンス向上が期待できます。

PHP 7

// PHP 7 の例
$ch = curl_init("https://example.com");
curl_exec($ch);
// 接続維持のための仕組みが存在せず、再接続が必要な場合が多かった

PHP 8.3

// PHP 8.3 の例(curl_upkeep)
$ch = curl_init("https://example.com");
curl_exec($ch);

// 一定時間経過後にidleな接続を維持
curl_upkeep($ch);
// これにより同じハンドルで再接続せず利用できる可能性が高まる

fsync()とfdatasync()の追加

fsync()fdatasync()関数が追加されました。
これらはファイルへの書き込みを強制的にストレージへ反映し、データを確実にディスクへ同期させます。
特にデータ損失を避けたい場合や、トランザクション的な処理を行う際に有用です。

PHP 7

// PHP 7 の例
// 明示的なfsync相当機能は存在しないため、
// シェルコマンドの実行などで対処する必要があった
$file = fopen("data.txt", "w");
fwrite($file, "重要なデータ\n");
fclose($file);
// データはOSのバッファに滞留しているかもしれない

PHP 8.3

// PHP 8.3 の例(fsync, fdatasync)
$file = fopen("data.txt", "w");
fwrite($file, "重要なデータ\n");
// ここでfsyncすることでストレージへの書き込みを保証
fsync($file);
fclose($file);

PHP 8.4

プロパティフック

プロパティに直接ゲッターとセッターのロジックを埋め込むことが可能になりました。これにより、対応するメソッドを定義する手間が省け、クリーンなコードを実現できます。

PHP 7

class MyClass {
    private $value;

    public function getValue() {
        return $this->value;
    }

    public function setValue($value) {
        $this->value = $value;
    }
}

$instance = new MyClass();
$instance->setValue(42);
echo $instance->getValue(); // 42

PHP 8.4

class MyClass {
    private $value;

    public function __get($name) {
        if ($name === 'value') {
            return $this->value;
        }
        throw new Exception("未定義のプロパティ: " . static::class . "::$name");
    }

    public function __set($name, $value) {
        if ($name === 'value') {
            $this->value = $value;
        } else {
            throw new Exception("未定義のプロパティ: " . static::class . "::$name");
        }
    }
}

$instance = new MyClass();
$instance->value = 42;
echo $instance->value; // 42

非対称可視性

非対称可視性では、クラスのプロパティに対する「読み取り」と「書き込み」の可視性を別々に設定することが可能です。これにより、外部からの書き込みを制限しつつ、読み取りは可能にするようなデザインが可能です。

PHP 7

class MyClass {
    private $data = '秘密の情報';

    public function getData() {
        return $this->data;
    }
}

$instance = new MyClass();
echo $instance->getData(); // 秘密の情報
// $instance->data = '新しい情報'; // エラー: プライベートプロパティにアクセスできません

PHP 8.4

class MyClass {
    private $data = '秘密の情報';

    public function get data() {
        return $this->data;
    }

    public function internalSetData($value) {
        $this->data = $value;
    }
}

$instance = new MyClass();
echo $instance->data; // 秘密の情報
// $instance->data = '新しい情報'; // エラー: 書き込みが許可されていません
$instance->internalSetData('新しい情報'); // 内部的な書き込みは可能
echo $instance->data; // 新しい情報

もちろん書き込めるけど外部からは読み込めないようにすることもできます。

class MyClass {
    private $data;

    public function set data($value) {
        $this->data = $value;
    }

    private function get data() {
        return $this->data;
    }

    // 外部から内部データを読み込みたい場合は、別のメソッドを用意
    public function revealData() {
        return $this->data;
    }
}

$instance = new MyClass();
$instance->data = '新しい情報'; // 書き込みは可能
// echo $instance->data; // エラー: 読み込みが許可されていません
echo $instance->revealData(); // 新しい情報

括弧なしの連鎖メソッド呼び出し

new MyClass()->method()のように、newを使ったインスタンス化とメソッド呼び出しを括弧なしで行うことが可能になり、コードがさらに読みやすくなります。

PHP 7

class MyClass {
    public function greet() {
        return 'こんにちは';
    }
}

$instance = (new MyClass())->greet();
echo $instance; // こんにちは

PHP 8.4

class MyClass {
    public function greet() {
        return 'こんにちは';
    }
}

$instance = new MyClass->greet();
echo $instance; // こんにちは

新しい配列操作関数の追加

array_find(), array_find_key(), array_all(), array_any()といった新しい関数が追加され、配列データの操作やフィルタリングが簡素化されました。

PHP 7

// PHP 7での従来の配列操作例

// array_find相当
$fruits = ['apple', 'banana', 'cherry', 'date'];
$found = null;
foreach ($fruits as $fruit) {
    if ($fruit === 'banana') {
        $found = $fruit;
        break;
    }
}
echo $found; // 'banana'

// array_map_keys相当
$fruitQuantities = ['apple' => 3, 'banana' => 2, 'cherry' => 5];
$prefixedKeys = [];
foreach ($fruitQuantities as $key => $value) {
    $prefixedKeys['prefix_' . $key] = $value;
}
print_r($prefixedKeys);

// array_reduce_keys相当
$scores = ['math' => 90, 'science' => 85, 'art' => 75];
$totalScore = 0;
foreach ($scores as $value) {
    $totalScore += $value;
}
echo $totalScore; // 250

PHP 8.4

// array_find
$fruits = ['apple', 'banana', 'cherry', 'date'];
$found = array_find($fruits, fn($fruit) => $fruit === 'banana');
echo $found; // 'banana'

// array_map_keys
$fruitQuantities = ['apple' => 3, 'banana' => 2, 'cherry' => 5];
$prefixedKeys = array_map_keys($fruitQuantities, fn($key) => 'prefix_' . $key);
print_r($prefixedKeys);

// array_reduce_keys
$scores = ['math' => 90, 'science' => 85, 'art' => 75];
$totalScore = array_reduce_keys($scores, fn($carry, $key, $value) => $carry + $value, 0);
echo $totalScore; // 250

HTML5サポートの強化

新たにDomHTMLDocumentクラスが追加され、HTML5ドキュメントの解析がよりサポートされました。これにより、最新のHTML標準に基づく操作が可能です。

PHP 7

$html = <<<HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>シンプルHTML5</title>
</head>
<body>
    <main>
        <article>PHP 8.4は機能が豊富です!</article>
        <article class="featured">PHP 8.4は新しいDOMクラスを追加しました。</article>
    </main>
</body>
</html>
HTML;

$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML($html);
libxml_clear_errors();

$xpath = new DOMXPath($dom);
$node = $xpath->query('//main/article[@class="featured"]')->item(0);

if ($node) {
    var_dump($node->textContent); // "PHP 8.4は新しいDOMクラスを追加しました。"
}

PHP 8.4

use Dom\HTMLDocument;
$html = <<<HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>シンプルHTML5</title>
</head>
<body>
    <main>
        <article>PHP 8.4は機能が豊富です!</article>
        <article class="featured">PHP 8.4は新しいDOMクラスを追加しました。</article>
    </main>
</body>
</html>
HTML;

$dom = HTMLDocument::createFromString($html);
$node = $dom->querySelector('main > article.featured');
var_dump($node->textContent); // "PHP 8.4は新しいDOMクラスを追加しました。"

マルチバイト文字のトリミング

mb_trim(), mb_ltrim(), mb_rtrim()といった関数が追加され、マルチバイト文字(日本語を含む)の処理がより扱いやすくなりました。

PHP 7

$str = " こんにちは ";

// mb_trimの代わりに正規表現を使う
$str = preg_replace('/^\s+|\s+$/u', '', $str);
echo $str; // "こんにちは"

PHP 8.4

$str = " こんにちは ";
echo mb_trim($str); // "こんにちは"

まとめ

PHP 8.0 移行のアップデートを一部紹介しましたが、便利になった機能もとても多く、PHP7からアップデートできていないと生産性も落ちてしまうことも多くなってしまうかと思います。

正直、Nullsafe演算子などは他の言語では使えることも多く、これが使えるようになるだけでもかなりメリットがあるのではという想いです。

紹介したアップデートの他にも様々な機能追加などがあるので是非自分の目で確認していただけたら幸いです。

最後までお読みいただき、ありがとうございました!
この記事が皆さまの開発や学びのお役に立てれば幸いです。
引き続き、よろしくお願いいたします!

GitHubで編集を提案

Discussion