プログラミング

【PHP/Laravel】QiitaAPIとLINEAPIを使ったバッチ開発

みなさん、こんにちは。

今回の記事はタイトルにもある通り「QiitaAPI」と「LINEAPI」を使ったバッチ処理をPHP/Laravelで実装してみました。

現在の現場業務ではPHPやJavaを使ったバッチ開発やAPI開発などを行っていますが、自習の一環としてGWの自分への課題ということでサクッと簡易的な処理を実装してみることにしました。

今回題材として「QiitaAPI」と「LINEAPI」を選んだ理由ですが、発端はQiitaの検索がめんどくさい、検索した記事を履歴としてLINEのメッセージに残しておければ便利なんじゃないかなどの考えがふと思い浮かんだからです。

本題に入る前に、私自身まだエンジニアになって1年目ということで経験が浅く、間違っている箇所や穴があるかもしれませんがそこはご容赦くださいませ。

バッチ/API開発の概要

今回は手動でバッチを実行し、検索キーワードでヒットしたQiitaの上位10記事のタイトルと記事URLを取得して、LINEのメッセージで配信される処理を作成していきます。

GitHub:https://github.com/Hiro1120/QiitaApi

目的

・Qiitaサイトからの記事検索がめんどくさいのでバッチ実行で情報を取得したい

・LINEのメッセージとしてQiita記事のタイトルとURLの履歴を残したい

環境

MAMP環境

・PHP 7.4.21

・Laravel Framework 8.83.27

・MySQL 5.7.34

・VSCode

作成するファイル・テーブル

作成する主なファイルは以下の5つです。

・LINEブロードキャスト配信バッチ

・リクエスト再送バッチ

・コンデンス(削除)バッチ

・QiitaAPI呼び出し処理

・LINEAPI呼び出し処理

・リクエスト再送テーブル(request_resend_tbl)

・プロパティテーブル(property_tbl)

仕様

・バッチは時間経過ではなくartisanコマンドで手動実行する。

artisanコマンドの引数で指定したキーワードにヒットした最新記事10記事を取得する。

取得情報のうちLINEで配信するのは「記事タイトル」と「記事URL」の2つ

・Qiita情報の取得に失敗した場合はリクエスト再送TBLにリクエスト種別1で登録

・LINE送信に失敗した場合はリクエスト再送TBLにリクエスト種別2で登録

・リクエスト再送バッチでは、リクエスト種別を判定し、1ならQiita取得APIを呼び出す。2ならLINE送信APIを呼び出す

・コンデンス(削除)batchでは、再送回数1のリクエストを削除する

処理フロー図

実装前にイメージしやすいように簡単な処理フロー図をざっと作ってみました。

①LINEブロードキャスト配信batch

リクエスト再送batch

コンデンス(削除)batch

処理の実装

ここからは実際に完成したバッチ処理についてデモ映像と紹介をしていきます。

デモ映像

事前準備

まずは本体の処理を実装する前に準備していきます。

①コマンドの作成

ターミナルでLaravelプロジェクトのルートディレクトリに移動して、Artisanコマンドを使って新しいコマンドを作成します。

このコマンドを作成しておくことで、例えば

php artisan Condense

上記のようなコマンドでバッチが実行できるようになります。

今回は以下の3つのコマンドを作成しておきます。

php artisan make:command LineSendApi
php artisan make:command RequestResend
php artisan make:command Condense

 

②API接続準備

「LINE Messeging API」と「QiitaAPI」の使い方に関しては今回の記事では省略するためご自身で調べてみてください。参考にしたURLは以下の2つです。

LINE Messeging APIについて

QiitaAPIについて

処理内容

ここからはコードの処理内容についてさらっと紹介していきますが、混乱しないようにあらかじめこちらを頭に入れておくとイメージがつきやすいと思います。

登録・更新される
リクエストデータ

Qiita API LINE API
1回目 2回目(再送) 1回目
2回目(再送)
成功 失敗 成功 失敗 成功 失敗 成功 失敗
リクエストタイプ:1
再送回数:0
 –
リクエストタイプ:1
再送回数:1
– 
リクエストタイプ:2
再送回数:0
 –
リクエストタイプ:2
再送回数:1
– 

 

①LineSendApi

GitHub

App\Console\Commands配下にファイルを設置することで、artisanコマンドを使って指定したファイル名のファイルを呼び出すことができます。

/**
* The name and signature of the console command.
* php artisan LineSendApi "keyword"
* @var string
*/
protected $signature = 'LineSendApi {keyword}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Send "Qiita_information" to LINE';

$signatureで呼び出すコマンドを指定し、「{}」で引数を取得できるようにします。

例えば以下のようにするとLineSendApiを呼び出す時に「PHP」という文字列を引数で受け取ることができます。

php artisan LinSendApi "PHP"

$descriptionにはコマンドの説明を指定することができます。こちらに指定した説明は画像のようにコマンドを一覧表示させた時に確認できます。

php artisan list
artisanコマンドで呼び出されたファイルは、handle()メソッド内の処理が実行されていくことになります。

 

