プログラミング

プログラミング初心者のPHP修羅の道#6〜PHP8上級問題を徹底追求する〜

この記事のキーワード

コンストラクタ・デストラクタ・シグネチャに関するルール・リスコフの置換原則・オーバーライド・マジックメソッド

PHP8上級資格取得へ向けたアウトプット記事になります。

PHP試験運営団体より公開されている模擬試験を1つ1つみていく形で事細かくみていきますが、勉強途中ゆえ間違っている部分に関してはご了承ください。

PHP上級資格のメイン教材はPHPマニュアルと指定されていますので、このマニュアルを主に踏襲した内容となっています。

主な勉強教材は以下になります。

1.独習PHP第4版

2.はじめてのPHP

 

3.PHP公式マニュアル

 

PHP8上級試験模擬問題

試験問題4

 

メソッドに関する説明の中で、誤っているものを1つ選びなさい。
なお、すべてのコードの先頭には下記のコードが書かれているものとする。

declare(strict_types=1);
error_reporting(-1);

 

 

選択肢①

 

PHPにおいて、コンストラクタは、__construct() メソッドで実装される。
そのため、以下のコード

class Hoge {
    public function __construct() {
        echo __METHOD__, PHP_EOL;
    }
}

$obj = new Hoge();

は正しく実行でき、結果は Hoge::__construct となる。

なお、コンストラクタを有している場合、親クラスのコンストラクタが暗黙の内にコールされることはない。
そのため、以下のコード

class Hoge {
    public function __construct() {
        echo __METHOD__, PHP_EOL;
    }
}

class Foo extends Hoge{
    public function __construct() {
        echo __METHOD__, PHP_EOL;
    }
}

$obj = new Foo();

を実行すると、結果は Foo::__construct となる。

親クラスのコンストラクタを実装する場合には、parent::をコールする事が必要となる。
そのため、以下のコード

class Hoge {
    public function __construct() {
        echo __METHOD__, PHP_EOL;
    }
}

class Foo extends Hoge{
    public function __construct() {
        parent::__construct();
        echo __METHOD__, PHP_EOL;
    }
}

$obj = new Foo();

を実行すると、結果は次のとおりとなる。

Hoge::__construct
Foo::__construct

 

 

まずはコンストラクタについて詳しく見ていきましょう。

コンストラクタはオブジェクトが生成される度コールされるメソッドのため、そのオブジェクトを使用する前に必要な初期化が行えるメリットがあります。

その他の特徴として以下が挙げられます。

【子クラスがコンストラクタを有している場合】

・親クラスのコンストラクタは明示的に「parent::__construct」と呼び出されない限り暗黙的にコールはされません。但し、選択肢と異なり「$obj = new Hoge();」と親クラスのオブジェクトを生成していれば親クラスのコンストラクタが呼び出されます。

【子クラスでコンストラクタが定義されていない場合】

・通常のメソッドと同じく、親クラスのコンストラクタを継承します。

 

通常のメソッドとコンストラクタの違い

子クラスでオーバーライドした場合に「シグネチャの互換性に関するルール」は適用されないこと。(privateメソッドも含む)

 

前回の記事で「オーバーライドルール」を説明しましたが、オーバーライド時には子クラスのシグネチャが親クラスのシグネチャと互換性がなければならないという「シグネチャの互換性に関するルール」が存在します。

互換性が壊れた場合、致命的なエラーが発生します。PHP 8.0.0 より前のバージョンでは互換性が壊れた場合に、E_WARNING レベルの警告が発生していました

 

シグネチャの互換性に関するルール

①「共変性と反変性」の規則が守られていること

②必須の引数はオプションにしていること

③新しい引数はオプションにしていること

これらのルールはリスコフの置換原則として知られています。

リスコフの置換原則について

リスコフの置換原則はis-a関係を確認するための代表的なアプローチです。

「サブクラスのインスタンスは、常にスーパークラスのインスタンスと置き換え可能である」という原則です。

is-a関係について

SubcCass is a SuperClass(サブクラスがスーパークラスの1種である)ということを指しています。

ベンツ(子)は車(親)ですが、車はベンツとは限りません。ランボルギーニもあればアストンマーチンもあり、これらの場合はis-a関係ではありません。

1つずつ深掘っていきましょう。

「共変性」と「反変性」とは

PHP7.4以降で完全にサポートされるようになりました。

共変性

子クラスのメソッドが、親クラスの戻り値よりも、より特定の、狭い型を返すことを許すことです。

反変性

親クラスのものよりも、より抽象的な、広い型を引数に指定することを許すものです。

 

つぎにルールの②、③についてを見ていきます。

以下様々なパターンで互換性を確認してみました。

