SELECT要素をPHPと連動して書き換える。(JavaScriptからPHPの関数を引数を受け渡して呼び出す)

HTML
<!DOCTYPE hrml>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>SELECTをPHPとで連動する</title>

<body>
<header>
  <nav>
    <ul>
      <li><select id="S0"></select>
      <li><select id="S1"></select>
      <li><select id="S2"></select>
    </ul>
  </nav>
</header>

<script>
{
  const

  //PHP側の関数名
    PHP_FUNCTION_NAME = 'ChainSelector',


  //対象の要素に change イベントを発火させる
    dispatchChangeEvent = element => {
      let
        doc = element.ownerDocument,
        event = doc.createEvent ('HTMLEvents');

      event.initEvent ('change', true, true);
      element.dispatchEvent (event);
    },


  // option要素を書き換える
    replaceOptions = (mapObj, obj) => {
      Object.keys (obj).forEach (name => {
        let
          e = mapObj[name],
          memory = null,
          current = null;

        while (e.hasChildNodes ())
          e.removeChild (e.firstChild);

        for (let r of obj[name]) {
          let
            { group = null, text = null, value = null, defaultSelected = false, selected = false } = r,
            option = new Option (('undefined' === typeof text ? value: text), value, defaultSelected, selected);

          if (memory !== group) {
            let optgroup = document.createElement ('optgroup');
            optgroup.setAttribute ('label', group);
            current = group ? e.appendChild (optgroup): null;
          }
          
          (current || e).appendChild (option);
          memory = group;
        }
      })
    },

  
  //ファイルを読み込む
    fileLoader = function (file, arg = null)  {
      let req = new XMLHttpRequest ();
        
      req.open ('POST', file, false);
      req.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
      req.setRequestHeader ('Pragma', 'no-cache');
      req.setRequestHeader ('Cache-Control', 'no-cache');
      req.setRequestHeader ('X-Requested-With', 'XMLHttpRequest');
      req.send (arg !== null ? JSON.stringify (arg): null);

      return req.responseText;
    };


  //______________________________________________________________________

  class ChainSelector {//本体

    constructor (selects = [ ], link, cbFunc = null) {
      if (2 > arguments.length)
        throw new Error ('引数が不足です');

      let
        map = new Map,
        map2 = { },
        es = [...selects];
 
      es.forEach (e => {
        let key = e.id || e.name;

        if (key) {//名前で特定できるもの
          map.set (e, key);
          map2[key] = e;
          e.addEventListener ('change', this, false);
        }
      });

      this.map    = map;    // JS Map Object
      this.map2   = map2;   // { name0: select0, name1: select1, ... }
      this.link   = link;   // php側のファイル名
      this.cbFunc = cbFunc; // select要素書き換え後に呼ばれる関数

      this.reLoad ();
    }
    
  
  //phpの関数を呼び出し se;ect要素を書き換える
    reLoad (...args) { //args = [name, value]
      let
        parameter = { func: PHP_FUNCTION_NAME, args },
        res = fileLoader (this.link, parameter);
      
      if (res)
        replaceOptions (this.map2, JSON.parse (res));
    }


  //最初のselect要素を基準として changeイベントを発火
    default () {
      let first = this.map.keys ().next ().value;
      dispatchChangeEvent (first);
    }
    

  //changeイベント処理
    handleEvent (event) {
      let { target } = event;
      if (this.map.has (target)) {
        let
          name   = this.map.get (target),
          value = target.value;

        this.reLoad (name, value);

        if ('function' === typeof this.cbFunc)
          this.cbFunc.call (this, event);
      }
    }
  }


//______________________________________________________________________
  
  this.ChainSelector = ChainSelector;

}


//設定編
let
  selects = document.querySelectorAll ('nav select'),
  callside = 'call.php',
  cbfunc   = (event) => location.hash = event.target.value,
  hashchangeHandler = (event) => console.log (location.hash);

  //例えばlocation.hashを監視する
  window.addEventListener ('hashchange', hashchangeHandler, false);

  new ChainSelector (selects, callside, /*cbfunc*/);
</script>

PHP call.php
<?php
const CHAR_CODE    = 'UTF-8';
mb_internal_encoding (CHAR_CODE);
mb_regex_encoding (CHAR_CODE);

//________________________________________

header ('Content-type: application/json; charset='. CHAR_CODE);
header ('X-Content-Type-Options: nosniff');

echo json_encode (
  call_user_func_array (
    'call_user_func_array',
    callBackFuncAplly (
      json_decode (file_get_contents ('php://input'), true),
      array ('func', 'args')
    )
  )
);
exit;


//________________________________________

function ChainSelector ($name = '', $selected_value, $chain = array ()) {
  $ary = array ();
  
  switch ($name) {
  case 'S0' : //HTML側のSELECT要素
    
    switch ($selected_value) {
    case 'A':
      $ary = array (
        array ('value' => 'AA', 'text' => 'aa'),
        array ('value' => 'AB', 'text' => 'ab')
      );
      break;
      
    case 'B':
      $ary = array (
        array ('value' => 'BA', 'text' => 'ba'),
        array ('value' => 'BB', 'text' => 'bb')
      );
      break;
    }

    $mess  = array (
      'value'    => '',
      'text'     => count ($ary) ? '選択してください': 'データはありません',
      'selected' => true
    );

    $chain['S1'] = array_merge (array ($mess), $ary);
    return ChainSelector ('S1', $selected_value, $chain);
    break;


  case 'S1' : //HTML側のSELECT要素
    switch ($selected_value) {
    case 'AA':
      $ary = array (
        array ('value' => 'AAa', 'text' => 'aaa'),
        array ('value' => 'AAb', 'text' => 'aab')
      );
      break;
      
    case 'AB':
      $ary = array (
        array ('value' => 'ABA', 'text' => 'aba'),
        array ('value' => 'ABB', 'text' => 'abb')
      );
      break;
    
    case 'BA':
      $ary = array (
        array ('value' => 'BAA', 'text' => 'baa'),
        array ('value' => 'BAB', 'text' => 'bab')
      );
      break;
    
    case 'BB':
      $ary = array (
        array ('value' => 'BBA', 'text' => 'bba'),
        array ('value' => 'BBB', 'text' => 'bbb')
      );
      break;
    }
    $mess  = array (
      'value'    => '',
      'text'     => count ($ary) ? '選択してください': 'データはありません',
      'selected' => true
    );
    $chain['S2'] = array_merge (array ($mess), $ary);
    break;

  case 'S2':
    break;
  
  default :
    $ary = array (
      array ('value' => 'A', 'text' => 'a'),
      array ('value' => 'B', 'text' => 'b'.$name)
    );
  
    foreach ($ary as &$a)
      $a['selected'] = ($a['value'] === $selected_value);
    $mess  = array (
      'value'    => '',
      'text'     => count ($ary) ? '選択してください': 'データはありません',
      'selected' => true
    );

    $chain['S0'] = array_merge (array ($mess), $ary);
    
    return ChainSelector ('S0', $selected_value, $chain);
    break;
  }
  
  return $chain;
}


//JSからの変数を関数内で取り出しやすいようにする
function callBackFuncAplly ($data, $args) {
  $cnt = count ($args);
  $rst = array ();

  for ($i = 0; $i < $cnt; $i += 1)
    if (isset ($data[$args[$i]]))
      $rst[] = $data[$args[$i]];
    else
      throw new Exception ('変数がありません', 9);

  return $rst;
}