SoLA2-TechBlog

退屈な作業はプログラムに任せましょう!日々の作業に少し工夫を足すだけであなたの時間はもっとクリエイティブになる。

【HTML実践】複数のプルダウンが連動するやつ Part1

複数のプルダウンが連動するやつ
皆さんこんにちは!SoLA2です。この前、製品管理システムを作っていたのですが、その時にエリアを選択するための入力フォームで親子関係を作る部分で迷ったのでメモしておきます。

今回の記事はかなりエンジニア向けになっております。次回以降の記事で、初心者向けに各ソースの解説をした記事を投稿しますので、わからなかった方は少々お待ちください。

親プルダウンと子プルダウン

皆さんも、親プルダウンが選択されると、子プルダウンの選択肢が変化するようなフォームを見たことがあると思います。具体例でだと親プルダウンに「地方」、子プルダウンに「都道府県」などがそれにあたると思います。以下のページの地域選択フォームがそれにあたります。
www.manpowerjobnet.com

下記の仕様概要にあるようなフォームをつくる為にはどうしたらよいか、ちょっと作り方を調べてみました。よく見かけるフォームなので、すぐ見つかるかと思ったのですが、割と苦労しました。

サーバ側の処理

こういった類のものでまず判断するべきは、サーバ処理が入るかどうかだと思います。選択肢が都道府県くらいの数であれば、画面ロード時にすべて取得してしまい、JSだけで切替処理をするのもアリだと思います。

ですが、割とそういった方法を採用せずに、選択された地域の値を一旦サーバ側に渡して、地域に紐づく都道府県の一覧を取得後、フロントエンドに反映する方式を紹介している解説サイトが多かった印象です。

確かに、地域と都道府県の紐づきをDBに持たせてしまえば、異動や組織改編などで情報が変わった時、スクリプトの改修が必要ないのでお客様に説明しやすいのかもしれませんね。

非同期処理

サーバ側の処理があるということは、何らかの形で処理をサーバ側に持っていく必要があるということです。今回はajaxを使って非同期処理でサーバ側に処理を投げてしまいましょう。

ちなみに、何でもかんでも非同期処理に回してしまうと、後で処理の整合性を取るのが難しくなり、後悔することもあるので気を付けましょう。

仕様概要

プルダウンが3つ存在する

  • プルダウン①
  • プルダウン②
  • プルダウン③

プルダウンはそれぞれ親子関係にある

  • プルダウン①の子がプルダウン②
  • プルダウン②の子がプルダウン③

親の値が変更されると子が以下のように状態変化する

  • 子の値が初期化
  • 親の値が初期値の場合、子は選択不可

各プルダウンの値はDBから取得する

  • プルダウン①:地域(東北とか関東とか)
  • プルダウン②:都道府県(青森とか東京とか)
  • プルダウン③:店舗(青森支店とか東京支店とか)

コーディング

フロントエンド

HTMLは下記の通りです。関係ないクラスは極力減らしましたが、一部bootstrapのクラスが残っています。

<div class="form-group row">
	<label> 登録店舗<span class="attention">*</span>:</label>
	<div class="col-sm-3">
		<select id="area_cd_1" name="area_cd_1">
			<option value="-1">エリアを選択</option>
			<?= $mcode->getSelecterList('area_cd_1', $app->preSetValues); ?>
		</select>
	</div>
	<div class="col-sm-3">
		<select id="area_cd_2" name="area_cd_2" <?= vSelectionDisabled('area_cd_1', $app->preSetValues); ?>>
			<option value="-1">エリアを絞込み</option>
			<?= $areaLink->getValue($app->preSetValues); ?>
		</select>
	</div>
	<div class="col-sm-3">
		<select id="area_cd_3" name="area_cd_3" <?= vSelectionDisabled('area_cd_2', $app->preSetValues); ?>>
			<option value="-1">店舗名</option>
			<?= $shop->getShopInArea2($app->preSetValues); ?>
		</select>
	</div>
</div>

getSelecterList、getValue、getShopInArea2は初期値と初期選択肢をセットするためのメソッドです。本件の本質とは異なりますので内容は割愛します。

またCSSに関しても、本件の仕様を満たすために必須な記述はないので省きます。

JSは下記のとおりです。jQueryとajaxを利用しています。

