僕なりの Codeigniter4 備忘録

# publicディレクトリ内に直接ルーティングさせる

クライアント様のサーバに直接アプリケーションを設置する場合も多々あるため、ApacheWebサーバをそのまま用いてアクセスするようにする。

そのためmod_rewriteを用いて直接public内にルーティングするよう、下記 `.htaccess` を設置する。

```htaccess
# ci4/.htaccess

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule ^(.*)$ public/$1 [L]
</IfModule>

<FilesMatch "^\.">
    Require all denied
    Satisfy All
</FilesMatch>
```



# 設定

> 参考：
>
> https://qiita.com/katsuhidem/items/c7b2ec5a1d7871bcc620
>
> https://zenn.dev/naente/articles/ed9b3303a1575c#session



## index.php を削除する

```php
# app/Config/App.php

    /**
     * --------------------------------------------------------------------------
     * Index File
     * --------------------------------------------------------------------------
     *
     * Typically, this will be your `index.php` file, unless you've renamed it to
     * something else. If you have configured your web server to remove this file
     * from your site URIs, set this variable to an empty string.
     */
    // public string $indexPage = 'index.php'; 
    public string $indexPage = ''; 
```

## csrf フィルターを有効に

```diff
--- a/app/Config/Filters.php
+++ b/app/Config/Filters.php
@@ -34,7 +34,7 @@
     public $globals = [
         'before' => [
             // 'honeypot',
-            // 'csrf',
+            'csrf',
             // 'invalidchars',
         ],
         'after' => [
```

## より安全な Session ベースの保護に切り替え

```diff
--- a/app/Config/Security.php
+++ b/app/Config/Security.php
@@ -15,7 +15,7 @@
      *
      * @var string 'cookie' or 'session'
      */
-    public $csrfProtection = 'cookie';
+    public $csrfProtection = 'session';
 
     /**
      * --------------------------------------------------------------------------
```

```diff
@@ -26,7 +26,7 @@
      *
      * @var bool
      */
-    public $tokenRandomize = false;
+    public $tokenRandomize = true;

     /**
     * --------------------------------------------------------------------------
     * CSRF Token Name
     * --------------------------------------------------------------------------
     *
     * Token name for Cross Site Request Forgery protection.
     */
-    public string $tokenName = 'csrf_test_name';
+    public string $tokenName = 'csrf_token';
```

## session の設定

```php
# app/Config/Session.php

// Session cookie name
 public string $cookieName = 'ci4app_session';

// Session Expiration
 public int $expiration = 3600;
```

## .env と configの関係

*.env* での定義はconfigでの定義より優先される。

`dotenv()` 等を使う必要はない。

*.env*

```php
# <ファイル名>.<プロパティ名>
hoge.name = 'hogehoge'
```

*app/Config/Hoge.php*

```php
<?php

namespace Config;

use CodeIgniter\Config\BaseConfig;

class Hoge extends BaseConfig
{
    public string $name = 'fugafuga';
}

```



## baseUrl, siteUrl

```php
# app/Config/App.php
	/**
     * --------------------------------------------------------------------------
     * Base Site URL
     * --------------------------------------------------------------------------
     *
     * URL to your CodeIgniter root. Typically, this will be your base URL,
     * WITH a trailing slash:
     *
     * E.g., http://example.com/form
     */
    public string $baseURL = 'http://localhost:8080/my-app';

    /**
     * --------------------------------------------------------------------------
     * Root Site URL
     * --------------------------------------------------------------------------
     *
     * URL to your Site root. Typically, this will be your base URL,
     * WITH a trailing slash:
     *
     * E.g., http://example.com/
     */
    public string $siteURL = 'http://localhost:8080/';
```

```
# .env

app.baseURL = ''
app.siteURL = ''
```



## locale, tz

```diff
diff --git a/app/Config/App.php b/app/Config/App.php
index f330a85..845e609 100644
--- a/app/Config/App.php
+++ b/app/Config/App.php
@@ -95,7 +95,7 @@ class App extends BaseConfig
      * strings (like currency markers, numbers, etc), that your program
      * should run under for this request.
      */
-    public string $defaultLocale = 'en';
+    public string $defaultLocale = 'ja';

     /**
      * --------------------------------------------------------------------------
@@ -135,7 +135,7 @@ class App extends BaseConfig
      * @see https://www.php.net/manual/en/timezones.php for list of timezones
      *      supported by PHP.
      */
-    public string $appTimezone = 'UTC';
+    public string $appTimezone = 'Asia/Tokyo';

     /**
      * --------------------------------------------------------------------------
```



## 参照の方法

`BaseController` でプロパティに定義した上で使う。

```php
# app/Controllers/BaseController.php

    protected $appConfig;

    public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
    {
        parent::initController($request, $response, $logger);

        $this->appConfig = new \Config\App();

    }

# app/Controllers/Home.php

    public function index(): string
    {
        $this->appConfig->siteURL;
```

`config()`ヘルパーで直接使う。

