📚

php7のコードをphp8系に変更するトレーニング

に公開

お題

以下のようなphp7対応のコードをphp8に改造していく

<?php

class UserRole
{
    public const ADMIN = 'admin';
    public const USER = 'user';

    public static function isAdmin($role)
    {
        return $role === self::ADMIN;
    }
}

class User
{
    public $id;
    public $name;
    public $email;
    public $role;

    public function __construct($id, $name, $email, $role = UserRole::USER)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->role = $role;
    }
}

class AuthMock
{
    private $user = null;

    private $users = [
        [
            'id' => 1,
            'name' => 'Taro Test',
            'email' => 'taro@example.com',
            'password' => 'pass123',
            'role' => 'admin',
        ],
        [
            'id' => 2,
            'name' => 'Hanako Mock',
            'email' => 'hanako@example.com',
            'password' => 'secret',
            'role' => 'user',
        ],
    ];

    private $nextId;

    public function __construct()
    {
        $this->nextId = $this->getMaxId() + 1;
    }

    private function getMaxId()
    {
        return max(array_column($this->users, 'id')) ?: 1;
    }

    private function defaultCreate($email, $password, $name)
    {
        return [
            'name' => $name ?: 'New User',
            'email' => $email,
            'password' => $password,
            'role' => 'user',
        ];
    }

    public function authenticateOrCreate($email, $password, $name = null, $createUsing = null)
    {
        foreach ($this->users as &$u) {
            if ($u['email'] === $email) {
                if ($u['password'] === $password) {
                    if ($name && $u['name'] !== $name) {
                        $u['name'] = $name;
                    }
                    $this->user = $this->arrayToUser($u);
                    return true;
                }
                return false;
            }
        }

        if ($createUsing === null) {
            $createUsing = [$this, 'defaultCreate'];
        }

        $newUser = call_user_func($createUsing, $email, $password, $name);

        if (!isset($newUser['id'])) {
            $newUser['id'] = $this->nextId++;
        }
        $this->nextId = max($this->nextId, $newUser['id'] + 1);
        if (!isset($newUser['role'])) {
            $newUser['role'] = 'user';
        }

        $this->users[] = $newUser;
        $this->user = $this->arrayToUser($newUser);

        return true;
    }

    private function arrayToUser($data)
    {
        return new User(
            $data['id'],
            $data['name'],
            $data['email'],
            isset($data['role']) ? $data['role'] : 'user'
        );
    }

    public function getUsers()
    {
        return $this->users;
    }

    public function getCurrentUser()
    {
        return $this->user;
    }
}

このコードの呼び出し

$customCreator = function ($email, $password, $name) {
    return [
        'name' => strtoupper($name ?: 'anon'),
        'email' => $email,
        'password' => $password,
        'role' => 'admin',
    ];
};

$auth = new AuthMock();

var_dump($auth->authenticateOrCreate('taro@example.com', 'pass123'));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";

var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', 'Dev User', $customCreator));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";

var_dump($auth->authenticateOrCreate('new@example.com', 'newpass', 'New User'));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";

var_dump($auth->authenticateOrCreate('dev@example.com', 'wrongpass'));

echo "\n全ユーザー一覧:\n";
print_r($auth->getUsers());

このような形で呼び出す。実行結果は以下の通り(ここではdockerを利用)

$ docker run --rm -v "$PWD":/app -w /app php:7.4-cli php test.php
bool(true)
ログインユーザー: Taro Test → 管理者です
bool(true)
ログインユーザー: DEV USER → 管理者です
bool(true)
ログインユーザー: New User → 一般ユーザーです
bool(false)

全ユーザー一覧:
Array
(
    [0] => Array
        (
            [id] => 1
            [name] => Taro Test
            [email] => taro@example.com
            [password] => pass123
            [role] => admin
        )

    [1] => Array
        (
            [id] => 2
            [name] => Hanako Mock
            [email] => hanako@example.com
            [password] => secret
            [role] => user
        )

    [2] => Array
        (
            [name] => DEV USER
            [email] => dev@example.com
            [password] => devpass
            [role] => user
            [id] => 3
        )

    [3] => Array
        (
            [name] => New User
            [email] => new@example.com
            [password] => newpass
            [role] => user
            [id] => 4
        )

)

このコードがやっていること

$auth = new AuthMock();

