12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【PHP next】PHPに静的コンストラクタが導入されるかもしれない

Last updated at Posted at 2024-09-23

静的コンストラクタとは、C#では静的コンストラクタJavaでは静的イニシャライザKotlinではCompanion objectsと何故か言語ごとにバラバラの名前で呼ばれている機能です。
実は細かい違いがあるのかもしれませんがよくわかりません。

Java
public class Hoge{
  static {
    System.out.println("hoge");
  }
}

このメソッドは、このクラスを初めて使おうとしたときに最大一回だけ実行されます。
使おうとしたときというのはnew()しようとしたとかstaticメンバーを呼び出そうとしたとかその他諸々です。
複数回呼ばれることはないので、一度だけ実行したいクラス変数の初期化などに使えます。
たとえばC#のドキュメントではログファイルへの接続がサンプルとしてあげられていました。

というわけでPHPにも静的コンストラクタがあると便利なんじゃないかなというRFCが出ていたので見てみることにします。
以下は該当のRFC、Static Constructorsの紹介です。

PHP RFC: Static Constructors

Introduction

PHPでは、静的プロパティを定数でない値で初期化するためには面倒な回避策が必要であり、コードが読みにくくなります。
以下は、いくつかのコードベースで遭遇したコードの例です。
このコードでは、静的プロパティを使用する前に必ず初期化メソッドを呼び出して、適切に初期化されているかどうかを確認する必要があります。

class MyClass
{
    private static \DatetimeImmutable $minDate;
 
    public function __construct(
        private \DateTimeInterface $scheduledDate
    ) {
        self::initializeMinDate();
 
        if ($scheduledDate < self::$minDate) {
            $errMsg = 'Cannot set a scheduledDate before [' . self::$minDate->format('d/m/Y H:i:s') . ']';
 
            throw new \DomainException($errMsg);
        }
    }
 
    private static function initializeMinDate()
    {
        if(isset(self::$minDate)) {
            return;
        }
 
        // コンフィグとかから取得
        self::$minDate = new \DatetimeImmutable('2024-01-01 00:00:00');
    }
 
    // ...
}

このような定型文は可読性が低下し、メソッドを追加したときに初期化メソッドを呼び忘れてバグを誘発する可能性があります。

class MyClass
{
    private static \DatetimeImmutable $minDate;
 
    public function __construct(
        private \DateTimeInterface $scheduledDate
    ) {
        self::initializeMinDate();
 
        if ($scheduledDate < self::$minDate) {
            $errMsg = 'Cannot set a scheduledDate before [' . self::$minDate->format('d/m/Y H:i:s') . ']';
 
            throw new \DomainException($errMsg);
        }
    }
 
    public static function getMinDate()
    {        
        return self::$minDate;
    }
 
    private static function initializeMinDate()
    {
        if(isset(self::$minDate)) {
            return;
        }
        self::$minDate = new \DatetimeImmutable('2024-01-01 00:00:00');
    }
 
    // ...
}
 
// メソッドを増やしたけど初期化し忘れたのでエラー
echo MyClass::getMinDate()->format('d/m/Y H:i:s') . PHP_EOL;

Proposal

静的プロパティの初期化を効率化し、コードの明確化を向上させるため、新たなマジックメソッド__staticConstructの導入を提案します。
このメソッドは、クラスの静的プロパティ初期化処理の直後に、PHPエンジンから呼び出されます。

このRFCを提案する前に、ユーザランドオートローダーを利用して同機能を実装できないか検討しましたが、この機能はオートローダーの範囲外であることは明らかでした。

Addressing Potential Criticism

あるデザインパターンで名指しされているように、またテスト可用性の視点から静的プロパティの使用は避けるべきであると言う人もいるかもしれません。
しかし、静的プロパティはPHPに既に存在しており、また一部のシナリオでは役立つことはあきらかです。
このRFCを否定しても、静的プロパティが有益である実際の使用例や自然なアプローチを否定することはできません。

Comparison with Other Object-Oriented Languages

Javaでは、この目的のためにメソッド呼び出しもしくは静的ブロックを使用します。
オブジェクト指向原則に厳格な言語でも、静的プロパティが役立つ場合があり、適切な初期化メソッドが必要であるという事実を示しています。

