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';
こうすることにより、存在チェックと代入を一行で記述でき、可読性と保守性が向上する。
さらに
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ではenum
とreadonly
プロパティが導入され、定数の取り扱いや不変データ構造の定義が大幅に改善された。
今の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