ここでAuthMockクラスを初期化


var_dump($auth->authenticateOrCreate('taro@example.com', 'pass123'));
echo 'ログインユーザー: '.$auth->getCurrentUser()->name;
echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";
  • taro@example.com
  • pass123

で認証後 $auth->getCurrentUser()->role で現在roleから管理者かどうかを判定


var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', 'Dev User', $customCreator));
echo 'ログインユーザー: '.$auth->getCurrentUser()->name;
echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";
  • dev@example.com
  • devpass

認証失敗後name: Dev Userで新規作成し管理者かどうかを判定。これはcustomCreaterにより管理者になる


var_dump($auth->authenticateOrCreate('new@example.com', 'newpass', 'New User'));
echo 'ログインユーザー: '.$auth->getCurrentUser()->name;
echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";
  • new@example.com
  • newpass

認証失敗後: defaultCreatorでユーザーを作成し一般ユーザーである事を確認する


var_dump($auth->authenticateOrCreate('dev@example.com', 'wrongpass'));

単純に認証を失敗させる


echo "\n全ユーザー一覧:\n";
print_r($auth->getUsers());

全ユーザーを取得する

php8化にあたり

以下の通り改造していく

  • issetの代替と??=
  • 名前付き引数の導入
  • コンストラクタープロモーション
  • Userクラスの分割とreadonly/enumの導入
  • call_user_funcを廃止しcallableを簡略化

issetの代替と??=

従来のisset()を用いた記述は、PHP 8以降ではより簡潔な構文で置き換え可能である。まずこのコードでisset()が使われている箇所を見ていこう。

if (!isset($newUser['id'])) {
    $newUser['id'] = $this->nextId++;
}
$this->nextId = max($this->nextId, $newUser['id'] + 1);
if (!isset($newUser['role'])) {
    $newUser['role'] = 'user';
}

これを

$newUser['id'] ??= $this->nextId++;
$this->nextId = max($this->nextId, $newUser['id'] + 1);
$newUser['role'] ??= 'user';

こうすることにより、存在チェックと代入を一行で記述でき、可読性と保守性が向上する。

いずれにせよifとissetが組み合わされているときは一度構文チェックを行うとすっきり書ける可能性が高い。

さらに

    private function arrayToUser($data)
    {
        return new User(
            $data['id'],
            $data['name'],
            $data['email'],
            isset($data['role']) ? $data['role'] : 'user'
        );
    }

この部分も、もちろん変更する

    private function arrayToUser($data)
    {
        return new User(
            $data['id'],
            $data['name'],
            $data['email'],
            $data['role'] ?? 'user',
        );
    }
??

??=

は地味に異なり ??はphp7から使えるけど、この際なので書き換えておく。序でに戻り値のヒントも付けておいた

    private function arrayToUser($data): User
    {
        return new User(
            $data['id'],
            $data['name'],
            $data['email'],
            $data['role'] ?? 'user',
        );
    }

名前付き引数の導入

PHP 8では「名前付き引数(Named Arguments)」が導入され、関数やメソッドの引数を名前付きで指定できるようになった。

new User(
    id: 10,
    name: 'Example User',
    email: 'example@example.com',
    role: UserRole::ADMIN
);

この記法により、可読性が高まり、引数の順番に依存せず柔軟に値を渡すことが可能となる。これはもちろん

 public function __construct($id, $name, $email, $role = UserRole::USER)

ここの引数名に対応しているわけだ。従って

    private function arrayToUser($data): User
    {
        return new User(
            $data['id'],
            $data['name'],
            $data['email'],
            $data['role'] ?? 'user',
        );
    }

これを

    private function arrayToUser($data): User
    {
        return new User(
            id: $data['id'],
            name: $data['name'],
            email: $data['email'],
            role: $data['role'] ?? 'user',
        );
    }

こんな風にすると見通しがよくなる

全てを名前付きにするのは冗長かも

例えば呼び出し元

var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', 'Dev User', $customCreator));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";

これを

var_dump($auth->authenticateOrCreate(email: 'dev@example.com', password: 'devpass', name: 'Dev User', createUsing: $customCreator));

としてもいいんだけど、ちょっと冗長かも

var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', name: 'Dev User', createUsing: $customCreator));

のように

public function authenticateOrCreate($email, $password, $name = null, $createUsing = null)

