PHPとオブジェクト指向編集

PHP のオブジェクト指向の特徴編集

  1. クラスベースのオブジェクト指向です。
  2. クラスは、キーワード class を使った構文で定義します。
  3. コンストラクターは、__constructで、キーワード newを使ってインスタンス(クラスのオブジェクト)を生成するとき暗黙に呼出されます。
  4. 継承機構は、単純継承です。
  5. 演算子オーバーロードは開発途上で、演算子オーバーロード用の拡張を導入すると使えますがメインラインに入るまでに仕様変更がありえます。

Hello World!:オブジェクト指向版編集

hello.php
<?php
class Hello {
    private string $who;

    public function __construct(string $who = "World") {
        $this->who = $who;
    }
    public function greet() : void {
        echo "Hello {$this->who}!", PHP_EOL;
    }
}

$hello = new Hello;
$hello->greet();
$universe = new Hello("Universe");
$universe->greet();

?>
実行結果
Hello World!
Hello Universe!
クラス Hello を定義して、クラス Hello のインスタンス $hello と $universe を生成し、それぞれメソッド greet() を呼出しています。
__construct()やgreet()のようにクラス定義の中で定義される関数をメソッドと呼びます。
コンストラクターを含むメソッドの中で $this は、クラスのインスタンスへの参照を示しています。
$who のように定義される変数はプロパティ(あるいはインスタンス変数)と呼ばれ、それぞれのインスタンスが独自に値を持ちます。
プロパティとメソッドには次のようなアクセス修飾子を持ちます。

アクセス修飾子編集

private
クラスのメソッドからしか参照できない
protected
クラスとそれを継承した派生クラスのメソッドからしか参照できない
public
クラスの外からも参照できる

コンストラクターとreadonlyプロパティ編集

コンストラクターとプロパティは、アクセス修飾子を指定することで同時に定義できます。 また、readonly修飾子で初期値から変更できないプロパティを作ることもできます。

hello2.php
<?php
class Hello2
{
    public function __construct(
        public readonly string $who = "World"
    ) {}
    public function greet()
    {
        echo "Hello {$this->who}!", PHP_EOL;
    }
}

$hello = new Hello2();
$hello->greet();
$universe = new Hello2("Universe");
$universe->greet();
echo '$hello->who --> ', $hello->who, PHP_EOL;
echo '$universe->who --> ', $universe->who, PHP_EOL;
// $universe->who = "Japon"; // PHP Fatal error:  Uncaught Error: Cannot modify readonly property Hello::$who in /workspace/Main.php:17

?>
実行結果
Hello World!
Hello Universe!
$hello->who --> World
$universe->who --> Universe
プロパティも同時に定義するコンストラクター
    public function __construct(
        public readonly string $who = "World"
    ) {}
public
クラス外からも参照可能(なプロパティ)
readonly
初期値から変更不能
string
引数=プロパティの型は文字列型
$who
仮引数名=プロパティ名
= "World"
省略時のディフォルト

デストラクター編集

インスタンスを参照する変数が1つもなくなると、インスタンスは回収されます。 そのタイミングで資源を返すためのデストラクターが呼出されます。 また、readonly修飾子で初期値から変更できないプロパティを作ることもできます。

destruct.php
<?php
class MyClass {
    function __destruct() {
        echo __CLASS__, " done!", PHP_EOL;
    }
}

echo '$obj = new MyClass()', PHP_EOL;
$obj = new MyClass();
echo '$o2 = $obj', PHP_EOL;
$o2 = $obj;
echo '$obj = null', PHP_EOL;
$obj = null;
echo '$o2 = null', PHP_EOL;
$o2 = null;
echo __FILE__, " done!", PHP_EOL;
?>
実行結果
$obj = new MyClass()
$o2 = $obj
$obj = null
$o2 = null
MyClass done!
/workspace/Main.php done!
クラスMyClassのインスタンスが作られ、参照が $obj で保持されます。
$obj の複製が $o2 に作られます。
参照がコピーされます。
$obj = null で、$obj からの参照はなくなりました。
$o2 からの参照は残っています。
$o2 = null で、すべての参照はなくなりました。
⇒ MyClass::__destruct()が暗黙にキックされた。

動的プロパティの非推奨と __get と __set編集

動的プロパティ編集

