プログラミング

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

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

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

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

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

1.独習PHP第4版

2.はじめてのPHP

 

3.PHP公式マニュアル

 

PHP8上級試験模擬問題

試験問題22

 

関数に関する説明の中で、誤っているものを1つ選びなさい。
なお「¥」はバックスラッシュに読み替えること。

 

選択肢①

 

Directory クラスは、ディレクトリ内を走査するためのクラスである。
Directory クラスのインスタンスを作るには dir() 関数を使います。new 演算子は使いません。
そのため、以下のコード

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

echo $dir = dir();
while (false !== ($entry = $dir->read())) {
    echo $entry;
}
$dir->close();

を実行すると

object(Directory)#1 (2) {
  ["path"]=>
  string(10) "/home/php_exam"
  ["handle"]=>
  resource(5) of type (stream)
}
.
..
.cache
.config
(以下略)

となる。

 

dir(string $directory, ?resource $context = null): Directory|false

 

dir()関数は、Directoryクラスの新しいインスタンスを作成します。このインスタンスは、ディレクトリ内のファイルとサブディレクトリを読み取るために使用されます。

whileループでは、ディレクトリ内のすべてのファイルおよびサブディレクトリを読み取っています。また、read()メソッドはディレクトリ内の次のエントリを返しています。

$dir->close();は、Directoryオブジェクトを閉じますが、これは使用が完了したら必ず行う必要があります。close()メソッドが呼び出されると、Directoryオブジェクトはクリーンアップされ、ディレクトリリソースが解放されます。

$dir->close();を実行すると、Directoryオブジェクトの情報が表示されます。これには、ディレクトリのパスハンドルの情報が含まれます。その後、ディレクトリ内のすべてのエントリが表示されます。... は、それぞれカレントディレクトリと親ディレクトリを表します。

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

 

選択肢②

 

chdir() 関数はカレントディレクトリを変更する。またgetcwd() 関数はカレントのワーキングディレクトリを取得する。

chdir( string $directory ) : bool
getcwd( ) : string

そのため、以下のコード

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

var_dump(getcwd());
chdir('../');
var_dump(getcwd());

を実行すると

string(14) "/home/php_exam"
string(5) "/home"

となる。

 

getcwd()関数は、カレントのワーキングディレクトリを取得し、chair()関数は、カレントディレクトリを変更します。

引数として、新しいディレクトリを指定します。この場合、../ は、親ディレクトリに移動することを意味しています。

最初のvar_dump(getcwd()) で、現在のワーキングディレクトリが /home/php_exam であることがわかりますが、chdir('../') を実行すると、ワーキングディレクトリが /home に変更されます。

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

 

選択肢③

 

DirectoryIterator クラスは、ファイルシステムのディレクトリを閲覧するためのシンプルなインターフェイスを提供する。