現在可変引数になっているものの順番を制御できるので便利かもしれない、たとえばこの場合

var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', createUsing: $customCreator));

でもいいわけだ。将来的に

public function authenticateOrCreate($email, $password, $name = null,
    $createUsing = nulll, $updateUsing = null)

とか加工された場合でも名前付きにしておけば対応は楽だろうと思える。

一旦ここまでのdiff

@@ -91,13 +91,9 @@ public function authenticateOrCreate($email, $password, $name = null, $createUsi

         $newUser = call_user_func($createUsing, $email, $password, $name);

-        if (!isset($newUser['id'])) {
-            $newUser['id'] = $this->nextId++;
-        }
+        $newUser['id'] ??= $this->nextId++;
         $this->nextId = max($this->nextId, $newUser['id'] + 1);
-        if (!isset($newUser['role'])) {
-            $newUser['role'] = 'user';
-        }
+        $newUser['role'] ??= 'user';

         $this->users[] = $newUser;
         $this->user = $this->arrayToUser($newUser);
@@ -105,13 +101,13 @@ public function authenticateOrCreate($email, $password, $name = null, $createUsi
         return true;
     }

-    private function arrayToUser($data)
+    private function arrayToUser($data): User
     {
         return new User(
-            $data['id'],
-            $data['name'],
-            $data['email'],
-            isset($data['role']) ? $data['role'] : 'user'
+            id: $data['id'],
+            name: $data['name'],
+            email: $data['email'],
+            role: $data['role'] ?? 'user',
         );
     }

@@ -140,11 +136,11 @@ public function getCurrentUser()
 echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
 echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";

-var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', 'Dev User', $customCreator));
+var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', name: 'Dev User', createUsing: $customCreator));
 echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
 echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";

-var_dump($auth->authenticateOrCreate('new@example.com', 'newpass', 'New User'));
+var_dump($auth->authenticateOrCreate('new@example.com', 'newpass', name: 'New User'));
 echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
 echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";

コンストラクタープロモーション

PHP 8では、コンストラクターの引数定義と同時にプロパティの宣言・初期化を行う「コンストラクタープロモーション」構文が導入された。これにより、クラス定義が簡潔となり、可読性や保守性の向上が期待できる。

従来のUserクラスは以下のように記述されていた:

class User
{
    public $id;
    public $name;
    public $email;
    public $role;

    public function __construct($id, $name, $email, $role = UserRole::USER)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->role = $role;
    }
}

これをコンストラクタープロモーションで書き直すと、以下のようになる:

class User
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email,
        public string $role = UserRole::USER
    ) {}
}

このように、プロパティの宣言と初期化をコンストラクターの引数内で完結させることで、コードの冗長性を排除できる。

diffは以下の通り

 class User
 {
-    public $id;
-    public $name;
-    public $email;
-    public $role;
-
-    public function __construct($id, $name, $email, $role = UserRole::USER)
-    {
-        $this->id = $id;
-        $this->name = $name;
-        $this->email = $email;
-        $this->role = $role;
-    }
+    public function __construct(
+        public int $id,
+        public string $name,
+        public string $email,
+        public string $role = UserRole::USER
+    ) {}
 }

 class AuthMock

User Classからenumの変換

PHP 8.1ではenumreadonlyプロパティが導入され、定数の取り扱いや不変データ構造の定義が大幅に改善された。

今のrole定義

class UserRole
{
    public const ADMIN = 'admin';
    public const USER = 'user';

    public static function isAdmin($role)
    {
        return $role === self::ADMIN;
    }
}

class User
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email,
        public string $role = UserRole::USER
    ) {}
}

このように、単純にclassになっており、その中でconstにてクラス定数としている。

userとかadminとかの文字が「そのまま」打たれている

たとえば

    private function arrayToUser($data): User
    {
        return new User(
            id: $data['id'],
            name: $data['name'],
            email: $data['email'],
            role: $data['role'] ?? 'user',
        );
    }

とか

    private function arrayToUser($data): User
    {
        return new User(
            id: $data['id'],
            name: $data['name'],
            email: $data['email'],
            role: $data['role'] ?? 'user',
        );
    }

とか。これはenumにする前にまずclass定数を見るように変更しておく事にする。