動的プロパティの例
<?php
class Simple {
    public int $prop1 = 1;
}

$instance = new Simple();
echo "\$instance->prop1 --> {$instance->prop1}", PHP_EOL;
var_dump(get_object_vars($instance));
echo PHP_EOL;

$instance->prop42 = 42;
echo "\$instance->prop42 --> {$instance->prop42}", PHP_EOL;
var_dump(get_object_vars($instance));

?>
実行結果
$instance->prop1 --> 1
array(1) {
  ["prop1"]=>
  int(1)
}

$instance->prop42 --> 42
array(2) {
  ["prop1"]=>
  int(1)
  ["prop42"]=>
  int(42)
}
クラスSimpleには、プロパティ prop1 の1つをプロパティに持ち、get_object_vars($instance) の結果からもわかります。
その後、$instance->prop42 = 42 で元々は存在していないプロパティ prop42 が新たにできました。get_object_vars($instance) の結果からもわかります。
prop42 のようなプロパティを「動的プロパティ」と呼び、クラスには新しいプロパティは生えず、インスタンスに固有なプロパティになります。
注意
動的プロパティは、PHP 8.2.0 以降では非推奨です。 代わりにプロパティを宣言することを推奨します。任意のプロパティ名を処理するために、クラスはマジックメソッド __get() と __set() を実装する必要があります。

__get と __set編集

__setと__getは動的プロパティへの代入と参照をインターセプトする特殊関数で、いわゆるアクセサです。

__get と __set
<?php
class Simple {
    public int $prop1 = 1;
    private $ary = array();

    public function __set($name, $value) {
        echo __METHOD__, "($name, $value)" . PHP_EOL;
        $this->ary[$name] = $value;
    }
    public function __get($name) {
        $value = $this->ary[$name];
        echo __METHOD__, "($name) = $value" . PHP_EOL;
        return $value;
    }
}

$instance = new Simple();
echo "\$instance->prop1 --> {$instance->prop1}", PHP_EOL;
var_dump(get_object_vars($instance));

$instance->prop42 = 42;
echo "\$instance->prop42 --> {$instance->prop42}", PHP_EOL;
var_dump(get_object_vars($instance));

?>
実行結果
$instance->prop1 --> 1
array(1) {
  ["prop1"]=>
  int(1)
}
Simple::__set(prop42, 42)
Simple::__get(prop42) = 42
$instance->prop42 --> 42
array(1) {
  ["prop1"]=>
  int(1)
}
動的プロパティとして振舞っているのは、array型(連想配列)のプロパティ ary のキーと値のペアで、いまは echo しているだけですが、受入検査を行ったり例外を上げたりするフックとして有用です。

object と array の相互変換編集

object (の公開プロパティ)を array(連想配列)に変換したり、array(連想配列)を object に変換することができます。 これは、「オブジェクトの配列」を使いたい場合、簡素に表現することを可能にします。

arrayのarrayからobjectのarrayへの変換
<?php
const vect = [
    ["name" => "Tom", "age" => 18],
    ["name" => "Joe", "age" => 16],
    ["name" => "Sam", "age" => 10],
];
echo "配列の要素のハッシュを全て表示", PHP_EOL;
echo '  implode(", ", array_map(fn($key) => "$key:$hash[$key]", array_keys($hash)))', PHP_EOL;
foreach (vect as $hash) {
    echo implode(", ", array_map(fn($key) => "$key:$hash[$key]", array_keys($hash))), PHP_EOL;
}
echo PHP_EOL;

$objs = array_map(fn($hash): object => (object) $hash, vect);

echo "配列の要素のオブジェクトを全て表示", PHP_EOL;
echo '  "name:{$obj->name}, age:{$obj->age}"', PHP_EOL;
foreach ($objs as $obj) {
    echo "name:{$obj->name}, age:{$obj->age}", PHP_EOL;
}
echo PHP_EOL;

echo "配列の要素のオブジェクトを全てハッシュに再変換して表示", PHP_EOL;
echo '  $hash = (array)$obj;
  implode(", ", array_map(fn($key) => "$key:$hash[$key]", array_keys($hash)))', PHP_EOL;
foreach ($objs as $obj) {
    $hash = (array)$obj;
    echo implode(", ", array_map(fn($key) => "$key:$hash[$key]", array_keys($hash))), PHP_EOL;
}

