PHPのexec()でjsonを引数として渡すにはシングルクォーテーションが必要

処理が重いファイルをバックグラウンドで動かす

 ユーザーが選択したデータをDBから抽出し、それを別のDBに別の情報と一緒にinsertするという処理をPHPでやる必要がありました。
 ただ、このinsert処理が遅く、10分以上かかります。その間はブラウザの再読み込みがくるくるしている状態でユーザーは画面から離れることができません(正しくはブラウザを閉じたり同じ画面の別の機能を使うことができません)。
そこでinsert処理をバックグラウンドで動かす必要がありました。

バックグラウンドで実行するにはnohupを使う

 既に色々な記事にnohupを使う旨がありますので詳細な説明は割愛します。PHPにおいては以下のコマンドで実行できます。
 またバックグラウンドで処理するには呼び出し元で用意したデータをコマンドライン引数として渡してやる必要があります。

<?php
$command = "nohup <filePath(hogehoge.php)> \"{$arg1}\" \"{$arg2}\" > /dev/null &";
exec($command);

 重要なのがコマンドライン引数では配列は渡せないということです。例えば以下のように配列で渡します

<?php
$command = "nohup <filePath(hogehoge.php)> {$data} > /dev/null &";
exec($command);

すると受け取り側での結果は

string(5) "Array"

となりデータが入っていません。

コマンドライン引数の受け取り方

 コマンドライン引数を受け取るには呼び出し先のファイルにて

<?php
$hoge1 = argv[1];
$hoge2 = argv[2];

というようにします。ちなみにargvはゼロオリジンですがargv[0]にはファイル名が入るため引数だけ受けるには1から入れます。

今回渡すデータの特徴

 今回コマンドライン引数で渡すデータの特徴は * 日本語を含む連想配列

<?php
$data['prefecture'] = array("東京都", "神奈川県", "茨城県");
$data['login']      = "2019/11/01";

です。上記のように配列はコマンドライン引数にできないことを考慮すると渡し方に工夫をする必要があります。

 また*ブラウザバック時の値保持を想定しているため全データは$_SESSIONに入れています。

最初にやろうとしたこと

 せっかくなので表題のやり方に行き着いた経緯も書きたいと思います。

セッションIDを渡してセッションデータを呼び出そうとした

 既に何度も述べていますがコマンドライン引数には配列を渡すことができないため他の渡し方を考える必要があります。そこで最初に考えたのがセッションデータを使うということです。
 nohupコマンドで別ファイルを呼び出した場合、呼び出し元と呼び出し先のプロセスは別となります。これは言い換えると別のセッションIDが発行されることになり、呼び出し先で呼び出し元のセッションデータを使えないことを意味します。
 そこで、呼び出し元でsession_id()を使って現在のセッションIDを取得し、コマンドライン引数につけて渡すことで呼び出し先でセッションをsession_start()で再スタートするということを思いつきました。

<?php
// 呼び出し元

$data['prefecture'] = array("東京都", "神奈川県", "茨城県");
$data['login']      = "2019/11/01";

$_SESSION['data']['pref']  = $data['prefecture'];
$_SESSION['data']['login'] = $data['login'];

$sessionId = session_id();

$command = "nohup <filePath(hogehoge.php)> \"{$sessionId}\" > /dev/null &";
exec($command);
<?php
// 呼び出し先

$sessionId = argv[1];

session_id($session_id);
session_start();

$prefData  = $_SESSION['data']['pref'];
$loginData = $_SESSION['data']['login'];

という感じです。
 確かにこれだと呼び出し先で呼び出し元のセッションを再スタートできるので呼び出し元で入れたセッションデータを取り出すことができます。

バックグラウンド処理なのに処理が終わるまで画面が読み込み中になる

 データを取り出すことができたので呼び出し先から期待する処理を実行することができました。ところが試しに実行中の画面をリロードしたところ再読み込み中になってしまいました。
 ログを流しながら確認するとバックグラウンド処理が終わるまで再読み込み中になっていることがわかりました。