//エリアの入力フォーム制御(プルダウン)
$(function () {
    //エリア1が変更された場合、それに紐づく都道府県プルダウンの選択肢を変更する
    $('#area_cd_1').on('change', function () {
        var area_cd_1_val = $(this).val();

        $.post('../_area_cd_1_updated_ajax.php', {
            mode: 'area_cd_1',
            area_cd_1: area_cd_1_val
        }, function (data) {
            //selectタグ(子) の option値 を一旦削除
            $('#area_cd_2 option').remove();
            
            //_area_cd_1_updated_ajax.php から戻って来た data の値をそれそれ optionタグ として生成し、
            // #area_cd_2 に optionタグ を追加する
            $('#area_cd_2').append($('<option>').text('エリアを絞込み').attr('value', -1));

            if (data.length == 0) {
                $('#area_cd_2').prop('disabled', true);
            } else {
                $('#area_cd_2').prop('disabled', false);
            }

            for (var area in data) {
                $('#area_cd_2').append($('<option>').text(data[area]['area_val_2']).attr('value', data[area]['area_cd_2']));
            }

            $('#area_cd_2').trigger('change');
        });
    });

    //エリア2(都道府県)が変更された場合、それに紐づく店舗プルダウンの選択肢を変更する
    $('#area_cd_2').on('change', function () {
        if($('#area_cd_3').length) {
            var area_cd_2_val = $('#area_cd_2').val();

            $.post('../_area_cd_2_updated_ajax.php', {
                mode: 'area_cd_2',
                area_cd_2: area_cd_2_val
            }, function (data) {
                //selectタグ(子) の option値 を一旦削除
                $('#area_cd_3 option').remove();
                
                //_area_cd_2_updated_ajax.php から戻って来た data の値をそれそれ optionタグ として生成し、
                // #area_cd_3 に optionタグ を追加する
                $('#area_cd_3').append($('<option>').text('店舗名').attr('value', -1));

                if (data.length == 0) {
                    $('#area_cd_3').prop('disabled', true);
                } else {
                    $('#area_cd_3').prop('disabled', false);
                }

                for (var area in data) {
                    $('#area_cd_3').append($('<option>').text(data[area]['shop_name']).attr('value', data[area]['id']));
                }
            });
        }
    });
});
バックエンド

バックエンド側の処理は渡された親の値を元に子の選択肢を生成ところがメインになります。

_area_cd_1_updated_ajax.php

<?php

//直接のページ遷移を阻止
$request = isset($_SERVER['HTTP_X_REQUESTED_WITH']) ? strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) : '';
if($request !== 'xmlhttprequest') exit;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

	try {
		$res = _area_cd_1();
		header('Content-Type: application/json');
		echo json_encode($res);
		exit;
	} catch (Exception $e) {
		header($_SERVER['SERVER_PROTOCOL'] . ' サーバ内処理エラーが発生しました。(500)', true, 500);
		echo $e->getMessage();
		exit;
	}
}

/**
 * 指定したエリアコード1に紐づくエリアコード2の情報を配列で返す
 * @return array: 指定したエリアコード1に紐づくエリアコード2の情報
 * @throws \Exception :エリアコード1の情報が存在しなかった場合に発生する
 */
function _area_cd_1()
{
	if (!isset($_POST['area_cd_1'])) {
		throw new \Exception('エリアコード1の情報がセットされていません。');
	}
	$areaLinks = new \MyApp\Model\AreaLink();

	return $areaLinks->getAjaxValue($_POST['area_cd_1']);
}


_area_cd_2_updated_ajax.php

<?php

//直接のページ遷移を阻止
$request = isset($_SERVER['HTTP_X_REQUESTED_WITH']) ? strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) : '';
if($request !== 'xmlhttprequest') exit;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

	try {
		$res = _area_cd_2();
		header('Content-Type: application/json');
		echo json_encode($res);
		exit;
	} catch (Exception $e) {
		header($_SERVER['SERVER_PROTOCOL'] . ' サーバ内処理エラーが発生しました。(500)', true, 500);
		echo $e->getMessage();
		exit;
	}
}

/**
 * 指定したエリアコード2に紐づく店舗情報を配列で返す
 * @return array: 指定したエリアコード2に紐づく店舗の情報
 * @throws \Exception :エリアコード2の情報が存在しなかった場合に発生する
 */
function _area_cd_2(){
	if (!isset($_POST['area_cd_2'])) {
		throw new \Exception('エリアコード2の情報がセットされていません。');
	}
	$shops = new \MyApp\Model\Shop();

	return $shops->getAjaxShopInArea2($_POST['area_cd_2']);
}

