CodeIgniter CRUDサンプルコード及び認証

# 1.始めに
　受注した案件でCodeIgniterを使うことになりましたので勉強と備忘録を兼ねて記事を残すことにします。CodeIgniterはPHPフレームワークの一つです。詳しく知りたい方はググってください。ここではチュートリアルのコードを拡張してCRUDのサンプルコード書きましたので入門者の参考になればと思います。なお、composer又は公式サイトからファイルをダウンロードする形のいずれかのやり方でプロジェクトを作成済みとして話を進めます。

動作環境：
Windows11
xampp
CodeIgniter4

# 2.CRUD
　Webフレームワークを扱う方にとってはCRUDって身近な用語だと思います。Create, Read, Update, Deleteの略で、この基本形と認証周りさえ扱えればアプリのバックエンドの構築には困りません。
　早速CRUDのコードの方から書いていきます。DB設計とかはCodeIgniterのチュートリアルに合わせてますのでチュートリアルがまだの方は後述のサイトからチュートリアルをやってみてください。

```php:アプリ名\app\Models\NewsModel.php
<?php

namespace App\Models;

use CodeIgniter\Model;

class NewsModel extends Model
{
    protected $table = 'news';

    protected $allowedFields = ['title', 'slug', 'body'];

    public function getNews($slug = false)
    {
        if ($slug === false) {
            return $this->findAll();
        }

        return $this->where(['slug'=>$slug])->first();
    }
}
```

```php:アプリ名\app\Controllers\News.php
<?php
namespace App\Controllers;

use App\Models\NewsModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;

class News extends Controller
{
    // チュートリアルで扱う箇所

    // 一覧
    public function index()
    {
        $model = model(NewsModel::class);

        $data = [
            'news'  => $model->getNews(),
            'title' => 'News archive',
        ];
        // dd($data);
    
        echo view('templates/header', $data);
        echo view('news/overview', $data);
        echo view('templates/footer', $data);
    }

    // 詳細
    public function view($slug = null)
    {
        $model = model(NewsModel::class);
    
        $data['news'] = $model->getNews($slug);
    
        if (empty($data['news'])) {
            throw new PageNotFoundException('Cannot find the news item: ' . $slug);
        }
    
        $data['title'] = $data['news']['title'];
        // dd($data);
    
        echo view('templates/header', $data);
        echo view('news/view', $data);
        echo view('templates/footer', $data);
    }

    // 新規追加
    public function create()
    {
        $model = model(NewsModel::class);

        if ($this->request->getMethod() === 'post' && $this->validate([
            'title' => 'required|min_length[3]|max_length[255]',
            'body'  => 'required',
        ])) {
            $model->save([
                'title' => $this->request->getPost('title'),
                'slug'  => url_title($this->request->getPost('title'), '-', true),
                'body'  => $this->request->getPost('body'),
            ]);

            echo view('news/success');
        } else {
            echo view('templates/header', ['title' => 'Create a news item']);
            echo view('news/create');
            echo view('templates/footer');
        }
    }

    // ここからチュートリアルで扱わない箇所

    // 詳細表示
    public function show($id)
    {
        $model = model(NewsModel::class);

        $data['news'] = $model->find($id);

        $data['title'] = $data['news']['title'];

        echo view('templates/header', $data);
        echo view('news/detail', $data);
        echo view('templates/footer', $data);
    }

    // 編集、更新
    public function update($id)
    {
        $model = model(NewsModel::class);

        $data['news'] = $model->find($id);

        if ($this->request->getMethod() === 'post' && $this->validate([
            'title' => 'required|min_length[3]|max_length[255]',
            'body'  => 'required',
        ])) {
            $model->save([
                'id' => $id,
                'title' => $this->request->getPost('title'),
                'slug'  => url_title($this->request->getPost('title'), '-', true),
                'body'  => $this->request->getPost('body'),
            ]);

            echo view('news/success');
        } else {
            $data['title'] = $data['news']['title'];
            echo view('templates/header', ['title' => 'Update a news item'], $data);
            echo view('news/update', $data);
            echo view('templates/footer', $data);
        }
    }

    // 削除
    public function delete($id)
    {
        $model = model(NewsModel::class);

        $data['news'] = $model->find($id);

        if ($data['news'] != null){
            $model->delete($id);
        }

        echo view('news/delete');
    }
}
```