?>
実行結果
配列の要素のハッシュを全て表示
  implode(", ", array_map(fn($key) => "$key:$hash[$key]", array_keys($hash)))
name:Tom, age:18
name:Joe, age:16
name:Sam, age:10

配列の要素のオブジェクトを全て表示
  "name:{$obj->name}, age:{$obj->age}"
name:Tom, age:18
name:Joe, age:16
name:Sam, age:10

配列の要素のオブジェクトを全てハッシュに再変換して表示
  $hash = (array)$obj;
  implode(", ", array_map(fn($key) => "$key:$hash[$key]", array_keys($hash)))
name:Tom, age:18
name:Joe, age:16
name:Sam, age:10

object はイテレーション可能編集

PHPの object は array と同じくイテレーション可能です。 object は、foreach で反復可能です。

クラスの継承に当たっては、可視性のコントロールに注意が必要です。

object はイテレーション可能
<?php
declare(strict_types=1);
header("Content-Type: text/plain");

class Base {
    public    $basePub1 = 'bPub 1';
    protected $basePro1 = 'bPro 1';
    private   $basePri1 = 'bPri 1';

    function showAll() {
       echo __METHOD__, PHP_EOL;
       foreach ($this as $key => $value) {
           echo "$key => $value", PHP_EOL;
       }
    }
}

$base = new Base();
foreach ($base as $key => $value) {
    echo "$key => $value", PHP_EOL;
}
echo PHP_EOL;
$base->showAll();

class Sub extends Base {
    public    $subPub1 = 'sPub 1';
    protected $subPro1 = 'sPro 1';
    private   $subPri1 = 'sPri 1';

    function subShowAll() {
       echo __METHOD__, PHP_EOL;
       foreach ($this as $key => $value) {
           echo "$key => $value", PHP_EOL;
       }
    }
}
echo PHP_EOL;

echo '---- class Sub ----', PHP_EOL;
$sub = new Sub();
foreach ($sub as $key => $value) {
    echo "$key => $value", PHP_EOL;
}
echo PHP_EOL;

$sub->subShowAll();
echo PHP_EOL;

$sub->showAll();
実行結果
basePub1 => bPub 1

Base::showAll
basePub1 => bPub 1
basePro1 => bPro 1
basePri1 => bPri 1

---- class Sub ----
basePub1 => bPub 1
subPub1 => sPub 1

Sub::subShowAll
basePub1 => bPub 1
basePro1 => bPro 1
subPub1 => sPub 1
subPro1 => sPro 1
subPri1 => sPri 1

Base::showAll
basePub1 => bPub 1
basePro1 => bPro 1
basePri1 => bPri 1
subPub1 => sPub 1
subPro1 => sPro 1
クラスの外からは、public なプロパティしかみえません。
クラスのメソッドからは、すべてのプロパティが見える。
派生クラスのメソッドから基底クラスのプロパティは、public に加え ptotected も見える。
派生クラスのメソッドから派生クラスのプロパティは、public だけが見える。

クラス変数とクラス定数編集

プロパティ(インスタンス変数)は、それぞれのインスタンスを構成する要素ですが、クラスに属する変数=クラス変数、そしてクラスに属する定数=クラス定数を定義することができます。

クラス変数とクラス定数
<?php
declare(strict_types=1);
header("Content-Type: text/plain");

class Math
{
    public static int $var1 = 42;
    const pi = 3.14159265359;
    const e = 2.71828182846;

    public static function showVar1()
    {
        echo 'self::$var1 --> ', self::$var1, PHP_EOL;
    }
    public static function setVar1($value)
    {
        self::$var1 = $value;
    }
    public static function showPI()
    {
        echo "self::pi --> ", self::pi, PHP_EOL;
    }
}

echo "クラス変数の参照と更新", PHP_EOL;
echo 'Math::$var1 --> ', Math::$var1, PHP_EOL;
echo 'Math::$var1 = 4423;', PHP_EOL;
Math::$var1 = 4423;
echo 'Math::$var1 --> ', Math::$var1, PHP_EOL;
echo "メソッド経由のクラス変数の参照と更新", PHP_EOL;
echo "Math::showVar1():", PHP_EOL;
Math::showVar1();
echo "Math::setVar1(123);", PHP_EOL;
Math::setVar1(123);
echo "Math::showVar1():", PHP_EOL;
Math::showVar1();

