PHP の static の種類，全部言えるかな？

## Q1

商品の在庫ごとに割り振られる連番の商品 ID を， PHP 上で自動採番するクラスを考えます。下記で使われている `static` に関して

- 🔰 どういう役目を持っているでしょうか？
- 🔰 どの PHP バージョン以降で有効でしょうか？<br>（但し，PHP 5.0 より前には遡りません）
- 🔰 **出力される商品 ID はどうなるでしょうか？**

```php
<?php

class Product
{          // ↓ ①
    protected static int $counter = 0;
    public readonly int $id;

    public function __construct(
        public readonly string $name,
        public readonly int $price,
    ) {            // ↓ ②
        $this->id = ++static::$counter;
    }
}

class SpecialProduct extends Product
{          // ↓ ③
    protected static int $counter = 100000;
}

// 出力はどうなる？
var_dump(
    (new Product(name: 'レンズ', price: 300000))->id,
    (new SpecialProduct(name: '高級レンズ', price: 1000000))->id,
    (new Product(name: 'カメラ', price: 250000))->id,
);
```

<details>
<summary>回答</summary>

:::note success
- ①③ **静的プロパティ** です。全インスタンスで共有されます。PHP 5.0 から利用できます。
    - ① も ② も `private` ではないため，継承クラスでは ③ が ① を上書きしています。
- ② **遅延静的束縛される静的プロパティ** のインクリメントです。 PHP **5.3** から利用できます。
    - `++static::$counter` が書かれた場所を見ただけでは参照するプロパティが確定しません。実際に `new` 演算子によるインスタンス化を記述した場所で， **「どのクラスのコンストラクタが呼ばれるか」** を考慮して参照するプロパティが決定されます。

出力
```
int(1)
int(100001)
int(2)
```
:::

:::note warn
もし `++self::$counter` であれば，以下のようになってしまいます。
```
int(1)
int(2)
int(3)
```
:::
</details>

## Q2

Q1 の商品に対して，下記のように使える，イミュータブルとミュータブルの特性を持った `Product` 専用のコレクションクラスをそれぞれ作成するとします。

```php:イミュータブル版
$products = new ProductCollection(
    new Product(name: '洗濯機', price: 250000),
    new Product(name: 'パソコン', price: 160000),
);

// 元の値は書き換えない
$updated = $products
    ->push(
        new Product(name: 'レンズ', price: 300000),
        new SpecialProduct(name: '高級レンズ', price: 1000000),
    )
    ->push(
        new Product(name: 'カメラ', price: 250000),
    )
    ->sort();
```

```php:ミュータブル版
$products = new MutableProductCollection(
    new Product(name: '洗濯機', price: 250000),
    new Product(name: 'パソコン', price: 160000),
);

// 最初にコピーした場合は書き換えずに済む
$updated = $products
    ->copy()
    ->push(
        new Product(name: 'レンズ', price: 300000),
        new SpecialProduct(name: '高級レンズ', price: 1000000),
    )
    ->push(
        new Product(name: 'カメラ', price: 250000),
    )
    ->sort();

// コピーしなければ元の値を書き換える
$products
    ->push(
        new Product(name: 'レンズ', price: 300000),
        new SpecialProduct(name: '高級レンズ', price: 1000000),
    )
    ->push(
        new Product(name: 'カメラ', price: 250000),
    )
    ->sort();
```

下記で使われている `static` に関して

- 🔰 どういう役目を持っているでしょうか？
- 🎓 どの PHP バージョン以降で有効でしょうか？<br>（但し，PHP 5.0 より前には遡りません）