```php
# app/Controllers/Home.php

	public function index(): string
    {
		config('App')->baseURL;
```



# Session

基本はSessionクラスを`BaseController`でインスタンス化して使い、必要に応じてsessionヘルパーを使う。



## BaseController でSessionクラスをインスタンス化する方法

Sessionクラスのインスタンスが全てのコントローラーで継承され、一貫性を持ってセッションを管理できる。

全てのコントローラでセッションを一貫して使う場合に適している。

セッションの設定や初期化を一か所で管理できるため、コードの重複を避けることができる。

フォームを作る時はこちらが適してるのでは？

```php
# app/Controllers/BaseController.php

 protected $session;

 public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
 {
     parent::initController($request, $response, $logger);

     $this->session = \Config\Services::session();
 }
```

## sessionヘルパーを使う方法

必要な時に`session()`関数を呼び出すことができる。

コントローラーやメソッドのみの特定の場所でのみセッション機能を使用したい場合に適している。

条件によってセッションの挙動を変更したい時に便利。

```php
helper('session');
session()->set('item', 'value');
$item = session()->get('item');
```

毎回のロードが手間だと感じたら、以下のように自動ロードができる。

ただし、Sessionクラスをインスタンス化する場合は自動ロードは冗長になるから注意。

```php
# app/Controllers/BaseController.php

protected $helpers = ['session'];
```



## flash

```php
    public function input(): string
    {
        // ...
        
        if ($this->session->has('error')) {
            $data['error']  = $this->session->get('error');
        }
        
        // ...

    }

	public function confirm(): RedirectResponse|string
    {
        if (!$this->session->has('posts')) {
            return redirect(route: 'input')->with(key: 'error', message: '不正なアクセスです。フォームから入力してください。');
        }
        
        //
```



# Mail

## 組み込みのEmailクラス

```php
# app/Config/Email.php
```

```php
# controller等

<?php

use Config\Services;

    public function sendEmail()
    {
        $email = Services::email();
		// $email = \Config\Services::email();


        $email->setFrom('your@example.com', 'Your Name');
        $email->setTo('someone@example.com');
        $email->setCC('another@another-example.com');
        $email->setBCC('them@their-example.com');
        $email->setSubject('Email Test');
        $email->setMessage('Testing the email class.');

        if ($email->send()) {
            echo "Email successfully sent";
        } else {
            $data = $email->printDebugger(['headers']);
            print_r($data);
        }
    }
```



# デプロイ

`.env`の編集

```htaccess
# system-admin/.htaccess

Options +SymLinksIfOwnerMatch

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule ^(.*)$ public/$1 [L]
</IfModule>

<FilesMatch "^\.">
    Require all denied
    Satisfy All
</FilesMatch>
```

```htaccess
# my-app/public/.htaccess

# Options +SymLinksIfOwnerMatch コメントアウトする
```



# pager

```php
# controller
		$pager = Services::pager();
        $pager->setPath('result/index');
        $page = (int)$pageNum;
        $perPage = 20;
        $facilities = array_slice($facilities, ($page - 1) * $perPage, $perPage);

        $data['pagerLinks'] = $pager->makeLinks($page, $perPage, $count, 'my_pagination', $segment = 3);
```

```php
# app/Config/Pager.php

public array $templates = [
    'default_full'   => 'CodeIgniter\Pager\Views\default_full',
    'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
    'default_head'   => 'CodeIgniter\Pager\Views\default_head',
    'my_pagination'  => 'App\Views\pagination',
];
```

```twig
# view.twig

{{ pagerLinks|raw }}
```

```php
# my_pagination.php

<?php $pager->setSurroundCount(1); ?>

<ul class="pager clearfix" id="search-pagenation">
    <?php if ($pager->hasPrevious()) : ?>
        <li class="prev">
            <a href="<?= $pager->getPrevious() ?>" rel="prev">&lt;</a>
        </li>
    <?php endif; ?>

    <?php foreach ($pager->links() as $i => $link): ?>
        <li <?= $link['active'] ? 'class="current"' : '' ?>>
            <a href="<?= $link['uri'] ?>">
                <?= $link['title'] ?>
            </a>
        </li>
    <?php endforeach ?>

    <?php if ($pager->hasNext()) : ?>
        <li class="next">
            <a href="<?= $pager->getNext() ?>" rel="next">&gt;</a>
        </li>
    <?php endif; ?>
</ul>
```



# image

```php
# controller

        // 画象を指定された幅にリサイズします。指定の幅より元となる画像の横幅が大きい場合のみ、縮小されます。
        try {
            $info = \Config\Services::image('gd')
                ->withFile(path: self::FOR_WRITEABLE_DIR . 'tmp/'. $randomName)
                ->getFile()
                ->getProperties(true);

            if ($info['width'] > 990) {
                $image = \Config\Services::image(handler: 'gd');
                $image->withFile(path: self::FOR_WRITEABLE_DIR . 'tmp/'. $randomName)
                    ->resize(width: 990, height: 990, maintainRatio: true, masterDim: 'width')
                    ->save(target: self::FOR_WRITEABLE_DIR . 'tmp/'. $randomName, quality: 90);
                log_message('info', 'Image: Resized Done. Uploaded image width was more than 990px.');
            }
            else {
                log_message('info', 'Image: Resized Unnecessary. Uploaded image width was less than 990px.');
            }
        }
        catch (ImageException $e) {
            log_message(level: 'error', message: $e->getMessage());
        }
```