@@ -31,14 +31,14 @@ class AuthMock
             'name' => 'Taro Test',
             'email' => 'taro@example.com',
             'password' => 'pass123',
-            'role' => 'admin',
+            'role' => UserRole::ADMIN,
         ],
         [
             'id' => 2,
             'name' => 'Hanako Mock',
             'email' => 'hanako@example.com',
             'password' => 'secret',
-            'role' => 'user',
+            'role' => UserRole::USER,
         ],
     ];

@@ -60,7 +60,7 @@ private function defaultCreate($email, $password, $name)
             'name' => $name ?: 'New User',
             'email' => $email,
             'password' => $password,
-            'role' => 'user',
+            'role' => UserRole::USER,
         ];
     }

@@ -87,7 +87,7 @@ public function authenticateOrCreate($email, $password, $name = null, $createUsi

         $newUser['id'] ??= $this->nextId++;
         $this->nextId = max($this->nextId, $newUser['id'] + 1);
-        $newUser['role'] ??= 'user';
+        $newUser['role'] ??= UserRole::USER;

         $this->users[] = $newUser;
         $this->user = $this->arrayToUser($newUser);
@@ -101,7 +101,7 @@ private function arrayToUser($data): User
             id: $data['id'],
             name: $data['name'],
admin@ip-172-31-23-167:~/laravel12-starterkit-react$ git --no-pager diff test.php
diff --git a/test.php b/test.php
index 06b70d0..3ba97b8 100644
--- a/test.php
+++ b/test.php
@@ -31,14 +31,14 @@ class AuthMock
             'name' => 'Taro Test',
             'email' => 'taro@example.com',
             'password' => 'pass123',
-            'role' => 'admin',
+            'role' => UserRole::ADMIN,
         ],
         [
             'id' => 2,
             'name' => 'Hanako Mock',
             'email' => 'hanako@example.com',
             'password' => 'secret',
-            'role' => 'user',
+            'role' => UserRole::USER,
         ],
     ];

@@ -60,7 +60,7 @@ private function defaultCreate($email, $password, $name)
             'name' => $name ?: 'New User',
             'email' => $email,
             'password' => $password,
-            'role' => 'user',
+            'role' => UserRole::USER,
         ];
     }

@@ -87,7 +87,7 @@ public function authenticateOrCreate($email, $password, $name = null, $createUsi

         $newUser['id'] ??= $this->nextId++;
         $this->nextId = max($this->nextId, $newUser['id'] + 1);
-        $newUser['role'] ??= 'user';
+        $newUser['role'] ??= UserRole::USER;

         $this->users[] = $newUser;
         $this->user = $this->arrayToUser($newUser);
@@ -101,7 +101,7 @@ private function arrayToUser($data): User
             id: $data['id'],
             name: $data['name'],
             email: $data['email'],
-            role: $data['role'] ?? 'user',
+            role: $data['role'] ?? UserRole::USER
         );
     }

@@ -120,7 +120,7 @@ public function getCurrentUser()
         'name' => strtoupper($name ?: 'anon'),
         'email' => $email,
         'password' => $password,
-        'role' => 'admin',
+        'role' => UserRole::ADMIN,
     ];
 };

enumになっていない事の問題点

たとえば

$customCreator = function ($email, $password, $name) {
    return [
        'name' => strtoupper($name ?: 'anon'),
        'email' => $email,
        'password' => $password,
        'role' => 'hacked',
    ];
};

$auth = new AuthMock();

var_dump($auth->authenticateOrCreate('taro@example.com', 'pass123'));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";

var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', name: 'Dev User', createUsing: $customCreator));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";

var_dump($auth->authenticateOrCreate('new@example.com', 'newpass', name: 'New User'));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";

var_dump($auth->authenticateOrCreate('dev@example.com', 'wrongpass'));

echo "\n全ユーザー一覧:\n";
print_r($auth->getUsers());

このようにすると

