プログラミング

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

この記事のキーワード

self・parent・static・遅延静的束縛・転送コール・PHP_EOL・オーバーライド・抽象クラス・抽象メソッド・abstract

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

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

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

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

1.独習PHP第4版

2.はじめてのPHP

 

3.PHP公式マニュアル

 

PHP8上級試験模擬問題

試験問題3

 

クラスに関する説明の中で、誤っているものを1つ選びなさい。
なお「¥」はバックスラッシュに読み替えること。
また、すべてのコードの先頭には下記のコードが書かれているものとする。

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

 

 

選択肢?

 

三つの特別なキーワード selfparent そして static がクラス定義の内部からプロパティまたはメソッドにアクセスする際に使用される。
そのため、以下のコード

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

    public static function testStatic() {
        echo __METHOD__, PHP_EOL;
    }

    public static function staticCall() {
        self::testStatic();
        static::testStatic();
    }
}

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

    public static function testStatic() {
      echo __METHOD__, PHP_EOL;
    }
}

$obj = new Foo();
$obj->test_1();
Foo::staticCall();

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

Hoge::test_1
Foo::test_1
Hoge::testStatic
Foo::testStatic

 

まずは継承の内「self」と「static」について押さえておきましょう。

self::」の性質:定義された時のクラスを参照する

親クラスの関数で「salf::testStatic();」が定義されている場合、子クラスでこの関数を呼び出すとオーバーライドされた子クラスのtestStatic()メソッドではなく、親クラスのtestStatic()メソッドが呼び出される。

static::」の性質:実行時に直近でコールされたクラスを参照する

親クラスの関数で「static::testStatic();」が定義されていても、直近でコールされたクラスを参照するため、今回の場合はFooクラスを参照する。

→これを「遅延静的束縛」と呼びます。

遅延静的束縛は直近の “非転送コール” のクラス名を保存します。

“非転送コール” のクラス名」とは、staticメソッドの場合に明示的に指定されたクラス(?「::」演算子の左側に書かれたもの)のことです。

また、?”転送コール” とは、self::?や?parent::static::?による 「::」 演算子を使ったコールのことを言います。?

 

以上のポイントを踏まえて順を追って見ていくことで簡単に解くことができます。

?
$obj = new Foo();
$obj -> test_1(); //Fooクラスのtest_1()メソッドの呼び出し

?
parent::test_1(); //Hogeクラスのtest_1()メソッドの呼び出し

 → 「Hoge::test_1」の出力

Fooクラスのtest_1()メソッドに戻り

 → 「Foo::test_1」の出力

?
Foo::staticCall() //FooクラスのstaticCall()メソッドの呼び出し

 → FooクラスにstaticCall()メソッドがないため親クラスを検索

?
self::testStatic(); //HogeクラスのtestStatic()メソッドの呼び出し

 → 「Hoge::testStatic」の出力

?
static::testStatic(); //呼び出し元がFooクラスのstaticCall()メソッドなので、

 → FooクラスのstaticCall()メソッドを呼び出し

 → 「Foo::testStatic」を出力

 

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

また、補足として「PHP_EOL」についても触れておきます。

PHP_EOLとは

実行環境のOSに対応する改行コードを出力するための定数で、文字列と結合させて使うことでコンソールに改行された状態で出力することが出来ます。

Windows環境rn(CRLF)

Linux/Unix系、MacOS系n(LF)

※但し、ブラウザで確認する場合はHTMLと認識されるため<br>でないと改行はされません。

 

選択肢?

 

static メソッドはオブジェクトのインスタンスを生成せずにコールするため、疑似変数 $this は、 static として宣言されたメソッドの内部から利用することはできない。
そのため、以下のコード

class Hoge {
    public static function test() {
        var_dump($this);
    }
}

Hoge::test();

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

Fatal error: Uncaught Error: 
Using $this when not in object context in ...

 

ここではstaticキーワードについて深掘りしていきましょう。

クラスのインスタンス化の必要なしにアクセス可能

staticプロパティにはオブジェクト演算子(->)を使ってアクセスできない

 

まずは「static変数」についてです。

static変数について

static変数はローカル関数スコープのみに存在しますが、プログラム実行がこのスコープの外で行われるようになってもその値を失いません。

つまり、関数内のローカル変数を維持しておくための方法がstatic変数であり、出力した$bは同じ名前の変数でも別の変数だと認識されます。

function test_1(): int{
    $a = 0; //ローカル変数
    return ++$a;
}

$b = 10; //グローバル変数
function test_2(): int{
    static $b = 0; //ローカル変数
    return ++$b;
}

print test_1(); //出力結果:1
print test_1(); //出力結果:1
print test_2(); //出力結果:1
print test_2(); //出力結果:2
print $b; //出力結果:10

 

次に「staticメソッド」についてです。

staticメソッドについて

staticメソッドオブジェクトのインスタンスを生成せずにコールすることができる為、現在のオブジェクトを指す$thisは使用できません。

また、クラス名の直接指定クラス名を値に持つ変数でアクセスできます。

class Foo {
    public static function aStaticMethod() {
        // ...
    }
}

Foo::aStaticMethod();
$classname = 'Foo';
$classname::aStaticMethod();

static でないメソッドをstaticメソッドとしてコールするとエラーがスローされます。

