家計簿アプリ「Zaim」のAPIをLaravelに組み込んでみよう

# はじめに

家計簿アプリ「Zaim」の入力履歴を
Laravel上からoAuth経由のAPIから取得して
自分用の日々のやりくり管理アプリに連携させてみました。

# 経緯
私は日々のお買い物やお金の管理などの管理に、
長らく「Zaim」というアプリを使ってきました。

レシートを撮影するだけで読み取ってレシート撮影のみでほぼ入力が終わります。
めんどくさがりな人でもきちんと家計簿がつけられるようになる素晴らしいアプリです。


また、今年「365日貯金」というものを始めましたので
これと別の自作アプリケーションに「いつ何、何円入れたか」を記録していました。

図にするとこんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2721358/38639e7b-b3e3-5d31-9b34-899167b12071.png)

つまるところ、Zaimと自作システムの両方に登録を行っていました。
となると、どうなるでしょう？

***そうです、面倒で入力を溜め込むことになります***（特に自作システム）

そこで、「この2つのどちらかに集約したいなァ!」と思い調べていると
Zaim大先生にはAPIがあるご様子でしたので、
Zaimに一本化させることにしました。

図にするとこんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2721358/1a3cbb75-c08f-1342-ede7-fab2491efcdb.png)


# 仕様（実現したいこと）
* PHP 8.2／Laravel 10で実装する
* 自作システムから**Zaim APIを呼び出して、Zaimの入力データを取得する**
  ※今回は同期のたびに都度、全レコードを削除して再登録を行う
* アクセストークンの取得は同期の際に自動で行い、`.env`の更新作業などは不要とする

# 実装
情報源が少なかったこともあり、けっこう苦戦しました。