処理が続いていたのはセッションが生きているから

 色々と条件を変えて試したところ、呼び出し元のセッションが呼び出し先で再利用されたことによりセッションが継続(破棄されていない)することでバックグラウンド処理になっていなかったことがわかりました。
 これはユーザーが画面から離れることができないということを意味しており別の方法を考える必要が生まれました。

 

jsonとしてコマンドライン引数に渡す

 セッションデータが使えない以上、配列を文字列として送るしか方法がありません。

serializeとunserialize

 そこで最初に考えたのが配列をserialize()して渡すというものでした。これなら配列も文字列として送ることができ、今度は呼び出し先でunserialize()することで配列として復元できるというものでした。

<?php
// 呼び出し元

$data['prefecture'] = array("東京都", "神奈川県", "茨城県");

$seriPrefData = serialize($data['prefecture']);

$command = "nohup <filePath(hogehoge.php)> \"{$seriPrefData}\" > /dev/null &";
exec($command);
<?php
// 呼び出し先

$seriPrefData = argv[1];

$prefData = unserialize($seriPrefData);

(後続処理)

 ところがunserializeした$prefDataをvar_dump()してみると

bool(false)

となりunserializeできていないことがわかりました。 原因を調べてみるとマルチバイト文字が影響しているようでした

string(113) "a:7:{i:0;s:6:茨?昌i:1;s:6:栃木県;i:2;s:6:群?禰;i:3;s:6:埼?匡;i:4;s:6:千葉県;i:5;s:6:東京都;i:6;s:8:神奈川県;}"

 文字コードの関係で文字化けしていますが、例えば神奈川県は漢字なので4文字扱いになるはずがs:8となり8文字扱いとなっています。バイトとしては正しいのですが文字数のズレが有るためにうまく復元できないようでした。

json

 そこで次に文字列として渡す方法として考えたのがjson化でした。jsonは上述のserialize/unserialize同様に呼び出し元のデータをjson_encode()、呼び出し先でデータをjson_decode()することで復元できます。

<?php
// 呼び出し元

// 実際にはEUC-JPなのでmb_convert_variable()でUTF-8にしています。
$data['prefecture'] = array("東京都", "神奈川県", "茨城県");

$jsonPrefData = json_encode($data['prefecture']);

$command = "nohup <filePath(hogehoge.php)> \"{$jsonPrefData }\" > /dev/null &";
exec($command);
<?php
// 呼び出し先

$jsonPrefData  = argv[1];

// 連想配列として取り出すので第二引数をtrueにします。
$prefData = json_decode($seriPrefData, true);

(後続処理)

ダブルクォーテーションは駄目

 上述のようにコマンドラインで文字列として渡すので

\"{$argv}\"

という感じでダブルクォーテーションをつけて渡していました。ところがjsonコマンドライン引数がダブルクォーテーションだとnullになってしまいました。
 どうやらjson化した時には

string(1970) "{"pref":["\u8328\u57ce\u770c","\u6803\u6728\u770c","\u7fa4\u99ac\u770c"]}"

とダブルクォーテーションがついていたのですが、argvで受け取ってみると

{pref:[u8328u57ceu770c,u6803u6728u770c,u7fa4u99acu770c}

ダブルクォーテーションが外されていました。

 考えてみたらダブルクォーテーションは文字列として扱うだけなので単につけただけでは消える(外される)ようでした。

シングルクォーテーションをつける

 結論としてはコマンドライン引数に渡す時にシングルクォーテーションをつけることで大丈夫でした。

<?php
// 呼び出し元

// 実際にはEUC-JPなのでmb_convert_variable()でUTF-8にしています。
$data['prefecture'] = array("東京都", "神奈川県", "茨城県");

$jsonPrefData = json_encode($data['prefecture']);

$command = "nohup <filePath(hogehoge.php)> '{$jsonPrefData }' > /dev/null &";
exec($command);

まとめ

  • バックグラウンドにデータ渡して処理するにはjson化して渡すと良い
  • その際にjson_encode()したデータをシングルクォーテーションで囲んでコマンドライン引数に渡すとうまく取り出せる
  • 連想配列として取り出すにはjson_decode($hoge, true)というように第二引数をtrueにするとOK