【CodeIgniter】プロファイラを外部ファイル出力できるようにする

## はじめに

この記事は [CodeIgniter Advent Calendar 2017](https://qiita.com/advent-calendar/2017/code_igniter) 4日目です。
3日目の記事は[@NEKOGET](https://qiita.com/NEKOGET)さんの『[CodeIgniterの歴史](http://pneskin2.nekoget.com/press/?p=1789)』でした。

## ■ プロファイラって？

→ [アプリケーションのプロファイリング](http://codeigniter.jp/user_guide/3/general/profiling.html)

早い話が下の画像のように、よさげな情報を画面下部に出力するものです。

![pro.png](https://qiita-image-store.s3.amazonaws.com/0/112838/d5027e10-77f0-818f-82e4-35252eb150f5.png)

ですが画面下部に出力するとなると、ダイアログとかリダイレクトとか絡んできたとき、取り回しが悪くなります。

## ■ プロファイラを外部ファイル出力できるようにする

###１）constantsに定数を追記する

```php:/application/config/config.php
defined('PROFILER_DISABLE')         OR define('PROFILER_DISABLE', 0);
defined('PROFILER_OUTPUT_INTERNAL') OR define('PROFILER_OUTPUT_INTERNAL', 1);
defined('PROFILER_OUTPUT_EXTERNAL') OR define('PROFILER_OUTPUT_EXTERNAL', 2);
```
###２）configに設定項目を追記する

```php:/application/config/constants.php
/*
 * デバッグ用：プロファイラの出力モード切替
 * 
 * PROFILER_DISABLE         : 出力しない
 * PROFILER_OUTPUT_INTERNAL : 画面上に出力する
 * PROFILER_OUTPUT_EXTERNAL : 外部ファイル上に出力する
 */
$config['profiler_mode'] = PROFILER_OUTPUT_EXTERNAL;
$config['profiler_external_path'] = FCPATH . 'application/logs';
```
###３）CI_Controllerクラスを継承して__construct()を変更する
※プロファイラを出力したいコントローラは```extends MY_Controller```してください。

```php:/application/core/MY_Controller.php
<?php

defined('BASEPATH') OR exit('No direct script access allowed');

class MY_Controller extends CI_Controller {

  public function __construct()
  {
    parent::__construct();
    if ($this->config->item('profiler_mode') !== PROFILER_DISABLE) {
      $this->output->enable_profiler(true);
    }
  }

}
```

###４）CI_Profilerクラスを継承して表題のことができるようにあれこれする
※Codeigniterのバージョンによっては、CI_Profilerに記述されている関数のアクセス修飾子が `private` になっていたりするので注意してください。

```php:/application/libraries/MY_Profiler.php
<?php

defined('BASEPATH') OR exit('No direct script access allowed');

class MY_Profiler extends CI_Profiler {

  private $total_execution_time = 0;

  protected function _compile_benchmarks()
  {
    $output = parent::_compile_benchmarks();
    $matches = array();
    if (preg_match('/Total Execution Time.*(\d+\.\d+)/', trim(html_entity_decode(strip_tags($output))), $matches) === 1 && isset($matches[1])) {
      $this->total_execution_time = $matches[1];
    }
    return $output;
  }

// --------------------------------------------------------------------

  protected function _compile_memory_usage()
  {
    $this->CI->load->helper('number');
    $usage = memory_get_usage();
    $output = str_replace('</div></fieldset>', '', parent::_compile_memory_usage());
    $output .= ' (' . ($usage !== '' ? byte_format($usage) . ' / ' . ini_get('memory_limit') : $this->CI->lang->line('profiler_no_memory')) . ')'
            . '</div></fieldset>';
    return $output;
  }

// --------------------------------------------------------------------

  public function run()
  {
    $output = parent::run();
    if ($this->CI->config->item('profiler_mode') !== PROFILER_OUTPUT_EXTERNAL) {
      return $output;
    }
    $profiler_file_name = $this->generate_profiler_file_name();
    file_put_contents($profiler_file_name, $output);
  }

// --------------------------------------------------------------------

  private function generate_profiler_file_name()
  {
    $external_path = $this->CI->config->item('profiler_external_path');
    $controller_name = $this->CI->router->fetch_class();
    $now = $this->fetch_now('Ymd_His_u');
    $execution_time = ceil($this->total_execution_time * 1000);
    return "{$external_path}/{$controller_name}_{$now}({$execution_time}ms).html";
  }

// --------------------------------------------------------------------

  private function fetch_now($format)
  {
    return \DateTime::createFromFormat('U.u', sprintf('%6F', microtime(true)))
                    ->setTimezone(new \DateTimeZone(ini_get('date.timezone')))
                    ->format($format);
  }

}
```
※\_compile_memory_usage()は、外部ファイル出力には関係していないので、必要なければ省略してください。
省略しないと `MEMORY USAGE` 部分が `485,656 bytes (474.0 KB / 2048M)` みたく表示されるようになるので、個人的に捗ります。

###５）画面を再読み込みしてプロファイラが外部ファイル出力されているか確認する

```
# ll /path/to/application/logs/
total 18
-rwxrwxrwx. 1 root root   131 Dec  4 10:26 index.html
-rwxrwxrwx. 1 root root 17329 Dec  4 10:44 welcome_20171204_104444_758158(40ms).html
```

`welcome_20171204_104444_758158(40ms).html` をブラウザで開くと、プロファイラ部分を閲覧することができます。

※プロファイラの外部ファイル出力を有効にすると、恐ろしい勢いで容量を逼迫させるので注意してください。
※この記事の記述はCodeigniterのスタイルガイドに沿っていません。phpdocがないのもごめんなさい。。。

おわり！