プログラミング

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

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

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

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

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

1.独習PHP第4版

2.はじめてのPHP

 

3.PHP公式マニュアル

 

PHP8上級試験模擬問題

試験問題24

 

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

 

選択肢①

 

PHPで外部プログラムを動かすためには、exec() 関数、passthru() 関数、shell_exec() 関数、system() 関数、バルティック演算子などを使う。
これらのうち、shell_exec() 関数とバルティック演算子は「同じもの」である。
また、exec() 関数とsystem() 関数は返り値として「コマンド結果の最後の行を返す」、passthru() 関数は「未整形の出力を表示(stdoutに出力)する」ところに差異がある。

exec(string $command, array &$output = null, int &$result_code = null): string|false
shell_exec(string $command): string|false|null
system(string $command, int &$result_code = null): string|false

そのため以下のコード

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

$cmd = 'vmstat11';
echo"exec()¥n";

$s = exec($cmd, $output, $return_var);
echo $s , "¥n";
echo "¥n";
echo "shell_exec()¥n";

$s = shell_exec($cmd);
echo $s,"¥n";
echo"¥n";
echo"system()¥n";

ob_start();
$s = system($cmd, $return_var);
ob_end_clean();
echo $s , "¥n";

を実行すると

exec()
1  0  67236  87052      0 606796    0    0     0     1    1    1  0  0 100  0  0

shell_exec()
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
1  0  67236  86728      0 606796    0    0     0     1    1    1  0  0 100  0  0


system()
1  0  67236  86564      0 606796    0    0     0     1    1    1  0  0 100  0  0

となる。

 

関数を1つずつ見ていきます。

exec()関数

exec()関数は、指定されたコマンドを実行し、その結果を返します。この関数は、実行されたコマンドの最後の行を返すため、返り値は文字列または false になります。また、コマンドの出力は引数 $output に格納され、終了コードは引数 $result_code に格納されます。

また、exec()がcommandを実行できない場合、E_WARNINGが発生し、commandが空だったり、nullバイトが含まれている場合、ValueErrorがスローされます。

shell_exec()関数

shell_exec()関数も指定されたコマンドを実行しますが、exec() 関数とは異なり、コマンドの結果を文字列として直接返します。返り値は実行したコマンドの出力を表す文字列です。ただし、コマンドの実行に失敗した場合は null を返します

エラーが発生した場合だけでなくプログラムが何も出力しなかった場合にも nullを返すため、実行に失敗したかどうかをこの関数では判断できません。 プログラムの終了コードを調べる場合はexec()関数を使う必要があります。

system()関数

system()関数も指定されたコマンドを実行しますが、出力は直接表示されます。関数の返り値は、コマンドの出力を表す文字列です。終了コードは引数 $result_code に格納されます。

各関数の実行結果についてまとめると以下のようになります。

exec():指定されたコマンドの実行結果の最後の行が出力されます。
shell_exec():指定されたコマンドの実行結果全体が出力されます。
system():指定されたコマンドの実行結果全体が出力されます。

また、よりわかりやすくexec()とsystem()についての例を以下に紹介します。

echo "# exec関数\n";
var_dump( exec('ls') ); // 引数なし
var_dump( exec('ls', $output, $result_code) ); // 引数あり
print_r( $output );
var_dump( $result_code );
 
echo "\n# system関数\n";
var_dump( system('ls', $result_code) );
var_dump( $result_code );

/**実行結果
# exec関数
string(9) "sleep.php" // 実行結果の最後の出力
string(9) "sleep.php" // 実行結果の最後の出力
Array // 変数に格納されたコマンドの出力
(
    [0] => define.php
    [1] => die-exit.php
    [2] => die-exit2.php
    [3] => exec.php
    [4] => sleep.php
)
int(0) // コマンド実行成功

# system関数
define.php
die-exit.php
die-exit2.php
exec.php
sleep.php
string(9) "sleep.php" // 実行結果の最後の出力
int(0) // コマンド実行成功
**/

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

 

選択肢②

 

シェル引数として使われる文字列をエスケープするための関数として、escapeshellarg() 関数と escapeshellcmd() 関数がある。

escapeshellarg( string $arg ):string

escapeshellarg() は、文字列をシングルクオートで括り、既存のシングルクオートを全てクオート/エスケープします。これにより、文字列を直接シェル関数に渡し、単一の安全な引数として処理することを可能にします

escapeshellcmd( string $command ) : string

escapeshellcmd() は、文字列中においてシェルコマンドをだまして勝手なコマンドを実行する可能性がある文字をエスケープします。
(中略)
&#;`|*?~<>^()[]{}$¥、¥x0A および ¥xFF については、その文字の前にバックスラッシュが 追加されます。’ および ” は、対になっていない場合にのみエスケープされます。