全ユーザー一覧:
Array
(
...
    [2] => Array
        (
            [name] => DEV USER
            [email] => dev@example.com
            [password] => devpass
            [role] => hacked
            [id] => 3
        )

このように意図しないhackedが埋まってしまう。これに対応するにはUserクラスを拡張する必要がある。

たとえば

class UserRole
{
    public const ADMIN = 'admin';
    public const USER = 'user';

    public static function isAdmin($role)
    {
        return $role === self::ADMIN;
    }

    public static function isValid(string $role): bool
    {
        return in_array($role, [self::ADMIN, self::USER], true);
    }
}
...
    private function arrayToUser($data): User
    {
        if (!UserRole::isValid($data['role'])) {
            die("Invalid role: {$data['role']}\n");
        }
        return new User(
            id: $data['id'],
            name: $data['name'],
            email: $data['email'],
            role: $data['role'] ?? UserRole::USER
        );
    }

...

$ php test.php
bool(true)
ログインユーザー: Taro Test → 管理者です
Invalid role: hacked

のように。

enumへの書き換え

まずclass UserRoleをenumにする

enum UserRole: string
{
    case ADMIN = 'admin';
    case USER = 'user';

    public function isAdmin(): bool
    {
        return $this === self::ADMIN;
    }
}

さらにUserクラスのプロモーションを変更する

class User
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email,
        // public string $role = UserRole::USER
        public UserRole $role = UserRole::USER
    ) {}
}

ここでstring型からUserRole型に変更された。基本的にはこれで使えるのであるが、呼び出し側

$auth = new AuthMock();

var_dump($auth->authenticateOrCreate('taro@example.com', 'pass123'));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
// echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";
echo $auth->getCurrentUser()->role->isAdmin() ? " → 管理者です\n" : " → 一般ユーザーです\n";

このように$auth->getCurrentUser()->role->isAdmin()で判定できるようになるのでより直感的になったと言えるだろう。

さらに

$customCreator = function ($email, $password, $name) {
    return [
        'name' => strtoupper($name ?: 'anon'),
        'email' => $email,
        'password' => $password,
        'role' => 'user',
    ];
};

$auth = new AuthMock();

var_dump($auth->authenticateOrCreate('taro@example.com', 'pass123'));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo $auth->getCurrentUser()->role->isAdmin() ? " → 管理者です\n" : " → 一般ユーザーです\n";

var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', name: 'Dev User', createUsing: $customCreator));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo $auth->getCurrentUser()->role->isAdmin() ? " → 管理者です\n" : " → 一般ユーザーです\n";

このようにするとエラーになる。これは

$customCreator = function ($email, $password, $name) {
    return [
        'name' => strtoupper($name ?: 'anon'),
        'email' => $email,
        'password' => $password,
        'role' => 'user', // <--------------------------これ
    ];
};

ここにuserという文字をそのまま渡しているためだ。これはたとえば

$role = UserRole::from('user');
$customCreator = function ($email, $password, $name) {
    return [
        'name' => strtoupper($name ?: 'anon'),
        'email' => $email,
        'password' => $password,
        'role' => $role,
    ];
};

$auth = new AuthMock();

var_dump($auth->authenticateOrCreate('taro@example.com', 'pass123'));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo $auth->getCurrentUser()->role->isAdmin() ? " → 管理者です\n" : " → 一般ユーザーです\n";

var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', name: 'Dev User', createUsing: $customCreator));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo $auth->getCurrentUser()->role->isAdmin() ? " → 管理者です\n" : " → 一般ユーザーです\n";

このように$role = UserRole::from('user');でenumに変換してから渡す。$roleをvar_dumpすると

enum(UserRole::USER)

となり、stringとは違うものになる。これがenumの特徴であり、hackedとかいう余計なものが構造上入らなくなっているという事になるわけだ。

customCreatorの方針

いま外部から注入される関数customCreatorにおいては

$role = UserRole::from('user');
$customCreator = function ($email, $password, $name) {
    return [
        'name' => strtoupper($name ?: 'anon'),
        'email' => $email,
        'password' => $password,
        'role' => $role,
    ];
};

すなわちUserRole::from('user')を強制しているが、シンプルにuserという文字列にしたい場合は内部でenumに変換する必要がある。それにおいては

UserRole::from('user')

みたいにして内部でenumへの変換を行う必要がある。まあ今回はそれは行わないので、このままの状態とする。

ここまでのdiff

@@ -1,13 +1,13 @@
 <?php

-class UserRole
+enum UserRole: string
 {
-    public const ADMIN = 'admin';
-    public const USER = 'user';
+    case ADMIN = 'admin';
+    case USER = 'user';

-    public static function isAdmin($role)
+    public function isAdmin(): bool
     {
-        return $role === self::ADMIN;
+        return $this === self::ADMIN;
     }
 }