```php:アプリ名\app\Config\Routes.php
/*
 * --------------------------------------------------------------------
 * Route Definitions
 * --------------------------------------------------------------------
 */

// We get a performance increase by specifying the default
// route since we don't have to scan directories.
$routes->get('/', 'Home::index');

$routes->get('news/delete/(:num)', 'News::delete/$1');
$routes->match(['get', 'post'], 'news/update/(:num)', 'News::update/$1');
$routes->get('news/detail/(:num)', 'News::show/$1');
$routes->match(['get', 'post'], 'news/create', 'News::create');
$routes->get('news/(:segment)', 'News::view/$1');
$routes->get('news', 'News::index');
```

```php:アプリ名\app\Views\news\create.php
<h2><?= esc($title) ?></h2>

<?= session()->getFlashdata('error') ?>
<?= service('validation')->listErrors() ?>

<form action="/news/create" method="POST">
    <?= csrf_field() ?>

    <label for="title">Title</label>
    <input type="text" name="title"><br>

    <label for="body">Text</label>
    <textarea name="body" cols="45" rows="4"></textarea><br>

    <input type="submit" name="submit" value="Create news item">
</form>
```

```php:アプリ名\app\Config\Views\news\delete.php
News item deleted successfully.
```

```php:アプリ名\app\Config\Views\news\detail.php
<h2><?= esc($news['title']); ?></h2>
<p><?= esc($news['body']); ?></p>
<a href="/news/update/<?= esc($news['id']) ?>">編集</a>
<a href="/news/delete/<?= esc($news['id']) ?>">削除</a>
```

```php:アプリ名\app\Config\Views\news\overview.php
<h2><?= esc($title) ?></h2>

<?php if (!empty($news) && is_array($news)): ?>

    <?php foreach($news as $news_item): ?>

        <h3><?= esc($news_item['title']) ?></h3>

        <div class="main">
            <?= esc($news_item['body']) ?>
        </div>
        <p><a href="/news/<?= esc($news_item['slug'], 'url') ?>">View article</a></p>

        <a href="/news/detail/<?= esc($news_item['id']) ?>">詳細</a>

    <?php endforeach ?>

<?php else: ?>

    <h3>No News</h3>

    <p>Unable to find any news for you.</p>

<?php endif ?>
```

```php:アプリ名\app\Config\Views\news\success.php
News item created successfully.
```

```php:アプリ名\app\Config\Views\news\update.php
<h2><?= esc($title) ?></h2>

<?= session()->getFlashdata('error') ?>
<?= service('validation')->listErrors() ?>

<form action="/news/update/<?= esc($news['id']) ?>" method="POST">
    <?= csrf_field() ?>

    <label for="title">Title</label>
    <input type="text" name="title" value="<?= esc($news['title']); ?>"><br>

    <label for="body">Text</label>
    <textarea name="body" cols="45" rows="4"><?= esc($news['body']); ?></textarea><br>

    <input type="submit" name="submit" value="Update news item">
</form>
```

```php:アプリ名\app\Config\Views\news\view.php
<h2><?= esc($news['title']); ?></h2>
<p><?= esc($news['body']); ?></p>
```

```php:アプリ名\app\Config\Views\templates\footer.php
<em>&copy; 2021</em>
</body>
</html>
```

```php:アプリ名\app\Config\Views\templates\header.php
<!doctype html>
<html>
<head>
    <title>CodeIgniter Tutorial</title>
</head>
<body>

    <h1><?= esc($title); ?></h1>
```