どちらも「エスケープをする」処理を行うため、どちらを使っても意味合いは同じなので、どちらを使ってもよい。
しかし、外部入力をシェル引数に使う場合は、必ずどちらかの関数でエスケープをする必要がある。

そのため、以下のコード

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

$arguments = 'ab&c "d ef" > ¥'gh¥'';

$e = escapeshellarg($arguments);
echo $e , "¥n";

$e = escapeshellcmd($arguments);
echo $e , "¥n";

を実行すると、

'ab&c "d ef" > '¥''gh'¥'''
'ab&c "d ef" > '¥''gh'¥'''

となる。

 

まず選択肢のコードでは以下の箇所でエラーが発生します。

これはシングルクウォート内でさらにシングルクウォートを使用しているためです。

$arguments = 'ab&c "d ef" > ¥'gh¥'';

// 出力結果
PHP Parse error:  syntax error, unexpected identifier "gh¥" in ・・・

では、その部分をダブルクウォートに置き換えた時の場合で考えていきます。

$arguments = 'ab&c "d ef" > ¥"gh¥"';

/**出力結果
'ab&c "d ef" > ¥"gh¥"'
ab\&c "d ef" \> ¥"gh¥"
**/

これらを踏まえてまとめると、以下のようになります。

要点まとめ

escapeshellarg()関数は文字列をシングルクオートで括り、escapeshellcmd()関数は特定の文字や記号の前にバックスラッシュが追加されます。

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

 

選択肢③

 

「システムコールとライブラリ関数を規定したPOSIX.1 (IEEE Std 1003.1)」標準ドキュメントで 定義された関数へのインターフェイスが提供されている、POSIX 関数がある。
デフォルトで有効になっているが、Windows 環境では利用できないので注意が必要である。
現在のプロセスのグループ ID を返すposix_getpgrp()、 現在のプロセス ID を返すposix_getpid()、 親プロセスの ID を返すposix_getppid()、 プロセス時間を得るposix_times() などがある。

そのため、以下のコード

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

var_dump( posix_getpgrp() );
var_dump( posix_getpid() );
var_dump( posix_getppid() );
var_dump( posix_times() );

を実行すると、

int(694)
int(694)
int(523)
array(5) {
  ["ticks"]=>
  int(1358853909)
  ["utime"]=>
  int(0)
  ["stime"]=>
  int(0)
  ["cutime"]=>
  int(0)
  ["cstime"]=>
  int(0)
}

となる(プロセスID、時間のため、値は実行毎に変わる)。

 

POSIX関数は、POSIX.1で定義された関数へのインターフェースを提供します。しかし、Windows環境では利用できないため注意が必要です。

また、プロセスIDや時間など、実行毎に異なる値が得られることに注意してください。

posix_access — ファイルのアクセス権限を判断する
posix_ctermid — 制御する端末のパス名を得る
posix_errno — posix_get_last_error のエイリアス
posix_get_last_error — 直近で失敗した posix 関数が設定したエラー番号を取得する
posix_getcwd — 現在のディレクトリのパス名
posix_getegid — 現在のプロセスの有効なグループ ID を返す
posix_geteuid — 現在のプロセスの有効なユーザー ID を返す
posix_getgid — 現在のプロセスの実際のグループ ID を返す
posix_getgrgid — 指定したグループ ID を有するグループに関する情報を返す
posix_getgrnam — 指定した名前のグループに関する情報を返す
posix_getgroups — 現在のプロセスのグループセットを返す
posix_getlogin — ログイン名を返す
posix_getpgid — ジョブ制御のプロセスグループ ID を得る
posix_getpgrp — 現在のプロセスのグループ ID を返す
posix_getpid — 現在のプロセス ID を返す
posix_getppid — 親プロセスの ID を返す
posix_getpwnam — 指定した名前のユーザーに関する情報を返す
posix_getpwuid — 指定 ID のユーザーに関する情報を返す
posix_getrlimit — システムリソース制限に関する情報を返す
posix_getsid — プロセスの現在の sid を得る
posix_getuid — 現在のプロセスの実際のユーザー ID を返す
posix_initgroups — グループアクセスリストを求める
posix_isatty — ファイル記述子が対話型端末であるかどうかを定義する
posix_kill — プロセスにシグナルを送信する
posix_mkfifo — fifo スペシャルファイル(名前付きパイプ)を作成する
posix_mknod — スペシャルファイルあるいは通常のファイルを作成する (POSIX.1)
posix_setegid — 現在のプロセスの実効 GID を設定する
posix_seteuid — 現在のプロセスの実効 UID を設定する
posix_setgid — 現在のプロセスの GID を設定する
posix_setpgid — ジョブ制御のプロセスグループ ID を設定する
posix_setrlimit — システムリソース制限を設定
posix_setsid — 現在のプロセスをセッションリーダーにする
posix_setuid — 現在のプロセスの UID を設定する
posix_strerror — 指定したエラー番号に対応するシステムのエラーメッセージを取得する
posix_times — プロセス時間を得る
posix_ttyname — 端末のデバイス名を調べる
posix_uname — システム名を得る

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

 

