
PHP8 上級/準上級試験の認定模擬問題として、プライム・ストラテジー様のPRIME STUDYが認定されているようです。ただ、この模擬問題には解説がありませんので解説をまとめていこうと思います。シリーズ第9回目です。

PHP初級レベルから脱却して中級/上級レベルにいきたい人にピッタリの内容ですね。
PRIME STUDY認定模擬問題のリンクはこちらです → https://study.prime-strategy.co.jp/
問題文
定義済みのインターフェイスとクラスおよび 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
}
(1)
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
(2)
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}", PHP_EOL;
}
は正しく実行でき、結果は次のとおりとなる。
ps => string private
i => 1
dummy => dummy value
(3)
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)
となる。
(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)
}
}
解説
選択肢1
正しいです。
Traversable インターフェイスは単独で実装できず、Iterator または IteratorAggregate を介して使用する必要があります。
Traversable インターフェイスは、「そのクラスのオブジェクトが foreach でループできるかどうかを示すためのもの」 です。ただし、これは「抽象インターフェイス」であり、単体では実装できません。つまり、Traversable を直接 implements しようとすると、エラーになります。代わりに、Iterator または IteratorAggregate のどちらかを実装する必要があります。
Iteratorを実装する方法をとる場合、current(), key(), next(), rewind(), valid() をメソッドとして実装しないといけません。
IteratorAggregateを実装する方法をとる場合、getIterator()をメソッドとして実装しないといけません。