A real-life example

既に存在する例のひとつとして、ComposerのClassLoaderクラスがあります。
このクラスは、クロージャを使用して静的プロパティ$includeFileを初期化しています。
ただしコンストラクタ内で行われているため、コードが読みにくくなっています。

// ※実際は長文コメントがあるがコピペにあたり削除している
class ClassLoader
{
    public function __construct($vendorDir = null)
    {
        $this->vendorDir = $vendorDir;
        self::initializeIncludeClosure();
    }
 
    private static function initializeIncludeClosure()
    {
        if (self::$includeFile !== null) {
            return;
        }
 
        self::$includeFile = \Closure::bind(static function($file) {
            include $file;
        }, null, null);
    }

__staticConstructが実装されたら、以下のように書けるようになります。

class ClassLoader
{
    public function __construct($vendorDir = null)
    {
        $this->vendorDir = $vendorDir;
    }
 
    private static function __staticConstruct()
    {
        self::$includeFile = \Closure::bind(static function($file) {
            include $file;
        }, null, null);
    }
}

静的コンストラクタが導入されると、静的プロパティの初期化が簡素化されるだけでなく、コードの可読性と保守性も向上することを示しています。
このRFCは、静的コンストラクタの導入によってPHPのオブジェクト指向機能を強化することを目的としています。

What this RFC is not

このRFCは、静的プロパティの無差別な使用を勧めるものではなく、静的プロパティが有用な場合に適切な初期化方法を提供することを目的としています。

Implementation on other programming languages

Java

Javaには静的コンストラクタそのものはありませんが、静的プロパティを初期化するための他の方法が用意されています。

1.staticイニシャライザ

import java.util.*;
import java.lang.*;
import java.io.*;
 
class Whatever {
    public static int myVar;
 
    static {
        myVar = 10;
    }
}
 
class Main {
    public static void main(String[] args) {
        System.out.println("Hello world (" + Whatever.myVar + ")!");
    }
}

2.変数に直接メソッド書く

import java.util.*;
import java.lang.*;
import java.io.*;
 
class Whatever {
    public static int myVar = initializeInstanceVariable();
 
    protected static int initializeInstanceVariable() {
        return 20;
    }
}
 
class Main {
    public static void main(String[] args) {
        System.out.println("Hello world (" + Whatever.myVar + ")!");
    }
}

Kotlin

コンパニオンオブジェクトと@JvmStaticアノテーションで実装できます。

class Whatever {
    companion object {
        @JvmStatic
        var myVar: Int
 
        init {
            myVar = 10
        }
    }
}
 
fun main() {
    println("Hello, world(" + Whatever.myVar + ")!")
}

C#

静的コンストラクタは修飾子のないstaticメソッドであり、ランタイムからのみ呼び出されます。

using System;
using System.Reflection;
 
public class C1
{
    public static int myVar;
 
    static C1()
    {
        C1.initStaticProps();
        Console.WriteLine("C1::__staticConstructor");
    }
 
    public static void initStaticProps()
    {
        C1.myVar = 10;
        Console.WriteLine("Initializing C1::test");
    }
 
    public static void test()
    {
        Console.WriteLine("C1::test");
    }
}

public class Program
{
    public static void Main()
    {
        /// どのように呼び出そうとしても静的コンストラクタがトリガーされる
 
        // Reflection
        Type c1Type = typeof(C1);
 
        // Instance constructor
        var c = new C1();
 
        // Static method call
        C1.test();
 
        // Property access
        Console.WriteLine("C1::myVar:" + C1.myVar);
    }
}

C++

C++17において、定数式ラムダを使用する方法が提供されました。

#include <iostream>
#include <vector>
 
class Example {
    public:
        inline static const std::vector<char> letters = [] {
            std::vector<char> letters;
 
            for (char c = 'a'; c <= 'z'; c++) {
                letters.push_back(c);
            }
 
            return letters;
        }();
};
 
int main() {
    for (auto i = 0; i < Example::letters.size(); i++) {
        std::cout << "Example::letters[" << i << "]: " << MyClass::letters[i] << std::endl;    
    }
 
    return 0;
}

Swift

C++同様、クロージャでプロパティの初期化が可能です。

class Example {
    static var myVar: Int = {
        print("Static initializer called")
        return 42
    }()
 