echo "クラス定数の参照", PHP_EOL;
echo "Math::pi --> ", Math::pi, PHP_EOL;
echo "Math::e --> ", Math::e, PHP_EOL;
echo "メソッド経由のクラス定数の参照と更新", PHP_EOL;
echo "Math::showPI():", PHP_EOL;
Math::showPI();
実行結果
クラス変数の参照と更新
Math::$var1 --> 42
Math::$var1 = 4423;
Math::$var1 --> 4423
メソッド経由のクラス変数の参照と更新
Math::showVar1():
self::$var1 --> 4423
Math::setVar1(123);
Math::showVar1():
self::$var1 --> 123
クラス定数の参照
Math::pi --> 3.14159265359
Math::e --> 2.71828182846
メソッド経由のクラス定数の参照と更新
Math::showPI():
self::pi --> 3.14159265359
クラス変数
プロパティ(インスタンス変数)と構文は似ていますが、キーワードstaticを伴っています。
同じクラスのメソッドから self::$変数名 の構文で参照できます(代入の左辺にもできます)。
クラス定義の外からは、クラス名::$変数名 の構文で参照できます(代入の左辺にもできます)。
クラス定数
グローバルスコープや関数スコープの定数と構文は同じですが、class定義内で定義されていることが違います。
static修飾子は不要です。
同じクラスのメソッドから self::定数名 の構文で参照できます。
代入の左辺にはできません。
クラス定義の外からは、クラス名::定数名 の構文で参照できます。
staticメソッド
メソッドの定義が static 修飾子を伴っていると、それはstaticメソッドです。
通常のメソッドは、アロー構文$インスタンス = new クラス(引数); $インスタンス->メソッド(引数)を使って呼出しますが、staticメソッドはこれに加えクラス::メソッド(引数)のインスタンスを経由しない呼出しもできます。
staticメソッドは、$thisを参照できません(インスタンス経由ではない…可能性があるので)。
staticメソッドは、selfで自分のクラスを参照できます。

マジックメソッド編集

__ からはじまるメソッドは、マジックメソッドといい処理系が特定のタイミングで暗黙に呼出します。 このまでに紹介した、コンストラクターもデストラクターもマジックメソッドです。

__toString編集

__toStringは、文字列を必要とする文脈にオブジェクトがあると呼出されます。

文字列化メソッド
<?php
declare(strict_types=1);
header("Content-Type: text/plain");

class Hello3
{
    public function __construct(
        public readonly string $who = "World"
    ) {}
    public function __toString()
    {
        return "Hello {$this->who}!";
    }
}

$hello = new Hello3();
echo $hello, PHP_EOL;
$universe = new Hello3("Universe");
echo $universe, PHP_EOL;
実行結果
Hello World!
Hello Universe!

Trait編集

Trait はクラスと似てプロパティやメソッドを持てますが、インスタンス化できず、継承元にも継承先にもなれませんが、クラスが use してプロパティとメソッドをMixinできます。

Traitの定義の例
<?php
declare(strict_types=1);

trait Point {
    public function __construct(
        private float $x = 0,
        private float $y = 0) {}

    public function __toString() : string {
        return "($this->x, $this->y)";
    }
    public function moveTo(float $dx = 0, float $dy = 0) : void {
        $this->x += $dx;
        $this->y += $dy;
    }
}
4行目の trait が class ならそのままクラス定義です。

Interface編集

Interface はメソッドの宣言だけを行い、クラス定義で Interface を implements 句で指定した場合、そのクラスは Interface で宣言したメソッドを定義しなければいけません。定義しないとエラーになります。

Interfaceの定義の例
interface Areable {
    public function area() : float;
}
メソッド area() は、float を返します。面積です。

Trait と Interface を 参照したクラス定義編集

前二節で解説した Trait と Interface を参照したクラスの例です。

図形クラス
class Shape implements Areable {
    use Point {
        Point::__construct as pointConstructor;
        Point::__toString as pointToString;
    }

    public function __construct(float $x = 0, float $y = 0) {
        $this->pointConstructor($x, $y);
    }
    public function __toString() : string {
        return $this->pointToString();
    }

