ついにきた!PHP 8.4でインターフェースにプロパティが書けるようになったぞ!!!!
はじめに
PHP 8.4.0からインターフェースにプロパティを定義することができるようになりました、今まで同じようなことを実現するためには getter / setter を実装する必要がありました。
さらに、プロパティを持つオブジェクトの抽象化が容易になり、コードの再利用性の向上が見込まれます。
今回の変更により、PHPにおけるコードの表現力が高まり、ソフトウェアプロダクト開発の際に、より採用しやすい言語になりました。
今までの課題
getter / setter を実装する煩わしさ
従来、インターフェースでプロパティの存在を強制したい場合、以下のようにgetter/setterメソッドを定義する必要がありました
code: php
interface UserInterface
{
public function getName(): string;
}
class User implements UserInterface
{
public function __construct(
private string $name,
) {
}
public function getName(): string
{
return $this->name;
}
}
これからどうなるのか
PHP 8.4.0新機能: Property Hooks
Property Hooksが導入されました、この変更により、getter / setter を用いた煩わしい実装が以下のように実装が変わります。
code: php
interface UserInterface
{
public string $name { get; set; }
}
class User implements UserInterface
{
public string $name { get; set; }
public function __construct(
string $name
) {
$this->name = $name;
}
}
変更点
getter / setter を使うことなくプロパティの抽象化が可能になった
余分なメソッドの実装が不要になった
Property Hooksの実装パターン
✅ プロパティフック の定義が可能
code:php
interface IHuman
{
public string $name { get; set; }
}
しかし、プロパティフックの実装を書くことはできません
code:php
interface IHuman
{
public string $name {
get;
set {} // ❌ 抽象プロパティフックに本体を書くことはできません
}
}
✅ 通常のプロパティの定義が可能
code:php
interface IHuman
{
public string $name { get; set; }
}
class Human implements IHuman
{
public string $name;
}
✅ readonlyプロパティの定義が可能
インターフェイス側のアクセサをgetのみに絞ることで、具象クラスはreadonlyとして実装することも可能
code:php
interface IHuman
{
public string $name { get; }
}
class Human implements IHuman
{
public readonly string $name;
}
実装パターンを踏まえたユースケース
Eloquent(ORM)モデルに依存しない処理の実装
引数にEloquentモデルを取ってしまうと、モデルの作成が必須となってしまい、純粋なビジネスロジックのテストが困難になる。
code:php
class UserService
{
public function pointAdding(User $user, int $point): void
{
$user->points = $this->calculatePointAdding($user->points, $point);
$user->save();
}
public function previewPointAdding(User $user, int $point): int
{
return $this->calculatePointAdding($user->points, $point);
}
private function calculatePointAdding(int $point, int $addingPoint): int
{
return $point + $addingPoint;
}
}
Eloquent(ORM)モデルに依存しない処理の実装の対処方法
この状況において、ビジネスロジックを検証するためのテストを書くには、以下の方法のいずれかを取る必要があります
テスト実行時にDBに接続する
Eloquentモデルという複雑なものを取り除き、純粋な処理に近づける
→ 🤓 PHP 8.4.0 にて プロパティの定義が可能になったため、DBに接続しなくても、引数のEloquentモデルをインターフェイスに置き換えることで、複雑な要素を取り除き純粋なビジネスロジック検証を行うためのテストを書くことが可能なはず。
→ ✅ Eloquentモデルという複雑なものを取り除き、純粋な処理に近づける
1. インターフェイスを実装
code:php
// 書き込みせざるを得ない処理の場合に利用する
interface EloquentModelInterfaceUser
{
public int $id { get; set; }
public string $name { get; set; }
public int $points { get; set; }
/**
* @return bool
*/
public function save(array $options = []);
}
code:php
// 書き込むことを想定していない処理の場合に利用する
interface ReadOnlyInterfaceUser
{
public int $id { get; }
public string $name { get; }
public int $points { get; }
}
2. インターフェイスをEloquentモデルに継承します
この実装の詳細については以下のページに書いています。
code:php
class User extends Model implements ReadOnlyInterfaceUser, EloquentModelInterfaceUser
{
public int $id {
get => $this->getAttribute('id');
set {
$this->setAttribute('id', $value);
}
}
public string $name {
get => $this->getAttribute('name');
set {
$this->setAttribute('name', $value);
}
}
public int $points {
get => $this->getAttribute('points');
set {
$this->setAttribute('points', $value);
}
}
public $timestamps = false;
}
4. App\Services\UserServiceに実装されている処理でEloquentモデルに依存している箇所をインターフェイスに依存するように書き換える
code:php
class UserService
{
public function addPoint(EloquentModelInterfaceUser $user, int $point): void
{
$user->points = $this->calculatePointAdding($user->points, $point);
$user->save();
}
public function previewPointAdding(ReadOnlyInterfaceUser $user, int $point): int
{
return $this->calculatePointAdding($user->points, $point);
}
private function calculatePointAdding(int $point, int $addingPoint): int
{
return $point + $addingPoint;
}
}
4. テストでスタブを作成する
Eloquentモデルを引数として取っていた箇所をインターフェイスに置き換えたことにより、スタブの作成が可能になりました。
完了となります。
code:php
$readOnlyUser = new class($initialPoints) implements ReadOnlyInterfaceUser {
public function __construct(private int $pointsValue) {}
public int $id { get => 1; }
public string $name { get => 'Test User'; }
public int $points { get => $this->pointsValue; }
};
$eloquentUser = new class($initialPoints) implements EloquentModelInterfaceUser {
public function __construct(private int $pointsValue) {}
public int $id { get => 1; set {} }
public string $name { get => 'Test User'; set {} }
public int $points {
get => $this->pointsValue;
set => $this->pointsValue = $value;
}
/**
* @return bool
*/
public function save(array $options = []) {
return true;
}
};
注意点
interface上で set を書かなかった場合でも処理を実行した時に値をsetできる
→ PHPStan(PHP Static Analysis Tool) で、歪な実装を検知して防ぐことができます。
レベル2から検知できます、絶対に設定してください。
https://scrapbox.io/files/68eb48af0a698554a419b1e6.png
感想
インターフェースにプロパティを定義することが可能になったことにより、PHPで表現できることの幅が広がりました。
これよって、今後積極的にインターフェースを使う機会が増えると思っています。
お願い
インターフェースのプロパティは便利だから使っていこうぜ。