    static func display() {
        print("Example::myVar: \(myVar)")
    }
}
 
Example.display()

Design decisions

この実装はC#からヒントを得ていますが、C#ほど厳しくはありません。
ユーザは必要に応じて__staticConstructメソッドを任意に呼び出すことが可能です。

__staticConstructメソッドの可視性はprivateです。
これにより、意図しない親クラスの静的プロパティのリセットを防ぎます。

このRFCは、以下の2シグネチャをサポートしています。

private static __staticConstruct();
private static __staticConstruct(): void;

__staticConstructメソッドに引数はありません。

Exception handling

静的コンストラクタから例外がスローされる可能性があり、ユーザはそれを回復することができます。
どのような対応を行うかはユーザに一任されます。

静的コンストラクタは、PHPエンジンからは2度呼び出されることはありません。
つまり、静的コンストラクタが例外を出した場合、そのクラスは初期化されません。

Example

子クラスのない例。

class MyClass
{
    protected static \DatetimeImmutable $minDate;
 
    public function __construct(
        private \DateTimeInterface $scheduledDate
    ) {
        if ($scheduledDate < self::$minDate) {
            $errMsg = 'Cannot set a scheduledDate before [' . self::$minDate->format('d/m/Y H:i:s') . ']';
 
            throw new \DomainException($errMsg);
        }
    }
 
    public static function getMinDate()
    {        
        return self::$minDate;
    }
 
    private static function __staticConstruct(): void
    {
        self::$minDate = new \DatetimeImmutable('2024-01-01 00:00:00');
    }

}
 
// エラーは出ない
echo MyClass::getMinDate()->format('d/m/Y H:i:s') . PHP_EOL;

子クラスのある例。

class MyClass1
{
    protected static \DateTimeImmutable $minDate;
 
    public function __construct(
        private \DateTimeImmutable $scheduledDate
    ) {
        if ($scheduledDate < self::$minDate) {
            $errMsg = 'Cannot set a scheduledDate before [' . self::$minDate->format('d/m/Y H:i:s') . ']';
 
            throw new \DomainException($errMsg);
        }
    }
 
    public static function getMinDate()
    {
        return static::$minDate;
    }
 
    public function getScheduledDate(): \DateTimeImmutable
    {
        return $this->scheduledDate;
    }
 
    protected static function initMinDate(): \DateTimeImmutable
    {
        return new \DatetimeImmutable('2024-01-01 00:00:00');
    }
 
    private static function __staticConstruct(): void
    {
        self::$minDate = self::initMinDate();
    }
}
 
class MyClass2 extends MyClass1
{
    protected static \DateTimeImmutable $minDate;
 
    private static function __staticConstruct(): void
    {
        self::$minDate = parent::initMinDate()->modify('+1 day');
    }
}
 
echo MyClass1::getMinDate()->format('d/m/Y H:i:s'); // 01/01/2024 00:00:00
echo MyClass2::getMinDate()->format('d/m/Y H:i:s'); // 02/01/2024 00:00:00

Backward Incompatible Changes

ユーザランドで__staticConstructメソッドを実装していた場合は動かなくなります。
ただし、__で始まるメソッドはユーザランド実装は推奨されていません。
GitHub内には__staticConstructメソッドを実装しているコードは存在しませんでした。

Version

次のマイナーバージョン。

Implementation

https://github.com/php/php-src/compare/master...erickcomp:php-src_static-constructor:master

感想

メーリングリストは意外なことにわりと否定的意見が多めです。
あれば便利だとは思うんですけどどうしてなんでしょうかね。

個人的には、既存実装を参考にするのであれば仕様も既存実装にしたがってほしかったですね。
要するに自力で__staticConstruct呼べちゃうのはやめようってことですが。
まあPHPにはprivateより狭い可視性ってないからしょうがないのかな。わからんけど。

そんなわけでもしかしたら今後PHP8.5とかで入るかもしれない、あるいはそのまま立ち消えになるかもしれない静的コンストラクタの紹介でした。

12
4
0

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
12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?