プログラミング

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

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

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

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

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

1.独習PHP第4版

2.はじめてのPHP

 

3.PHP公式マニュアル

 

PHP8上級試験模擬問題

試験問題28

 

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

 

選択肢①

 

call_user_func() 関数 と call_user_func_array() 関数は、最初の引数で指定したコールバック関数をコールする。

call_user_func(callable $callback, mixed...args ) : mixed
call_user_func_array(callable $callback, array $args ) : mixed

call_user_func() 関数が「渡す引数を可変長引数で指定」するのに対して、call_user_func_array() 関数は「渡す引数を配列で指定」する。
第一引数に[オブジェクト, メソッド名]または[クラス名, メソッド名]の配列を渡すと、オブジェクトメソッドや静的メソッドを呼ぶ事も出来る。
また、引数を参照渡しで受け取る関数を call_user_func() から呼ぶと、エラー(Warning)が発生する。
そのため、以下のコード

選択肢のコードが破綻していたため、問題についての正誤は扱わない
以下省略

 

 

call_user_func()関数とcall_user_func_array()関数について触れていきます。

call_user_func()は、最初の引数で指定したコールバック関数をコールします。

call_user_func()では、参照渡しで引数を受け取るとWarningとなるため、そのような場合はcall_user_func_array()を使用する。

例1:call_user_func() の例と参照

error_reporting(E_ALL);
function increment(&$var)
{
    $var++;
}

$a = 0;
call_user_func('increment', $a);
echo $a."\n";

// このようにしてもかまいません
call_user_func_array('increment', array(&$a));
echo $a."\n";

// 関数名を変数として使ってもかまいません
$increment = 'increment';
$increment($a);
echo $a."\n";


/**出力結果
 Warning: Parameter 1 to increment() expected to be a reference, value given in …
 0
 1
 2
**/

 

例2 call_user_func() の例

function barber($type)
{
    echo "$type ですね、わかりました。\n";
}
call_user_func('barber', "マッシュルームカット");
call_user_func('barber', "髭剃り");

/**出力結果
 マッシュルームカット ですね、わかりました。
 髭剃り ですね、わかりました。
**/

 

例3 名前空間を使用した call_user_func()

namespace Foobar;

class Foo {
    static public function test() {
        print "Hello world!\n";
    }
}

call_user_func(__NAMESPACE__ .'\Foo::test');
call_user_func(array(__NAMESPACE__ .'\Foo', 'test'));

/**出力結果
 Hello world!
 Hello world!
**/

 

選択肢②

 

forward_static_call() 関数 と forward_static_call_array() 関数は、静的メソッドをコールする。

forward_static_call(callable $callback, mixed...args ) : mixed

callback パラメータで指定したユーザー定義の関数あるいはメソッドを、 それに続く引数を指定してコールします。この関数はメソッドのコンテキストでコールしなければなりません。 クラスの外部で使用することはできません。 この関数は 遅延静的束縛を使います。

forward_static_call_array(callable $callback,array $args ) : mixed

callback パラメータで指定したユーザー定義の関数あるいはメソッドをコールします。 この関数はメソッドのコンテキストでコールしなければなりません。 クラスの外部で使用することはできません。 この関数は 遅延静的束縛 を使います。 転送先のメソッドへのすべての引数は値渡しで、 call_user_func_array() と同様に配列で指定します。

そのため、以下のコード

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

class Hoge {
    public static function func() {
        echo __METHOD__ , "¥n";
        var_dump(get_called_class());
        echo "¥n";
    }
}

class Foo extends Hoge {
    public static function callMethod() {
        forward_static_call(['Hoge', 'func']);
        echo "¥n";
        call_user_func(['Hoge', 'func']);
    }
}

Foo::callMethod();

forward_static_call(['Hoge', 'func']);

を実行すると

Hoge::func
string(3) "Foo"

Hoge::func
string(4) "Hoge"

Fatal error: Uncaught Error: Cannot call forward_static_call() when no class scope is active in ...

となる。

 