DirectoryIterator extends SplFileInfo implements SeekableIterator {
public DirectoryIterator::__construct( string $path )

SeekableIteratorを実装しているためforeachなどが使える。
またDirectoryIteratorがSplFileInfoを継承しているため、ファイルの情報に纏わるメソッドなどを一通り使う事ができる。
またSplFileInfo クラスには __toString() が実装されているため、ファイルへのパスを文字列で返すことが出来る。

そのため、以下のコード

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

// 現在のディレクトリを走査
foreach (new DirectoryIterator(__DIR__) as $file) {
    if($file->isDot()) {
        continue;
    }
    echo $file , "¥n";
    if ($file->isDir()) {
        // DirectoryIteratorのインスタンスを使って1階層深いディレクトリを走査
        foreach (new DirectoryIterator($file) as $file2) {
            if($file2->isDot()) continue;
            echo "¥t{$file2}¥n";
        }
    }
}

を実行すると

.cache
.config
.bashrc
Beginner
    sample
    src
Advanced
    sample
    src
(以下略)

となり、ディレクトリ階層を1段深くまで処理する事ができる。

 

DirectoryIteratorクラスで利用できるメソッド

DirectoryIterator::isDot() メソッドは、DirectoryIterator オブジェクトがカレントディレクトリまたは親ディレクトリを指しているかどうかを判定するために使用されます。

カレントディレクトリは「.」、親ディレクトリは「..」で表されます。isDot() メソッドは、現在のDirectoryIterator オブジェクトがこれらのディレクトリを表している場合に TRUE を返し、それ以外の場合は FALSE を返します。

このメソッドはディレクトリ内のファイルやサブディレクトリのみを処理するために使用されます。isDot() メソッドを使用することで、カレントディレクトリと親ディレクトリを無視することができます。

選択肢文では「ディレクトリ階層を1段深くまで処理する事ができる」とありますが、実際にはディレクトリ内のファイルやサブディレクトリを列挙していることから、現在のディレクトリを走査してそれぞれのファイル名を表示し、ディレクトリが見つかった場合はそれを走査してファイル名を表示しているということがわかります。

よって、この選択肢は誤りです。

 

選択肢④

 

RecursiveDirectoryIterator は、ファイルシステムのディレクトリを再帰的に反復処理するためのインターフェイスである。
ただしこのクラス単体では再帰的な反復処理はできず、RecursiveIteratorIteratorクラスを合わせて使う必要がある。

RecursiveDirectoryIterator extends FilesystemIterator implements SeekableIterator , RecursiveIterator {

RecursiveIteratorIterator implements OuterIterator {
public RecursiveIteratorIterator::__construct( Traversable iterator,intmode = RecursiveIteratorIterator::LEAVES_ONLY , int $flags = 0 )

SeekableIterator extends Iterator {
RecursiveIterator extends Iterator {
Iterator extends Traversable {

そのため、以下のコード

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

$iterator = new RecursiveIteratorIterator(directorys);
foreach($iterator as $file) {
    echo $file , "¥n";
}

を実行すると

/home/php_exam/.cache
/home/php_exam/.config
/home/php_exam/.bashrc
/home/php_exam/Beginner
/home/php_exam/Beginner/sample
/home/php_exam/Beginner/src
/home/php_exam/Beginner/src/1.php
/home/php_exam/Beginner/src/2.php
(略)
/home/php_exam/Advanced
/home/php_exam/Advanced/sample
/home/php_exam/Advanced/src
(以下略)

となり、再帰的にファイルを処理する事ができる。

 

RecursiveDirectoryIterator」と「RecursiveIteratorIterator」を組み合わせることで、指定したディレクトリ以下のファイルやサブディレクトリを再帰的に反復処理することができます。以下のようなコードを実行すると、再帰的にファイルを処理することができます。

$directory = new RecursiveDirectoryIterator('/path/to/directory');
$iterator = new RecursiveIteratorIterator($directory);
foreach ($iterator as $file) {
    echo $file, "\n";
}

上記のコードでは、指定したディレクトリ以下のファイルやサブディレクトリを再帰的に処理して、ファイル名を表示しています。

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

再起的な処理とは

ある関数の中でその関数自身を呼び出すことによって、同じ処理を繰り返し行うことを指します。

再帰的な処理は、同じ処理を繰り返し行う必要がある場合に有用で、たとえば、ディレクトリの中にディレクトリがある場合、再帰的な処理を用いることで階層構造をたどって処理を行うことができるためスマートに処理できます。

イテレータ(Iterator)とは

配列やオブジェクトのような反復可能な構造から順番に値を取り出すためのインターフェースのことを指します。イテレータを実装したクラスは、foreach文で反復処理することができます

Traversableは、foreach文で反復処理できるクラスに実装するインターフェースで、Iteratorは、基本的なイテレータの機能を定義するインターフェースで、IteratorAggregateは、イテレータを返すためのインターフェースです。

 

試験問題23

 

関数に関する説明の中で、誤っているものを1つ選びなさい。
なお「¥」はバックスラッシュに読み替えること。

 

選択肢①

 

mail() 関数は、mailを送る事ができる。

mail( string $to, string $subject, string $message, array|string $additional_headers = [] , string $additional_params=""):bool

とmessage(mail本文)に日本語などを用いる場合は、例えばsubjectであればRFC2047の仕様を満たす必要があるため、適切な処理が必要になる。

その場合はmb_send_mail()関数を使う事で、ヘッダと本文がmb_language()の設定に基づき変換、エンコードされる。そのため、以下のコード

declare(strict_types=1);
error_reporting(−1);
$r = mb_send_mail('php-exam@example.com', '日本語タイトル', 'mail本文');
var_dump($r);

を実行するとbool(true)となり、同時にmailが送られる。送られたmailの本文とそれに関連するヘッダは

Subject: 日本語タイトル
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: BASE64

bWFpbOacrOaWhw==

となる。

 

まずは規格について見ていきます。

RFC 2822

電子メールの構文に関する規格で、この規格にはメールのヘッダや本文、アドレスの表記法などが定義されています。

例えば、メールアドレスは “user@example.com” のような形式で表記され、メールのヘッダには「From:」、「To:」、「Subject:」などが含まれます。

このように、メールの送信に必要な様々な要素が定義されています。

RFC 2047

電子メールのエンコーディングに関する規格で、この規格にはメールの件名や本文などの文字列がマルチバイト文字を含む場合、適切なエンコーディング方式によってエンコードされる必要があることが定義されています。

RFC 2047では、Base64エンコーディングやQuoted-Printableエンコーディングなどのエンコーディング方式が定義されており、これらのエンコーディング方式を使用することで、日本語や他のマルチバイト文字を含むメールの送信が可能になります。

つまり、RFC 2822はメールの構文に関する規格であり、RFC 2047はマルチバイト文字を含むメールのエンコーディングに関する規格であるということです。

mb_send_mail()関数は、件名や本文にマルチバイト文字を含んだメールを送信するための関数ですが、mail()関数はマルチバイト文字に対応していないため、文字エンコーディングの変換を自前で行う必要があります。

以下2つはmb_send_mail()関数を利用してメールを送信する例です.

配列を使わない例

// 宛先
$to = 'user111@example.com';
// 件名
$subject = '転生してもPHPの勉強が必要だった件について';
// 本文
$body = 'こんにちは';
// ヘッダー情報
$headers = "From : user222@example.com\r\n";
$headers .= "Cc : user333@example.com\r\n";
$headers .= "X-Mailer : PHP/" . phpversion();

// メールを送信
if(mb_send_mail($to, $subject, $body, $headers)){
    print 'メールを送信しました。';
}else{
    print 'メール送信に失敗しました。';
}

 

配列を使用する例(PHP7.2以降可能)

// 宛先
$to = 'user111@example.com';
// 件名
$subject = '転生してもPHPの勉強が必要だった件について';
// 本文
$body = 'こんにちは';
// ヘッダー情報
$headers = [
        'From' => 'user222@example.com',
        'Cc' => 'user333@example.com',
        'X-Mailer' => 'PHP/' . phpversion()
];

// メールを送信
if(mb_send_mail($to, $subject, $body, $headers)){
    print 'メールを送信しました。';
}else{
    print 'メール送信に失敗しました。';
}

 

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

 

選択肢②

 

mail() 関数は、mailを送る事ができる。
第四引数のadditional_headersには「追加のヘッダ」の情報が入る。
そのため、以下のコード

// 外部からのデータを仮定
$external_value = "test ¥r¥ninjection_header: hoge";

// additional_headersの組み立て
$headers = [];
$headers[] = "X-test: test";
$headers[] = "X-test2: {$external_value}";


$r = mail('php-exam@example.com', 'subject', 'mail body', implode("¥r¥n", $headers));
var_dump($r);

を実行するとbool(true)となり、同時にmailが送られる。送られたmailの本文とそれに関連するヘッダは

X-test: test
X-test2: test injection_header: hoge

となる。

 

メールを送信する際には、必ずFromヘッダが含まれていなければなりません。 additional_headersパラメータで指定するか、 あるいは php.ini にデフォルト値を指定します。

指定しなかった場合は、以下のようなエラーメッセージが返ります。

Warning: mail(): "sendmail_from" not set in php.ini or custom "From:" header missing

 

よってこの選択肢は誤りといえます。

 

選択肢③

 

random_bytes() 関数は、暗号論的に安全な、疑似ランダムなバイト列を生成する事ができる。

random_bytes(int length):string

この関数が返す値はバイナリであるため、使う場合にはbin2hex()関数やbase64encode()関数を合わせて使う事が多い。

そのため、以下のコード

declare(strict_types=1);
error_reporting(−1);
$s = random_bytes(16);
var_dump( bin2hex($s));
var_dump(base64encode($s));

を実行すると

string(32) "9338779e0f8a377bcac56f0b7e299a03"
string(24) "kzh3ng+KN3vKxW8LfimaAw=="

となる(乱数の値は実行毎に変わる)。

 

random_bytes()関数は、指定された長さの暗号論的に安全なランダムなバイト列を生成します。今回は、16バイトのランダムなバイト列が生成されていますが、var_dump()関数を使って表示するために、bin2hex()関数を使って16進数の文字列に変換しています。

一方で、base64encode()関数の出力は、生成されたバイト列をBase64エンコードしたものです。Base64エンコードはバイナリデータをテキスト形式に変換するためのエンコーディング方式であり、結果として24文字の文字列が表示されます。

なお、実行ごとに異なる結果が得られますが、これはrandom_bytes()関数が暗号論的に安全な乱数を生成するためであり、ランダムなデータが生成されるたびに異なる結果が得られるからです。

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

選択肢④

 

random_int() 関数は、暗号論的に安全な、疑似ランダムな整数を生成する事ができる。

random_int(int min,int max):int

minはPHP_INT_MIN 以上、maxはPHP_INT_MAX 以下である必要がある。
そのため、以下のコード

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

random_int(PHP_INT_MIN, PHP_INT_MAX + 1);

を実行すると

Fatal error: Uncaught TypeError: random_int(): Argument #2 ($max) must be of type int, float given in ...

となる。

 

random_int()関数は、指定された範囲内で暗号論的に安全なランダムな整数を生成しますが、関数の引数には、最小値 (min) と最大値 (max) を指定する必要があります。

PHP_INT_MAXは PHP の整数型が表現できる最大の整数値であり、PHP_INT_MINは最小の整数値のため、範囲指定を行う際にはこれらの範囲内の数値である必要があります。

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

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