PHP 8.0.0 以前:E_DEPRECATEDレベルの警告

PHP 8.0.0 以降:Errorがスローされる

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

 

選択肢?

 

static プロパティは、矢印演算子 -> によりオブジェクトからアクセス することはできない。
そのため、以下のコード

class Hoge {
    static $i = 100;

    public function test() {
        echo $this->i;
    }
}

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

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

Notice: Accessing static property Hoge::$i as non static in ...
Warning: Undefined property: Hoge::$i in ...

 

選択肢?で「staticプロパティにはオブジェクト演算子(->)を使ってアクセスできない」と説明しました。

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

 

選択肢?

 

抽象クラスから継承する際、親クラスの宣言で abstract としてマークされた全てのメソッドは、子クラスで定義されなければならない。
また、これらのメソッドは同等(あるいはより緩い制約) の可視性で定義される必要がある。
一方でメソッドのシグネチャ(引数の型と順番)については、必須引数の数は同じである必要があるが、型宣言は異なってもかまわない。
そのため、以下のコード

abstract class Hoge {
    abstract public function __construct(string $s, array $a);
    protected $s;
    protected $a;
}

class Foo extends Hoge {
    public function __construct(string $s, ¥arrayObject $a) {
        $this->s = $s;
        $this->a = $a;
    }
}

$obj = new Foo('', new ¥arrayObject());
var_dump($obj);

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

object(Foo)#1 (2) {
  ["s":protected]=>
  string(0) ""
  ["a":protected]=>
  object(ArrayObject)#2 (1) {
    ["storage":"ArrayObject":private]=>
    array(0) {
    }
  }
}

 

 

まずは「抽象クラス」について見ていきましょう。

抽象クラスについて

抽象メソッドを含んだクラスのこと。

抽象クラスを継承したサブクラスではすべての抽象メソッドをオーバーライドする必要があります(そうでない場合、抽象クラスは元よりサブクラスはインスタンス化できません)。また、Fatal errorの対象となります。

つまり、抽象クラスは特定のメソッドがサブクラスで実装されることを「保証」するためのものということになります。

 

次に「抽象メソッド」について見ていきます。

抽象メソッドについて

メソッド自体は機能を持たない「空のメソッド」のこと。

抽象メソッドの定義は以下のようにabstract修飾子を用い、シグネチャ(引数,引数の型,戻り値の型)の宣言のみが可能となります。

メソッドの本体を表す「{〜}」ブロックの記述はFatal errorの対象です。

アクセス修飾子 abstract function メソッド名(データ型 引数,・・・):戻り値のデータ型;

 

以上を踏まえて選択肢のコードを見ていきます。

クラス自体の継承に問題はありませんが、ここでは仮引数がオーバーライドのルールに反しています。

オーバーライドルール

メソッド名一致(一致していること)

戻り値の型指定なし(共変性(:子クラスがより狭い型を返すことを許容)であること)

アクセス修飾子一致(親クラスと一致か子クラスでより緩い制限であること)

仮引数型が異なる(型が一致かより広い型であること)

※呼び出し元から渡される値のことを「実引数」、受け取り側の変数のことを「仮引数」と呼びます。

今回の選択肢のコードでは、仮引数で空文字とarrayObjectクラスのオブジェクトを受け取っていますが、子クラスのコンストラクタが親クラスのコンストラクタの引数とは型が異なっている為、オーバーライドルールに反しています

ちなみに、実行しようとすると下記エラーが出力されるためこの選択肢は誤りです。

PHP Fatal error:  
Declaration of Foo::__construct(string $s, ¥arrayObject $a) 
must be compatible with Hoge::__construct(string $s, array $a)

 

出力させたい場合は以下のようにオーバーライドルールに沿ってどちらかの型に合わせる必要があります。

array(配列)型の場合

abstract class Hoge {
    abstract public function __construct(string $s, array $a);
    protected $s;
    protected $a;
}

class Foo extends Hoge {
    public function __construct(string $s, array $a) {
        $this->s = $s;
        $this->a = $a;
    }
}

$obj = new Foo('', array());
var_dump($obj);
-----------------------------------------------------------------
-----------------------------------------------------------------
//出力結果
object(Foo)#1 (2) {
  ["s":protected]=>
  string(0) ""
  ["a":protected]=>
  array(0) {
  }
}

 

ArrayObjectクラスのオブジェクトの場合

abstract class Hoge {
    abstract public function __construct(string $s, arrayObject $a);
    protected $s;
    protected $a;
}

class Foo extends Hoge {
    public function __construct(string $s, arrayObject $a) {
        $this->s = $s;
        $this->a = $a;
    }
}

$obj = new Foo('', new arrayObject());
var_dump($obj);
-----------------------------------------------------------------
-----------------------------------------------------------------
//出力結果
object(Foo)#1 (2) {
  ["s":protected]=>
  string(0) ""
  ["a":protected]=>
  object(ArrayObject)#2 (1) {
    ["storage":"ArrayObject":private]=>
    array(0) {
    }
  }
}
ABOUT ME
ヒロ
社会人4年目/25歳/食品商社で2年間営業した後、IT業界にシステムエンジニアとして転職/Java,PHP言語を扱う開発エンジニア