Foo::callMethod()を呼び出すと、forward_static_call(['Hoge', 'func'])が実行されます。これにより、Hogeクラスのfunc()メソッドが呼び出され、呼び出し元のクラス名であるFooが表示されます。

これは遅延静的束縛を伴っているからです。

次に、call_user_func(['Hoge', 'func'])が実行され、再びHogeクラスのfunc()メソッドが呼び出されます。この場合、get_called_class()関数が実行され、その名であるHogeが表示されます。

最後に、forward_static_call(['Hoge', 'func'])が直接呼び出されますが、forward_static_call()関数をクラスのスコープ外で呼び出そうとしているためエラーが発生します。

forward_static_call()関数は、クラスの内部から呼び出す必要があるため、上記のコードではクラス内のメソッドからのみforward_static_call()関数を使用することができます。

遅延静的束縛については過去の記事でも触れています。

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

 

選択肢③

 

function_exists() 関数 は指定した関数が定義されている場合に true を返す。

function_exists( string $function_name ) : bool

組み込みの内部関数およびユーザー定義関数の中から、 function_name で指定した名前の関数を探します。

そのため、以下のコード

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

class Hoge {
    public function func() {
    }
}

function foo() {
}

$fn = function($v) {
    var_dump( $v, function_exists($v) );
    echo "¥n";
};
$fn( [Hoge:class, 'func'] );
$fn( 'foo' );
$fn( 'function_exists' );
$fn( 'echo' );
$fn( 'dummy' );

を実行すると

array(2) {
  [0]=>
  string(4) "Hoge"
  [1]=>
  string(4) "func"
}
bool(true)

string(3) "foo"
bool(true)

string(15) "function_exists"
bool(true)

string(4) "echo"
bool(true)

string(5) "dummy"
bool(false)

となる。

 

function_exists()関数は、指定した名前の関数がグローバルスコープに存在するかどうかを確認します。クラスメソッドや名前空間内の関数には適用されません

また、この関数はinclude_onceechoのような言語構造についてはfalseを返します。

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

 

選択肢④

 

register_shutdown_function() 関数はシャットダウン時に実行する関数を登録する。

register_shutdown_function(callable $callback, mixed...args ) : void

スクリプト処理が完了したとき、あるいは exit() がコールされたときに実行するコールバック関数を登録します。
register_shutdown_function() は複数回コールすることが可能で、登録された順に関数がコールされます。 登録した関数内で exit()をコールした場合、 処理はそこで終了してその他のシャットダウン関数はコールされません。

そのため、以下のコード

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

register_shutdown_function(function() {
    echo "register_shutdown_function 1¥n";
});
register_shutdown_function(function($v) {
    echo "register_shutdown_function 2 {$v} ¥n";
}, 'val');
register_shutdown_function(function() {
    echo "register_shutdown_function 3¥n";
    exit();
});
register_shutdown_function(function() {
    echo "register_shutdown_function 4¥n";
});

echo "start¥n";
exit();

を実行すると

start
register_shutdown_function 1
register_shutdown_function 2 val
register_shutdown_function 3

となる。

選択肢のコードでは3番目の関数内でexit()が呼び出されています。

そのため、4番目の関数が呼び出されることなく処理はそこで終了するので、「start」が出力された後、exit()がコールされた際に3つの関数が実行されて処理が終了します。

// register_shutdown_functionの例

function shutdown()
{
    // これがシャットダウン関数で、
    // スクリプトの処理が完了する前に
    // ここで何らかの操作をすることができます

    echo 'Script executed with success', PHP_EOL;
}

register_shutdown_function('shutdown');

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

SIGTERM あるいは SIGKILL でプロセスが終了した場合は、シャットダウン関数を実行しません。

 

試験問題29

 

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

 

選択肢①

 

openssl_get_cipher_methods() 関数は利用可能な暗号メソッドを取得する。
そのため、以下のコード

var_dump(openssl_get_cipher_methods());

を実行すると

array(201) {
  [0]=>
  string(11) "AES-128-CBC"
  [1]=>
  string(21) "AES-128-CBC-HMAC-SHA1"
  [2]=>
  string(23) "AES-128-CBC-HMAC-SHA256"
(中略)
  [199]=>
  string(8) "seed-ecb"
  [200]=>
  string(8) "seed-ofb"
}