class Base
{
    public function foo(int $a = 30, int $b = 40) {
        echo $a.$b." ";
    }

    public function bar(int $a,int $b) {
        echo $a.$b." ";
    }
    
    public function hoge(int $a,int $b = 2) {
        echo $a.$b." ";
    }
}
class Extend extends Base{
    //引数を必須foo($a,$b)にすると致命的なエラー
    //引数を削除foo()しても致命的なエラー
    //引数が少なくfoo($a = 1)ても致命的なエラー
    function foo($a = 1, $b = 2) {
        parent::foo();  
        parent::foo($a);
        parent::foo($b);
        parent::foo($a,$b);      
    }
    
    function bar($a, $b) {
        //parent::bar();  致命的なエラー
        //parent::bar($a); 致命的なエラー
        //parent::bar($b); 致命的なエラー
        parent::bar($a,$b);
    }

    function hoge($a=3, $b = 1) {
        //parent::hoge();  致命的なエラー
        parent::hoge($a);
        parent::hoge($b);
        parent::hoge($a,$b);
    }
}

$w = new Extend();
var_dump($w->foo());
var_dump($w->bar(2,8));
var_dump($w->hoge(3,7));

//foo() → 出力結果:3040 140 240 12 NULL
//bar() → 出力結果:2 8 NULL
//hoge() → 出力結果:32 72 37 NULL

 

fooメソッド

親クラスのメソッドの引数にデフォルト値が設定されているため、オーバーライドされたメソッドにもデフォルト値を設定する必要がある」ことを確認しています。

barメソッド

親クラスのメソッドの引数が必須になっているため、指定の個数の値を渡す必要がある」通常のメソッドと同様の動作を確認しています。

hogeメソッド

親クラスのメソッドの引数が必須になっていても、オーバーライドされたメソッドが親クラスのメソッドのシグネチャを踏襲していればオプション引数になっていても問題ない」ことを確認しています。

メソッドの引数の名前は統一しよう!

メソッドの引数の名前を子クラスで変更してもシグネチャ上は非互換になりませんが、名前付き引数」を使った時に実行時エラーになるので、推奨されていません。

class A {
    public function test($foo, $bar) {}
}
class B extends A {
    public function test($a, $b) {}
}
$obj = new B;
//オーバーライドされたtest()の引数に値を渡すことが出来ない
$obj->test(foo: "foo", bar: "bar"); // 致命的なエラー発生!

名前付き引数とは

呼び出し時に名前を明示的に指定できる引数のこと。

仮引数名:値」の形式で呼び出すことで、引数が増えても意味を把握しやすくなるメリットがあります。

よって、この選択肢は合っています。

 

選択肢②

 

PHPにおいて、デストラクタは、__destruct() メソッドで実装される。
そのため、以下のコード

class Hoge {
    public function __destruct() {
        echo __METHOD__, PHP_EOL;
    }
}

$obj = new Hoge();

は正しく実行でき、結果は Hoge::__destruct となる。

なお、デストラクタを有している場合、親クラスのデストラクタは、暗黙の内にコールされる。コールされる順番は「子クラスのデストラクタ → 親クラスのデストラクタ」の順番である。
そのため、以下のコード

class Hoge {
    public function __destruct() {
        echo __METHOD__, PHP_EOL;
    }
}

class Foo extends Hoge{
    public function __destruct() {
        echo __METHOD__, PHP_EOL;
    }
}

$obj = new Foo();

を実行すると、結果は次のとおりとなる。

Foo::__destruct
Hoge::__destruct

 

 

コンストラクタはオブジェクトが生成される度にコールされるメソッドでした。

一方デストラクタは、「オブジェクトが破棄されるタイミング、つまりスクリプト終了のタイミングで実行されるメソッドです。

デストラクタは以下の特徴を持っています。

・引数を受け取ってはいけない

・戻り値を返してはいけない(コンストラクタも同様)

・アクセス修飾子は必ずpublic

・スクリプト終了のタイミングでデストラクターが呼び出される

・特定のオブジェクトを参照するリファレンスがひとつもなくなったときにコールされる

また、コンストラクタ同様に子クラスで定義されていなければ親クラスから継承します。

下記コードはデストラクタの特徴を表しています。

class Super {
    //スクリプト終了のタイミングで実行される
    function __destruct() {
        print "Destroying " . __CLASS__ . "\n";
    }
    //オブジェクトのインスタンス化のタイミングで実行される。
    function __construct() {
        print "In constructor\n";
    }
}
class Sub extends Super { }

$obj = new Sub();

//出力結果
//In constructor
//Destroying MyDestructableClass

 

