PHP
HTML
JavaScript
Web
framework

[ページ遷移しないWebシステム] JavaScriptからPHP関数を直接呼ぶ感覚で使えるアダプタ

 非同期でJavaScriptとPHPがデータのやりとりをする際、ネットワークを介するため作業が繁雑になります。その中でデータの種類ごとに送受信のロジックを書いていたのでは地獄としか言いようがありません。

 今回は、言語の境界線を打ち破るべく、面倒な記述をほとんど消し飛ばす方法を思いついたので、それを解説します。

関連リンク

ソースコード https://github.com/mofon001/JS_PHP_adapter

以前の内容

[ページ遷移しないWebシステム] JavaScriptによるWindow Framework
[ページ遷移しないWebシステム]Qiitaの投稿をページ遷移無しで表示してみる
[ページ遷移しないWebシステム] JavaScriptによるURLの操作と、ページ遷移しないプログラム
[ページ遷移しないWebシステム] JavaScriptによるリッチテキストエディタ

サンプルフォルダの構造

  • [Modules] ここにJavaScriptから呼び出されるクラスを入れる
    • Test.php
  • [script]
    • Adapter.js
  • Main.php
  • index.html
  • index2.html

 まずは以下のコードを見てください。

やりとりに必要なコード

Modules/Test.php
<?php
//JavaScriptから呼び出されるクラス
class Test{
    //JavaScriptから呼び出して良いクラスか確認のため記述する
    static $JS_ENABLE;  
    //不正な呼び出しを防ぐために、「JS_」を付ける
    public static function JS_add($a,$b){
        return $a+$b;
    }
}
index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <script type="text/javascript" src="script/Adapter.js"></script>
    <title>PHPとJavaScriptのアダプター</title>
</head>
<body>
<script type="text/javascript">
    //サーバサイドのアドレスを設定してアダプターを作成
    var adapter = ADP.createAdapter("Main.php");
    //命令を実行("クラス.ファンクション名",引数...) 「on」でデータの受け取り
    adapter.exec("Test.add",10,20).on=function(value){
        //受け取ったデータを表示
        document.body.innerHTML += "10+20の結果: "+value;
    }   
</script>
</body>
</html>

 いつもはHTMLファイルにJavaScriptのソースを記述したりはしないのですが、今回は出来るだけ簡単にするためにやっています。

出力結果.
10+20の結果: 30

 JavaScriptから10と20を送ってPHP側で足し算し、結果をJavaScriptに戻しています。PHP側の作業は、呼び出されるクラスをModulesフォルダに格納するだけで、それ以外の処理は一切書かなくて構いません。ちなみに「JS_」と付けているのは、勝手に一般のファンクションが呼び出されないように制限を行うためのものです。

実装内容

 これを実現するために書いたソースです。まずはPHPの方から。

Main.php
<?php
define("MODULE_PATH","/Modules/");  //クラスの置き場所
define("DECORATION","JS_");         //呼び出しファンクションに付ける修飾文字列

//クラスが見つからなかったら、requireを行う
spl_autoload_register(function ($className) {
    try{
        require_once(dirname(__FILE__).MODULE_PATH.$className.".php");
    }finally{}
});

//クライアントとのデータのやりとりを管理
class MG{
    static $mCommand;   //コマンドキャッシュ
    static $mParams;    //パラメータキャッシュ
    //パラメータ処理用
    public static function getParam($name){
        //GETやPOSTに関わらず、統合してパラメータを受信
        if(Self::$mParams === null){
            if(isset($_GET["command"])){
                Self::$mCommand = $_GET["command"];
                Self::$mParams = array();
            }else{
                $json_string = file_get_contents('php://input');
                Self::$mParams = json_decode($json_string,true);
                Self::$mCommand = isset(Self::$mParams["command"])?Self::$mParams["command"]:"";
            }
        }
        if(isset(Self::$mParams[$name])){
            return Self::$mParams[$name];
        }
        if(isset($_GET[$name]))
            return $_GET[$name];
        return null;
    }
    //コマンド取得
    public static function getCommand(){
        if(Self::$mCommand !== null)
            return Self::$mCommand;
        return Self::getParam("command");
    }
    //命令実行処理
    public static function init(){
        //実行コマンドが送られてきているか?
        if(MG::getCommand() == "exec"){
            $values = [];   //戻り値格納用
            $funcs = MG::getParam("functions");
            foreach($funcs as $func_info){
                if(!isset($func_info["function"]))
                    break;
                //クラス名とファンクションを分ける
                $name = explode(".",$func_info["function"],2);
                if(count($name) != 2)
                    break;
                $class = $name[0];
                $func = DECORATION.$name[1];
                //実行可能か判断
                if(!property_exists($class,"JS_ENABLE") || !method_exists($class,$func))
                    break;
                $count = (new ReflectionClass($class))->getMethod($func)->getNumberOfRequiredParameters();
                if(count($func_info["params"]) < $count)
                    break;
                //命令の実行
                $values[] = $class::$func(...$func_info["params"]);
            }
            //全ての命令が実行できたか?
            if(count($values) != count($funcs))
                return ["result"=>0,"message"=>"execの実行失敗"];
            //データを返す
            return ["result"=>1,"message"=>"execの実行","values"=>$values];
        }
        return null;
    }
}