[Zaim API](https://dev.zaim.net/)は[oAuth 1.0a](https://openid-foundation-japan.github.io/rfc5849.ja.html)に対応しているのですが、
少し前の方式ということもあり、PHPでの事例も古かったり、
Laravelでサクッと使えそうなライブラリの情報源がなかなか見つかりませんでした。

最終的に落ち着いたライブラリが以下の2つです。
（１本化できたりなど、他に良いやり方があるかもしれません）
* APIを実行して入力データを取得する
　`GuzzleHttp\Subscriber\Oauth\Oauth1`
* APIを経由してアクセストークンを発行する
　`https://github.com/thephpleague/oauth1-client`

## 何はともあれ、一旦APIを叩いてみましょう
[PostmanでZaim APIを使ってみる](https://jikoblog.netlify.app/zaim-api-with-postman/)の記事を参考にさせていただき、
Zaim APIのコンシュマーキーの発行からアクセストークンの発行まで行っておきます。

ここまでで発行したパラメータは後で使うので
`.env`ファイルに記載しておきます
```
ZAIM_API_CONSUMER_ID=
ZAIM_API_CONSUMER_SECRET=
ZAIM_API_OAUTH_ACCESS_TOKEN=
ZAIM_API_OAUTH_ACCESS_TOKEN_SECRET=
```

## APIを実行して入力データを取得
1. composerで「oauth-subscriber」をインストール
```composer require guzzlehttp/oauth-subscriber```

2. ライブラリのReadmeを参考に、下記のコードを実行します
※今回はサンプルコードのためコントローラに実装しました。

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Subscriber\Oauth\Oauth1;

class TestController extends Controller
{
    public function index   ()
    {
        $stack = HandlerStack::create();

        $middleware = new Oauth1([
            'consumer_key'              => env('ZAIM_API_CONSUMER_ID'),
            'consumer_secret'           => env('ZAIM_API_CONSUMER_SECRET'),
            'token'                     => env('ZAIM_API_OAUTH_ACCESS_TOKEN'),
            'token_secret'              => env('ZAIM_API_OAUTH_ACCESS_TOKEN_SECRET'),
        ]);
        $stack->push($middleware);

        $client = new Client([
            'base_uri' => 'https://api.zaim.net/v2/',
            'handler' => $stack,
            'auth' => 'oauth',
        ]);

        // 取得コードを実行
        $response = $client->get('home/money');

        return $response->getBody()->getContents();
    }
}

```

## APIを経由してアクセストークンを発行
先ほどの実装でアクセストークンがあれば叩くことができるようになりました。

では続いて、アクセストークンを発行できるようにしていきましょう。

認証時にコールバックが発生するので
コールバック先のURLを`.env`に追加しておきましょう。

```
ZAIM_CALLBACK_URL=
```

1. composerで「socialite」をインストール
※socialiteのインストール時に一緒についてくる`league/oauth1-client`を使用します。
```composer require laravel/socialite```

2. `League\OAuth1\Client\Server\Server`クラスを継承させたServerクラスを作成します
※一部のメソッドはオーバライドさせています。
```php
<?php

namespace App\Services\External\OAuth;

use League\OAuth1\Client\Credentials\TokenCredentials;
use League\OAuth1\Client\Server\Server;
use League\OAuth1\Client\Server\User;
use League\OAuth1\Client\Signature\HmacSha1Signature;

class TestServer extends Server
{
    public function __construct()
    {
        $clientCredentials = $this->createClientCredentials([
            'identifier'  => env('ZAIM_API_CONSUMER_ID'),
            'secret'      => env('ZAIM_API_CONSUMER_SECRET'),
            'callback_uri'=> env('ZAIM_CALLBACK_URL'),
        ]);
        $this->clientCredentials = $clientCredentials;
        $this->signature = new HmacSha1Signature($clientCredentials);
    }

    public function authorize($temporaryIdentifier)
    {
        return
            $this->getAuthorizationUrl($temporaryIdentifier);
    }

    public function getUserDetails(TokenCredentials $tokenCredentials, $force = false)
    {
        $data = $this->fetchUserDetails($tokenCredentials, $force);

        return $this->userDetails($data, $tokenCredentials);
    }

    public function getUserUid(TokenCredentials $tokenCredentials, $force = false)
    {
        $data = $this->fetchUserDetails($tokenCredentials, $force);

        return $this->userUid($data, $tokenCredentials);
    }

    public function getUserEmail(TokenCredentials $tokenCredentials, $force = false)
    {
        $data = $this->fetchUserDetails($tokenCredentials, $force);

        return $this->userEmail($data, $tokenCredentials);
    }

    public function getUserScreenName(TokenCredentials $tokenCredentials, $force = false)
    {
        $data = $this->fetchUserDetails($tokenCredentials, $force);

        return $this->userScreenName($data, $tokenCredentials);
    }

    public function urlTemporaryCredentials(){
        return "https://api.zaim.net/v2/auth/request";
    }

    public function urlAuthorization(){
        return 'https://auth.zaim.net/users/auth';
    }

    public function urlTokenCredentials(){
        return 'https://api.zaim.net/v2/auth/access';
    }

    public function urlUserDetails(){
        return 'https://api.zaim.net/v2/home/user/verify';
    }

    public function userDetails($data, TokenCredentials $tokenCredentials){
        return $user = new User();
    }

    public function userUid($data, TokenCredentials $tokenCredentials){
        return null;
    }

    public function userEmail($data, TokenCredentials $tokenCredentials){
        return null;
    }

    public function userScreenName($data, TokenCredentials $tokenCredentials){
        return null;
    }
}
```

3.ルーティングを追加しておきます
```php
Route::get('test/', 'App\Http\Controllers\TestController@index')->name('test.index');
Route::get('test/zaim', 'App\Http\Controllers\TestController@zaim')->name('test.zaim');
Route::get('test/callback', 'App\Http\Controllers\TestController@callback')->name('test.callback');
```

4.コントローラに連携時の処理、コールバック時の処理を以下のように実装します
```php
<?php

namespace App\Http\Controllers;

use App\Services\External\OAuth\ZaimServer;
use Illuminate\Http\Request;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Subscriber\Oauth\Oauth1;
use App\Services\External\OAuth\TestServer;
use Illuminate\Support\Facades\Session;

class TestController extends Controller
{
    public function index()
    {
        return view('test');
    }

    public function zaim(TestServer $server)
    {
        // リクエストトークンを発行
        $temporaryCredentials = $server->getTemporaryCredentials();
        // 認証URL
        $urlAuthorize = $server->authorize($temporaryCredentials);

        // セッションにリクエストトークンを保存
        Session::put('zaim_temporary_credentials', serialize($temporaryCredentials));

        // 認証URLにリダイレクト
        return redirect($urlAuthorize);
    }

    public function callback(Request $request, ZaimServer $zaimServer)
    {
        // セッションからリクエストトークンを取得
        $temporaryCredentials = unserialize(Session::pull('zaim_temporary_credentials'));

        // アクセストークンを発行する
        $tokenCredentials = $zaimServer->getTokenCredentials(
            $temporaryCredentials,
            $temporaryCredentials->getIdentifier(),
            $request->input('oauth_verifier')
        );

        $stack = HandlerStack::create();

        $middleware = new Oauth1([
            'consumer_key'              => env('ZAIM_API_CONSUMER_ID'),
            'consumer_secret'           => env('ZAIM_API_CONSUMER_SECRET'),
            'token'                     => $tokenCredentials->getIdentifier(),
            'token_secret'              => $tokenCredentials->getSecret(),
        ]);
        $stack->push($middleware);

        $client = new Client([
            'base_uri' => 'https://api.zaim.net/v2/',
            'handler' => $stack,
            'auth' => 'oauth',
        ]);

        $response = $client->get('home/money');

        $contents = json_decode($response->getBody()->getContents(), true);

        dd($contents);

        return $contents;
    }
}
```

これで組み込みは完成です。

# こんな感じになりました

![Sep-19-2023 14-13-47.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2721358/43d4de0f-afc7-e714-9576-fdf345198f82.gif)

# 結論
無事に叩くことができました！
Zaimユーザの方はAPIを使って楽しい家計簿ライフをお送りくださいね！

# おわりに
最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを募集しております。
詳しくは採用情報ページをご確認ください。
https://eda-inc.jp/recruit/
みなさまからのご応募をお待ちしております。