@@ -17,7 +17,7 @@ public function __construct(
         public int $id,
         public string $name,
         public string $email,
-        public string $role = UserRole::USER
+        public UserRole $role = UserRole::USER
     ) {}
 }

@@ -115,28 +115,32 @@ public function getCurrentUser()
         return $this->user;
     }
 }
+
+
+$role = UserRole::from('user');
 $customCreator = function ($email, $password, $name) {
     return [
         'name' => strtoupper($name ?: 'anon'),
         'email' => $email,
         'password' => $password,
-        'role' => UserRole::ADMIN,
+        'role' => $role,
     ];
 };
+var_dump($role);

 $auth = new AuthMock();

 var_dump($auth->authenticateOrCreate('taro@example.com', 'pass123'));
 echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
-echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";
+echo $auth->getCurrentUser()->role->isAdmin() ? " → 管理者です\n" : " → 一般ユーザーです\n";

 var_dump($auth->authenticateOrCreate('dev@example.com', 'devpass', name: 'Dev User', createUsing: $customCreator));
 echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
-echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";
+echo $auth->getCurrentUser()->role->isAdmin() ? " → 管理者です\n" : " → 一般ユーザーです\n";

 var_dump($auth->authenticateOrCreate('new@example.com', 'newpass', name: 'New User'));
 echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
-echo UserRole::isAdmin($auth->getCurrentUser()->role) ? " → 管理者です\n" : " → 一般ユーザーです\n";
+echo $auth->getCurrentUser()->role->isAdmin() ? " → 管理者です\n" : " → 一般ユーザーです\n";

 var_dump($auth->authenticateOrCreate('dev@example.com', 'wrongpass'));

readonly

$auth = new AuthMock();

var_dump($auth->authenticateOrCreate('taro@example.com', 'pass123'));
echo 'ログインユーザー: ' . $auth->getCurrentUser()->name;
echo $auth->getCurrentUser()->role->isAdmin() ? " → 管理者です\n" : " → 一般ユーザーです\n";
$user = $auth->getCurrentUser();
$user->id = 999;
$user->name = 'Hacked User';
$user->email = 'hacked@example.com';
$user->role = UserRole::ADMIN;
print_r($user);

このような呼び出しをすると

bool(true)
ログインユーザー: Taro Test → 管理者です
User Object
(
    [id] => 999
    [name] => Hacked User
    [email] => hacked@example.com
    [role] => UserRole Enum:string
        (
            [name] => ADMIN
            [value] => admin
        )
)

このように呼び出されたプロパティーが勝手に後から挿入されてしまっている。ここで一度代入したら二度とさわれないようにするreadonlyを与えてみよう

 class User
 {
     public function __construct(
-        public int $id,
-        public string $name,
-        public string $email,
-        public UserRole $role = UserRole::USER
+        public readonly int $id,
+        public readonly string $name,
+        public readonly string $email,
+        public readonly UserRole $role = UserRole::USER
     ) {}
 }

すると

PHP Fatal error:  Uncaught Error: Cannot modify readonly property User::$id

という例外が出て終了する。このように再代入を禁止したい場合はreadonlyを検討すること。この例はpublicであるがprivateであっても。

call_user_funcを廃止しcallableを簡略化

        // if ($createUsing === null) {
        //     $createUsing = [$this, 'defaultCreate'];
        // }
        $createUsing ??= $this->defaultCreate(...);

        // $newUser = call_user_func($createUsing, $email, $password, $name);
        $newUser = $createUsing($email, $password, $name);

この改造。

まず

$createUsing ??= $this->defaultCreate(...);

これはつまり

if ($createUsing === null) {
    $createUsing = $this->defaultCreate(...);
}

なのであるが(...)の3点リーダーは結構いろんな所で出てきて困ったやつの1つではあるのだけど、これはファーストクラスコール可能(first-class callable syntax)というPHP 8.1 以降の構文であり、関数呼び出しではない。これはdefaultCreate メソッドをクロージャとして取得するということになり、その後の

$newUser = $createUsing($email, $password, $name);

$createUsing起動することができるようになるというわけである。ただし構文的に結構きわどいのでチーム開発するときの可読性が上がるかどうかは考えた方がいいかもしれないが...

Discussion