Laravelで楽天APIのリクエスト制限を対策する

# はじめに
Laravelで楽天APIを使ったWEBアプリを開発していたところ、APIを提供している[楽天ウェブサービスのヘルプページ](https://webservice.faq.rakuten.net/hc/ja/articles/900001974383-%E5%90%84API%E3%81%AE%E5%88%A9%E7%94%A8%E5%88%B6%E9%99%90%E3%82%92%E6%95%99%E3%81%88%E3%81%A6%E3%81%8F%E3%81%A0%E3%81%95%E3%81%84-)でリクエスト制限があることを知りました。

>1つのapplication_idにつき、1秒に1回以下のリクエストとしてください。

アフィリエイトサイトとして高い成果が見込まれる場合は制限緩和ができるみたいですが、そうでない場合はAPIの使用が制限されています。
もし継続的に制限を超えたリクエストを行った場合はアプリIDが利用停止になる可能性があるみたいなので対策が必要だと考えました。

楽天APIの実装方法についての記事はいくつかありましたが、リクエスト制限まで触れているものは見かけなかったので、自分なりの対策を備忘録として残しておきます。

# 環境
PHP 7.4.27
Laravel 8.78.1

# 楽天APIの実装
### アプリIDを取得
楽天APIの利用にはアプリIDが必要になります。
[楽天ウェブサービス](https://developers.rakuten.com)でアプリを新規登録しアプリIDを発行します。
![アプリIDを取得.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1615860/c35a48a0-5425-9c66-3ded-e02ecad3b010.png)

### SDKをインストール
次にSDK(ソフトウェア開発キット)を導入します。
PHPで楽天APIを使うための機能がパッケージされているものがあるのでそちらを使用します。

https://github.com/rakuten-ws/rws-php-sdk

Laravelのプロジェクトディレクトリ直下の`composer.json`に下記を追加します。

```composer.json
"require": {
    （略）
    "rakuten-ws/rws-php-sdk": "1.*"
}
```

SDKをインストールするため下記のコマンドを実行します。

```
$ composer install
```

インストールできていれば`vendor`ディレクトリに`rakuten-ws`が追加されています。

### 環境設定
取得したアプリIDを`.env`ファイルに設定します。

```
RAKUTEN_APPLICATION_ID='アプリケーションID'
```

`config`で中継しておきます。

```php:config/services.php
'rakuten' => [
    'application_id' => env('RAKUTEN_APPLICATION_ID'),
],
```

### コントローラを作成
楽天検索の実行部分を実装します。
楽天ブックス書籍検索APIでの検索例です。
なお検索フォームや商品データ取得後の表示処理などについては割愛しています。

```php:SearchController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use RakutenRws_Client;

class SearchController extends Controller
{
    public function index(Request $request)
    {
        // RakutenRws_Clientインスタンスを作成
        $client = new RakutenRws_Client();

        // 定数化
        define("RAKUTEN_APPLICATION_ID", config('services.rakuten.application_id'));

        // アプリIDをセット
        $client->setApplicationId(RAKUTEN_APPLICATION_ID);

        // リクエストから検索キーワードを取得
        $inputs = array_filter($request->only(['title', 'author']));

        $items = [];
        if (count($inputs) > 0) { 
            // 楽天ブックス書籍検索を実行
            $response = $client->execute('BooksBookSearch', $inputs);

            if ($response->isOk()) {
                foreach($response as $item) {
                    // 取得したいパラメータでフィルタリングして保存
                    $items[] = Arr::only($item, [
                        'title',
                        'author',
                        'itemPrice',
                        'itemUrl',
                        'mediumImageUrl',
                    ]);
                }
            } else {
                echo 'Error:'.$response->getMessage();
            }
        }
        // 省略
    }
}
```

ここまでで楽天APIで商品データを取得してくることができました。
ただこのままでは冒頭で記述したようにリクエスト制限値を超えるリクエストが送られて来た場合に、アプリIDが利用停止になってしまう可能性があるため追加で対策していきます。

# リクエスト制限対策
どうやって対策しようかと調べていたところ、Laravel公式ドキュメントの[キャッシュ](https://readouble.com/laravel/8.x/ja/cache.html)のページにあるロック管理の機能がぴったりだったのでこれを使って実装しました。
リクエスト制限対策後のコードを先に記載しておきます。

```php:SearchController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use RakutenRws_Client;
use Illuminate\Contracts\Cache\LockTimeoutException; // 追加
use Illuminate\Support\Facades\Cache; // 追加

class SearchController extends Controller
{
    public function index(Request $request)
    {
        // 省略
        if (count($inputs) > 0) { 
            // ロックを1秒間取得
            $lock = Cache::lock('rakuten_api', 1);
            
            try {
                // ロック取得を最大5秒待つ
                $lock->block(5);

                $response = $client->execute('BooksBookSearch', $inputs);

                if ($response->isOk()) {
                    foreach($response as $item) {
                        $items[] = Arr::only($item, [
                            'title',
                            'author',
                            'itemPrice',
                            'itemUrl',
                            'mediumImageUrl',
                        ]);
                    }
                } else {
                    echo 'Error:'.$response->getMessage();
                }
            } catch (LockTimeoutException $e) {
                // ロック取得失敗時の処理
            }
        }
        // 省略
    }
}
```

追加した部分については以下の通りです。

```php
// ロックを1秒間取得
$lock = Cache::lock('rakuten_api', 1);
```

`Cache::lock`メソッドによって第一引数の名前のロックを第二引数で指定した秒数取得した状態になります。
ロックを取得している間は別のプロセスがロックを取得できなくなっています。

```php
try {
    // ロック取得を最大5秒待つ
    $lock->block(5);
    
    // ロック取得時の処理

} catch (LockTimeoutException $e) {
    // ロック取得失敗時の処理
}
```

`block`メソッドはロックを取得していない場合、処理を止めて指定の秒数ロックが解放されるのを待ちます。
ロックが解放されればロックを取得して処理を進めますが、時間内にロックが解放されなければ`LockTimeoutExecption`を投げます。

これで同時にリクエストがきた場合でも楽天検索の実行間隔が1秒以上離れるため、制限に引っかからなくなったのではないかと考えています。

# まとめ
Qiita初投稿で至らない点もあったとは思いますが最後まで読んでいただきありがとうございます。
今回はリクエスト制限に焦点を当てましたが、さらにAPIへの理解を深めるために楽天以外のものも使っていきたいと思いました。
間違いや改善点などがあればご教示いただけますと幸いです。