選択肢④

 

プロセスに働きかける関数として、proc_nice() 関数や posix_kill() 関数がある。

proc_nice(int $increment) : bool
posix_kill(int $pid , int $sig) : bool

proc_nice() は自プロセスの優先度を変更し、posix_kill() は指定したプロセスにシグナルを送信する。
シグナル SIGKILL は「プロセスの強制終了」なので、SIGKILL を送信されたプログラムはそこで実行を停止する。

そのため、以下のコード

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

var_dump(trim('nice'));
proc_nice(10);
var_dump(trim('nice'));

posix_kill(posix_getpid(), SIGKILL);

echo "test¥n";

を実行すると

string(1) "0"
string(2) "10"
Killed

となる。

 

proc_nice()関数は、自プロセスの優先度を変更するための関数であり、posix_kill()関数は指定したプロセスにシグナルを送信するための関数です。

proc_nice()関数は、引数として与えられたincrementの値に基づいて、プロセスの優先度を変更します。posix_kill()関数は、指定したpidのプロセスに対して指定したsigのシグナルを送信します。

ここで、SIGKILLは「プロセスの強制終了」を表すシグナルです。SIGKILLが送信されると、そのプロセスは即座に停止します。

上記のコードを実行すると、次のような結果が得られます。

・最初のvar_dump()では、現在の優先度が”0″であることを示します。

・proc_nice(10)によって優先度が変更され、2番目のvar_dump()では優先度が”10″になっていることが示されます。

・posix_kill(posix_getpid(), SIGKILL)によって、自プロセスにSIGKILLが送信され、”Killed”と表示されます。

最後のecho “test\n” ;は実行されないため、プロセスがSIGKILLによって停止されていることがわかります。

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

 

試験問題25

 

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

 

選択肢①

 

PHPでは「ファイル、ネットワーク、データ圧縮などに関する、 共通した一連の関数群と利用法を持つ操作の一般化の手法」として、ストリームがある。
また「ストリームにおいてどのように特定の プロトコル/エンコーディングを扱うかを扱うかを指示する付加的なコード」をラッパーと呼称する。

PHPにおいて、組み込みのラッパーは様々にあるが、例えば php:// はさまざまな入出力ストリームへのアクセスを提供している。
例えば php://input は読み込み専用のストリームで、リクエストの body 部から生のデータを読み込むことができる。

そのため、以下のコード

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

var_dump( file_get_contents('php://input') );

curl https://www.example.com/stream.php -d '{"exam_num":100,"exam_string":"value"}'

の形で呼び出すと

string(38) "{"exam_num":100,"exam_string":"value"}"

となる。

 

