Laravelアプリ同士をWeb APIで連携させるには

オンプレで動く Laravel と、クラウドで動く Laravel を、Web API でシステム間連携させてみた話。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/990023f8-3c07-27a6-f225-7ed013663ad5.png)

# やりたいこと
社内オンプレの給与明細APサーバにアクセスすると、クラウドの勤怠打刻APサーバのデータも取得し、給与明細と合わせて勤怠実績明細も表示できるようにする。
シーケンス図で書くと次のようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/807f5916-ea81-721f-c6df-ababbd270e2c.png)
勤怠実績明細の表示例。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/7727c658-c60d-1623-0f9b-86776c53f03e.png)

# Web API の設計
設計時に決めておくべき項目。

- **HTTPメソッド**
GET

- **エンドポイント**
`/api/v1/kintai/{yyyyMM}/{勤怠個人番号}`

- **機能概要**
従業員の勤怠実績データを返す

- **リクエスト**
    - `yyyyMM` ・・・ 年月（"last" と指定された場合は最終年月とする）
    - `勤怠個人番号` ・・・ 従業員の勤怠個人番号（Active Directory に照会すること）

- **レスポンス**
    - ステータスコード
        - `200` ・・・ 成功
        - `404` ・・・ リクエストされた勤怠実績が存在しなかった
    - 応答形式 ・・・ JSON

REST APIは**ステートレス**なので、認証にCookieやセッションは使えない。APIでは**トークン認証**や**BASIC認証**が一般的だが、今回は社内限定利用ということもあり、簡素化のため認証は省略する。

## (参考) クラウド人事労務パッケージのAPI
- 人事労務freee API ～ https://developer.freee.co.jp/docs/hr
- SmartHR API ～ https://developer.smarthr.jp/api/index.html

# Laravelでの実装方法
## 勤怠打刻APサーバ
APIが呼ばれる側のサーバ。
`api.php`にエンドポイントを定義する。`web.php`とは違い`api.php`にはCSRF保護が無い。

```php:routes\api.php
Route::get('v1/kintai/{yyyymm}/{id}', 'Api\KintaiController@index');
```

呼ばれるコントローラを作成する。


```php:app\Http\Controllers\Api\KintaiController.php
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Models\Result;
use Carbon\Carbon;
use App\Http\Controllers\Controller;

/*
  給与明細サーバから呼ばれるクラス
*/
class KintaiController extends Controller
{
    public function index(Request $request, $yyyymm, $syaincd)
    {
        if ($yyyymm == 'last') {  // last指定時は最終年月を仮定
            $yyyymm = Carbon::yesterday()->format('Ym');
        }
        $baseDate = Carbon::create(substr($yyyymm, 0, 4), substr($yyyymm, 4, 2), 1);  // 基準日の設定

        // 勤怠実績データベースの読み込み
        $query = Result::where('syaincd', $syaincd)
            ->whereBetween('kintaijissekiymd', array($baseDate->copy()->startOfMonth()->format('Ymd'), $baseDate->copy()->endOfMonth()->format('Ymd')))
            ->orderBy('kintaijissekiymd');

        // レコード件数が0より大きければデータを返し、そうでなければエラーレスポンスとする
        if ($query->count() > 0) {
            return response()->json([
                'code'     => 200,
                'contents' => $query->get()
              ], 200);
        } else {
            return response()->json([
                'code'     => 404,
                'message'  => 'Not Found'
              ], 404);
        }
    }
}
```

Eloquentモデルでは、文字列にキャストされるときJSONに変換されるため、Eloquentオブジェクトをそのまま返しても良い。


## コマンドからAPIをテストする
`curl`コマンドで得られるJSONデータを`jq`コマンドで整形して確認。

```sh:シェルからコマンドを実行
curl -s https://example.com/api/v1/kintai/202006/12765 | jq
```

```python:Pythonの場合
import requests
res = requests.get('https://example.com/api/v1/kintai/202006/12765')
print(res.json())
```

```json:出力結果
{
  "code": 200,
  "contents": [
    {
      "syaincd": 12765,
      "syainmei": "山田 太郎",
      "kanrisyouninkengencode": "一般社員",
      "kintaijissekiymd": "20200625",
      "syugyozikantaikaishihhmi": "0850",
      "syugyozikantaisyuryohhmi": "1730",
      "meisyo": "出勤",
      "barcodesyukkinhhmi": "0842",
      "barcodetaikinhhmi": "1738",
      :
```

## 給与明細APサーバ
APIを呼ぶ側のサーバ。
昨今のWebアプリでは、APIで取得したデータはフロントエンドの Vue や React で表示することが多いが、ここではLaravelで受けてビュー（Blade）に渡す。

```php:app\Http\Controllers\KintaiController.php（一部抜粋）
$context = stream_context_create(array(
  'http' => array('ignore_errors' => true)
));
$kintaicd = $request->session()->get('user')->kintaicd;
// APIを呼び出す
$json = file_get_contents("https://example.com/api/v1/kintai/$yyyymm/$kintaicd", false, $context);
$ary = json_decode($json);  // JSONを配列に

// 正常なら "contents" をViewに渡す
if ($ary->code == 200) {
    $reports = $ary->contents;
} else {
    $reports = null;
}

return view('kintai', compact('reports'))
   ->with('spec_type', 'kintai')
   ->with('ym', substr($yyyymm, 0, 4) . '年' . ltrim(substr($yyyymm, 4, 2), '0') . '月')
   ->with('prev_month', $dt->copy()->subMonth(1)->format('Ym'))
   ->with('next_month', $dt->copy()->addMonth(1)->format('Ym'));
```

ビュー（Blade）で $reports を展開する。

```php:resources\views\kintai.blade.php
<div class="table-responsive">
    <table class="table text-nowrap table-bordered table-striped table-hover table-condensed">
      <thead>
      <tr>
        <td rowspan="2">日付</td>
        <td rowspan="2">勤怠種別</td>
        <td colspan="2">打刻</td>
        ： 省略
      </tr>
      <tr>
        <td>出勤</td>
        <td>退勤</td>
        <td>就業開始</td>
        ： 省略
      </tr>
      </thead>
      <tbody>
      @foreach ($reports as $report)
      <tr>
          <td>{{ $report->kintaijissekiymd }}</td>
          <td>{{ $report->syukkinhhmi }}</td>
          <td>{{ $report->taikinhhmi }}</td>
          ： 省略
      </tr>
      @endforeach
      </tbody>
    </table>
</div>
```