codeigniter4をWindowsローカルで動かす（PHPCSとPHPUnitの設定もあるよ）

# はじめに
仕事場がCodeigniter3（On Docker）なんだけど、Dockerで動かそうとしてあまりに失敗するんで、Djangoみたいにローカルバッテリーサーバで動かせないの！？って思ってたらあったわ...

https://codeigniter4.github.io/userguide/installation/installing_composer.html

# php.ini
```diff_php
// 763行目
- ;extension_dir = "ext"
+ extension_dir = "C:/php/ext"

// 915行目
- ;extension=curl // プロジェクト作成時にcomposerが動かない
+ eextension=curl
- ;extension=fileinfo
- ;extension=gd
+ extension=fileinfo
+ extension=gd
- ;extension=intl // codeigniterの動作要件
+ extension=intl
- ;extension=mbstring // codeigniterの動作要件
+ extension=mbstring
- ;extension=mysqli // MySQLが動かない
+ extension=mysqli
- ;extension=openssl // プロジェクト作成時にcomposerが動かない
+ extension=openssl


// 966行目
- ;date.timezone = // 任意
+ date.timezone = "Asia/Tokyo"
```

# ci4というプロジェクトを作成
```powershell:ci4というプロジェクト名です
dev> composer create-project codeigniter4/appstarter ci4
  Creating a "codeigniter4/appstarter" project at "./ci4"
  Warning from https://repo.packagist.org: Support for Composer 1 is deprecated and some packages will not be available. You
  should upgrade to Composer 2. See https://blog.packagist.com/deprecating-composer-1-support/
  Installing codeigniter4/appstarter (v4.1.4)
  :
```

# .env 変更
envファイルをコピーして設定

- デバッグモードへ
- db接続設定

```powershell
dev\> cd ci4
dev\ci4> cp env .env
```
```diff_php:.env
// 17行目
- # CI_ENVIRONMENT = production
+ CI_ENVIRONMENT = development

// 40行目
- # database.default.hostname = localhost
- # database.default.database = ci4
- # database.default.username = root
- # database.default.password = root
- # database.default.DBDriver = MySQLi
+ database.default.hostname = localhost
+ database.default.database = ci4db
+ database.default.username = root
+ database.default.password = your_cool_password
+ database.default.DBDriver = MySQLi

- # database.tests.hostname = localhost
- # database.tests.database = ci4
- # database.tests.username = root
- # database.tests.password = root
- # database.tests.DBDriver = MySQLi
- # database.tests.DBPrefix = 
+ database.tests.hostname = localhost
+ database.tests.database = ci4db
+ database.tests.username = root
+ database.tests.password = your_cool_password
+ database.tests.DBDriver = MySQLi
+ database.tests.DBPrefix = test_
```

# データベースを手で作っておく
`ci4db` で作成。テーブルはひとつも無い状態です

```powershell:※これでもいいよ（utf8mb4の指定の仕方はわかんないけど）
dev\ci4> php spark db:create ci4db
```

# サーバーを動かす
プロジェクトルートフォルダにある `spark` ってとこに `cd` してないとコマンド通らないよ。djangoでいう `manage.py`

```powershell
dev\ci4> php spark serve
  CodeIgniter v4.1.4 Command Line Tool - Server Time: 2021-09-18 19:41:09 UTC-05:00
  CodeIgniter development server started on http://localhost:8080
  :
```

# すぐにテストをつくる
## PHPUnitのインストール
```diff_php:composer.json
    "require-dev": {
        "fakerphp/faker": "^1.9",
        "mikey179/vfsstream": "^1.6",
+       "phpunit/phpunit": "^9.1"
    },
```

## testsフォルダにある _support フォルダについて

https://codeigniter.com/user_guide/concepts/structure.html#tests

>このディレクトリは、テストファイルを保持するように設定されています。このディレクトリには、テストの作成中に使用できるさまざまなモッククラスやその他のユーティリティが含まれています。このディレクトリを本番サーバーに転送する必要はありません。_support

## 削除するファイル
この2つのファイルを削除しないと、PHPUnitを動かした瞬間にサンプルテーブルが作成される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/08aa75ed-b6b7-4f39-2eb5-32a2fec468dd.png)

実は上記でdb接続情報やdb作成を先に済ませたのもPHPUnitがdb使用しないのになぜか読みたがってエラーが出るから

## phpunit.xmlを設定
なくても実は動くが、いわゆるphpunitの設定ファイルなので有効化しといて損はない

```powershell
dev\ci4> cp phpunit.xml.dist phpunit.xml
```

- autoload.php
- phpunit.xml

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/ca15c86f-45f4-1efb-0c04-18b266644cee.png)

