CodeIgniter4のコアクラスを拡張して、エラーログをSlackに通知する

# CodeIgniter4のコアクラスの拡張はわかりにくい

[CodeIgniter3は簡単にログを拡張できた](https://qiita.com/bananacoffee/items/bd11b41c202e1217b1a0)が、CodeIgniter4のコアクラスの拡張はちょっとわかりにくいので、Slack通知を例に作成してみた。

勉強中のLaravelでやってみようかと思ったが、すでにあったので、CodeIgniter4で記述する。


## CodeIgniter4のコアクラスの拡張（基本編＋愚痴）

マニュアルはこれ
https://codeigniter4.github.io/userguide/extending/core_classes.html

ルーターの拡張を行っているが、**このままでは動かない**。なんでやねん。

四苦八苦した結果、以下のように書くと動作した

```app/Config/Services.php
public static function routes(bool $getShared = true) {
	if ($getShared) {
		return static::getSharedInstance('routes');
	}
	$locator = Services::locator();
	$moduleConfig = config('Modules');
	return new \App\Libraries\RouteCollection($locator, $moduleConfig);
}
```

つまり、**コンストラクタに設定を渡さなければならない** ということ。
もちろん、**利用する（拡張する）クラスごとにコンストラクタの内容が変わるので、systemディレクトリを徘徊することになる**。

これがマニュアルから非常にわかりづらい。
名前空間を使い慣れていればすぐに気づくのかもしれないが、CodeIgniter3から上がってくるユーザーには厳しいのではないか。

## ログクラスを拡張してみる

- ログは`Service::logger`を拡張すれば良い。
- 拡張ログクラス`Logger.php`は`Libraries`に置いた。
- POST先の`POST_URL_TO_SLACK`の作り方はこちらが詳しいので割愛  
[slackのIncoming webhookが新しくなっていたのでまとめてみた](https://qiita.com/kshibata101/items/0e13c420080a993c5d16)

```app/Config/Services.php
public static function logger($getShared = true) {
	if ($getShared) {
		return static::getSharedInstance('logger');
	}
	$config = config('Logger');
	return new \App\Libraries\Logger($config);
}
```

```app/Libraries/Logger.php
<?php
namespace App\Libraries;
use CodeIgniter\Log\Logger AS BaseLogger;

class Logger extends BaseLogger {
  public function log($level, $message, array $context = []): bool {
    // 継承元クラスでログの書き込み（成功するとtrueが返る）
    $ret = parent::log($level, $message, $context);
    if ($ret) {
      // Slackが落ちていたりした場合のためにTryにしている。
      try {
        // Slackに通知するためのメッセージ作成
        $request = \Config\Services::request();
        $router = \Config\Services::router();
        $data = [
          "text" => "New Paid Time Off request from Fred Enriquez",
          'blocks' => [
            [
              'type' => 'header',
              'text' => [
                'type' => 'plain_text',
                'text' => 'New Error'
              ]
            ],
            [
              'type' => 'section',
              'fields' => [
                [
                  "type" => "mrkdwn",
                  "text" => "*Class:*" . PHP_EOL . $level
                ],
                [
                  "type" => "mrkdwn",
                  "text" => "*Date:*" . PHP_EOL . date('Y-m-d H:i:s')
                ]
              ]
            ],
            [
              'type' => 'section',
              'fields' => [
                [
                  "type" => "mrkdwn",
                  "text" => "*Router:*" . PHP_EOL . $router->controllerName()
                ],
                [
                  "type" => "mrkdwn",
                  "text" => "*URL:*" . PHP_EOL . current_url()
                ]
              ]
            ],
            [
              'type' => 'section',
              'text' => [
                "type" => "mrkdwn",
                "text" => "*IP / UA:*" . PHP_EOL . $request->getIPAddress() . PHP_EOL . $request->getUserAgent()
              ]
            ],
            [
              'type' => 'section',
              'text' => [
                "type" => "mrkdwn",
                "text" => '```' . PHP_EOL . $message . PHP_EOL . '```'
              ]
            ],
          ],
        ];

        $header = [
          'Content-Type: application/json',
        ];

        // SlackにPOSTする
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, POST_URL_TO_SLACK);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST'); // post
        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); // jsonデータを送信
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header); // リクエストにヘッダーを含める
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HEADER, true);

        $response = curl_exec($curl);
        $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
        $header = substr($response, 0, $header_size);
        $body = substr($response, $header_size);
        $result = json_decode($body, true);
        curl_close($curl);
      } catch (Exception $ex) {
        
      }
    }
    return $ret;
  }
}
```

## 結果

![error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104999/c6208cb5-25c4-07e4-f8b4-9ce6856f40a0.png)

trace内容がうまく取れていない（恐らくcontextに入っている）が、メッセージが長くなること請け合いなので、ここまでわかれば解析はかなり楽になるとして妥協。

**↓↓ trace取れました ↓↓**


## trace対応版

やっぱりcontextにいたので対応してみた。
ただし、予想通り、メッセージが長くなってしまうので、対応しなくてもいいかもしれない。
これ以上のことをやろうと思うと、エラーメッセージをテキストファイルにしてアップロード・添付するしかない。

```app/Libraries/Logger.php
<?php
namespace App\Libraries;
use CodeIgniter\Log\Logger AS BaseLogger;

class Logger extends BaseLogger {
  public function log($level, $message, array $context = []): bool {
    // 継承元クラスでログの書き込み（成功するとtrueが返る）
    $ret = parent::log($level, $message, $context);
    if ($ret) {
      // Slackが落ちていたりした場合のためにTryにしている。
      try {
        // Slackに通知するためのメッセージ作成
        $request = \Config\Services::request();
        $router = \Config\Services::router();
        
        $traceMessage = $message;
        if (count($context) > 0) {
          foreach ($context as $key => $data) {
            $slackMessage = str_replace('{' . $key . '}', $data, $traceMessage);
          }
        }
        
        $data = [
          "text" => "New Paid Time Off request from Fred Enriquez",
          'blocks' => [
            [
              'type' => 'header',
              'text' => [
                'type' => 'plain_text',
                'text' => 'New Error'
              ]
            ],
            [
              'type' => 'section',
              'fields' => [
                [
                  "type" => "mrkdwn",
                  "text" => "*Class:*" . PHP_EOL . $level
                ],
                [
                  "type" => "mrkdwn",
                  "text" => "*Date:*" . PHP_EOL . date('Y-m-d H:i:s')
                ]
              ]
            ],
            [
              'type' => 'section',
              'fields' => [
                [
                  "type" => "mrkdwn",
                  "text" => "*Router:*" . PHP_EOL . $router->controllerName()
                ],
                [
                  "type" => "mrkdwn",
                  "text" => "*URL:*" . PHP_EOL . current_url()
                ]
              ]
            ],
            [
              'type' => 'section',
              'text' => [
                "type" => "mrkdwn",
                "text" => "*IP / UA:*" . PHP_EOL . $request->getIPAddress() . PHP_EOL . $request->getUserAgent()
              ]
            ],
            [
              'type' => 'section',
              'text' => [
                "type" => "mrkdwn",
                "text" => '```' . PHP_EOL . $traceMessage . PHP_EOL . '```'
              ]
            ],
          ],
        ];

        $header = [
          'Content-Type: application/json',
        ];

        // SlackにPOSTする
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, POST_URL_TO_SLACK);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST'); // post
        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); // jsonデータを送信
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header); // リクエストにヘッダーを含める
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HEADER, true);

        $response = curl_exec($curl);
        $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
        $header = substr($response, 0, $header_size);
        $body = substr($response, $header_size);
        $result = json_decode($body, true);
        curl_close($curl);
      } catch (Exception $ex) {
        
      }
    }
    return $ret;
  }
}
```

## 備考

- 案件によってはログイン情報やDebugbarの内容なんかを入れてもいいかもしれない。
- Slackへの通知部分はモデルかライブラリ化をしたほうが良い（エラー通知以外にも使えるので）  
（まぁ、手で書かずにComposer使えばいいんだけど）