getAjaxValueや、getAjaxShopInArea2はここでは定義しておりませんが、基本的にDBからHTML形式で値を生成するメソッドです。本件の本質とは異なりますので、割愛します。

ソースを貼ったら文字数がとんでもないことになってしまったので、それぞれの解説は以降の記事で実施しようと思います。

まとめ

プルダウンの選択肢が多すぎたとしても、致命的な問題になるわけではありませんが、やはりUIにこだわるのであれば、選択肢の数はスクロールバーが表示されない程度に抑えたいものですね。

何より、お客様からこの形式のプルダウンをご要望頂くことが多いので、覚えておいて損はないと思います。

今回はこのソースを見て足りないところを補完できるエンジニアに向けて、先に全体像をお伝えいたしました。次回以降の記事で、初心者の方にもわかるようにソースを解説していきたいと思います。

ではでは

【PHP】非同期処理の内容が反映されないときのチェックポイント

非同期処理の反映

WEBアプリを開発していると、非同期処理をする事があると思います。

今回の記事では非同期で処理したサーバ側のデータがフロントサイドにうまく反映されない問題について取り上げてみたいと思います。理由は私が今日この問題が原因で4時間近くも浪費してしまったためです。。。

前提

開発言語はPHP、非同期処理にはajaxを利用します。

チェックポイント

まず初歩的な問題を確認してみましょう。

  • 非同期処理の結果をちゃんとHTMLに反映しているか
  • 非同期処理が正常に動作しているか
  • 反映の方法に間違いは無いか

Google先生に聞くと多くの回答が上記3点に集約されます。私もよく失敗してしまうのですが、非同期で処理した内容をフロントサイドに戻さないと、当然ブラウザにも反映されません。更新ボタンを押すと正常にデータが更新されているならまずはこの線を疑うと良いと思います。

非同期処理が正常に動作しているかを確認する

冷静にフロントサイドへ処理を戻していることを確認して、それでもブラウザ側に反映されていないという場合、次に確認すべきは非同期処理です。下記の点を確認してみましょう。(※非同期処理が正常に動作していない場合の確認事項ですので、更新ボタンを押すと正常に反映される場合は飛ばしてください。)

  • ajax用のjavascriptはちゃんとインポートしているのか
  • ajax内でエラーは発生していないか
  • ajaxから呼ばれるPHPファイルは読み込まれているのか

※2点目、3点目の調査にはディベロッパーツールを使うと便利です

反映の方法に問題が無いか確認する

さてさて、ここまで確認してなおフロントサイドに反映されてないとなると、残るはその反映の仕方です。下記の点について確認してみましょう。

  • 反映させたいタグは正しく取得出来ているか
  • 反映させたいタグに対して正しいメソッドを使っているのか
  • 確認しているブラウザの仕様ではないか

1点目については、よくあるケアレスミスなので一応確認してみると、バグの神様が微笑んでくれるかもしれません。

2点目については、割りと見落としがちですがHTMLの仕様についての問題です。値を変更するためのメソッドはいくつかあると思いますが(「innerHTML」「innerText」「textContent」「value」など)タグによって対応するメソッドが変わったりします。一度反映させたいタグがそのメソッドに対応しているか確認してみると幸せになれるかもしれません。

3点目については、値を変更するためのメソッドがブラウザによっては使用出来ない問題です。他のメソッドについては調べていませんが、InternetExplorerであれば「textContent」、Firefoxであれば「innerText」が使用出来ないそうです。ちなみに「innerHTML」はどちらのブラウザでも利用出来ます。

全部確認した、でも反映されない・・・

私の場合、上記すべてを確認しても反映されない頑固な輩がいらっしゃいました。もういっそ同期通信で妥協しようかとすら思ったのですが・・・今回は粘り勝ちました!

タグの取得方法で、「$('ID名')」を用いると、値を反映出来ないケースが有るらしいので、「getElement系のメソッド」を用いることで、今回の問題は解決しました。

success: function (res) {
  //修正前
  $('#SampleID_' + id).innerHTML = String(res.vote); 

  //修正後
  document.getElementById('SampleID_' + id).innerHTML = String(res.vote); 
}

これってajaxを使う人だと割りと当たり前のことなのでしょうか?
まだ根本的な原因がわかっていないので、調査の必要がありそうです。もし原因がわかる方いらっしゃいましたら是非ともご教授ください!

今さらTwitterボットを開発してみた。【part 2】