## テストするためのフォルダを作成
```powershell
dev\ci4> mkdir tests/controller
```

## Homeコントローラからテストを作る
`Home` というコントローラ（app/Controllers/Home.php）はテンプレとして作成される。

https://codeigniter4.github.io/userguide/testing/feature.html

テストの作成はphpStormの機能を使う

https://pleiades.io/help/phpstorm/using-phpunit-framework.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/94c6d6da-5c9c-d89a-f0fc-217f0c4d4098.png)
下図の `Directory:` と `Namespace:` を、作成したフォルダの場所に書き換えた
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/ad4b2214-e506-ad65-1ff1-5612f9b38df8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/5ca5c140-1a88-04b0-e1e3-050a92e2ee23.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/1b1c05ce-193f-b53d-d2f8-80b34b23a103.png)

`extends TestCase` は `extends CIUnitTestCase` に変更すること

```php:tests/controller/HomeTest.php
<?php

namespace Tests\Controller;

use CodeIgniter\Router\Exceptions\RedirectException;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
use Exception;

class HomeTest extends CIUnitTestCase
{
    use DatabaseTestTrait;
    use FeatureTestTrait;
    
    /**
     * @throws RedirectException
     * @throws Exception
     */
    public function testIndex()
    {
        // Get a simple page
        $result = $this->call('get', '/');
        $result->assertOK();
        
        // $result = $this->call('get', '/Home/store');
        // $result->assertOK();
    }
    
}
```

## 確認
安心感が違うわぁ。これがTDD
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/d2dbf3fd-c446-6ab4-38f0-22ccd174862a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/2c2f3713-a3c8-ee3d-3675-b5d9d7782269.png)

## ※テスト実行するとこんなんが出る
```console
C:\php\php.exe D:/OneDrive/dev/ci4/vendor/phpunit/phpunit/phpunit --configuration D:\OneDrive\dev\ci4\phpunit.xml --filter "/(database\\Models\\Domain\\Logic\\School\\Demo\\HighSchool\\Csv\\GradesEntityTest::testCreateEntity)( .*)?$/" --test-suffix GradesEntityTest.php D:\OneDrive\dev\ci4\tests\database\Models\Domain\Logic\School\Demo\HighSchool\Csv --teamcity
Testing started at 10:17 ...
PHP Warning:  require(./app/Config/Paths.php): Failed to open stream: No such file or directory in D:\OneDrive\dev\ci4\vendor\codeigniter4\framework\system\Test\bootstrap.php on line 40

Warning: require(./app/Config/Paths.php): Failed to open stream: No such file or directory in D:\OneDrive\dev\ci4\vendor\codeigniter4\framework\system\Test\bootstrap.php on line 40
PHPUnit 9.5.21 #StandWithUkraine

Error in bootstrap script: Error:
Failed opening required './app/Config/Paths.php' (include_path='.;C:\php\pear')
#0 D:\OneDrive\dev\ci4\vendor\phpunit\phpunit\src\Util\FileLoader.php(65): include_once()
#1 D:\OneDrive\dev\ci4\vendor\phpunit\phpunit\src\Util\FileLoader.php(49): PHPUnit\Util\FileLoader::load('D:\\OneDrive\\dev...')
#2 D:\OneDrive\dev\ci4\vendor\phpunit\phpunit\src\TextUI\Command.php(562): PHPUnit\Util\FileLoader::checkAndLoad('D:\\OneDrive\\dev...')
#3 D:\OneDrive\dev\ci4\vendor\phpunit\phpunit\src\TextUI\Command.php(344): PHPUnit\TextUI\Command->handleBootstrap('D:\\OneDrive\\dev...')
#4 D:\OneDrive\dev\ci4\vendor\phpunit\phpunit\src\TextUI\Command.php(111): PHPUnit\TextUI\Command->handleArguments(Array)
#5 D:\OneDrive\dev\ci4\vendor\phpunit\phpunit\src\TextUI\Command.php(96): PHPUnit\TextUI\Command->run(Array, true)
#6 D:\OneDrive\dev\ci4\vendor\phpunit\phpunit\phpunit(98): PHPUnit\TextUI\Command::main()
#7 {main}

Process finished with exit code 1
```
落ち着いてよく読んでみると `require(./app/Config/Paths.php)` に定義しろよ、と書いてある。定義してあるパスを総当りで試すようだ
```diff_php:app/Config/Paths.php（最後に追記。変数名はなんでもいい）
   :
+ public string $phpUnit = 'D:\OneDrive\dev\ci4\phpunit.xml';
```

# Config関連
## App.php
```diff_php:app/Config/App.php
- public $defaultLocale = 'en';
+ public $defaultLocale = 'ja';
- public $appTimezone = 'America/Chicago';
+ public $appTimezone = 'Asia/Tokyo';
```

