プログラミング

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

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

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

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

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

1.独習PHP第4版

2.はじめてのPHP

 

3.PHP公式マニュアル

 

PHP8上級試験模擬問題

試験問題9

 

定義済みのインターフェイスとクラスおよび SPL インターフェイスに関する説明の中で、誤っているものを1つ選びなさい。
なお、すべてのコードの先頭には下記のコードが書かれているものとする。

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

下記はマニュアルから一部引用した内容である。

Iterator extends Traversable {
    abstract public current ( ) : mixed
    abstract public key ( ) : scalar
    abstract public next ( ) : void
    abstract public rewind ( ) : void
    abstract public valid ( ) : bool
}

Countable {
    abstract public count ( ) : int
}

ArrayAccess {
   abstract public offsetExists ( mixed $offset ) : bool
   abstract public offsetGet ( mixed $offset ) : mixed
   abstract public offsetSet ( mixed $offset , mixed $value ) : void
   abstract public offsetUnset ( mixed $offset ) : void
}

 

 

選択肢①

 

Traversable インターフェイスは「そのクラスの中身が foreach を使用してたどれるかどうかを検出するインターフェイス」である。
これは抽象インターフェイスであり、単体で実装することはできず、 IteratorAggregate あるいは Iterator を実装しなければならない。
そのため、以下のコード

class Hoge implements Traversable {
}

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

Fatal error: 
Class Hoge must implement interface Traversable as part of
either Iterator or IteratorAggregate in Unknown on line 0

 

 

Traversableインターフェイス

  • このインターフェイスを実装したオブジェクトはforeachで反復できるようになる。
  • 内部エンジンのインターフェイスであるため、PHPコードでこのインターフェイスを直接実装することはできない特殊な抽象インターフェイスである。
  • IteratorAggregateあるいはIteratorを実装しなければなりません。
  • PHP5で追加された。

 

Iteratorインターフェイス

  • 外部のイテレータあるいはオブジェクト自身から反復処理を行うためのインターフェイス
  • Iteratorインターフェースを実装すると、foreach文でループできるクラスを作成することができる。
  • 下記5つのメソッドを実装する必要がある
/**
*1回目のループ
*rewaind()→valid()→current()→key()
*2回目〜最終ループ
*next()→valid()→current()→key()
*最終ループ後
*next()→valid()
*/

/**foreachループの開始時に最初にコールされるメソッド*/
public Iterator::rewind(): void

/**Iterator::rewind() および Iterator::next()の後にコールされ、現在の位置が有効かどうかを調べる*/
public Iterator::valid(): bool

/**現在の要素を返す*/
public Iterator::current(): mixed

/**現在の要素のキーを返す*/
public Iterator::key(): mixed

/**各foreachループの後にコールされ,現在位置を次の要素に移動する。*/
public Iterator::next(): void

 

IteratorAggregate インターフェイス

  • カスタムの反復機能を提供するためのメソッドを取り決めている。
  • カスタムの反復処理を実装するには、IteratorAggregateインターフェイスを実装した上で、getIterator()メソッドをオーバーライドする必要がある。
class myData implements IteratorAggregate {
    public $property1 = "Public property one";
    public $property2 = "Public property two";
    public $property3 = "Public property three";

    public function __construct() {
        $this->property4 = "last property";
    }

    public function getIterator() {
        return new ArrayIterator($this);
    }
}

$obj = new myData;

foreach($obj as $key => $value) {
    var_dump($key, $value);
    echo "\n";
}

上記の出力結果は以下のようになります。

string(9) "property1"
string(19) "Public property one"

string(9) "property2"
string(19) "Public property two"

string(9) "property3"
string(21) "Public property three"

string(9) "property4"
string(13) "last property"

 

よって、Traversableインターフェイスは単体で実装できず、IteratorAggregateあるいはIteratorを実装しなければならないため、この選択肢は合っています。

選択肢②

 

Iterator インターフェイスは「外部のイテレータあるいはオブジェクト自身から反復処理を行うためのインターフェイス」である。
また、Traversable を継承しているため foreach でも使う事ができる。
そのため、以下のコード

class Hoge implements Iterator {
    public function current() {
        $key = $this->key();
        if ('dummy' === $key) {
            return 'dummy value';
        }
        return $this->$key;
    }

    public function key() {
        return $this->keys[$this->position];
    }

    public function next() {
        $this->position ++;
    }

    public function rewind() {
        $this->position = 0;
    }

    public function valid() {
        return isset($this->keys[$this->position]);
    }

    public $s = 'string';
    public $i = 1;
    private $ps = 'string private';
    private $pi = 2;
    private $position = 0;
    private $keys = [ 'ps', 'i', 'dummy' ];
}

$obj = new Hoge();
foreach($obj as $k => $v) {
    echo "{$k} => {$v}¥n";
}

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

ps => string private
i => 1
dummy => dummy value

 

 

選択肢のコードを見ていく前に「オブジェクトの反復処理」について深掘りしていきます。

以下に示す例では、MyClassの各プロパティに指定されたアクセス権限によって出力される値の違いを確認しています。

class MyClass
{
    public $var1 = 'value 1';
    protected $protected = 'protected var';
    private   $private   = 'private var';

    function iterateVisible() {
       echo "MyClass::iterateVisible:\n";
       foreach ($this as $key => $value) {
           print "$key => $value\n";
       }
    }
}

$class = new MyClass();

foreach($class as $key => $value) {
    print "$key => $value\n";
}
echo "\n";

$class->iterateVisible();

/**
*<出力値>
*var1 => value 1
*
*MyClass::iterateVisible:
*var1 => value 1
*protected => protected var
*private => private var
*/

