元ネタは、 https://speakerdeck.com/twada/growing-reliable-code-phperkaigi-2022?slide=84 を見て「コンストラクタが2回動くの!?」って質問があったので。
端的には「PHPにおいてコンストラクタはマジックメソッドで、マジックメソッドはメソッドだから、callすれば動く」っていうのが身も蓋もない回答になります。
かみ砕いて。
まずPHPにおける「マジックメソッド」は、以下の通り。
https://www.php.net/manual/ja/language.oop5.magic.php
マジックメソッドは、 ある動作がオブジェクトに対して行われた場合に、 PHP のデフォルトの動作を上書きする特別なメソッドです。
"ある動作がオブジェクトに対して行われた場合"をトリガーに"PHP のデフォルトの動作を上書きする特別なメソッド"、なので、基本「メソッド」です。
以下の関数名は、マジックメソッドと見なされます: __construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __serialize(), __unserialize(), __toString(), __invoke(), __set_state(), __clone(), __debugInfo()
とあるように、マジックメソッドです。
なので、PHPの挙動としては
・インスタンスを作る
→ "インスタンスを作る"動作がオブジェクトに対して行われた場合、 PHPのデフォルトの動作を上書きする __construct() メソッドがあるなら、そいつを実行する
ってな感じになります。
なのでまぁいわゆる「コンストラクト時の動作を __construct() メソッドに書いておけば」インスタンスが生成される時に「明示的には呼んでないけど、自動でcallされる」んですね。
んで。
とはいえまぁ__construct()くんも「メソッド」なので。
「明示的にcallされた」ら、それはまぁ「呼ばれたから動く」わけなんですね。
かくしてまぁ。元ネタのDateTimeImmutableで「コンストラクタを明示的に叩くとコンストラクタが呼ばれる(ので、値が書き換えられる」ってのは、まぁ「ですよね~」くらいの挙動になります。
「はて他言語はどうなってるんだろ?」と思い、なんとかするっと目に書けるC++を(幾分思い出しつつ)書いてみたのですが。
#include <iostream>
class Hoge {
public:
// コンストラクタ
Hoge() {
std::cout << "construct" << std::endl;
}
};
int main() {
Hoge *h = new Hoge();
h->Hoge();
return 0;
}うんそもそもコンストラクタはコンストラクタであって「クラス関数ではない」んだよなぁ。
なのでまぁ
t.cpp: 関数 ‘int main()’ 内:
t.cpp:13:8: エラー: 無効な ‘Hoge::Hoge’ の使用です
h->Hoge();
^
って言われるですだよ。うん。
……それ以外の言語はど~なんだろうねぇ?*1
話を戻して。
なのでまぁ、PHPのコンストラクタは所詮「メソッド」なので、「呼べば動く」ですます。
お仕事で考える場合、この辺は「明示的に呼ぶな」で終わっていいような気がするんだよなぁ……なんていうか「そこまで配慮せにゃならんのを現場で使いたいかね?」とか、正直。
ただまぁ「思考実験として」であれば、例えばこんな感じか。
<?php
declare(strict_types=1);
error_reporting(-1);
class Hoge
{
public function __construct()
{
// コンストラクタ二重callのロック機構
static $double_call_lock = [];
$object_id = spl_object_id($this);
if (null !== ($double_call_lock[$object_id] ?? null)) {
throw new \Exception('コンストラクタ明示的に呼ぶとか、おまえ、正気か?');
}
$double_call_lock[$object_id] = true;
}
}
//
$obj = new Hoge();
$obj2 = new Hoge();
$obj3 = new Hoge();
//$obj->__construct();ただ、これ(spl_object_id)も「このオブジェクトidはオブジェクトが生きている間ユニーク」なので。
// $obj = new Hoge(); $obj = new Hoge(); $obj = new Hoge();
ってやると
[gallu@ik1-111-11111 ~]$ php t.php
int(1)
int(2)
int(1)Fatal error: Uncaught Exception: コンストラクタ明示的に呼ぶとか、おまえ、正気か? in /home/gallu/t.php:14
Stack trace:
#0 /home/gallu/t.php(23): Hoge->__construct()
#1 {main}
thrown in /home/gallu/t.php on line 14
ってな感じになる事があるんで、あんまり十全に大丈夫でもなし。
いやまぁがっちょりとデストラクタまで書いて
<?php
declare(strict_types=1);
error_reporting(-1);
class Hoge
{
public function __construct()
{
// コンストラクタ二重callのロック機構
$object_id = spl_object_id($this);
var_dump($object_id);
if (null !== ($this::$double_call_lock[$object_id] ?? null)) {
throw new \Exception('コンストラクタ明示的に呼ぶとか、おまえ、正気か?');
}
$this::$double_call_lock[$object_id] = true;
}
public function __destruct()
{
// 「コンストラクタ二重callのロック機構」の解除
$object_id = spl_object_id($this);
echo "unset {$object_id}\n";
unset($this::$double_call_lock[$object_id]);
}
private static $double_call_lock = [];
}
//
$obj = new Hoge();
$obj = new Hoge();
$obj = new Hoge();
//$obj->__construct();ってやれば多分
[gallu@ik1-111-11111 ~]$ php t.php
int(1)
int(2)
unset 1
int(1)
unset 2
unset 1
ってな具合にはなるんだろうけど、「そこまで書く?」って話になりそうだしねぇ。
なのでまぁ「コンストラクタで値を設定、そのインスタンスの値は不変」っていうのは、PHPの場合ある程度「善意と良識」に守られている所があるんだろうなぁ、と。
ただまぁ「その辺を横紙に破りまくるような御方」は、ちょっと……って思うので。
その辺はまぁ「どーゆースタンスを持つか」次第なんじゃないかなぁ? とか思うんだけど、ど~なんだろうねぇ???
*1:コメントとかでいただければ追記いたしまする