CodeIgniterメモ

### コントローラーやモデルを作る上での注意

クラス名は被らないようにすること（後々困ったりする　ネームスペースを使用できるようにすれば多少はましになるかも）
フォルダで分けた場合はプレフィックスをつける等

**loadまでは問題なかったがメソッドを呼び出したところでメモリ不足になった。**


同じ名前のクラスは作らない

### プロファイラ出力

```php
$this->output->enable_profiler(TRUE);
```

### JSON出力

http://codeigniter.jp/user_guide/3/libraries/output.html#CI_Output::set_content_type

output後も処理は続くので注意

* コントローラから使うとき

```php
$this->output
    ->set_content_type('application/json')
    ->set_status_header(200)
    ->set_output(json_encode([]));
return;
```

* コントローラから使うときまたは、コントローラから呼び出されるメソッドで使うとき

```php
$this->output
    ->set_content_type('application/json')
    ->set_status_header(200)
    ->set_output(json_encode([]))
    ->_display();
exit(0);
```

#### APIの時はJSONでCSRFのエラーを出すようにする

APIの時はJSONでCSRFのエラーを出すようにExceptionsを拡張しようとしたところ`$CI`が`null`になってしまう。

[get_instance return null](https://forum.codeigniter.com/archive/index.php?thread-63667.html)

ということで以下は動きません。

```php:application\core\MY_Exceptions.php
defined('BASEPATH') OR exit('No direct script access allowed');
require_once BASEPATH.'core/Controller.php';

class MY_Exceptions extends CI_Exceptions {

    protected $CI;

    public function __construct()
    {
        $this->CI =& get_instance();
        parent::__construct();
    }

    public function show_error($heading, $message, $template = 'error_general', $status_code = 500)
    {
        if ($this->CI->uri->segment(1) === 'api')
        {
            // APIの場合JSONで返す
            $this->CI->output
                ->set_content_type('application/json')
                ->set_status_header($status_code)
                ->set_output(json_encode([
                    'message' => $message,
                    'csrf_token_name' => $this->CI->security->get_csrf_token_name(),
                    'csrf_hash' => $this->CI->security->get_csrf_hash()
                ]))->_display();
            exit(0);
        }

        // 通常
        return parent::show_error($heading, $message, $template, $status_code);
	}

    public function show_exception($exception)
    {
        if ($this->CI->uri->segment(1) === 'api')
        {
            // APIの場合JSONで返す
            $this->CI->output
                ->set_content_type('application/json')
                ->set_status_header(500)
                ->set_output(json_encode([
                    'message' => $this->CI->exception->getMessage(),
                    'csrf_token_name' => $this->CI->security->get_csrf_token_name(),
                    'csrf_hash' => $this->CI->security->get_csrf_hash()
                ]))->_display();
            exit(0);
        }

        // 通常
        return parent::show_exception($exception);
    }

}
```

とりあえず個別で`new`してもここについては問題なさそうなのでしてみる。
ここでの注意は`CI_Security`は`new`するだけでは`hash`が更新されるだけでクッキーには登録されないので`csrf_set_cookie`も行う必要がある。それを行わないと判定はクッキーのものと`input`を比較しているので永遠に一致しない。

```php:application\core\MY_Exceptions.php
defined('BASEPATH') OR exit('No direct script access allowed');

class MY_Exceptions extends CI_Exceptions {

    public function show_error($heading, $message, $template = 'error_general', $status_code = 500)
    {
        $uri = new CI_URI();
        if ($uri->segment(1) === 'api')
        {
            // APIの場合JSONで返す
            $output = new CI_Output();
            $security = new CI_Security();
            $security->csrf_set_cookie();
            $output
                ->set_content_type('application/json')
                ->set_status_header($status_code)
                ->set_output(json_encode([
                    'message' => $message,
                    'csrf_token_name' => $security->get_csrf_token_name(),
                    'csrf_hash' => $security->get_csrf_hash()
                ]))->_display();
            exit(0);
        }

        // 通常
        return parent::show_error($heading, $message, $template, $status_code);
	}

    public function show_exception($exception)
    {
        $uri = new CI_URI();
        if ($uri->segment(1) === 'api')
        {
            // APIの場合JSONで返す
            $output = new CI_Output();
            $security = new CI_Security();
            $security->csrf_set_cookie();
            $output
                ->set_content_type('application/json')
                ->set_status_header(500)
                ->set_output(json_encode([
                    'message' => $exception->getMessage(),
                    'csrf_token_name' => $security->get_csrf_token_name(),
                    'csrf_hash' => $security->get_csrf_hash()
                ]))->_display();
            exit(0);
        }

        // 通常
        return parent::show_exception($exception);
    }

}
```

https://www.codeigniter.com/user_guide/general/core_classes.html

API判定はリクエストヘッダに独自ヘッダ（X-から始まる）を付けたりして判定するのでも良いかも
`X-Custom-Header: api`とか
`$_SERVER['HTTP_X_CUSTOM_HEADER']`で判定（`CI_Input`をnewすると応答が返ってこなくなったのでinputは使えない）(CIが使える箇所では`$this->input->get_request_header('X-Custom-Header')で)

### config値の取得

```php
$this->config->item('base_url');
```

http://codeigniter.jp/user_guide/3/libraries/config.html#id7

### リクエストメソッド

http://codeigniter.jp/user_guide/3/libraries/input.html#CI_Input::method

### リクエストパラメータ

http://codeigniter.jp/user_guide/3/libraries/input.html#CI_Input::post
http://codeigniter.jp/user_guide/3/libraries/input.html#CI_Input::get

アイテムが存在しない場合NULLを返す

#### XSS

入力を画面に出力する際は
[xss_clean](https://codeigniter.jp/user_guide/3/helpers/security_helper.html#xss_clean)では`script`タグは`[removed]`に置き換えたり有害と思われるものを削除したりHTMLエンティティ化を行ったりします。これで有害と思われなかったものは削除されたりHTMLエンティティ化をされないので **HTMLインジェクション** の対策は行われないので`htmlspecialchars`を通すこと。

また、XSSのあるSVGの画像には有効ではないので管理された画像を使用する必要がある。

```php
echo htmlspecialchars($this->input->get('html', true), ENT_QUOTES, $this->config->item('charset'));
```

**`get`や`post`や`get_cookie`の第二引数が`xss_clean`をするかどうかになっている**

**`xss_clean`のデフォルト値は`$config['global_xss_filtering']`に依存している**

### リクエストメソッド判定

[method](https://www.codeigniter.com/user_guide/libraries/input.html#CI_Input::method)

* POSTとPOST以外のページを同じコントローラのメソッドで実装する例

```php
$data = [];
$data['csrf_token_name'] = $this->security->get_csrf_token_name();
$data['csrf_hash'] = $this->security->get_csrf_hash();

if ($this->input->method() !== 'post')
{
    $this->smarty->view('index.html', $data);
    return;
}

// POST時の処理
```

### セグメント

http://codeigniter.jp/user_guide/3/libraries/uri.html#CI_URI::segment

### パス

* アプリケーション

```php
APPPATH . '../public/uploads/';
```

### バリデーション

http://codeigniter.jp/user_guide/3/libraries/form_validation.html

```php
$this->load->library('form_validation');
$this->form_validation->set_error_delimiters('', '');// デフォルトはpで囲われるけど無くしたい
$this->form_validation->set_rules(
    'password',
    'パスワード',
    'required|min_length[8]|regex_match[/^(?=.*?[A-Za-z])(?=.*?[0-9])(?=.*?[ -\/:-@\[-`\{-\~])/]',
    ['regex_match' => '半角英数記号を含めてください'],
);
```

#### getの場合

post以外のバリデーションは対象の配列をセットしたら良い
**`set_rules`の前に設定する必要があります**

```
$this->form_validation->set_data($this->input->get());
```

#### 配列バリデーション

```
$this->form_validation->set_rules('input_array[]', '配列', 'required');
```

#### 画面にエラー表示と入力復帰を行う

```php
const ERROR_KEY = 'error';
```

入力画面側コントローラ

```php
$cache_prefix = 'login_';
$field_list = [
    'id',
    'password',
];
$field_list[] = self::ERROR_KEY;
foreach ($field_list as $field) {
    $cache_key = $cache_prefix . $field . '_' . 'ユーザーのID等';
    $cache = $this->cache->memcached->get($cache_key);
    if ($cache !== false) {
        $this->cache->memcached->delete($cache_key);
        $data[$field] = $cache;
    }
}
```

POST受け取り側コントローラ

```php
$cache_prefix = 'login_';
$field_list = [
    'id',
    'password',
];

foreach ($this->input->post($field_list, true) as $field => $value) {
    if (isset($value)) {
        $this->cache->memcached->save($cache_prefix . $field . '_' . 'ユーザーのID等', $value);
    }
}

$error_key = $cache_prefix . self::ERROR_KEY . '_' . 'ユーザーのID等';
if ($this->form_validation->run() !== true) {
    // バリデーションエラー
    $this->cache->memcached->save($error_key, validation_errors());
    redirect('入力画面');
}

// 成功時削除
foreach ($this->input->post($field_list, true) as $field => $value) {
    if (isset($value)) {
        $this->cache->memcached->delete($cache_prefix . $field . 'ユーザーのID等');
    }
}
```

エラーを個別に判定する

```php
$error_list = $this->form_validation->error_array();
// $error_list[フィールド名']でエラーメッセージが入っている配列が取得可能
```

### CSRF対策

http://codeigniter.jp/user_guide/3/libraries/security.html#csrf

#### 特定のページでcsrf protectionを無効にする方法

ファイルダウンロードをPOSTで行った際にそのページのまま再度ダウンロードを行うとcsrf protectionのエラーになってしまうので何とかする。

以下をconfig.phpに追加する

```php
$config['csrf_exclude_uris'] = array(
    'download/execute',
);
```

### キャッシュドライバ

```php
$this->cache->memcached->get();
```

http://codeigniter.jp/user_guide/3/libraries/caching.html#CI_Cache::get

アイテムが存在しない場合、このメソッドは FALSEを返す。
NULLと間違えやすい。

### ログ

http://codeigniter.jp/user_guide/3/general/errors.html?highlight=log_message#log_message

### 404

http://codeigniter.jp/user_guide/3/general/errors.html?highlight=log_message#show_404

### エラーページに前のページに戻るリンクを追加する

views/errors/html/error_404.php
views/errors/html/error_general.php

の<div id="container">内に以下を追加

```php
<?php
if (isset($_SERVER['HTTP_REFERER']))
{
    echo '<p><a href="' . $_SERVER['HTTP_REFERER'] . '">back to previous page</a></p>';
}
?>
```

### status code

http://codeigniter.jp/user_guide/3/general/common_functions.html?highlight=set_status_header#set_status_header

### ダウンロード

http://codeigniter.jp/user_guide/3/helpers/download_helper.html

### ファイルアップロード

http://codeigniter.jp/user_guide/3/libraries/file_uploading.html

**formタグにenctype="multipart/form-data"は忘れずにつけてください。**

**input type fileは** $_POST **ではなく** $ _FILES **に格納されるため、以下では取得できません。**

```php
$this->input->post('file_field_name')
```

**複数ファイルには対応していない様子でした。**（アップロードするファイルが選択されていません。となる）

```php
$config['upload_path'] = APPPATH . 'uploads/';
$config['file_name'] = 'image';// 拡張子を省略すると元々の拡張子で保存してくれます。
$config['allowed_types'] = 'jpg|png';
$config['overwrite'] = true;

$this->load->library('upload', $config);
// アップロード
if ( ! $this->upload->do_upload('file_field_name'))
{
    throw new RuntimeException($this->upload->display_errors());
}
```

ファイル名をそのまま使う場合は```$config['file_name']```を省略します。
省略して後々取得したい場合は```$this->upload->data('file_name')```等を使って取得できます。

http://codeigniter.jp/user_guide/3/libraries/file_uploading.html#CI_Upload::data

### ディレクトリ・ファイルの削除

http://codeigniter.jp/user_guide/3/helpers/file_helper.html#delete_files

### メール

http://codeigniter.jp/user_guide/3/libraries/email.html

### Cookie

```php:config/config.php
$config['cookie_secure']	= FALSE;
$config['cookie_httponly'] 	= true;
```

**httponlyはJavaScriptからセッションIDを取得できないようにするのでtrueにしておくset_cookieの引数でも設定できるけど。**

http://codeigniter.jp/user_guide/3/helpers/cookie_helper.html

```php
$this->load->helper('cookie');
set_cookie('name', '名前', $expire_second);
get_cookie('name', true);
```

クッキーの値又は見つからない場合は NULL

1回のアクセス内でセットと同時に取得はできませんでした。
cookieヘルパーは`CI_Input::set_cookie() `のエイリアスということなので
`$this->input`に反映されないのかもしれません。

### DB

[クエリビルダクラスリファレンス](https://codeigniter.jp/user_guide/3/database/query_builder.html#id15)

##### 結果の取得の方法

http://codeigniter.jp/user_guide/3/database/results.html#CI_DB_result::result

```php
// array [object]
$result = $this->db
    ->where('name', $name)
    ->get($table_name)
    ->result();
```

```php
// object|null
$result = $this->db
    ->where('name', $name)
    ->get($table_name)
    ->row();
```

http://codeigniter.jp/user_guide/3/database/query_builder.html#CI_DB_query_builder::insert

```php
$this->db->insert($table_name, $set_array);
$result = $this->db->affected_rows();
if ($result === 1)
{
    $insert_id = $this->db->insert_id();
}
```

http://codeigniter.jp/user_guide/3/database/db_driver_reference.html?highlight=affected_rows#CI_DB_driver::affected_rows
http://codeigniter.jp/user_guide/3/database/helpers.html#id2

##### カウント

https://codeigniter.jp/user_guide/3/database/query_builder.html#id6

```php
$this->db->where('name', $name)->count_all_results($table_name);
```

SELECT count を使用しているわけではない
以下の方が良さそう。

```php
$result = $this->db
    ->select('count(*) as count')
    ->where('name', $name)
    ->get($table_name)
    ->row();
$count = $result->count;
```

##### トランザクション

```php
if ($this->db->trans_begin())
{
    try
    {
        $this->db->trans_commit();
    }
    catch (\Throwable $error)
    {
        $this->db->trans_rollback();
    }
}
```

##### SQLのエスケープ

* クエリービルダ

setやinsertの第三引数をデフォルトにしておけばエスケープされるので気にしなくてよい。

* プリペアドステートメント（CIではサポートされていない）のようなことをする

[クエリバインディング](https://codeigniter.jp/user_guide/3/database/queries.html#id8)を使うとエスケープは自動でやってくれる

```php
$sql = "SELECT * FROM some_table WHERE id IN ? AND status = ? AND author = ?";
$this->db->query($sql, [[3, 6], 'live', 'Rick']);
```

```:プリペアドステートメントのようなsqlを発行しないのでログがきれいになる
SELECT * FROM some_table WHERE id IN (3,6) AND status = 'live' AND author = 'Rick'
```

* SQLを直接書く

変数を用いる場合はエスケープすること

```php
$this->db->escape_str();
```

```php
$this->db->escape();
```

`escape()`はシングルクォートで囲まれてしまうので使い勝手が悪いです。

```php
$sql = 'SELECT * FROM `table_name` WHERE `name` = ' . $this->db->escape_str($name) . ';';
$query = $this->db->query($sql);
// object[]
$result = $query->result();
```

http://codeigniter.jp/user_guide/3/database/queries.html#id7


##### クエリを確認したい場合

```php
$this->db->last_query()
```

最後に実行されたクエリの文字列を返します


```php
$search_bigram = "あい いう";
$search_bigram = $this->db->escape_str($search_bigram);
// object[]
$result = $this->db
    ->where('match(`bigram_contents`) against(' . "'" . '+"' . $search_bigram . '" in boolean mode' . "'" . ')');
    ->get($table_name)
    ->result();

var_dump($this->db->last_query());
```

```sql
"SELECT * FROM `table_name` WHERE match(`bigram_contents`) against('+"あい いう" in boolean mode');"
```

##### クエリービルダーでの注意

`where_in`を使う際には第二引数の配列には必ず空配列以外を渡すこと。
`where_in`の前に配列のチェックする必要があります。
http://codeigniter.jp/user_guide/3/database/query_builder.html#CI_DB_query_builder::where_in

### バッチ実行

* Batchコントローラを作成し、testメソッドを作成

* testメソッドの最初には以下を追加してCLIからのみ実行を許可([CLI で実行する](http://codeigniter.jp/user_guide/3/general/cli.html))

```php
// ブラウザからのアクセスは禁止
if ( ! is_cli())
{
    show_404();
}
```

* 次のコマンドでバッチの実行が可能

```
CI_ENV=production php <application root path>/index.php batch test
```

[ENVIRONMENT 定数](http://codeigniter.jp/user_guide/3/general/environments.html#environment)

$_SERVER['CI_ENV']で環境を判定しているのでCI_ENVの設定が必要