クラスの作成

拡張性を重視するなら個人的にはオブジェクト指向で記述した方が便利に感じるので、適当な粒度でクラス化していきます。とはいえ基本的な方針は「Twitterボットプログラミングテクニック」に載っている内容に沿って開発します。

Botクラス

Twitterと通信してボットに対して呟く命令等をやり取りするクラスです。基本的には「TwitterOAuth」が面倒な部分をカバーしてくれていますので、私が開発する箇所は、後で出てくるResponderクラスで処理した結果を「TwitterOAuth」に渡すところと、そのエラーチェックの部分だけです。

Responderクラス

ボットに対して命令したい内容を生成するクラスにする予定ですが、現在は固定文言を呟く命令を生成するためのクラスとなっております。後々抽象化して、機能毎に継承するように改良していきます。

ソースコード

前回の記事から変更点があった「bot.php」と、新しく作成した「responder.php」の2点を紹介します。

bot.php
<?php

require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/responder.php');

use Abraham\TwitterOAuth\TwitterOAuth;

echo '処理開始' . PHP_EOL;
$responderInstance = new Responder();
$responderInstance->Response('クラス化テスト');
echo '処理終了' . PHP_EOL;

class Bot{
    private $connection;    //直接Twitterとやり取りする
    private $responder;     //Twitterにどのような命令を投げるかを処理する

    public function Bot($responder,$consumerKey = CONSUMER_KEY,$consumerSecret = CONSUMER_SECRET,
                        $accessToken = ACCESS_TOKEN,$accessTokenSecret = ACCESS_TOKEN_SECRET){
        $this->connection = new TwitterOAuth($consumerKey, $consumerSecret, $accessToken, $accessTokenSecret);
        $this->responder = $responder;
    }

    //処理結果を検証する
    private function checkResult($result){
        if ($this->connection->getLastHttpCode() === 200) {
            echo '成功!' . PHP_EOL;
        } else {
            echo '失敗!' . $result->errors[0]->message . PHP_EOL;
        }
    }

    //つぶやく処理
    public function Tweet(){
        $opt = $this->responder->getArgs();
        $res = $this->connection->post("statuses/update",$opt);
        $this->checkResult($res);
    }
}

処理結果を検証するメソッドとつぶやく処理を分けた理由は、今後追加機能が発生した場合でも、おそらくチェック処理だけは共通なのではないかという淡い期待を抱いてのものです。

responder.php
<?php

require_once(__DIR__ . '/responder.php');

//ただ呟くだけのResponder
//機能追加はこのクラスを継承して実装
class Responder{
    private $args;

    public function Responder(){
        $this->opt = array();
    }

    //このメソッドをオーバーライドする
    public function Response($input){
        $this->args['status'] = $input;

        $bot = new Bot($this);
        $bot->tweet();
        return true;
    }

    public function getArgs(){
        return $this->args;
    }
}

ResponderクラスにBotクラスを持たせているのには理由があります。今後cronにて自動実行することになると、おそらく、ボットに対して「何かを命令する回数」よりも「何も命令しない回数」の方が多くなるはずです。

であれば、何もしない場合はTwitterへの接続を控えたいので、まずResponderクラスを生成し、必要であればBotクラスを生成する手順にしております。言ってしまえば、好みの問題です。

実行結果

早速、開発環境で実行してみます。ちなみに前回の記事で、ターミナルの余計な部分を表示させないために改行しまくるというアナログチックな対応をしておりましたが、「Ctrl+l」で全て消せる見たいです。早速今回利用してみます。

f:id:gootor3030:20170814202708p:plain

無事処理が終了していそうです。表示内容もすっきりしていてスマートです!

f:id:gootor3030:20170814202549p:plain

肝心のツイートもちゃんとされています。これで心置きなく追加機能を実装できそうですね!

次回予告

次回はいよいよ決まった時間につぶやく機能を作っていきたいと思います。お楽しみに!

今さらTwitterボットを開発してみた。【part 1】

認証について

ボット用のアカウントに接続する方法はいくつかありますが、ドットインストールでは「TwitterOAuth」というパッケージを用いて認証を行っていましたので、今回はこちらを利用したいと思います。

composerを利用してTwitterOAuthをインストールする

まずはcomposerの公式サイトにアクセスし、「Getting Started」ボタンからページ中段にあるコマンドをPHPがインストールされているローカル開発環境の作業ディレクトリで実行します。

php composer-setup.php --install-dir=bin --filename=composer