    public function area() : float { return 0; }

}
$shape = new Shape(1, 2);
echo '$shape --> ', $shape, PHP_EOL;
use Point で Point をMixinすると同時に、Pointのマジカルメソッドに別名を与えています。ここで別名を付けないと呼出す手段がありません。
public function area() : float { return 0; }は、interface Areable の実装。
$shape = new Shape(1, 2);でインスタンス化し
echo '$shape --> ', $shape, PHP_EOL;$shapeで暗黙にマジカルメソッド __toString() が呼出されます。

Enum編集

Enumをクラスと一緒に解説するのは奇異に感じられるかもしれませんが、言語処理系としては、Enumはclassのインフラを使って実装しているので、メソッドやメソッドのアクセス記述子などの働きは同じです。

Enumの例
<?php
declare(strict_types=1);
header("Content-Type: text/plain");

// 列挙型 Seasons の定義
enum Seasons
{
    case Spring;
    case Summer;
    case Autumn;
    case Winter;

    // 漢字で名前を返すメソッド
    public function kanji()
    {
        return match ($this) {
            Seasons::Spring => "春",
            Seasons::Summer => "夏",
            Seasons::Autumn => "秋",
            Seasons::Winter => "冬",
        };
    }
}

foreach (Seasons::cases() as $season) {
    echo $season->name, " --> ", $season->kanji(), PHP_EOL;
}
四季をコード化しています。
switch で使った case 句がここでも使われており、定義されている Spring などの識別子は、Seasons::Spring で参照できるシングルトンです(=== が安心して使えます)
public function kanji() のようにメソッドを定義でき、$season->kanji()のように(クラスのインスタンスからのメソッドと同じように)参照できます。
このように全称的に case を網羅したときは match に defrault を設けてはいけません。default がないことで、入ってくるはずのない値が与えられたときをエラーにできます。
Seasons::cases()は、Enum Seasons で定義されている case をすべて含む array です。
class と Enum の違い
Enum は、継承することもされることもできません。
Enum は、原則的にマジカルメソッドを定義できません。
Enum は、const も含めプロパティを持てません。
一見定義できても参照できません。
Enumのインスタンスは、プロパティ name で case の識別子と同じ文字列を得ることができます(書込み不可)。

UnitEnum interface編集

今節で取り上げた case に値を与えない Enum を Pure Enum といいます。 Pure Enum は UnitEnum interface の Implement です[1]

interface UnitEnum
/**
 * @since 8.1
 */
interface UnitEnum
{
    public readonly string $name;

    /**
     * @return static[]
     */
    #[Pure]
    public static function cases(): array;
}
#[Pure]属性は、純粋関数(副作用を起こさない関数)であることを示します。
属性はPHP8で導入された、PHPDocのアノテーションにかわる修飾子です。

Backed Enum編集

前節で紹介したEnumのように case が特定のスカラー値と結びついていない Enum を Pure Enum といいます。 これに対して、これから紹介する case にスカラー型とスカラー値を明示した Enum を Backed Enum といいます。

Backed Enumの例
<?php

enum W3C16 : int
{
    case Black   = 0x000000;
    case Glay    = 0x808080;
    case Silver  = 0xC0C0C0;
    case Blue    = 0x0000FF;
    case Navy    = 0x000080;
    case Teal    = 0x008080;
    case Green   = 0x008000;
    case Lime    = 0x00FF00;
    case Aqua    = 0x00FFFF;
    case Yellow  = 0xFFFF00;
    case Red     = 0xFF0000;
    case Fuchsia = 0xFF00FF;
    case Olive   = 0x808000;
    case Purple  = 0x800080;
    case Maroon  = 0x800000;

    public function hex() { return sprintf("#%06X", $this->value); }
}

foreach (W3C16::cases() as $colour) {
    echo $colour::class, "::$colour->name --> ", $colour->hex(), "($colour->value)", PHP_EOL;
}
echo PHP_EOL;

var_export(W3C16::from(0xFF00FF));    echo PHP_EOL;
# var_export(W3C16::from(0x606060));    echo PHP_EOL;
#  => PHP Fatal error:  Uncaught ValueError: 6316128 is not a valid backing value for enum "W3C16"
var_export(W3C16::tryFrom(0xFF00FF)); echo PHP_EOL;
var_export(W3C16::tryFrom(0x606060)); echo PHP_EOL;