となる。

 

openssl_get_cipher_methods()関数は、利用可能な暗号メソッド(暗号化アルゴリズム)の一覧を取得するために使用されます。

戻り値に関して、利用可能な暗号メソッドの array OpenSSL 1.1.1 より前のバージョンでは、 暗号メソッドは大文字、小文字両方で記されたものを返していました。 OpenSSL 1.1.1 以降では、小文字のみで記されたものが返されることに注意して下さい。

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

 

選択肢②

 

openssl_encrypt() 関数はデータを暗号化する。

openssl_encrypt(
    string $data,
    string $cipher_algo,
    string $passphrase,
    int $options = 0,
    string $iv = "",
    string &$tag = null,
    string $aad = "",
    int $tag_length = 16
): string|false

そのため、以下のコード

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

$method='AES-128-CBC';
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method));
$crypt = openssl_encrypt('exam', $method, 'key', 0, $iv);

var_dump($crypt);

を実行すると

string(24)"GoaKntvUhPOEl5VS92Uiyg=="

となる(initializationvectorがrandomなので、値は実行毎に変わる)。

なお、例えばCBCのような「initializationvectorが必要な暗号利用モード」でinitializationvectorを指定しないとWarningが出る。

そのため以下のコード

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

$method = 'AES-128-CBC';
$crypt=openssl_encrypt('exam', $method, 'key', 0);
var_dump( $crypt );

を実行すると

Warning: openssl_encrypt(): Using an empty Initialization Vector (iv) is potentially insecure and not recommended in ...
string(24) "5kBbn5hweqDYYT6VQjj2ag=="

となる。

data
暗号化するプレーンテキストメッセージ

cipher_algo
暗号メソッド。 使用可能なメソッドの一覧を取得するには、openssl_get_cipher_methods() を用います。

openssl_encrypt()関数は、指定された暗号方式でデータを暗号化するために使用されます。この関数は、暗号化するデータ、暗号方式、暗号化に使用する鍵、オプションの設定、タグ(認証タグ)などの引数を受け取ります。関数の実行結果として、暗号化されたデータが返されます。

openssl_encrypt()関数によってデータが暗号化されます。

実行するたびに初期化ベクトル(IV)がランダムに生成されるため、値は実行ごとに異なります

ただし、暗号利用モード(例: CBC)の場合、初期化ベクトル(IV)を指定しないと警告が表示されます

指定されたコードでは、初期化ベクトル(IV)が指定されていないため、警告が表示されています。セキュリティ上の理由から、空の初期化ベクトル(IV)を使用することは推奨されないため、正しい実装では適切な初期化ベクトルを指定する必要があります。

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

 

選択肢③

 

openssl_decrypt() 関数はデータを復号する。

openssl_decrypt(
    string $data,
    string $cipher_algo,
    string $passphrase,
    int $options = 0,
    string $iv = "",
    ?string $tag = null,
    string $aad = ""
): string|false

そのため、以下のコード

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

$method='AES-128-CBC';
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method));
$crypt = openssl_encrypt('exam', $method, 'key', 0, $iv);

var_dump($crypt);

$decrypt_string=openssl_decrypt($crypt, $method, 'key');
var_dump($decrypt_string);

を実行すると

string(24) "MWFlUhetizqXeV321jVUvA=="
string(4) "exam"

となる。

initialization vectorがrandomなので $crypt は実行毎に変わるが、復号された「exam」は常に同じになる。
また initialization vector は $crypt の中に含まれているため、引数として与えなくても正しく復号される。

 

選択肢のコードでは、実際には復号化するとfalseが出力されます。

openssl_decrypt()関数は、復号化に使用する鍵($key)と復号化アルゴリズム($method)を必要とします。しかし、このコードでは初期化ベクトル(IV)を指定せずに呼び出しています

$ivは初期化ベクトル(IV)を表す変数です。暗号化されたデータを復号化する際には、暗号化時に使用した初期化ベクトル(IV)を正確に指定する必要があります