$result = MG::init();

if($result !== null){
    ob_start("ob_gzhandler");
    header("Access-Control-Allow-Origin: *");
    echo json_encode($result);
}

クラスが呼び出されると、Modulesディレクトリから自動的に必要なファイルを読み出します。そしてJavaScriptが要求してきた命令を実行し、結果を返す仕組みです。

 今度はJavaScript側のソースです。

Adapter.js
(function(){
ADP = {};
//Json形式のデータを送る
function postJson(url,param,proc) {
    var xmlHttp = xmlHttp = new XMLHttpRequest();
    xmlHttp.open('POST', url, true);
    xmlHttp.setRequestHeader("Content-Type", "application/json");
    xmlHttp.onreadystatechange = function (){
        if(this.readyState == 4){
            proc(JSON.parse(xmlHttp.response));
        }
    }
    var p = JSON.stringify(param);
    xmlHttp.send(p);
}

ADP.createAdapter = function(scriptUrl,sessionHash){
    var adapter = {"url":scriptUrl,"sessionHash":sessionHash};
    adapter.exec = function(){
        if(arguments.length == 0)
            return;
        var _this = this;
        var functions = [];
        if(arguments[0] instanceof Array)
            for(var i=0;i<arguments.length;i++)
                functions.push({"function":arguments[i][0],"params":Array.prototype.slice.call(arguments[i], 1)});
        else
            functions.push({"function":arguments[0],"params":Array.prototype.slice.call(arguments, 1)});

        var values = {"command":"exec","sessionHash":adapter.sessionHash,"functions":functions};
        postJson(this.url,values,function(r){
            if(_this.on){
                if(r === null || r.result==0)
                    _this.on(null);
                else
                    _this.on(functions.length==1?r.values[0]:r.values);
            }

        });
        return this;
    }
    adapter.setSession = function(sessionHash){this.sessionHash=sessionHash;}
    adapter.getSession = function(){return this.sessionHash};
    return adapter;
}
})();

 PHP側に送るデータを整理し送受信を行います。後のことを考えて、セッション情報を送る機能も付けてあります。拡張する余地は残してあります。

もう少し色々やってみる

 クライアントとサーバ間のやりとりは、出来るだけ回数が少ない方が良いのは言うまでもありません。ということで一回のやりとりで複数のファンクションを呼び指すサンプルを作ります。

Modules/Test2.php
<?php
//JavaScriptから呼び出されるクラス
class Test2{
    //JavaScriptから呼び出して良いクラスか確認のため記述する
    static $JS_ENABLE;  
    static $DATA = ["たけやり","こんぼう","どうの剣"];
    //不正な呼び出しを防ぐために、「JS_」を付ける
    public static function JS_getValues(){
        return Self::$DATA;
    }
    public static function JS_getValue($index){
        return Self::$DATA[$index];
    }
}
index2.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <script type="text/javascript" src="script/Adapter.js"></script>
    <title>PHPとJavaScriptのアダプター</title>
</head>
<body>
<script type="text/javascript">
    //サーバサイドのアドレスを設定
    var adapter = ADP.createAdapter("Main.php");
    //複数の命令を実行(["クラス.ファンクション名",引数...)][...] 「on」でデータの受け取り
    adapter.exec(["Test2.getValues"],["Test2.getValue",2]).on=function(values){
        var output = "<pre>"
        for(var i=0;i<values.length;i++)
            output += i+"件目のデータ\n"+JSON.stringify(values[i])+"\n\n"; 
        document.body.innerHTML += output+"</pre>";
    }   
</script>
</body>
</html>
出力結果.
0件目のデータ
["たけやり","こんぼう","どうの剣"]

1件目のデータ
"どうの剣"

 getValueとgetValuesの二つを一回で呼び出して処理しています。戻ってくるデータは複数件になります。配列で命令を記述するだけで、簡単に複数の処理が行えます。

まとめ

 クライアントとサーバ間で命令の処理をする時の煩雑さが、一気に解消されました。私自身、今まで面倒な手続きを散々記述してきたのですが、それとはもうおさらばです。

 追加で必要な機能も考えています。例えば呼び出した命令の戻り値を使って、もう一度命令を呼び出すような場合の処理です。クライアントにデータを戻さず、サーバ側でデータを再投入するような機構を作れば、やりとりの回数が確実に減ります。

 そして今回の構造にセッション処理を加えれば、ページ遷移しないフレームワークを作るという目標にかなり近づくことが出来ます。ちょっとした思いつきで作ってみたのですが、想像以上に便利なものになりました。