composerをダウンロードが完了しましたら、いよいよ「TwitterOAuth」をインストールしていきます。TwitterOAuthの公式サイトにアクセスし、Installationに記述された文をローカル開発環境で実行します。

ちなみにこのcomposerでインストールできるpackageの一覧についてはPackagistというサイトで確認することが出来るそうです。

定型文を呟かせてみる

実装

さてTwitterOAuthのインストールが完了しましたら、まずは簡単なモックを作成してみましょう。プログラムから固定文をボットアカウントにつぶやくだけの仕組みを作ってみたいと思います。

<?php

require_once(__DIR__ . '/twitteroauth/autoload.php');
define('CONSUMER_KEY', '*');
define('CONSUMER_SECRET', '*');
define('ACCESS_TOKEN', '*');
define('ACCESS_TOKEN_SECRET', '*');

アカウント情報はプログラム内で頻繁に利用するため、定義しておきます。こういった定義は、後々増えていきますので、定義用のファイルとして本体から切り離しておきます。

<?php

require_once(__DIR__ . '/config.php');

use Abraham\TwitterOAuth\TwitterOAuth;

//接続
$connection = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET);

$res = $connection->post("statuses/update", [
    'status' => 'さんぷる'
]);

if ($connection->getLastHttpCode() === 200) {
    echo '成功!' . PHP_EOL;
} else {
    echo '失敗!' . $res->errors[0]->message . PHP_EOL;
}

純粋にTwitterへ接続し、サンプルメッセージ「さんぷる」を投稿するだけのシンプルなプログラムですね。ではこれを実行してみましょう。

動作検証

f:id:gootor3030:20170814131224p:plain

コマンドライン上では成功!と表示されました。

f:id:gootor3030:20170814131341p:plain

実際のTwitterでも「さんぷる」と呟かれていますね。

次回予告

今回はとりあえず動くことを目標にコーディングしてきました。次回はこれらのコードをクラス化し今後の機能拡張に向けた準備を整えます。お楽しみに!

今さらTwitterボットを開発してみた。【part 0】

定期的にランダムな内容を発言するボットを開発します

薄々と感じる違和感

事の発端は、1ヶ月くらい前でした。元々は動画の企画で、パーソナリティのボットを作成しようというありきたりなモノでした。

でも、ただボットを作成するだけではつまらないので、発言内容を募集することになったんです。募集方法は主に動画のコメントか、募集サイトを開設してそこに視聴者が投稿する方式を採用しました。

この時既に薄々感じていました。ある違和感に・・・。とはいえものづくりは準備段階が楽しいもので、あれやこれやとたくさんアイデアが出てくるものです。それを前にしてもなお、そのような違和感を気にかけられる程、私は玄人ではありませんでした。

違和感の正体

企画の方向性もある程度決まり、そろそろ開発にも着手し始める頃、その違和感は確信に変わっていくのです。

「自動でつぶやく設定とかどこでするんだろう?」

そう、Twitter初心者だった私は愚かにもボット機能は公式で提供している機能だと思い込んでいたのです。見つかるはずのないその機能を探すこと1時間余り・・・私は認めざるを得ませんでした。その非情と向き合う覚悟をすると共に、心の中でこうつぶやくのです。

Twitterが公式で提供しているボット機能なんてなかったんや・・・)

対応策はエンジニアらしく

という訳で!公式で提供されていない以上、対応策は「一般のBOT作成サービスを利用する」か、「Twitterが提供しているAPIを利用して自分で作る」の2択なわけです。今後の事を考えると、呟く内容はDBから取得できるようにしたいので「自分で作る」事にします。

実はAPIとか初めてなんです

お恥ずかしい話なのですが、私はWEBサービスが提供するAPIを利用して開発した経験が無いため、下記の教材を利用しながら開発を進めていきます。言語はPHP(教材が殆どこの言語だったので)、エディタはPHP Stormを使います。

書籍

Amazonでそれっぽい古本を購入しました。合計400円(送料込み)は非常に安いと思うんです!

動画

以前から利用している動画サイト。幅広い知識を手軽に吸収できるので重宝してます。

  • ドットインストール
  • paizaラーニング

開発環境

今回はVagrantでローカル開発環境を立ててそこでデバッグしながら開発していきます。主にドットインストールで学んだ手法です。最終的には今契約しているロリポップのcronに設定しようと思います。

※ちなみにこちらがボットアカウントです。
twitter.com