暗号化したものを復号する場合、特に共通鍵暗号の場合は、次の3つが必要になります。

・暗号文そのもの
・暗号化時に与えた鍵
・同じく、暗号化時に与えた初期化ベクトル(iv)

選択肢のコードでは、暗号化したときと iv が異なる(指定していない)ため、当然復号には失敗します。

選択肢の出力にするためには、暗号化する時に指定した初期ベクトルと同一の初期ベクトルを指定して復号化させる必要があるため、

$decrypt_string=openssl_decrypt($crypt, $method, 'key', 0, $iv);

このように適切に引数を設定することで期待通りの出力となります。

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

 

選択肢④

 

openssl_x509_parse() 関数はX509 証明書をパースし、配列として情報を返す。

openssl_x509_parse(mixed $x509cert , bool $shortnames = true ) : array

そのため、以下のコード

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

$resource = @stream_socket_client(
    'ssl://www.phpexam.jp:443',
    $errno,
    $errstr,
    60,
    STREAM_CLIENT_CONNECT,
    stream_context_create(['ssl' => ['capture_peer_cert' => true]]),
);

$cont = stream_context_get_params($resource);
$x509 = openssl_x509_parse($cont['options']['ssl']['peer_certificate']);
var_dump($x509);

を実行すると

array(16) {
  ["name"]=>
  string(18) "/CN=www.phpexam.jp"
  ["subject"]=>
  array(1) {
    ["CN"]=>
    string(14) "www.phpexam.jp"
  }
  ["hash"]=>
  string(8) "491a204d"
  ["issuer"]=>
  array(3) {
    ["C"]=>
    string(2) "US"
    ["O"]=>
    string(13) "Let's Encrypt"
    ["CN"]=>
    string(2) "R3"
  }
  ["version"]=>
  int(2)
(後略)

となる。

 

openssl_x509_parse()関数の第1引数に解析するX509証明書を指定します。この証明書は、バイナリ形式のデータやPEM形式の文字列として提供されることがあります。

openssl_x509_parse()関数の第2引数はオプションですが、trueに設定すると、配列のキーに短縮された形式が使用されます。

このコードでは、stream_socket_client()関数を使用してwww.phpexam.jpの443ポートにSSL接続を確立しています。その際にcapture_peer_certオプションを使用して、対向の証明書を取得します。

stream_context_get_params()関数を使用して、取得した証明書を配列として取得します。 そして、openssl_x509_parse()関数を使用して、取得した証明書の情報を解析し、配列形式で取得します。

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

 

試験問題30

 

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

 

選択肢①

 

date_create関数を使うと DateTime クラスのインスタンスが作成できる。

この関数は次の関数のエイリアスです。 DateTime::__construct()

そのため、以下のコード

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

$date_obj=date_create('2021−01−0111:22:33');
var_dump($date_obj);

を実行すると

object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2021-01-01 11:22:33.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(3) "UTC"
}

となる。

 

DateTimeクラスは、日付と時間を表すオブジェクトです。date_create関数は、指定した日付と時刻を使用してDateTimeクラスのインスタンスを作成します。

このコードでは、date_create関数に2021-01-01 11:22:33という日付文字列を渡しています。そして、date_objという変数に結果を格納し、var_dump関数を使用してその内容を表示しています。

datetime に渡された文字列が不正な場合には、 例外をスローする代わりに false を返します。

 

選択肢②

 

strtotime関数を使うと英文形式の日付を Unixタイムスタンプに変換することが出来る。

strtotime( string $datetime , int|null $baseTimestamp = null ) : int|false

この関数は英語の書式での日付を含む文字列が指定されることを期待しており、 baseTimestamp で与えられたその形式から Unix タイムスタンプ (1970 年 1 月 1 日 00:00:00 UTC からの経過秒数) への変換を試みます。 baseTimestamp が指定されていない場合は現在日時に変換します。

strtotimeは様々な書式を扱う事が出来るため、例えば”2008/6/30″、”30-6-2008″、”30-June 2008″、”July 1st, 2008″などの様々な書式に対応している。
この関数の戻り値は成功時はタイムスタンプ、そうでなければ false を返します。