よって、「Foo::__destruct」のみ出力されなければならないため、こちらの選択肢は誤りです。

 

選択肢③

 

__call() マジックメソッドを使うと「アクセス不能なメソッドがオブジェクトのコンテキストで呼び出された時」に、処理を入れる事ができる。
そのため、以下のコード

class Hoge {
    public function __call(string $name, array $arguments) {
        echo "call: {$name}", PHP_EOL;
        var_dump($arguments);
        echo PHP_EOL;
    }
}

$obj = new Hoge();
$obj->test();
$obj->test2(1, '2', [3]);

は正しく実行でき、結果は次のとおりとなる。

call: test
array(0) {
}

call: test2
array(3) {
  [0]=>
  int(1)
  [1]=>
  string(1) "2"
  [2]=>
  array(1) {
    [0]=>
    int(3)
  }
}

 

 

まずはマジックメソッドについて深掘っていきましょう。

マジックメソッドとは

「あらかじめ特定の役割を与えられたメソッド」のこと。「__construct」や「__destruct」もマジックメソッドの1種です。

マジックメソッドの特徴には以下が挙げられます。

・シグネチャ(名前と引数、戻り値の組み合わせ)は決められている

・呼び出しのタイミングも決められている

・中身のないメソッドのため、機能は自分で実装する必要がある

マジックメソッドには注意点もあります。

__construct(), __destruct(), __clone()を除く全てのマジックメソッドは、publicとして宣言しなければE_WARNINGレベルの警告が発生します。

・マジックメソッドの定義で型宣言が使われている場合、 「シグネチャに関するルール」 に沿っている必要があります。ルールに沿っていない場合は、PHP8.0.0以降では致命的なエラーが発生します。(PHP8.0.0以前はエラーなし)

__construct()__destruct()については、戻り値の型を宣言してはいけません。 宣言すると、致命的なエラーが発生します。

 

PHPにおける主なマジックメソッドとして以下が挙げられます。

マジックメソッド 呼び出しのタイミング
__get 未定義のプロパティを取得しようとした時
__set 未定義のプロパティを設定しようとした時
____isset 未定義のプロパティをisset関数で処理しようとした時
__unset 未定義のプロパティをunset関数で処理しようとした時
__call 未定義のインスタンスメソッドをコールした時
__callStatic 未定義の静的メソッドをコールした時
__toString print命令等でオブジェクトの文字列表現を要求された時
__invoke オブジェクトが関数の形式で呼び出された時
__clone clone命令でオブジェクトを複製した時
__debugInfo var_dump命令でオブジェクトを出力しようとした時

 

 

ここの選択肢では__callメソッドの使い方について聞かれています。

__callメソッド__callStaticメソッド

$name:メソッド名

$arguments:メソッドに渡された引数

__call/__callStaticメソッドを利用するメリットは、

クラス定義の時点では決めておけない任意のメソッドを処理できる

ことにあります。 ちなみに、__call/__callStaticメソッドの第2引数以降が指定されている場合は無視されます

  よって、この選択肢は合っています。

 

選択肢④

 

__callStatic() マジックメソッドを使うと「アクセス不能なメソッドが静的コンテキストで呼び出された時」に、処理を入れる事ができる。

そのため、以下のコード

class Hoge {
    public static function __callStatic(string $name, array $arguments) {
        echo "call: {$name}", PHP_EOL;
        var_dump($arguments);
    }
}

Hoge::test(1, '2', [3]);

は正しく実行でき、結果は次のとおりとなる。

call: test
array(3) {
  [0]=>
  int(1)
  [1]=>
  string(1) "2"
  [2]=>
  array(1) {
    [0]=>
    int(3)
  }
}

なお、__callStatic() マジックメソッドが動くのは「静的コンテキストで呼び出された時」だけのため、「オブジェクトのコンテキストで呼び出された時」には動かない。
そのため、以下のコード

class Hoge {
    public static function __callStatic(string $name, array $arguments) {
        echo "call: {$name}", PHP_EOL;
        var_dump($arguments);
    }
}

$obj = new Hoge();
$obj->test();

を実行すると、結果は次のとおりとなる。

Fatal error: Uncaught Error: Call to undefined method Hoge::test() in ...

 

 

__callStaticメソッドの特徴は前の選択肢③で挙げていますが、注意点は問題文にもある通りアロー演算子を使用しての呼び出しはできません

また、__callStaticメソッドをstaticメソッドとして宣言しない場合も致命的なエラーが発生します。

よって、この選択肢は合っています。

ABOUT ME
ヒロ
社会人4年目/25歳/食品商社で2年間営業した後、IT業界にシステムエンジニアとして転職/Java,PHP言語を扱う開発エンジニア