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

「QiitaAPI」と「LINEAPI」を使ったバッチ処理をPHP/Laravelで実装した処理の紹介記事となります。

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

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

間違っている箇所や穴があるかもしれませんがそこはご容赦くださいませ。

<h1>目次</h1>

[1. 概要](#概要)
　- [目的](#目的)
　- [環境](#環境)
　- [作成するファイル・テーブル](#作成するファイル・テーブル)
　- [仕様](#仕様)
　- [処理フロー図](#処理フロー図)
[2. 処理の実装](#処理の実装)
　- [デモ映像](#デモ映像)
　- [事前準備](#事前準備)
　- [処理内容](#処理内容)

[3. まとめ](#まとめ)

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

&nbsp;
<strong>GitHub：<a href="https://github.com/Hiro1120/QiitaApi">https://github.com/Hiro1120/QiitaApi</a></strong>
<h3>目的</h3>


><div class="simple-box8">
>
><strong>・Qiitaサイトからの記事検索がめんどくさいのでバッチ実行で情報を取得したい</strong>
>
><strong>・LINEのメッセージとしてQiita記事のタイトルとURLの履歴を残したい</strong>
>
></div>
<h3>環境</h3>
<div class="simple-box8">

><strong>・<span style="color: #3366ff;">MAMP</span>環境</strong>
>
><strong>・PHP <span style="color: #3366ff;">7.4.21</span></strong>
>
><strong>・Laravel Framework <span style="color: #3366ff;">8.83.27</span></strong>
>
><strong>・MySQL <span style="color: #3366ff;">5.7.34</span></strong>
>
><strong>・VSCode</strong>

</div>
<h3>作成するファイル・テーブル</h3>
作成する主なファイルは以下の5つです。
<div class="simple-box8">

><span style="color: #3366ff;"><strong>・LINEブロードキャスト配信バッチ</strong></span>
>
><span style="color: #3366ff;"><strong>・リクエスト再送バッチ</strong></span>
>
><span style="color: #3366ff;"><strong>・コンデンス（削除）バッチ</strong></span>
>
><span style="color: #ff6600;"><strong>・QiitaAPI呼び出し処理</strong></span>
>
><span style="color: #ff6600;"><strong>・LINEAPI呼び出し処理</strong></span>
>
><span style="color: #993366;"><strong>・リクエスト再送テーブル（request_resend_tbl）</strong></span>
>
><span style="color: #993366;"><b>・プロパティテーブル（property_tbl）</b></span>

</div>
<h3>仕様</h3>
<div class="simple-box6">

><strong>・バッチは時間経過ではなくartisanコマンドで手動実行する。</strong>
>
><strong>・</strong><b>artisanコマンドの引数で指定したキーワードにヒットした最新記事10記事を取得する。</b>
>
><strong>・</strong><b>取得情報のうちLINEで配信するのは「記事タイトル」と「記事URL」の2つ</b>
>
><b>・Qiita情報の取得に失敗した場合はリクエスト再送TBLにリクエスト種別1で登録</b>
>
><b>・LINE送信に失敗した場合はリクエスト再送TBLにリクエスト種別2で登録</b>
>
><b>・リクエスト再送バッチでは、リクエスト種別を判定し、1ならQiita取得APIを呼び出す。2ならLINE送信APIを呼び出す</b>
>
><b>・コンデンス（削除）batchでは、再送回数1のリクエストを削除する</b>

</div>
<h3>処理フロー図</h3>
実装前にイメージしやすいように簡単な処理フロー図をざっと作ってみました。
<h4><strong>①LINEブロードキャスト配信batch</strong></h4>
<a href="https://hiro-hobbyblog.com/wp-content/uploads/2023/05/3f9f4d82f85b2d6618ca43ca1ed75321.png"><img src="https://hiro-hobbyblog.com/wp-content/uploads/2023/05/3f9f4d82f85b2d6618ca43ca1ed75321.png" alt="" width="2076" height="1462" class="aligncenter size-full wp-image-2272" /></a>
<h4><strong>②</strong>リクエスト再送batch</h4>
<a href="https://hiro-hobbyblog.com/wp-content/uploads/2023/05/6be572d1fdcfaef1faa6c56031d198ab.png"><img src="https://hiro-hobbyblog.com/wp-content/uploads/2023/05/6be572d1fdcfaef1faa6c56031d198ab.png" alt="" width="2086" height="1474" class="aligncenter size-full wp-image-2273" /></a>
<h4><strong>③コンデンス（削除）batch</strong></h4>
<a href="https://hiro-hobbyblog.com/wp-content/uploads/2023/05/8955101e47ff5dfc690b20340cb79891.png"><img src="https://hiro-hobbyblog.com/wp-content/uploads/2023/05/8955101e47ff5dfc690b20340cb79891.png" alt="" width="2074" height="1458" class="aligncenter size-full wp-image-2274" /></a>
<h2>処理の実装</h2>
ここからは実際に完成したバッチ処理についてデモ映像と紹介をしていきます。
<h3>デモ映像</h3>
画質は上げて見てください

<iframe width="560" height="315" src="https://www.youtube.com/embed/cYr9R-fcDF4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen"></iframe>
<h3>事前準備</h3>
まずは本体の処理を実装する前に準備していきます。
<h4><strong>①コマンドの作成</strong></h4>
<span>ターミナルでLaravelプロジェクトのルートディレクトリに移動して、Artisanコマンドを使って新しいコマンドを作成します。</span>

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

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

今回は以下の3つのコマンドを作成しておきます。
```
php artisan make:command LineSendApi
php artisan make:command RequestResend
php artisan make:command Condense
```

&nbsp;
<h4><strong>②API接続準備</strong></h4>
「LINE Messeging API」と「QiitaAPI」の使い方に関しては今回の記事では省略するためご自身で調べてみてください。参考にしたURLは以下の2つです。

><strong><a href="https://qiita.com/aikouchino/items/98cdf98900b900f83236">LINE Messeging API</a></strong>について
>
><strong><a href="https://qiita.com/api/v2/docs">QiitaAPI</a></strong>について
<h3>処理内容</h3>
ここからはコードの処理内容についてさらっと紹介していきますが、混乱しないようにあらかじめこちらを頭に入れておくとイメージがつきやすいと思います。


![スクリーンショット 2023-05-03 15.28.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2731537/73c353fc-81e7-5bab-8800-5d5b3dbf98b5.png)

&nbsp;
<h4><strong>①LineSendApi</strong></h4>
<strong><a href="https://github.com/Hiro1120/QiitaApi/blob/main/qiita_line_api/app/Console/Commands/LineSendApi.php">GitHub</a></strong>

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


```php
/**
* 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
```
<a href="https://hiro-hobbyblog.com/wp-content/uploads/2023/05/f6e11ee4cfb72fe3294a0d7ca677dd91.png"><img src="https://hiro-hobbyblog.com/wp-content/uploads/2023/05/f6e11ee4cfb72fe3294a0d7ca677dd91.png" alt="" width="1014" height="274" class="aligncenter size-full wp-image-2268" /></a>
<div>
<div><span>artisanコマンドで呼び出されたファイルは、</span><span>handle</span><span>()メソッド内の処理が実行されていくことになります。</span></div>
&nbsp;
<div><span><a href="https://hiro-hobbyblog.com/wp-content/uploads/2023/05/f5e014cf1ccaef9fc3d7628c9c2b1ffa.png"><img src="https://hiro-hobbyblog.com/wp-content/uploads/2023/05/f5e014cf1ccaef9fc3d7628c9c2b1ffa.png" alt="" width="858" height="352" class="aligncenter size-full wp-image-2269" /></a></span></div>
<div>
<div>実際の処理はシンプルで、先ほどの引数で受け取った検索キーワードを変数に格納し、QiitaAPIの呼び出し処理とLINEAPIの呼び出し処理へとそれぞれ処理が移るようになっています。</div>
<div></div>
<div>このようにクラスを分けることでそれぞれの処理で修正を行っても他への影響を極力減らすことができるようになります。</div>
<div></div>
<div>最後に、バッチ処理が正常に終了すればコンソール上に緑色の文字で出力されるようにしています。</div>
&nbsp;
<h4><strong>②<span style="font-size: 16px;">QiitaReceivedController</span></strong></h4>
<strong><a href="https://github.com/Hiro1120/QiitaApi/blob/main/qiita_line_api/app/Http/Controllers/QiitaReceivedController.php">GitHub</a></strong>

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

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

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

```php
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ブロック内です。

```php
}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」で更新し、再送対象から外して、削除対象のリクエストにする必要があります。
<h4><strong><span>③</span></strong><strong style="font-size: 16px;">LineSendController</strong></h4>
<strong><a href="https://github.com/Hiro1120/QiitaApi/blob/main/qiita_line_api/app/Http/Controllers/LineSendController.php">GitHub</a></strong>

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

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

<span style="font-size: 16px;"><strong>「初回LINE配信失敗→2回目のLINE配信」パターン</strong>では、「リクエストタイプ：2（LINE）」で「通知再送回数を0か1」で登録する必要があります。</span>

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

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

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

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

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

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

```php
}else{
  $params = $resend_params;           
}
```
&nbsp;

</div>
<div>
<h4><strong><span>④</span></strong><strong style="font-size: 16px;">RequestResend</strong></h4>
<strong><a href="https://github.com/Hiro1120/QiitaApi/blob/main/qiita_line_api/app/Console/Commands/RequestResend.php">GitHub</a></strong>

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

```php
// テーブルから再送回数が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ブロック内ではテーブルに登録されているリクエストを使用するという違いがあります。
<h4><strong><span>⑤Condense</span></strong></h4>
<strong><a href="https://github.com/Hiro1120/QiitaApi/blob/main/qiita_line_api/app/Console/Commands/Condense.php">GitHub</a></strong>

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

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

</div>
<h2>まとめ</h2>
今回は初めてAPIを利用して自作バッチを作成してみました。

&nbsp;
説明が長々となってしまいましたが、実際の処理はデモ動画を見ていただくのとソースを直接見ていただくのが早いかなと思います。
&nbsp;
「作りたいものを想像して、実現するにはどうすれば良いのかを考えて、実装する」この流れはどの現場でも同じになると思うので、これからも気ままに勉強しながら知識をつけていきたいと思います。

</div>