?>
実行結果
W3C16::Black --> #000000(0)
W3C16::Glay --> #808080(8421504)
W3C16::Silver --> #C0C0C0(12632256)
W3C16::Blue --> #0000FF(255)
W3C16::Navy --> #000080(128)
W3C16::Teal --> #008080(32896)
W3C16::Green --> #008000(32768)
W3C16::Lime --> #00FF00(65280)
W3C16::Aqua --> #00FFFF(65535)
W3C16::Yellow --> #FFFF00(16776960)
W3C16::Red --> #FF0000(16711680)
W3C16::Fuchsia --> #FF00FF(16711935)
W3C16::Olive --> #808000(8421376)
W3C16::Purple --> #800080(8388736)
W3C16::Maroon --> #800000(8388608)

W3C16::Fuchsia
W3C16::Fuchsia
NULL
スカラー型を int と明示
int 以外には string が有効
異なる case に同じ値を割り当てることはできません(エラーになります)。
Pure Enum の name に加え value もプロパティに持ちます。
クラスメソッド from() は値から case を引きます。
無効な値を与えると ValueError を上げます。
クラスメソッド tryFrom() は値から case を引きます。
無効な値を与えると NULL を返します。

機能的には連想配列と似ていますが、連想配列はキーが重複しないことが保証されますが、Backed Enum は case が重複しないとともに、value も重複しないことが保証されていることが異なります。

BackedEnum interface編集

Backed Enum は BackedEnum interface の Implement です[2]

interface BackedEnum
/**
 * @since 8.1
 */
interface BackedEnum extends UnitEnum
{
public readonly int|string $value;

    /**
     * @param int|string $value
     * @return static
     */
    #[Pure]
    public static function from(int|string $value): static;

    /**
     * @param int|string $value
     * @return static|null
     */
    #[Pure]
    public static function tryFrom(int|string $value): ?static;
}

コードキャラリー編集

まとまった規模で、実用的な目的に叶うコードを読まないと機能の存在理由などの言語設計に思い至りにくいので、コードの断片から少し踏み込んだプログラムを掲載します。

複素数クラス編集

PHPには複素数がないので、クラスとして実装しました。 ただ、8.1.13 の時点ではまだ演算子オーバーロードはないので、$x->add($y) のようにメソッドで実装しました。

複素数
<?php
declare(strict_types=1);
header("Content-Type: text/plain");

class Complex
{
    public function __construct(
        private float $real = 0,
        private float $img = 0) {}

    public function __toString()
    {
        if ($this->img == 0) {
            return "$this->real";
        }
        if ($this->real == 0) {
            return "{$this->img}i";
        }
        if ($this->img < 0) {
            $neg = -$this->img;
            return "$this->real - {$neg}i";
        }
        return "$this->real + {$this->img}i";
    }

    private static function coerce($right)
    {
        switch (gettype($right)) {
            case "integer":
            case "double":
                $right = new self($right, 0);
                break;
            case "object":
                if (get_class($right) != get_class()) {
                    throw new Exception("Type mismatch", 500);
                }
                break;
            default:
                throw new Exception("Type mismatch", 500);
        }
        return $right;
    }
    public function add($right)
    {
        $right = self::coerce($right);
        return new self($this->real + $right->real, $this->img + $right->img);
    }
    public function sub($right)
    {
        $right = self::coerce($right);
        return new self($this->real - $right->real, $this->img - $right->img);
    }
    public function abs()
    {
        return sqrt($this->real * $this->real + $this->img * $this->img);
    }
}
$x = new Complex(3, 4);
echo "$x", PHP_EOL;
echo "($x)->abs() --> {$x->abs()}", PHP_EOL;
echo "$x + 10 --> {$x->add(10)}", PHP_EOL;
echo "$x + 10.0 -->  {$x->add(10.0)}", PHP_EOL;
$y = new Complex(1, 9);
echo "$x + $y --> {$x->add($y)}", PHP_EOL;
echo "$x - $y --> {$x->sub($y)}", PHP_EOL;
実行結果
3 + 4i
(3 + 4i)->abs() --> 5
3 + 4i + 10 --> 13 + 4i
3 + 4i + 10.0 -->  13 + 4i
3 + 4i + 1 + 9i --> 4 + 13i
3 + 4i - 1 + 9i --> 2 - 5i

図形と面積編集