## Routes.php
### ルーティングを理解する

```diff_php:app/Config/Routes.php
$routes->setDefaultNamespace('App\Controllers');
$routes->setDefaultController('Home');
$routes->setDefaultMethod('index');
- $routes->setAutoRoute(true);
+ $routes->setAutoRoute(false);

// We get a performance increase by specifying the default
// route since we don't have to scan directories.
$routes->get('/', 'Home::index');
$routes->get('/api/(\w+)/(\d+)', 'Home::store/$1/$2');
$routes->get('school', 'School::index');
```
- `http://localhost:8080/` で `Home` コントローラの `index` をトリガーする（`setDefaultController` と `setDefaultMethod` が効いている）
- `http://localhost:8080/api/hoge/8` で `Home` コントローラの `store` をパラメータつき `[hoge, 8]` でトリガーする
- codeigniterの仕組みとして、`http://localhost:8080/Home/index` で `Home` コントローラの `index` をトリガーする。`setAutoRoute(false)`でそれを止めることができる（[公式が推奨](https://codeigniter.com/user_guide/tutorial/static_pages.html#routing)）

## Filters.php
[CSRFフィルターを有効にする](https://codeigniter4.github.io/userguide/tutorial/create_news_items.html#enable-csrf-filter)

```diff_php
- public $methods = [];
+ public $methods = [
+     'get' => ['csrf'],
+     'post' => ['csrf'],
+ ];
```

# Database Configuration
https://codeigniter4.github.io/userguide/database/configuration.html#database-configuration

## Migrationファイルの作成
いまのところこんな認識だけどあってんのかな。。。DjangoだとModelを更新するとMigrationが差分を自動作成してくれるから全部独立してるといまいちわからんわ。。。

- Migrationはあくまで実体のテーブルを作りにいくための仕組み（下準備）
- Modelはciが実体のテーブルを取り扱うための手段（Migrationとはリンクしてないので調整する必要がある。findAll()とか）
- Entityは実体のレコードを取り扱うための型（全部textで返ってくるので、idはintegerで取り扱う、など）

## マイグレーションファイルの作成
ファイルの雛形を作成します

```powershell
dev\ci4> php spark make:migration --suffix
  Migration class name : VocabularyBook
  CodeIgniter v4.1.4 Command Line Tool - Server Time: 2021-09-18 20:37:01 UTC-05:00
  File created: APPPATH\Database\Migrations\2021-09-19-013702_VocabularyBookMigration.php
```

## up() と down() メソッドを記述
ってことはここで実体のテーブルを作り込むんだな？

```php:app/Database/Migrations/2021-09-19-013702_VocabularyBookMigration.php
<?php

namespace app\Database\Migrations;

use CodeIgniter\Database\Migration;

class VocabularyBookMigration extends Migration
{
    public function up()
    {
        // ここから
        $this->forge->addField(
            [
                'id' => [
                    'type' => 'INT',
                    'constraint' => 11,
                    'unsigned' => true,
                    'auto_increment' => true,
                ],
                'sentence' => [
                    'type' => 'VARCHAR',
                    'constraint' => '200',
                    'null' => false,
                ],
                'created_at' => ['type' => 'datetime', 'null' => true],
                'updated_at' => ['type' => 'datetime', 'null' => true],
            ]
        );
        $this->forge->addPrimaryKey('id');
        $this->forge->createTable('vocabulary_book');
        // ここまで
    }

    public function down()
    {
        // ここから
        $this->forge->dropTable('vocabulary_book');
        // ここまで
    }
}
```

## マイグレーションの実行

```powershell
dev\ci4> php spark migrate
  CodeIgniter v4.1.4 Command Line Tool - Server Time: 2021-09-19 03:18:14 UTC-05:00
  Running all new migrations...
        Running: (App) 2021-09-19-013702_app\Database\Migrations\VocabularyBookMigration
  Done migrations.
```

## 確認
マイグレーションを実行したらmysqlにテーブルが出来上がったことを確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/c4a2cbb2-9f91-2813-5457-adcaf59d96e5.png)

```sql:DDLを確認してみた
CREATE TABLE `vocabulary_book` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `sentence` varchar(200) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
```

### ※テーブル構造に差分が出る場合は、マイグレーションファイルを増やすか、勝手に出来上がっているmigrationsテーブルの履歴を削除してマイグレーションする
ci4db.migrations というテーブルは自動でできる。

## seeder（サンプルデータ）を作る

```powershell
dev\ci4> php spark make:seeder --suffix
  Seeder class name : Simple
  File created: APPPATH\Database\Seeds\SimpleSeeder.php
```

```php:dev\ci4\app\Database\Seeds\SimpleSeeder.php
<?php

namespace App\Database\Seeds;

use CodeIgniter\Database\Seeder;

class SimpleSeeder extends Seeder
{
    public function run()
    {
        // ここから
        $data = [
            ['sentence' => 'I would like an aisle seat.'],
            ['sentence' => 'My son asked for a window seat.'],
            ['sentence' => 'They wrapped the baby in a soft blanket.'],
            ['sentence' => 'Can I see your boarding pass?'],
        ];
    
        // Using Query Builder
        $this->db->table('vocabulary_book')->insertBatch($data);
        // ここまで
    }
}
```
```powershell
dev\ci4> php spark db:seed SimpleSeeder
```

### SQLで表すとこれと同じです
```sql
INSERT INTO ci4db.vocabulary_book (sentence,created_at,updated_at) VALUES
	 ('I would like an aisle seat.',NULL,NULL),
	 ('My son asked for a window seat.',NULL,NULL),
	 ('They wrapped the baby in a soft blanket.',NULL,NULL),
	 ('Can I see your boarding pass?',NULL,NULL);
```

## VocabularyBookエンティティを作成
https://codeigniter4.github.io/userguide/models/entities.html#working-with-the-entity-class

エンティティはcodeIgniterが取り扱うモデルのレコードの構造体らしい。`str` 以外の型で取り扱いたいときは `casts` に指定する。

```powershell:VocabularyBookというエンティティ
dev\ci4> php spark make:entity --suffix
  Entity class name: VocabularyBook
  File created: APPPATH\Entities\VocabularyBookEntity.php
```
```diff:app/Entities/VocabularyBookEntity.php
<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    protected $datamap = [];
-   protected $dates   = ['created_at', 'updated_at', 'deleted_at'];
+   protected $dates   = ['created_at', 'updated_at'];
    protected $casts   = [
+       'id' => 'integer',
    ];
}
```

## VocabularyBookModelモデルを作成
あくまでテンプレとして出来上がるので、実体にあわせて調整する。
- `@method` タグを指定するとfind() が entity を返してくれる
    - https://forum.codeigniter.com/showthread.php?tid=81455
- `$allowedFields` は `foreign key` も含めてください。
    - https://forum.codeigniter.com/showthread.php?tid=81575


```powershell
dev\ci4> php spark make:model --suffix
  Model class name: VocabularyBook
  File created: APPPATH\Models\VocabularyBookModel.php
```
```diff_php
<?php

namespace App\Models;

use CodeIgniter\Model;

class VocabularyBookModel extends Model
{
    protected $DBGroup          = 'default';
-   protected $table            = 'vocabularybooks';
+   protected $table            = 'vocabulary_book';
    protected $primaryKey       = 'id';
    protected $useAutoIncrement = true;
    protected $insertID         = 0;
-   protected $returnType       = 'array';
+   protected $returnType       = VocabularyBookEntity::class;
    protected $useSoftDeletes   = false;
    protected $protectFields    = true;
-   protected $allowedFields    = [];
+   protected $allowedFields    = ['cool_your_fk_id', 'sentence'];

    // Dates
-   protected $useTimestamps = false;
+   protected $useTimestamps = true;
    protected $dateFormat    = 'datetime';
    protected $createdField  = 'created_at';
    protected $updatedField  = 'updated_at';
-   protected $deletedField  = 'deleted_at';

    // Validation
    protected $validationRules      = [];
    protected $validationMessages   = [];
    protected $skipValidation       = false;
    protected $cleanValidationRules = true;

    // Callbacks
    protected $allowCallbacks = true;
    protected $beforeInsert   = [];
    protected $afterInsert    = [];
    protected $beforeUpdate   = [];
    protected $afterUpdate    = [];
    protected $beforeFind     = [];
    protected $afterFind      = [];
    protected $beforeDelete   = [];
    protected $afterDelete    = [];
}
```

# バックエンド側をつくる
## controller
view()の第2引数（型はArray）が、キーを変数名としたものに変換されてView上の変数として定義される。テンプレートからのアクセスの際は `$vocabulary_book` のような形で参照する。

```diff_php:app/Controllers/Home.php
<?php

namespace App\Controllers;

use App\Models\VocabularyBookModel;

class Home extends BaseController
{
    public function index()
    {
        // ここから
        $vocabularyBook = new VocabularyBookModel();
    
        $sentences = [];
        foreach ($vocabularyBook->findAll() as $row) {
            var_dump($row->id); // entityに接続されているか(intになっているか）点検
            $sentenceArray = explode(' ', $row->sentence);
            shuffle($sentenceArray);
            $sentences[] = $sentenceArray;
        }
        $data['vocabulary_book'] = $sentences;
        // ここまで


-       return view('welcome_message');
+       return view('welcome_message', $data);
    }
}
```

## html
```php:app/Views/welcome_message.php
<!DOCTYPE html>
<html lang="en">
<head>
 :
</head>
<body>
 :
    <h1>About this page</h1>

    <!--ここから-->
    <table>
        <tbody>
        <?php
        if (!empty($vocabulary_book)) {
            foreach ($vocabulary_book as $item): ?>
                <tr>
                    <?php foreach ($item as $td): ?>
                        <td><?= $td ?></td>
                    <?php endforeach;?>
                </tr>
            <?php endforeach;
        } ?>
        </tbody>
    </table>
    <!--ここまで-->

    <p>The page you are looking at is being generated dynamically by CodeIgniter.</p>
 :
</body>
</html>
```

## 確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/79c124a4-f114-3a7f-518f-4e93067bf00d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/fc9bfedc-5f79-f34c-ea18-77b8f79d6a9c.png)

# フロントエンド側をつくる
## ※（大ハマリ）publicフォルダにassetsフォルダを作成
```powershell
dev\ci4> mkdir public/assets
```

[フォーラム](https://forum.codeigniter.com/thread-80965-post-392851.html) で質問して解決したけど、どうやらサーバーのルーティングには2段階あるようだ。まぁ言われてみればそうやな。

> First, the web server looks for paths in the DOCUMENT_ROOT (public directory) and if it doesn’t find it, then it passes control to php.
まず、WebサーバーはDOCUMENT_ROOT（パブリックディレクトリ）でパスを探し、見つからない場合は、制御をphpに渡します。

> If you have a /public/school directory, then when you go to the address `http://localhost/school`, the web server will try to read it. So in your case, I would recommend changing the paths.
`/public/assets/school`
`/public/assets/home`
`/public/school` ディレクトリがある場合、アドレス 
`http//localhost/school` にアクセスすると、Webサーバーはそれを読み取ろうとします。したがって、あなたの場合は、パスを変更することをお勧めします。

> The web server first of all looks for the directory or file specified in the URL. By default, if a file or directory is not found, the server generates a 404 error. For software routing to work, instructions are prescribed to the web server to redirect the processing of requests to the front controller in the absence of a file or directory. Absolutely all frameworks work on this principle.
Webサーバーはまず、URLで指定されたディレクトリまたはファイルを探します。デフォルトでは、ファイルまたはディレクトリが見つからない場合、サーバーは404エラーを生成します。ソフトウェアルーティングが機能するために、ファイルまたはディレクトリがない場合に要求の処理をフロントコントローラーにリダイレクトするようにWebサーバーに指示が規定されています。絶対にすべてのフレームワークがこの原則に基づいて機能します。

## html
```html:
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Welcome to CodeIgniter 4!</title>
        <meta name="description" content="The small framework with powerful features">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="shortcut icon" type="image/png" href="/favicon.ico"/>

        <!-- for bootstrap -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
        <!-- for css -->
        <link rel="stylesheet" href="assets/css/index.css">
        <!-- fontawesome -->
        <link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">
        
    </head>
    <body>
        <div class="jumbotron">
            <h1 class="display-4">Let's shuffle your sentence</h1>
            <p class="lead">Can be inspected by drag and drop.</p>
            <a class="btn btn-primary btn-sm" href="#" role="button">register</a>
            <a class="btn btn-success btn-sm" href="#" role="button">check</a>
            <hr class="my-4">
        </div>
        
        <table class="table table-bordered">
            <tbody>
                <?php
                if (isset($vocabulary_book)) {
                    foreach ($vocabulary_book as $item): ?>
                        <tr class="sortable correct">
                            <?php foreach ($item as $td): ?>
                                <td><?= $td ?></td>
                            <?php endforeach;?>
                        </tr>
                    <?php endforeach;
                } ?>
            </tbody>
        </table>
        
        <!-- for bootstrap -->
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
        
        <!-- for drug and drop -->
        <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
        <script src="https://code.jquery.com/ui/1.12.0/jquery-ui.js"></script>
        
        <!-- my script -->
        <script src="assets/js/script.js"></script>
    </body>
</html>
```

## css
```css:public/css/index.css
table.table {
    border-collapse:separate;
    border-spacing: 10px 10px;
    text-align: center;
}
.correct:before {
    font-family: "Font Awesome 5 Free";
    font-size: 2.5em;
    content: '\f058';
    font-weight: 900;
}
```

## js
```js:public/js/script.js
$(function() {
    $(".sortable").sortable();
    $(".sortable").disableSelection();
    $("#submit").click(function() {
        var listIds = $(".sortable").sortable("toArray");
        $("#list-ids").val(listIds);
        $("form").submit();
    });
});
```

## 確認
リッチになったね。英単語をドラッグ・アンド・ドロップできる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/0172df0d-067c-f8d9-319b-f820ec051892.png)

## phpcs
```diff_php:composer.json
    "require-dev": {
        "fakerphp/faker": "^1.9",
        "mikey179/vfsstream": "^1.6",
        "phpunit/phpunit": "^9.1",
+       "squizlabs/php_codesniffer": "*"
    },
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/9fced90e-9fcf-8a72-2108-8630efd93d1b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/bf941506-aeb0-f5b8-476b-fa9f98748edb.png)
`C:\Users\yoshi\AppData\Roaming\Composer\vendor\squizlabs\php_codesniffer\bin\phpcs.bat`

# ログイン機能
http://blog.a-way-out.net/blog/2021/12/09/codeigniter4-myth-auth/

```diff_json:ci4login/composer.json
      :
    "require": {
        "php": "^7.3 || ^8.0",
        "codeigniter4/framework": "^4",
+       "myth/auth": "*"
    },
      :
```
```diff_php:app/Config/Validation.php
      :
+   use Myth\Auth\Authentication\Passwords\ValidationRules;
      :
    public $ruleSets = [
          :
+       ValidationRules::class,
    ];
      :
```
```diff_php:app/Config/Toolbar.php
      :
+   use Myth\Auth\Collectors\Auth;
      :
    public $collectors = [
          :
+       Auth::class
    ];
      :
```
```diff_php:app/Config/Filters.php
      :
+   use Myth\Auth\Filters\LoginFilter;
+   use Myth\Auth\Filters\PermissionFilter;
+   use Myth\Auth\Filters\RoleFilter;
      :
    public $aliases = [
          :
+       'login'         => LoginFilter::class,
+       'role'          => RoleFilter::class,
+       'permission'    => PermissionFilter::class,
    ];
      :
```

## ログインページ割り込み場所の設定
こうすると、ドキュメントルートに入った瞬間にログインページが出てくる
```diff_php:app/Config/Filters.php
-   public $filters = [];
+   public $filters = ['login' => ['before' => ['/']],];
```

いっぽうで、以下のようにすると、ニュース用のアプリとしてつくった `news/` ページ配下でログインページが出てくる。通常はこっちに設定する（いきなり「名前を言え！」なんていわれたら高圧的だから、会社だけで使うツールでもないかぎりは、誰でも着地できるページは作るべきだ）
```diff_php:app/Config/Filters.php
-   public $filters = [];
+   public $filters = ['login' => ['before' => ['news/*']],];
```

## ルート処理キュー
https://codeigniter4.github.io/userguide/incoming/routing.html#route-processing-queue

>モジュールを操作するときに、アプリケーションのルートにワイルドカードが含まれていると問題が発生する可能性があります。そうすると、モジュールルートが正しく処理されなくなります。この問題は、priorityオプションを使用してルート処理の優先度を下げることで解決できます。このパラメーターは、正の整数とゼロを受け入れます。「priority」で指定された数値が大きいほど、処理キューのルート優先度は低くなります。
```diff_php:app/Config/Routes.php
  /*
   * --------------------------------------------------------------------
   * Router Setup
   * --------------------------------------------------------------------
   */
    :
+ $routes->setPrioritize();

/*
 * --------------------------------------------------------------------
 * Route Definitions
 * --------------------------------------------------------------------
 */
  $routes->get('/', 'Home::index');
+ $routes->get('(:any)', 'Pages::view/$1', ['priority' => 1]);
```

## マイグレーション
マイグレーションを実行して必要なテーブルを作成
```console
$ php spark migrate -n 'Myth\Auth'
```

- auth_activation_attempts
- auth_groups
- auth_groups_permissions
- auth_groups_users
- auth_logins
- auth_permissions
- auth_reset_attempts
- auth_tokens
- auth_users_permissions
- users

```sql
-- 例えば users テーブルはこんなつくり
CREATE TABLE `users` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `email` varchar(255) NOT NULL,
  `username` varchar(30) DEFAULT NULL,
  `password_hash` varchar(255) NOT NULL,
  `reset_hash` varchar(255) DEFAULT NULL,
  `reset_at` datetime DEFAULT NULL,
  `reset_expires` datetime DEFAULT NULL,
  `activate_hash` varchar(255) DEFAULT NULL,
  `status` varchar(255) DEFAULT NULL,
  `status_message` varchar(255) DEFAULT NULL,
  `active` tinyint(1) NOT NULL DEFAULT '0',
  `force_pass_reset` tinyint(1) NOT NULL DEFAULT '0',
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  `deleted_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

## アカウントを作る
ログインページが割り込んでくるページに移動するとログイン画面が表示され、アカウントを作る画面（Need an account?）にも移動できるので、アカウントを作ろう。しかし、メールは送ることができない :sob:
SMTPの設定が必要だ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/dd947c9e-e086-46e4-646a-decd60e656c5.png)

## SMTPの設定
`.env` で、キーワード（この例では `email`）と入力すると、`app\Config\Email.php` の値を書き換えることができるようだ。えぇこと聞いたわ

https://forum.codeigniter.com/showthread.php?tid=81816

```diff_php:.env
  :
+ #----------
+ # Email
+ #----------
+ email.fromEmail = admin@example.jp
+ email.fromName = 'Admin User'
+ email.protocol = smtp
+ email.SMTPHost = smtp.googlemail.com
+ email.SMTPUser = xxxxxxxxxxx@gmail.com
+ email.SMTPPass = xxxxxxxxxxx
+ email.SMTPPort = 465
+ email.SMTPTimeout = 60
+ email.SMTPCrypto = ssl
+ email.mailType = html
```

## アプリ用パスワードを発行する
メール関連のノウハウサイトを見てると「安全性の低いアプリ」の許可をすることでメールを送っていることが多い。が、2022年5月末以降は、許可すること自体ができなくなるので、アプリケーション用のパスワードを発行することで対応することにした。

https://forum.codeigniter.com/showthread.php?tid=81800

https://myaccount.google.com/security

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/7d095398-aa62-4e5d-9ab3-f6f79fc52ad8.png)
CodeIgniter4用のパスワードを作成し、生成されたパスワードをコピーする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/7982ee34-dff3-1bb3-80ef-c483f92a9932.png)

生成されたパスワードをこの変数に置き換えるとメールを送信できた。
```diff_php:.env
+ email.SMTPPass = xxxxxxxxxxx
```

## ログイン機能を作ったあとに
例えば学校管理システムを作るような場合は、ログインユーザと学校が紐づかないので、承認管理画面を作る必要があると思う（学校のメールアドレス一覧をもらう想定をすればいいと思う）
### カスタマイズ：ユーザー登録時のバリデーションをいじる
Myth/Authはデフォルトで便利なんだが、じつは `Username` に日本語を使用できない。これは学校管理システムを作るような場合はちと辛い（ローマ字の名前なんて何にも使えないから:sob:）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/dbb6fdd0-360b-8a15-dd7a-c3e6e72d8033.png)

[フォーラムでいろいろと相談してみた](https://forum.codeigniter.com/showthread.php?tid=81825) が、Myth/Auth のカスタマイズで「英数字縛り」を解除できるようだ。

- 現在の状態を知る
`php spark routes` を実行すると、`[POST]register` の `Handler` は `\Myth\Auth\Controllers\AuthController` に向いていることがわかる。これはどこを見ているかというと `vendor/myth/auth/src/Controllers/AuthController.php` 、つまり `vendor` フォルダ（＝git管理外）を見ている。

```console:console
dev\ci4> php spark routes

  CodeIgniter v4.1.9 Command Line Tool - Server Time: 
  2022-05-04 12:28:17 UTC+09:00
  +--------+-------------------------------------------+---------------------------------------------------------------+
  | Method | Route                                     | Handler                                                       |
  +--------+-------------------------------------------+---------------------------------------------------------------+
  | GET    | /                                         | \App\Controllers\HomeController::index                        |
  　：
  | GET    | login                                     | \Myth\Auth\Controllers\AuthController::login                  
  | GET    | logout                                    | \Myth\Auth\Controllers\AuthController::logout                 |
  | POST   | register                                  | \Myth\Auth\Controllers\AuthController::attemptRegister               |
```

- Myth/Auth のカスタマイズでコントローラとモデルを `vendor` フォルダからコピーしてくる

```console:console
dev\ci4> php spark auth:publish                          

  CodeIgniter v4.1.9 Command Line Tool - Server Time: 2022-05-04 13:25:58 UTC+09:00
  Publish Migration? [y, n]: n
  Publish Models? [y, n]: y
    Created: Models/LoginModel.php　　　←（注目）
    Created: Models/UserModel.php 　　　←（注目）
  Publish Entities? [y, n]: y
    Created: Entities/User.php 　　　←（注目）
  Publish Controller? [y, n]: y
    Created: Controllers/AuthController.php　　　←（注目）
  Publish Views? [y, n]: n
  Publish Filters? [y, n]: n
  Publish Config file? [y, n]: n
  Publish Language file? [y, n]: n
```

- つぎに、`[POST]register` の向き先を変える

```diff_php:app/Config/Routes.php
$routes->get('/', 'HomeController::index', ['as' => 'home']);
　：
+ $routes->post('register', 'AuthController::attemptRegister');
```

- 確認する
`php spark routes` を実行すると、`register` の `Handler` は `\App\Controllers\AuthController` に向いている

```console:console
dev\ci4> php spark routes

  CodeIgniter v4.1.9 Command Line Tool - Server Time: 
  2022-05-04 12:28:17 UTC+09:00
  +--------+-------------------------------------------+---------------------------------------------------------------+
  | Method | Route                                     | Handler                                                       |
  +--------+-------------------------------------------+---------------------------------------------------------------+
  | GET    | /                                         | \App\Controllers\HomeController::index                        |
  　：
  | GET    | login                                     | \Myth\Auth\Controllers\AuthController::login                  
  | GET    | logout                                    | \Myth\Auth\Controllers\AuthController::logout                 |
  | POST   | register                                  | \App\Controllers\AuthController::register               |
```

- つぎに、コントローラから `alpha_numeric_space` 除外する

```diff_php:app/Controllers/AuthController.php::attemptRegister()
    /**
     * Attempt to register a new user.
     */
    public function attemptRegister()
    {
        // Check if registration is allowed
        if (! $this->config->allowRegistration) {
            return redirect()->back()->withInput()->with('error', lang('Auth.registerDisabled'));
        }

        $users = model(UserModel::class);

        // Validate basics first since some password rules rely on these fields
        $rules = [
-           'username' => 'required|alpha_numeric_space|min_length[3]|max_length[30]|is_unique[users.username]',
+           'username' => 'required|min_length[3]|max_length[30]|is_unique[users.username]',
            'email'    => 'required|valid_email|is_unique[users.email]',
        ];
　　　　　：
```

- つぎに、モデルから `alpha_numeric_space` 除外する

```diff_php:app/Models/UserModel.php
　　　：
    protected $validationRules = [
        'email'         => 'required|valid_email|is_unique[users.email,id,{id}]',
-       'username'      => 'required|alpha_numeric_space|min_length[3]|max_length[30]|is_unique[users.username,id,{id}]',
+       'username'      => 'required|min_length[3]|max_length[30]|is_unique[users.username,id,{id}]',
        'password_hash' => 'required',
    ];
　　　：
```

できた！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/ee7998a2-b9bd-cc9d-629f-0910f9f79ec9.png)

# Xdebug
## phpinfo()のhtmlを手に入れる

```php
class HomeController extends BaseController
{
    /**
     * @return string
     */
    public function index(): string
    {
        return phpinfo();
    }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/a013b62a-400d-3fa3-9fff-3f5f0a746693.png)

```html:phpinfo()のソースを表示して全コピー
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
  :
```

## phpinfo()のhtmlを貼り付ける
https://xdebug.org/wizard
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/f0e0a664-3d0d-0321-0797-4f449c645cc2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/064ea9b2-952c-f007-6010-cecf6e7acdaf.png)
まぁそのままだけｄ

1. `dll` をダウンロード
1. `C:\php\ext` に移動して、`php_xdebug.dll`にリネーム
1. `php.ini L953` のコメントを外して `zend_extension=xdebug` に変更

https://pleiades.io/help/phpstorm/configuring-xdebug.html#updatingPhpIni

> Xdebug を有効にするには、php.ini ファイルで [xdebug] セクションを見つけるか作成して、次のように更新します。

```diff_php:C:\php\ext L953
- ;zend_extension=opcache

+ [xdebug]
+ zend_extension="C:\php\ext\php_xdebug.dll"
+ xdebug.mode=debug
+ xdebug.client_host=localhost
```

## Xdebug のインストールを確認
```console:console
dev\ci4> php --version
  PHP 8.1.1 (cli) (built: Dec 15 2021 10:31:43) (ZTS Visual C++ 2019 x64)
  Copyright (c) The PHP Group
  Zend Engine v4.1.1, Copyright (c) Zend Technologies
    with Xdebug v3.1.4, Copyright (c) 2002-2022, by Derick Rethans
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/f2faa16d-7075-9b68-cdd9-5efae82e263b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/fc9a3a3e-61ef-b9a4-c713-839c74c55f86.png)

## ブレークポイントを仕掛けて止まることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/c5e73fe5-b5d9-9563-ed57-af4731f21f9b.png)

# その他の処理を作る
## Slackと連携する
https://qiita.com/YoshitakaOkada/items/b8b9db11a336fbb02b2d

## バッチ処理をつくる
https://qiita.com/YoshitakaOkada/items/e07b45d80d359797f088