```php
<?php

class ProductCollection
{
    protected array $products;

    public function __construct(Product ...$products)
    {
        $this->products = $products;
    }

    public function copy(): static
    {           // ↓ ①
        return new static(...$this->products);
    }
                                             // ↓ ③
    public function push(Product ...$products): static
    {           // ↓ ①
        return new static(...$this->products, ...$products);
    }

    public function all(): array
    {
        return $this->products;
    }
                         // ↓ ③
    public function sort(): static
    {        // ↓ ②
        $copy = static::copy();
        usort(
            $copy->products,
         // ↓ ④
            static fn (Product $x, Product $y): int => $x->id <=> $y->id,
        ); 
        return $copy;
    }
}

class MutableProductCollection extends ProductCollection
{                                            // ↓ ③
    public function push(Product ...$products): static
    {
        array_push($this->products, ...$products);
        return $this;
    }
                         // ↓ ③
    public function sort(): static
    {
        usort(
            $this->products,
         // ↓ ④
            static fn (Product $x, Product $y): int => $x->id <=> $y->id,
        );
        return $this;
    }
}
```

<details>
<summary>回答</summary>

:::note success
- ① **遅延静的束縛されるコンストラクタコール** です。PHP **5.3** から利用できます。
    - `new static()` が書かれた場所を見ただけではインスタンス化するクラスが確定しておらず， `new` 演算子で作られたクラスから実際にメソッドを呼ぶ記述をした場所で確定します。
- ② **遅延静的束縛されるメソッドコールです**。 PHP **5.3** から利用できます。
    - `static::copy()` が書かれた場所を見ただけではどのクラスのメソッドを呼び出すか確定しておらず， `new` 演算子で作られたクラスから実際にメソッドを呼ぶ記述をした場所で確定します。
- ③ **遅延静的束縛される返り値** です。PHP **<font color=red>8.0</font>** から利用できます。
    - `: static` が書かれた場所を見ただけでは返り値として拘束する型が確定しておらず， `new` 演算子で作られたクラスから実際にメソッドを呼ぶ記述をした場所で確定します。
- ④ **クロージャへの `$this` バインドを行わない宣言** です。 （アロー関数を使わなければ） PHP **5.3** から利用できます。
    - エッジケースではありますが，明示的に宣言することによって，メモリ使用量の削減に繋がる場合があります。
:::


:::note warn
- ① もし `new self()` であれば， `MutableProductCollection` から `->copy()`　を呼んでも `ProductCollection` が作られてしまいます。
- ③ もし `: self` であれば，静的解析ツールでは `MutableProductCollection` の返り値を正しく認識することができません。 `MutableProductCollection` から `->copy()` を呼んでも **（実際は `MutableProductCollection` になるものの）** `ProductCollection` であると解析されてしまいます。
    - 但し， `static` はほぼ静的解析ツールのためだけのものです。 **<font color=blue>継承先の `static` 型として見なせるものは全て継承元 `self` 型でもある</font>** ため，動作上はこの 2 つの間に差異はありません。
- ④ ここではクロージャ内部に `$this` は登場していませんが， **<font color=red>参照していなくても暗黙的に `$this` がバインドされています</font>**。以下のようなコードを書いた場合は `static` 宣言がないとメモリを大量消費してしまいます。
    ```php
    class Example
    {
        private readonly array $veryLargeProperty;

        public function __construct()
        {
            $this->veryLargeProperty = array_fill(0, 10000, true);
        }

        public function createSizeCalculator(): Closure
        {
            $count = count($this->veryLargeProperty);
            return static fn () => $count; // $this のバインドを回避
        }
    }

    $callbacks = [];
    for ($i = 0; $i < 10000; ++$i) {
        // Example インスタンス自体は破棄できているので，
        // $count の値を持つクロージャの分しかメモリは消費しない
        $callbacks[] = (new Example())->createSizeCalculator();
    }
    ```
:::
</details>

## Q3

下記で使われている `static` に関して

- 🔰 どういう役目を持っているでしょうか？
- 🎓 どの PHP バージョン以降で有効でしょうか？<br>（但し，PHP 5.0 より前には遡りません）
- 🎓 **出力はどうなるでしょうか？**

