38
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PHP の static の種類,全部言えるかな?

Last updated at Posted at 2022-06-27

Q1

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

  • 🔰 どういう役目を持っているでしょうか?
  • 🔰 どの PHP バージョン以降で有効でしょうか?
    (但し,PHP 5.0 より前には遡りません)
  • 🔰 出力される商品 ID はどうなるでしょうか?
<?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,
);
回答
  • ①③ 静的プロパティ です。全インスタンスで共有されます。PHP 5.0 から利用できます。
    • ① も ② も private ではないため,継承クラスでは ③ が ① を上書きしています。
  • 遅延静的束縛される静的プロパティ のインクリメントです。 PHP 5.3 から利用できます。
    • ++static::$counter が書かれた場所を見ただけでは参照するプロパティが確定しません。実際に new 演算子によるインスタンス化を記述した場所で, 「どのクラスのコンストラクタが呼ばれるか」 を考慮して参照するプロパティが決定されます。

出力

int(1)
int(100001)
int(2)

もし ++self::$counter であれば,以下のようになってしまいます。

int(1)
int(2)
int(3)

Q2

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

イミュータブル版
$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();
ミュータブル版
$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 バージョン以降で有効でしょうか?
    (但し,PHP 5.0 より前には遡りません)
<?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;
    }
}
回答
  • 遅延静的束縛されるコンストラクタコール です。PHP 5.3 から利用できます。
    • new static() が書かれた場所を見ただけではインスタンス化するクラスが確定しておらず, new 演算子で作られたクラスから実際にメソッドを呼ぶ記述をした場所で確定します。
  • 遅延静的束縛されるメソッドコールです。 PHP 5.3 から利用できます。
    • static::copy() が書かれた場所を見ただけではどのクラスのメソッドを呼び出すか確定しておらず, new 演算子で作られたクラスから実際にメソッドを呼ぶ記述をした場所で確定します。
  • 遅延静的束縛される返り値 です。PHP 8.0 から利用できます。
    • : static が書かれた場所を見ただけでは返り値として拘束する型が確定しておらず, new 演算子で作られたクラスから実際にメソッドを呼ぶ記述をした場所で確定します。
  • クロージャへの $this バインドを行わない宣言 です。 (アロー関数を使わなければ) PHP 5.3 から利用できます。
    • エッジケースではありますが,明示的に宣言することによって,メモリ使用量の削減に繋がる場合があります。
  • ① もし new self() であれば, MutableProductCollection から ->copy() を呼んでも ProductCollection が作られてしまいます。
  • ③ もし : self であれば,静的解析ツールでは MutableProductCollection の返り値を正しく認識することができません。 MutableProductCollection から ->copy() を呼んでも (実際は MutableProductCollection になるものの) ProductCollection であると解析されてしまいます。
    • 但し, static はほぼ静的解析ツールのためだけのものです。 継承先の static 型として見なせるものは全て継承元 self 型でもある ため,動作上はこの 2 つの間に差異はありません。
  • ④ ここではクロージャ内部に $this は登場していませんが, 参照していなくても暗黙的に $this がバインドされています。以下のようなコードを書いた場合は static 宣言がないとメモリを大量消費してしまいます。
    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();
    }
    

Q3

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

  • 🔰 どういう役目を持っているでしょうか?
  • 🎓 どの PHP バージョン以降で有効でしょうか?
    (但し,PHP 5.0 より前には遡りません)
  • 🎓 出力はどうなるでしょうか?
<?php

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

// 出力はどうなる?
var_dump(counter());
var_dump(counter());
<?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();
回答
  • ①② 静的変数 です。 PHP 5.0 の時点で既に利用できます。
    • ①普通の関数の内部でも状態保持のために利用できますし,②プロパティの機能を持っているクラス・オブジェクトもメソッド単位で利用できます
  • 遅延静的束縛されるクラス名文字列 です。PHP 5.5 から利用できます。
    • 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)

引っ掛け問題ですが… static 変数はクラスの継承関係なくメソッドの存在ごとに独立しているので,カウンターは親子で共有されています

Q4

  • 👹 出力はどうなるでしょうか?
<?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();
回答

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)

先ほど

static 変数はクラスの継承関係なくメソッドの存在ごとに独立している

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

PHP バージョン コンストラクタ内での状態保持スコープ コンストラクタ以外での状態保持スコープ
8.1 以降 メソッド単位で独立 メソッド単位で独立
~8.0 以前 メソッド単位で独立 クラス・メソッド単位で独立

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

関連記事

38
21
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?