クラスの外からオブジェクトの反復処理をかけた場合は「publicなプロパティ値のみ」の出力となり、

MyClass内で定義されているiterateVisible()メソッドを介してオブジェクトの反復処理をかけた場合は「すべてのプロパティ値」が出力されます。

protectedなプロパティ値まで」を出力させたい場合はMyClassを継承した子クラス内でオブジェクトの反復処理をかける必要があります。例としては次のソースで実現できます。

class MyClass {
    public $var1 = 'value 1';

    protected $protected = 'protected var';
    private $private = 'private var';

}

class SubClass extends MyClass {

    public function printParentProperties() {
        foreach ($this as $key => $value) {
            if (property_exists('MyClass', $key)) {
                echo "MyClass->$key = $value\n";
            }
        }
    }
}

$subObj = new SubClass();
$subObj->printParentProperties();

/**
*<出力結果>
*MyClass->var1 = value 1
*MyClass->protected = protected var
*/

ちなみに、こちらのコードで記述している「property_exists()メソッド」はオブジェクトもしくはクラスにプロパティが存在するかどうかを調べるメソッドです。

isset()とは異なり、プロパティの値がnullの場合でもproperty_exists()trueを返します。

よって、選択肢①で解説したイテレータ(反復処理を行うオブジェクト)でforeachを使ったときに呼ばれるメソッドの順番と先ほど解説したオブジェクト反復処理の動作から、この選択肢は合っているといえます。

 

選択肢③

 

Countable インターフェイスを実装したクラスは、count() 関数で使用することができる。
そのため、以下のコード

class Hoge {
    public $s = 'string';
    public $i = 1;
    private $ps = 'private string';
    private $pi = 2;
}

$obj = new Hoge();
var_dump( count($obj) );

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

Fatal error: Uncaught TypeError: count(): Argument #1 ($var) must be of type Countable|array, Hoge given in ...

一方で以下のコード

class Hoge implements Countable {
    public function count() {
        return 4;
    }

    public $s = 'string';
    public $i = 1;
    private $ps = 'private string';
    private $pi = 2;
}

$obj = new Hoge();
var_dump( count($obj) );

は正しく実行でき、結果は int(4) となる。

 

 

まずはcount()関数について軽く触れておきます。

count()関数

count(Countable|array $value, int $mode = COUNT_NORMAL): int

配列またはCountableオブジェクトに含まれるすべての要素の数を数える

Countableインターフェイスを実装したオブジェクトの場合は、Countable::count()の戻り値を返す

オプションのmode引数がCOUNT_RECURSIVE(または 1)にセットされた場合、多次元配列の全ての要素をカウントする。

$food = array('fruits' => array('orange', 'banana', 'apple'),
              'veggie' => array('carrot', 'collard', 'pea'));

var_dump(count($food, 1));
var_dump(count($food, COUNT_RECURSIVE));
/**
*出力結果
*int(8)
*int(8)
*/

 

・パラメータに不正な型を渡した場合、PHP8.0.0ではFatal error(TypeError)をスローし、PHP7.2.0以下では警告(Warning)を発生させます。

//不正な値
var_dump(count(null));
var_dump(count(false));

 

 

この選択肢のコードでは、Countableインターフェースを実装していない場合はTypeErrorが出力され、実装している場合はCountable::count()の戻り値である4が返されているため、合っています。

 

選択肢④

 

ArrayAccess インターフェイスは「配列としてオブジェクトにアクセスするための機能のインターフェイス」である。
そのため、以下のコード

class Hoge extends ArrayAccess {
    private $data = [
        's' => null,
        's2' => null,
        'i' => null,
        'j' => null,
    ];
}

$obj = new Hoge();
$obj['j'] = 999;
echo $obj['j'], PHP_EOL;
var_dump($obj);

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

999
object(Hoge)#1 (1) {
  ["data":"Hoge":private]=>
  array(4) {
    ["s"]=>
    NULL
    ["s2"]=>
    NULL
    ["i"]=>
    NULL
    ["j"]=>
    int(999)
  }
}

 

 

ArrayAccessを実装したクラスでは以下のメソッドを実装する必要があります。

//isset() あるいはempty()を使用した場合にオフセットが存在するかどうかを返す
public offsetExists(mixed $offset): bool
//オフセットがempty()かどうかを調べる際に実行され、指定したオフセットの値を返す
public offsetGet(mixed $offset): mixed
//指定したオフセットに値を代入します。
public offsetSet(mixed $offset, mixed $value): void
//オフセットの設定を解除する
public offsetUnset(mixed $offset): void

 

まず、選択肢のコードではArrayAccessを継承しようとしているため誤りで、インターフェースを実装するためにはimplementsキーワードの記述が必要です。。

また、上記4つのメソッドを実装していないためFatal Errorとなってしまいます。

これは、「クラスHogeには4つの抽象メソッドが含まれているため、抽象メソッドとして宣言するか、残りのメソッドを実装する必要がある」からです。

/**抽象メソッドとして宣言する場合*/
abstract class Hoge implements ArrayAccess {
    
abstract public function offsetExists(mixed $offset):bool;
abstract public function offsetGet(mixed $offset):mixed;
abstract public function offsetSet(mixed $offset, mixed $value): void;
abstract public function offsetUnset(mixed $offset): void;

    private $data = [
        's' => null,
        's2' => null,
        'i' => null,
        'j' => null,
    ];
}

/**抽象クラスはインスタンス化できない*/
// $obj = new Hoge();
// $obj['j'] = 999;
// echo $obj['j'], PHP_EOL;
// var_dump($obj);

 

選択肢のコードでは、ArrayAccessインターフェースの実装及び抽象メソッドとしての宣言またはメソッドの実装がないため、誤りです。

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