```php
<?php

function counter(): int
{// ↓ ①
    static $counter = 0;
    return ++$counter;
}

// 出力はどうなる？
var_dump(counter());
var_dump(counter());
```

```php
<?php

class ParentClass
{
    public function __construct()
    {
     // ↓ ②
        static $i = 0;
        ++$i;
          // ↓ ③
        echo static::class . "::__construct() has been called {$i} time(s)\n";
    }
}

class ChildClass extends ParentClass
{
}

// 出力はどうなる？
new ParentClass();
new ChildClass();
new ParentClass();
new ChildClass();
```

<details>
<summary>回答</summary>

:::note success
- ①② **<font color=blue>静的変数</font>** です。 PHP 5.0 の時点で既に利用できます。
    - **①普通の関数の内部でも状態保持のために利用できますし，<font color=red>②プロパティの機能を持っているクラス・オブジェクトもメソッド単位で利用できます</font>。**
- ③ **遅延静的束縛されるクラス名文字列** です。PHP **<font color=blue>5.5</font>** から利用できます。
    - `static::class` が書かれた場所を見ただけでは文字列の内容が確定していません。実際に `new` 演算子によるインスタンス化を記述した場所で，  「どのクラスのコンストラクタが呼ばれるか」 を考慮して文字列が決定されます。

出力
```
int(1)
int(2)
```

```
ParentClass::__construct() has been called 1 time(s)
ChildClass::__construct() has been called 2 time(s)
ParentClass::__construct() has been called 3 time(s)
ChildClass::__construct() has been called 4 time(s)
```

引っ掛け問題ですが… **<font color=red>`static` 変数はクラスの継承関係なくメソッドの存在ごとに独立している</font>ので，カウンターは親子で共有されています**。
:::
</details>

## Q4

- 👹 **出力はどうなるでしょうか？**

```php
<?php

class ParentClass
{
    public function __construct()
    {
        echo static::class . '::__construct() has been called '
           . static::increment() . " time(s)\n";
    }

    public static function increment(): int
    {
        static $i = 0;
        return ++$i;
    }
}

class ChildClass extends ParentClass
{
}

// 出力はどうなる？
new ParentClass();
new ChildClass();
new ParentClass();
new ChildClass();
```

<details>
<summary>回答</summary>

:::note warn
PHP バージョンによって出力が変わります。

PHP 8.1 以降
```
ParentClass::__construct() has been called 1 time(s)
ChildClass::__construct() has been called 2 time(s)
ParentClass::__construct() has been called 3 time(s)
ChildClass::__construct() has been called 4 time(s)
```

PHP 8.0 以前
```
ParentClass::__construct() has been called 1 time(s)
ChildClass::__construct() has been called 1 time(s)
ParentClass::__construct() has been called 2 time(s)
ChildClass::__construct() has been called 2 time(s)
```

先ほど

> **<font color=red>`static` 変数はクラスの継承関係なくメソッドの存在ごとに独立している</font>**

と書きましたが，これは PHP **<font color=red>8.1</font>** 以降で初めて担保された話です。 PHP 8.0 以前は，コンストラクタとそれ以外のメソッドで一貫性のない動きをしていました。

| PHP バージョン | コンストラクタ内での状態保持スコープ | コンストラクタ以外での状態保持スコープ |
|:---|:---|:---|
| 8.1 以降 | メソッド単位で独立 | メソッド単位で独立 |
| ~8.0 以前 | メソッド単位で独立 | **<font color=red>クラス・メソッド単位で独立</font>** |

- [【PHP8.1】静的変数を継承したときの挙動が変更になる - Qiita](https://qiita.com/rana_kualu/items/e65844540581952e73ff)

PHP 8.0 以前の動きを活用したことがあったので，個人的にはこちらの動作が正でコンストラクタの動作がバグであってほしかったのですが，逆になってしまいましたね…
:::
</details>

## 関連記事

https://qiita.com/mpyw/items/835050cbb5ad8a4c0710