自分でループの動作を制御したいならIterator、 簡単に foreach を使いたいならIteratorAggregate といったところです。
選択肢2
正しいです。
Iterator インターフェイス は、オブジェクトを foreach でループできるようにするための仕組みです。
Traversable を継承しているため、foreach を使ってオブジェクトの中身を簡単に取り出せるようになります。
ループ対象のデータについて
public $s = 'string';
public $i = 1;
private $ps = 'string private';
private $pi = 2;
private $position = 0;
private $keys = [ 'ps', 'i', 'dummy' ];
public $s や public $i などのプロパティがありますが、foreach で使うのは $keys に定義された ps, i, dummy の3つだけです。private $ps や private $pi のように private な変数も、リストに含めればループ可能です。
Iterator のメソッド実装について
current():現在の値を取得します。
まず、現在のキーを取得し、もしキーが ‘dummy’ なら、固定の ‘dummy value’ を返す、それ以外のキーなら、そのキーのプロパティの値を返すという要領です。
public function current() {
$key = $this->key(); // 現在のキーを取得
if ('dummy' === $key) { // dummy の場合は固定値を返す
return 'dummy value';
}
return $this->$key; // それ以外はプロパティの値を取得
}
key():現在のキーを取得します。
$keys[$position] の値(現在のキー)を返します。例えば $position = 0 なら、$keys[0] = ‘ps’ なのでキーは ‘ps’ になります。
public function key() {
return $this->keys[$this->position];
}
next():次の要素に移動します。
$position を 1 増やし、次のキーに移動します。
public function next() {
$this->position++;
}
rewind():最初の要素に戻ります。
$position を 0 にして、ループを最初から始めます。
public function rewind() {
$this->position = 0;
}
valid():現在の位置が有効かどうか確認します。
$keys[$position] が存在するか確認し、true ならループを続けます。
public function valid() {
return isset($this->keys[$this->position]);
}
foreach の動作について
$obj = new Hoge();
foreach($obj as $k => $v) {
echo "{$k} => {$v}", PHP_EOL;
}
コードは以下のように動作します。
- rewind() 実行 → $position = 0
- valid() が true のためループ開始
- key() で “ps” を取得
- current() で private $ps = “string private” の値を取得 → 出力
- next() で $position = 1 に進む
- valid() が true のためループ継続
- key() で “i” を取得
- current() で public $i = 1 の値を取得 → 出力
- next() で $position = 2 に進む
- valid() が true のためループ継続
- key() で “dummy” を取得
- current() の特別処理で “dummy value” を取得 → 出力
- next() で $position = 3 に進む
- valid() が false になり、ループ終了
ここでのポイントは、private なプロパティも foreach で扱える、キーのリスト($keys)を自由に設定できる、current() に特別な処理を加えられる(例:’dummy’ のときだけ固定値といったように)
このように、Iterator を使うとオブジェクトの中身をカスタマイズして foreach で扱えるようになります。
選択肢3
正しいです。
Countable インターフェイスを実装すると、オブジェクトを count() 関数で扱えるようになります。
通常、count() は配列や Countable を実装したオブジェクトにしか使えません。
1つ目のコードの流れを追って見ましょう。
まず、Hoge クラスを定義しています。これはpublic や private なプロパティをいくつか持っています。count($obj) を実行しています。Hoge クラスは Countable を実装していないので count() を使えないため、エラーが発生します。
次に2つ目のコードの流れを追っていきましょう。
まず、Hoge クラスが Countable を implements します。count() メソッドを定義し、return 4; を指定しています。count($obj) を実行すると、count() メソッドが呼び出され 4 が返ります。実行結果は int(4) となります。
選択肢4
ArrayAccess インターフェイスのメソッドを実装していないため、正しく動作しません。
ArrayAccess インターフェイスというのは、オブジェクトを配列のように扱えるようにする仕組み です。通常、オブジェクトのプロパティには $obj->property のようにアクセスしますが、
ArrayAccess を実装すると $obj[‘key’] のように配列のような書き方ができるようになります。
ArrayAccess はインターフェイスですので、そのまま継承して使うことはできません。ArrayAccess を使用するには、インターフェイスで定義されたすべてのメソッド(offsetExists、offsetGet、offsetSet、offsetUnset)を実装する必要があります。
正しく動作させるには以下のようなコードにする必要があります。
class Hoge implements ArrayAccess {
private $data = [
's' => null,
's2' => null,
'i' => null,
'j' => null,
];
public function offsetExists($offset) {
return array_key_exists($offset, $this->data);
}
public function offsetGet($offset) {
return $this->data[$offset];
}
public function offsetSet($offset, $value) {
$this->data[$offset] = $value;
}
public function offsetUnset($offset) {
unset($this->data[$offset]);
}
}
$obj = new Hoge();
$obj['j'] = 999; // 配列のようにアクセス
echo $obj['j'], PHP_EOL; // 999 を表示
var_dump($obj);
まずは「class Hoge implements ArrayAccess 」で Hoge クラスが ArrayAccess を実装します。これで $obj[‘key’] のようにアクセスできるようになります。
次に「private $data = 」でデータを格納します。この配列は ‘s’, ‘s2’, ‘i’, ‘j’ のキーを持ち、最初はすべて null とします。
必須のメソッドを実装していきます。
offsetExists($offset)
isset($obj[‘key’]) のときに呼ばれます。$data 配列の中にキーがあるかを array_key_exists() でチェックします。
public function offsetExists($offset) {
return array_key_exists($offset, $this->data);
}
offsetGet($offset)
$obj[‘key’] で値を取得するときに呼ばれます。$data の該当するキーの値を返します。
public function offsetGet($offset) {
return $this->data[$offset];
}
offsetSet($offset, $value)
$obj[‘key’] = 値; のときに呼ばれます。$data に値をセットします。
public function offsetSet($offset, $value) {
$this->data[$offset] = $value;
}
offsetUnset($offset)
unset($obj[‘key’]); のときに呼ばれます。$data の該当するキーを削除します。
public function offsetUnset($offset) {
unset($this->data[$offset]);
}
ちなみにArrayAccess だけでは foreach でループできません。Iterator も実装すればループできるようになりますので補足しておきます。
正解選択肢
以上より選択肢4が正解となります。
コメント