そのため、以下のコード

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

$t = strtotime('July 1st, 2008 4:08:37 pm');
var_dump($t);
echo date('Y-m-d H:i:s', $t) , "¥n";
echo "¥n";

$t = strtotime('July 1st, 1000 4:08:37 pm');
var_dump($t);

とを実行すると

int(1214928517)
2008-07-01 16:08:37

bool(false)

となる。

 

1970年1月1日より前の日付や、2038年1月19日以降の日付に対しては、32ビット環境では不正な日付としてfalseが返されることがあります。

しかし、64ビット環境では正常に変換されたUnixタイムスタンプが返されます。

これは、64 ビットでは過去側も未来側も約2930億年分を表せるため実質タイムスタンプの有効範囲は無制限となります。

そのため、選択肢の出力は正確には以下のようになります。

int(1214928517)
2008-07-01 16:08:37

int(-30594527483)
// 1000-07-01 16:08:37

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

 

選択肢③

 

time関数を使うと現在の Unix タイムスタンプを得る事ができる。

time() : int

そのため、以下のコード

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

$t = time();
var_dump($t);

を実行すると

int(1610912345)

となる。

 

Unixタイムスタンプは、1970年1月1日午前0時(UTC)からの経過秒数を表します。この値は整数(integer)で表されます。

time()関数は、現在のUnixタイムスタンプを返すPHPの組み込み関数です。コード内でtime()関数を呼び出すことで、現在のUnixタイムスタンプを取得することができます。

コードの実行結果としてvar_dump()関数が使用されており、これにより変数$tの値が表示されます。int(1610912345)という結果が表示される場合、現在のUnixタイムスタンプは1610912345という値であることを示しています。

なお、Unixタイムスタンプは、時刻や日付に関する情報を簡単に表現するために使用されます。Unixタイムスタンプを任意の日付や時刻形式に変換する場合は、PHPのdate()関数などを使用することができます。

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

 

選択肢④

 

date_default_timezone_set関数を使うとスクリプト中の日付/時刻関数で使用されるデフォルトタイムゾーンを設定する事ができる。
また、date_default_timezone_get関数を使うとスクリプト中の日付/時刻関数で使用されるデフォルトタイムゾーンを取得する事ができる。

date_default_timezone_set(string $timezoneId):bool
date_default_timezone_get():string

この関数は、デフォルトのタイムゾーンを以下の優先順位で取得して返します。

・date_default_timezone_set()関数を使用して設定したタイムゾーン(もし何か設定されていれば)を読み込む

・php.iniファイルのdate.timezoneオプション(設定されていれば)を読み込む

・上のすべてが失敗した場合は、date_default_timezone_get()はデフォルトのタイムゾーンであるUTCを返します。

そのため、以下のコード

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

$t = strtotime('2038-1-19 3:14:8');
var_dump( date_default_timezone_get() );
echo date(DATE_ATOM, $t),"\n";
echo"\n";

date_default_timezone_set('Asia/Tokyo');
var_dump(date_default_timezone_get());
echo date(DATE_ATOM, $t), "\n";

を実行すると

string(3) "UTC"
2038-01-01T03:14:08+00:00

string(10) "Asia/Tokyo"
2038-01-01T12:14:08+09:00

となる。

 

date_default_timezone_set関数は、以下の優先順位でデフォルトのタイムゾーンを取得して返します:

①関数を使用して設定したタイムゾーン

②iniオプションで設定されているタイムゾーン

③上記のいずれも設定されていない場合は、デフォルトのタイムゾーンであるUTCを返します。

最初のvar_dumpはデフォルトのタイムゾーンがUTCであることを示しており、date関数で表示される時刻もUTCのタイムゾーンに基づいています。

その後、date_default_timezone_set関数を使用してタイムゾーンを’Asia/Tokyo’に変更し、再度var_dumpとdate関数を使用して時刻を表示しています。この時はデフォルトのタイムゾーンが’Asia/Tokyo’に変更され、時刻もそれに基づいて表示されています。

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

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