実際の処理はシンプルで、先ほどの引数で受け取った検索キーワードを変数に格納し、QiitaAPIの呼び出し処理とLINEAPIの呼び出し処理へとそれぞれ処理が移るようになっています。
このようにクラスを分けることでそれぞれの処理で修正を行っても他への影響を極力減らすことができるようになります。
最後に、バッチ処理が正常に終了すればコンソール上に緑色の文字で出力されるようにしています。

 

QiitaReceivedController

GitHub

このクラスでは実際にQiitaAPIに接続して情報を取得しています。

try文のcatchブロック内でif分岐させ、さらにその内側でtry文を記述してというコードになっています。

最初は可読性の部分から悩みましたが、正常レスポンスの時の処理はこっち、異常レスポンスの時の処理はこっち、再送処理の時はこっち・・・など明確に処理が分けやすかったので採用することにしました。

try{
            // curlセッションを実行
            $response = curl_exec($curl);
            //$response = false;
            // curlセッションをクローズ
            curl_close($curl);

            if ($response === false) {
                // curl_execが失敗した場合の例外処理
                throw new Exception("curl_exec failed" . PHP_EOL);
            }

            // レスポンスをJSONデコード
            $items = json_decode($response, true);
        
            if (json_last_error() !== JSON_ERROR_NONE) {
                // JSONデコードが失敗した場合の例外処理
                throw new Exception("json_decode failed" . PHP_EOL);
            }

            echo "\033[36m" .QIITA_RECEIVE_RESURT . PHP_EOL;

            if($id !== null){

                // DB接続
                $options = array(
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
                );
                $dsn = 'mysql:host=' . HOSTNAME. ';dbname=' . DATABASE . ';port=' . PORT . ';';
                $pdo = new PDO($dsn, USERNAME, PASSWORD, $options);

                // 再送回数を更新する
                $sql = config('sql.update_request_resend');
                $stmt = $pdo->prepare($sql);
                $stmt->bindValue(':value1', $id, PDO::PARAM_INT);
                $stmt->execute();

                echo "\033[36m" .UPDATE_SUCCESS . PHP_EOL;

            }

            return $items;

        }catch(Exception $e){

例えば上記のこのtry文では、初回のQiitaAPI接続は失敗しようが成功しようがtryブロックの中ではリクエスト再送テーブルにデータを登録・更新することはありません。

しかし、リクエスト再送バッチを実行した時の2回目のQiitaAPI接続の場合は、成功するとリクエスト再送テーブルに登録されている該当リクエストの通知再送回数を1に更新して次回以降のリクエスト再送の対象にならないようにする必要があります。

これは失敗した時も同様で、今回の使用では再送は1回しか行わず通知再送回数が1回のリクエストはコンデンス(削除)バッチの削除対象となるため、やはりどちらにせよデータの更新が必要になります。

次にcatchブロック内です。

}catch(Exception $e){

            // DB接続
            $options = array(
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
            );
            $dsn = 'mysql:host=' . HOSTNAME. ';dbname=' . DATABASE . ';port=' . PORT . ';';
            $pdo = new PDO($dsn, USERNAME, PASSWORD, $options);

            /**
            * Qiita情報初回取得失敗 $id === null 再送回数:0で登録
            * Qiita情報再取得失敗 $id === not null 再送回数:1で更新
            */
            if($id === null){

                try{
                    // 初回Qiita取得失敗メッセージ
                    echo "\033[31m" . QIITA_FALSE . $e->getMessage();

                    // テーブルにデータを挿入する
                    $sql = config('sql.insert_request_resend');
                    $stmt = $pdo->prepare($sql);
                    $stmt->bindValue(':value1', INSERT_QIITA_REQUEST_TYPE, PDO::PARAM_INT);
                    $stmt->bindValue(':value2', $keyword, PDO::PARAM_STR);
                    $stmt->bindValue(':value3', INSERT_QIITA_RESEND_TIME, PDO::PARAM_INT);
                    $stmt->execute();

                    echo "\033[36m" .INSERT_SUCCESS . PHP_EOL;

                    //処理終了
                    exit;

                }catch(Exception $e){

                    echo "\033[31m" . INSERT_FALSE . $e->getMessage() . PHP_EOL;
    
                    //処理終了
                    exit;
                }

            }else{

                try{
                    // Qiita情報再取得失敗メッセージ
                    echo "\033[31m" ."【再取得失敗】" . QIITA_FALSE . $e->getMessage();

                    // 再送回数を更新する
                    $sql = config('sql.update_request_resend');
                    $stmt = $pdo->prepare($sql);
                    $stmt->bindValue(':value1', $id, PDO::PARAM_INT);
                    $stmt->execute();

                    echo "\033[36m" .UPDATE_SUCCESS . PHP_EOL;

                    return;

                }catch(Exception $e){

                    echo "\033[31m" . UPDATE_FALSE . $e->getMessage() . PHP_EOL;
    
                    //処理終了
                    exit;
                }

            }

catchブロック内では正常にAPI通信ができない場合に処理されるため、このリクエストが初回のリクエストなのか2回目の再リクエストのものなのかをリクエストのidで判別し、if文で分岐した後それぞれでデータの更新を行っていきます。

初回のAPI通信に失敗した場合は、「リクエストタイプ:1(Qiita)」「リクエスト」「再送回数:0」をリクエスト再送テーブルに登録して、リクエスト再送バッチによって再送されるようにしておく必要があります。

一方で、2回目の通信、つまりリクエスト再送バッチによって再度リクエストされたAPI通信に失敗した場合は、一意に識別するidによってそのリクエストを「再送回数:1」で更新し、再送対象から外して、削除対象のリクエストにする必要があります。

LineSendController

GitHub

このクラスではLINEAPIに接続して、メッセージを配信します。

基本的な考え方は先ほどのQiitaReceivedControllerの処理と同じような形になりますが、このクラスではさらに「①初回LINE配信成功」「②初回LINE配信失敗→2回目のLINE配信」「③QiitaAPI失敗→2回目のQiitaAPI成功→初回LINE配信」の3パターンについて考える必要があります。

「初回LINE配信失敗→2回目のLINE配信」パターンでは、「リクエストタイプ:2(Qiita)」で「通知再送回数を0か1」で登録する必要があります。

「QiitaAPI失敗→2回目のQiitaAPI成功→初回LINE配信」パターンでは、2回目のQiitaAPI成功時にはリクエスト再送データが、「リクエストタイプ:1」「通知再送回数:1」で登録されたままで更新および削除はされません。これはコンデンスバッチでまとめて削除するようにしているためです。

ですので、このパターンでLINE配信に失敗した場合はリクエストタイプ・リクエスト・再送回数が更新されるのではなく、新たに「リクエストタイプ:2」のリクエスト再送データが登録される実装となっています。

また、パターン③ではLINE配信の成否によってパターン①かパターン②と同じ処理となるため、条件をつけてあげれば同じ箇所を通すことでうまく処理が繋がるようになります。

以下のコードだと、idがnullの時つまり初回のLINE配信とリクエストタイプが1の時つまりQiitaAPIの再リクエストが成功時(初回のLINE配信と同義)は同じ処理を通ります。

if($id === null || $request_type === QIITA_REQUEST_TYPE){

一方でesleつまり2回目のLINE配信の際はテーブルに登録されたリクエストを使用するため以下のように分岐させる必要があります。これはそれぞれの場合でメソッドに渡される引数に違いがあるからになります。(リクエスト再送バッチで説明します。)

}else{
  $params = $resend_params;           
}

 

RequestResend

GitHub

リクエスト再送バッチです。

// テーブルから再送回数が0のデータを取得する
$selectData = $pdo -> query(config('sql.select_request_resend'))->fetchAll();

foreach ($selectData as $requestData) {

     $id = $requestData["id"];
     $request = $requestData["request"];
     $request_type = $requestData["request_type"];

     /**
     * リクエストタイプ:1
     * Qiita API 再送
     * 
     * リクエストタイプ:2
     * LINE API 再送
     */
     if($request_type === QIITA_REQUEST_TYPE){

     // Qiita APIの呼び出し
     $qiita_controller = app()->make(QIITA_RECEIVED_CONTROLLER_PATH);
     $items = $qiita_controller->doProc($request, $id );

     if($items !== null){
         // LINE APIの呼び出し
         $line_controller = app()->make(LINE_SEND_CONTROLLER_PATH);
         $line_controller->doProc($items, null, $id ,$request_type);
     }

   }elseif($request_type === LINE_REQUEST_TYPE){

       // LINE APIの呼び出し
       $line_controller = app()->make(LINE_SEND_CONTROLLER_PATH);
       $line_controller->doProc(null, $request, $id, null);
   }

}

特に重要なのが再送対象のデータがどれかということです。

リクエスト再送テーブルに登録されているデータが、QiitaAPIが対象のリクエストデータであれば、「QiitaAPIで情報取得→LINEAPIでメッセージ送信」の処理が必要ですし、LINEAPIが対象のリクエストデータであれば「LINEAPIでメッセージ送信」のみで十分です。

LINEAPI呼び出しで見比べればわかりますが、doProc()メソッドの引数に違いがあるのはそれぞれの場合でどの段階でのAPIを実行しているのかの判別をするためです。

ifブロック内であればQiitaから取得した情報を使ってLINE配信するのに対し、elseifブロック内ではテーブルに登録されているリクエストを使用するという違いがあります。

⑤Condense

GitHub

このクラスでは再送回数が1で登録されているリクエストデータをまとめて削除します。

ここでは単純に再送回数が1のデータを取得した後、念の為、「再送上限回数≦再送回数」で判定し、今回であれば1以上の回数であれば削除するという処理になっています。

まとめ

説明が長々となってしまいましたが、実際の処理はでも動画を見ていただくのとソースを直接見ていただくのが早いかなと思います。

今回は初めてAPIを利用して自作バッチを作成してみました。

「作りたいものを想像して、実現するにはどうすれば良いのかを考えて、実装する」この流れはどの現場でも同じになると思うので、自主勉強としてはやってよかったなと思いました。

これからも気ままに勉強しながら知識をつけていきたいと思います。

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