# Form

ヘルパーに頼る方法と頼らない方法がある。

もともとは頼らない方法で実装していた。

が、頼る方がコード量も少なく済む。よきにsessionの管理を調整してくれる。

ヘルパーはコーディングを助けてくれるためのツール。ただ、使い勝手をしらない間は使わない方が良い。



## Form Helper

### 復元

`set_value(string $field, ?string $default, ?bool $html_escape = true)`

```php
<input type="text" name="quantity" value="<?= set_value('quantity', '0') ?>" size="50">
```



`set_select(string $field, ?string $value, ?string $default = false)`

```php
<select name="myselect">
    <option value="one" <?= set_select('myselect', 'one', true) ?>>One</option>
    <option value="two" <?= set_select('myselect', 'two') ?>>Two</option>
    <option value="three" <?= set_select('myselect', 'three') ?>>Three</option>
</select>
```



`set_checkbox(string $field, ?string $value, ?string $default = false)`

```php
<input type="checkbox" name="mycheck[]" value="1" <?= set_checkbox('mycheck', '1') ?>>
<input type="checkbox" name="mycheck[]" value="2" <?= set_checkbox('mycheck', '2') ?>>
```



`set_radio(string $field, ?string $value, ?string $default = false)`

```php
<input type="radio" name="myradio" value="1" <?= set_radio('myradio', '1', true) ?>>
<input type="radio" name="myradio" value="2" <?= set_radio('myradio', '2') ?>>
```



# Validation

## Validation Interface

controllerにおいて、

`$this->validator`も`$validation = \Config\Services::validation()`も `Validation\ValidationInterface`を実装している。故に同じ関数を使える。

ただ、`run()`関数においては少し挙動が異なるから注意。
`$this->validator->run($data, 'input')`はこの実行前にvalidationクラスをインスタンス化しないとエラーになる。
`$validation->run($data, 'input')`は内部的にvalidationクラスをインスタンス化しているため使える。

`$this->validateData()`関数等は内部的にValidationクラスをインスタンス化して、便利に使いやすいようにしているだけで、実質的にはValidationクラスを使っているだけ。

`$this->validatedata(data: $data, rule: $rule)`は第二引数にデフォルトではルールの配列を期待しているが、ここにはルールグループ名を渡しても大丈夫。

## Redirect and Validation Errors

PHPはリクエスト間でデータの共有をしない。例えば、前のリクエストに含まれていつpost値やクエリパラメータを次のリクエストに持ち越せない。つまり、バリデーション失敗時にリダイレクトすると思うが、そのバリデーションのリクエストと、リダイレクトするリクエストは別のリクエストであるため、バリデーション失敗のエラーメッセージ等を持ち越せないということになる。持ち越せないということは、Formヘルパーの`validation_errors()`関数等が使えない。

これを解決するために、`WithInput()`関数を使う。一時的にセッションにバリデーションエラーを追加してくれる。リロードで消えてしまうが。

```php
// In Controller.
if (! $this->validateData($data, $rules)) {
    return redirect()->back()->withInput();
}
```



## Working With Errors

基本は、 *system/Language/en/Validation.php* を使って表示している。

オーバライドするには、 *app/Language/en/Validation.php* をいじる。

他言語の場合は、 *app/Language/ja/Validation.php* のように対象言語ディレクトリを作っていじる。



## Creating Custom Rules

### Using Rule Classes

> 参考：
>
> https://codeigniter.com/user_guide/libraries/validation.html#creating-a-rule-class

```php
# app/Config/Validation.php

class Validation extends BaseConfig
{
    // ...

    public array $ruleSets = [
        Rules::class,
        FormatRules::class,
        FileRules::class,
        CreditCardRules::class,
    ];

    // ...
}
```

```php
class MyRules
{
    public function even($value, $params, $data): bool
    {
        return (int) $value % 2 === 0;
    }
    
    public function is_same($value, $params, $data): bool
    {
        // $post = $_POST;
		// $otherValue = $post['otherValue']
        $otherValue = $data[$param[0]];
        return $value === $otherValue;
    }
}
```

引数`$value`に検証する値を。返り値はbool。`$params`を使う時はルール呼び出し箇所に`[fieldName]`を追加する。そうすると`$data`に全てのpostされているデータが入ってくる。あとはkeyをfieldNameで指定してあげれば値が取れる。

```php
$validation->setRules([
    'foo' => 'required|max_length[19]|even|is_same[other]',
]);
```