# 3.Myth Authを用いた認証の実装
　学習に使用したCodeIgniter4ではMyth Authを使って認証機能の実装ができます。[こちらのサイト](https://zenn.dev/kenjis/articles/95f38b7e447155)を参考にするのが手っ取り早いですが、コードの追加箇所がよくわからず2時間くらい溶かしてしまいましたので本記事でも改めて取り上げようと思います。

・ComposerでMyth Authインストール
```terminal
composer require myth/auth
```

・メール設定
```php:アプリ名\app\Config\Email.php
    /**
     * @var string
     */
    public $fromEmail = 'samplemail@gmail.com'; // 12行目に自分のメアド追加

    /**
     * @var string
     */
    public $fromName = 'Admin User'; // 17行目のここも=から先を追加

    /**
     * @var string
     */
    public $recipients;
```

・バリデーション設定
```php:アプリ名\app\Config\Validation.php
    public $ruleSets = [
        Rules::class,
        FormatRules::class,
        FileRules::class,
        CreditCardRules::class,
        \Myth\Auth\Authentication\Passwords\ValidationRules::class, // ここに追加
    ];
```

・デバッグツールバー設定
```php:アプリ名\app\Config\Toolbar.php
class Toolbar extends BaseConfig
{
    /**
     * --------------------------------------------------------------------------
     * Toolbar Collectors
     * --------------------------------------------------------------------------
     *
     * List of toolbar collectors that will be called when Debug Toolbar
     * fires up and collects data from.
     *
     * @var string[]
     */
    public $collectors = [
        Timers::class,
        Database::class,
        Logs::class,
        Views::class,
        // \CodeIgniter\Debug\Toolbar\Collectors\Cache::class,
        Files::class,
        Routes::class,
        Events::class,
        \Myth\Auth\Collectors\Auth::class, // ここに追加
    ];
```

・コントローラフィルター設定(ここで躓きました)
```php:アプリ名\app\Config\Filters.php
class Filters extends BaseConfig
{
    /**
     * Configures aliases for Filter classes to
     * make reading things nicer and simpler.
     *
     * @var array
     */
    public $aliases = [
        'csrf'          => CSRF::class,
        'toolbar'       => DebugToolbar::class,
        'honeypot'      => Honeypot::class,
        'invalidchars'  => InvalidChars::class,
        'secureheaders' => SecureHeaders::class,
        'login'      => \Myth\Auth\Filters\LoginFilter::class, // 追加
        'role'       => \Myth\Auth\Filters\RoleFilter::class, // 追加
        'permission' => \Myth\Auth\Filters\PermissionFilter::class, // 追加
    ];
```

・認証設定
```php:アプリ名\app\Config\Filters.php.php
    /**
     * List of filter aliases that should run on any
     * before or after URI patterns.
     *
     * Example:
     * 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
     *
     * @var array
     */
    public $filters = [
        'login' => ['before' => ['news/create*']], // 追加(認証フィルターかけたい箇所をここで設定する)
    ];
```

・ルーティング
```php:アプリ名\app\Config\Routes.php
$routes->get('/', 'Home::index');

$routes->get('news/delete/(:num)', 'News::delete/$1');
$routes->match(['get', 'post'], 'news/update/(:num)', 'News::update/$1');
$routes->get('news/detail/(:num)', 'News::show/$1');
$routes->match(['get', 'post'], 'news/create', 'News::create');
$routes->get('news/(:segment)', 'News::view/$1');
$routes->get('news', 'News::index');
$routes->get('(:any)', 'Pages::view/$1', ['priority' => 1]); // 追加(又はチュートリアルのルーティングを修正する)
```

　ここから先は[参考サイト](https://zenn.dev/kenjis/articles/95f38b7e447155)に従って進めていきますと問題なく認証機能が実装できるはずです。

# 4.終わりに
　MVCがわかってればそれなりに使いこなせるフレームワークですし軽量なのはLaravelにない強みでしょうけれども基本的に英語を翻訳しないとアプリを作りこめないです。Laravelの方が圧倒的に扱いやすいです。それ以上にPythonが書きたい…。

## 参考サイト
・[CodeIgniter4入門 公式チュートリアル (1)静的ページの表示](https://zenn.dev/kenjis/articles/3d5e42f872ff18)
・[CodeIgniter4のCodeIgniter\Model (1)CRUDメソッド](https://zenn.dev/kenjis/articles/6fa2af22ba4a5c)
・[URI ルーティング](https://ci-trans-jp.gitlab.io/user_guide_4_jp/general/routing.html)
・[CodeIgniter4の認証ライブラリMyth Authを使う](https://zenn.dev/kenjis/articles/95f38b7e447155)