図形クラス(Shape)から正方形(Square)や楕円形(Oval)などを派生しています。 Shape自身は、interface Areable の実装で、Areableは共通するメソッド area()を定義しており、area() を実装しないとShapeから派生できません。

図形と面積
<?php
declare(strict_types=1);
header("Content-Type: text/plain");

trait Point {
    public function __construct(
        private float $x = 0,
        private float $y = 0) {}

    public function __toString() : string {
        return "($this->x, $this->y)";
    }
    public function moveTo(float $dx = 0, float $dy = 0) : void {
        $this->x += $dx;
        $this->y += $dy;
    }
}

interface Areable {
    public function area() : float;
}

class Shape implements Areable {
    use Point {
        Point::__construct as pointConstructor;
        Point::__toString as pointToString;
    }

    public function __construct(float $x = 0, float $y = 0) {
        $this->pointConstructor($x, $y);
    }
    public function __toString() : string {
        return $this->pointToString();
    }

    public function area() : float { return 0; }
}

$shape = new Shape(1, 2);
echo '$shape --> ', $shape, PHP_EOL;

class Square extends Shape {
    public function __construct($x, $y, private float $wh = 0) {
        parent::__construct($x, $y);
    }
    public function __toString() : string {
        return "[$this->wh] + " . parent::__toString();
    }
    public function area() : float { return $this->wh * $this->wh; }
}
$square = new Square(1,2,3,4);
echo '$square --> ', $square, PHP_EOL;

class Rectangle extends Shape {
    public function __construct($x, $y, private float $width = 0, private float $height = 0) {
        parent::__construct($x, $y);
    }
    public function __toString() : string {
        return "[$this->width x $this->height] + " . parent::__toString();
    }
    public function area() : float { return $this->width * $this->height; }
}
$rectangle = new Rectangle(1,2,3,4);
echo '$rectangle --> ', $rectangle, PHP_EOL;

class Circle extends Shape {
    public function __construct($x, $y, private float $radius = 0) {
        parent::__construct($x, $y);
    }
    public function __toString() :string {
        return "($this->radius) + " . parent::__toString();
    }
    public function area() : float { return 3.14159265359 * $this->radius * $this->radius; }
}
$circle = new Circle(8,9,10);
echo '$circle --> ', $circle, PHP_EOL;

class Oval extends Shape {
    public function __construct($x, $y, private float $width = 0, private float $height = 0) {
        parent::__construct($x, $y);
    }
    public function __toString() : string {
        return "($this->width x $this->height) + " . parent::__toString();
    }
    public function area() : float { return 3.14159265359 * $this->width * $this->height / 4; }
}
$oval = new Oval(1,2,3,4);
echo '$oval --> ', $oval, PHP_EOL;
$oval->moveTo(10, 20);
echo '$oval --> ', $oval, PHP_EOL;

const shapes = array(
    new Square(0,1,2),
    new Rectangle(3,4,5,6),
    new Circle(8,9,10),
    new Oval(3,4,5,6)
    );
foreach(shapes as $shape) {
    echo get_class($shape),":", $shape, ",\tarea:", $shape->area(), PHP_EOL;
}
実行結果
$shape --> (1, 2)
$square --> [3] + (1, 2)
$rectangle --> [3 x 4] + (1, 2)
$circle --> (10) + (8, 9)
$oval --> (3 x 4) + (1, 2)
$oval --> (3 x 4) + (11, 22)
Square:[2] + (0, 1),	area:4
Rectangle:[5 x 6] + (3, 4),	area:30
Circle:(10) + (8, 9),	area:314.159265359
Oval:(5 x 6) + (3, 4),	area:23.561944901925
Shapeを、抽象クラスとして area() の実装を必須化する設計も考えられますが、実際のShapeは具象メンバー location を持っているので、抽象クラス化する事はできません(PHPでは、具象メンバーを持つと抽象クラスにはできません)。
Point は、trait を使って実装しています。
Shapeでコンストラクターや文字列化メソッドで Point のマジックメソッドを呼出すために別名を用意しています(具象メンバーをもつ trait は、マジックメソッドのハンドリングがわからず敬遠されがちですが、別名の定義でマジックメソッド問題を解決できます)。
moveTo() はShapeでは定義していないので、Point::moveTo()にルーティングされます。
  1. ^ The UnitEnum interface
  2. ^ The BackedEnum interface