選択肢のコードでは、file_get_contents(‘php://input’)を使用してphp://inputストリームからデータを読み取り、その結果をvar_dump()を使って表示しています。

次に、curlコマンドを使用してhttps://www.example.com/stream.phpに対してリクエストを送信しています。-dオプションを使用して、リクエストのボディに{“exam_num”:100,”exam_string”:”value”}というデータを含めています。

結果として、コードが実行されるとstring(38) “{“exam_num”:100,”exam_string”:”value”}”と表示されます。これは、php://inputストリームから読み取ったデータが表示されており、送信したデータが正しく受け取られたことを示しています。

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

 

選択肢②

 

php://memory および php://temp は読み書き可能なストリームで、一時データをファイルのように保存できるラッパーである。
そのため、以下のコード

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

$csv_string = "1,2,3¥n4,5,6¥n";
$fp = fopen('php://memory', 'r+');
$fwrite($fp,$csv_string);

fseek($fp,0,SEEKSET);
while($row = fgetcsv($fp)) {
  var_dump($row);
}

を実行すると

array(3) {
  [0]=>
  string(1) "1"
  [1]=>
  string(1) "2"
  [2]=>
  string(1) "3"
}
array(3) {
  [0]=>
  string(1) "4"
  [1]=>
  string(1) "5"
  [2]=>
  string(1) "6"
}

となる。

 

fopen関数を使用してphp://memoryストリームを読み書き可能モードで開いています。このストリームはメモリ上にデータを保存するため、ファイルのように操作できます。

fwrite関数を使って定義したCSV文字列をfpストリームに書き込んでいます。

fseek関数を使用して、fpストリームの位置をファイルの先頭に戻しています

その後、fgetcsv関数を使ってfpストリームから行ごとにデータを読み取り、var_dump()を使ってデータを表示しています。

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

fopen関数

fopen(string $filename, string $mode):resource

fopen関数は、ファイルのオープンに成功すると、戻り値としてそのファイルを操作するためのキー情報(handle)を返します。

<オープンモード>
r : 読み込み専用
r+ : 読み込み/書き込み可能
w : 書き込み専用(ファイルの内容をクリア/存在しない時は新規作成)
w+ : 読み込み/書き込み狩野(上記と同じ)
a : 書き込み専用(既存の内容に追記/存在しない時は新規作成)
a+ : 読み込み/書き込み可能(上記と同じ)
x : 書き込み専用(ファイルが存在するときはエラー)
x+ : 読み込み/書き込み可能(上記と同じ)
b : バイナリモード(Windows環境で改行文字の扱いを決めるオプション)
t : テキストモード(Windows環境で改行文字の扱いを決めるオプション)

オープンモードにはバイナリモードを使用してください。

「t」テキストモードでは書き込む際に「¥n」を「¥r¥n」に変換し、読み込む際には「¥r¥n」を「¥n」に変換しますが、コードの可搬性の観点からあまり利用すべきではありません。一方で、バイナリモードではテキストモードのようなデータの変換は行いません。

 

選択肢③

 

stream_wrapper_register() 関数を使うと、新しいラッパーとその挙動を登録する事ができる。

stream_wrapper_register( string $protocol , string $class , int $flags = 0 ) : bool

成功した場合に true を、失敗した場合に false を返します。
stream_wrapper_register() は、 protocol というハンドラが既にある場合、 false を返します。

そのため、以下のコード

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

$r = stream_wrapper_register('dummy', 'PhpStreamDummy');
var_dump($r);

を実行すると

Warning: stream_wrapper_register(): class 'PhpStreamDummy' is undefined in ...
bool(false)

となるが、一方で以下のコード

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

class PhpStream {
}

$r = stream_wrapper_register('php', 'PhpStream');
var_dump($r);

を実行すると

bool(true)

となる。

 

stream_wrapper_register()関数は、新しいラッパーを登録するためのものです。ラッパーは、特定のプロトコルに対してファイルアクセスやデータの読み書きなどの挙動を定義します。

最初のコードの実行時には、stream_wrapper_register('dummy', 'PhpStreamDummy') という呼び出しを行っています。しかし、PhpStreamDummy というクラスが存在しないため、stream_wrapper_register()エラーを発生させます

具体的には、“Uncaught TypeError: stream_wrapper_register(): Argument #2 ($class) must be a valid class name, PhpStreamDummy given” というエラーメッセージが表示されます。したがって、var_dump($r) の結果は表示されません。

一方、2番目のコードでは、stream_wrapper_register('php', 'PhpStream') という呼び出しを行っています。ここでは、PhpStream というクラスが定義されているため、登録に成功し、true が返されています。したがって、var_dump($r) の結果は bool(true) となります。

注意点として、既に同じプロトコルのハンドラが登録されている場合は、stream_wrapper_register() は失敗し、false を返します。

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

 

選択肢④

 

stream_filter_prepend() 関数を使うと、フィルタをストリームに付加する事ができる。

stream_filter_prepend(resource $stream, string $filtername, int $read_write = ?, mixed $params = ?): resource

そのため、exam.txtファイルに「文字コード sjisで書かれた文字」を入れた上で、以下のコード

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

// ①
$filename='./exam.txt';
$fp = fopen($filename,'r');
var_dump(fgets($fp) );
$fclose($fp);

// ②
$fp = fopen($filename,'r');
$fp2 = stream_filter_prepend($fp,'convert.iconv.SJIS−win/UTF−8');
var_dump(fgets($fp) );
fclose($fp);

を実行すると

string(17) "sjis?????????"
string(23) "sjisでこんにちは"

となる。

 

stream_filter_prepend() 関数は、ストリームにフィルタを追加するためのものです。フィルタは、データの読み書きや変換などの処理を行うために使用されます。

今回与えられたコードでは、exam.txt ファイルに「文字コード sjis で書かれた文字」という内容が入っています。

①の処理によって、exam.txt ファイルの最初の行を読み込み、var_dump() で表示します。結果は string(17) "sjis?????????\n" となります。

これは、sjisの文字コードで書かれた文字が正しく表示されていないことを示しています

次に、②の処理では、stream_filter_prepend() を使用して、ストリーム $fpconvert.iconv.SJIS-win/UTF-8 フィルタを追加します。このフィルタは、sjis の文字列を UTF-8 に変換する役割を持っています。

その後、再び fgets() を使用してストリームから行を読み込み、var_dump() で表示します。結果は string(23) "sjisでこんにちは\n" となります。

これは、sjisの文字列が正しく UTF-8 に変換され、”sjisでこんにちは” という文字列が表示されていることを示しています

つまり、stream_filter_prepend() を使用することで、ストリームにフィルタを追加することができ、データの変換や処理を行うことができます。

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

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