CodeIgniter3.1.13をPHP8.2環境で動かすためにやったこと

# はじめに

一番良いのは、当然ながらCodeIgniter4やTwig3のような最新版にすることです。(というかLaravel使ったほうが...)
本記事は、そんな時間も予算も余裕がなく、
明日までにPHPのバージョンアップ対応を間に合わせなければいけないような
切羽詰まった人に向けて、とりあえずの解決策を提示します。

# 環境情報

* Amazon Linux 2
* Apache 2.4
* PHP 8.2.8
* CodeIgniter 3.1.13
* Twig 1.44
* MySQL(10.2.38-MariaDB)

```json:composer.json
{
	"description" : "The CodeIgniter Application with Composer",
	"require": {
		"php": ">=8.2.0",
		"codeigniter/framework": "3.1.*",
		"vlucas/phpdotenv": "^3.3",
		"kenjis/codeigniter-ss-twig": "^0.4.0",
		"chriskacerguis/codeigniter-restserver": "^3.0",
		"twig/twig": "^1.44"
	}
}
```

# PHP8.2のローカル環境を用意する

以下の記事の方法でPHP8.2を用意しました

https://qiita.com/qwe001/items/6855beee4d2cc9fc3e32

# PHP8.2で動かすといっぱいエラーが出てきて草

よく見かけたエラーは以下の通り

```php
Creation of dynamic property CI_URI::$config is deprecated
Creation of dynamic property CI_Router::$uri is deprecated
Creation of dynamic property CI_DB_mysqli_driver::$failover is deprecated
Creation of dynamic property CI_Loader::$log is deprecated
Creation of dynamic property CI_Loader::$session is deprecated
Return type of Twig\Node\Node::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice
Return type of Twig\Node\Node::getIterator() should either be compatible with IteratorAggregate::getIterator(): Traversable, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice
```

# 何が問題か

PHP 8.2では動的プロパティの割り当てが非推奨となり、
これがCodeIgniter3の動作に影響を及ぼしています。

[対策方法を議論しているページはこちら](https://github.com/bcit-ci/CodeIgniter/issues/6192)

以下に、筆者が試してみて実際にエラーが解消された修正方法を説明します。

# 修正方法

## 1. 動的プロパティの作成が非推奨となったクラスに対する修正

特定のクラスに対して、動的プロパティの作成が非推奨となっています。
これらのクラスには、CI_URI, CI_Router, CI_Loader, CI_Controller, CI_DB_driverなどが含まれます。
これらのクラスには、公開（public）プロパティを追加することで修正が可能です。
例えば、CI_URIクラスにはpublic $config;を追加します。

直接 `vendor/codeigniter/framework/system/core/URI.php` を修正しても動作しますが、
次に `composer update` を実行した時に、修正内容が吹っ飛びますので、以下のように
アプリケーション側でコアファイルの記述をオーバーライドすることで、
フレームワークのアップデート時にカスタマイズが上書きされることを防ぐことができます。
なお、該当のファイルが存在しない場合は新規作成してください。

```php:application/core/MY_URI.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class MY_URI extends CI_URI {
    public $config;
}
```

## 2. `#[\AllowDynamicProperties]` 属性の使用

PHP8系で新しく追加された `#[\AllowDynamicProperties]` 属性を使用して、
クラスが動的属性を許可するように指示するという方法もあります。
これにより、動的プロパティの割り当てが非推奨となっても、クラスが動的プロパティを許可するようになります。

どのファイルを修正するべきかは、プロジェクトによって異なると思いますが、
皆さんがアプリケーション内で共通で呼び出している親コントローラーや親モデルに設定します

## 3. `#[\ReturnTypeWillChange]` 属性の使用

PHP8.1からは内部クラスのメソッドは、それをオーバライドする際、
互換性がある戻り値の型を宣言することが必須になりました。

ただ、今回それが発生しているのはTwigライブラリであり、
Ver1系を使用している限り、出続けます。

`[\ReturnTypeWillChange]` 属性を使用すれば、
戻り値の型の互換性に関するエラーを一時的に抑制することができます。

しかし、この方法は一時的な対策であり、
いずれはTwigライブラリをアップデートする必要があります

# 修正実施

## Step1. コアファイルをオーバーライドする

`application/core/` ディレクトリに以下のファイルを新規作成します

```
MY_Controller.php
MY_Loader.php
MY_Router.php
MY_URI.php
```

それぞれ、以下の内容をコピペします。これだけでエラーがだいぶ消えるはずです。

```php:application/core/MY_Controller.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class MY_Controller extends CI_Controller {
    public $benchmark;
    public $config;
    public $log;
    public $hooks;
    public $utf8;
    public $uri;
    public $router;
    public $exceptions;
    public $output;
    public $security;
    public $input;
    public $lang;
}
```

```php:application/core/MY_Loader.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class MY_Loader extends CI_Loader {
    public $load;
    public $benchmark;
    public $config;
    public $log;
    public $hooks;
    public $utf8;
    public $uri;
    public $router;
    public $exceptions;
    public $output;
    public $security;
    public $input;
    public $lang;
}
```

```php:application/core/MY_Router.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class MY_Router extends CI_Router {
    public $uri;
}
```

```php:application/core/MY_URI.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class MY_URI extends CI_URI {
    public $config;
}
```

2023.07.24 追記
別のプロジェクトでは `Creation of dynamic property CI_Image_lib::$dest_image is deprecated` というエラーが出ました。
これはCI3の画像操作ライブラリであるImage_libクラス( `vendor/codeigniter/framework/system/libraries/Image_lib.php` )で
動的プロパティの割り当てがあったことに対するエラーです。

コアファイルでないものは、以下のようにライブラリに記述することでオーバーライド可能です。

```php:application/libraries/MY_Image_lib.php
<?php
if (!defined('BASEPATH')) exit('No direct script access allowed');

class MY_Image_lib extends CI_Image_lib {
    public $dest_image;
}
```

## Step2. アプリケーションファイルを修正する

これはプロジェクトにより異なると思います。

`Creation of dynamic property Auth::$user is deprecated` みたいな、
ご自身が実装した覚えのあるアプリケーションレイヤーでエラーが出た場合に試してみてください。

どのファイルを修正するべきかはプロジェクトによって異なると思いますが、
皆さんがアプリケーション内で共通で呼び出している親コントローラーや親モデルなどに設定します。
classの上に、 `#[\AllowDynamicProperties]` を追記するだけです。
これにより、動的プロパティの割り当てを有効にします。

```php:application/controllers/Common_Controller.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

/**
 * Common_Controller
 * All controllers should inherit this.
 */
#[\AllowDynamicProperties]
class Common_Controller extends CI_Controller
```

```php:application/models/Common_Model.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

/**
 * Common_Model
 * All models should inherit this.
 */
#[\AllowDynamicProperties]
class Common_Model extends CI_Model
```

## Step3. コアファイルを直接書き換える

コアファイルのオーバーライド方法がわからなかったやつらです。
わかる人いたら至急、コメントくれや。
ただし、 `composer update` したら、あなたが修正した内容が
なかったことになる(エラーが再発する)ということに留意してください。

### 1. `Creation of dynamic property CI_DB_mysqli_driver::$failover is deprecated` の修正

データベースドライバーの上書きは難しいようです。
[この記事を見ると、方法はありそう](https://forum.codeigniter.com/thread-68512-post-345512.html#pid345512)ですが、私の環境では動きませんでした。

というわけで、コアファイルを直接修正して対応します。

classの一番上に `public $failover;` を追記します

```php:vendor/codeigniter/framework/system/database/DB_driver.php
abstract class CI_DB_driver {
	public $failover;
```

### 2. Twigのエラーを修正

```
Return type of Twig\Node\Node::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice
Return type of Twig\Node\Node::getIterator() should either be compatible with IteratorAggregate::getIterator(): Traversable, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice
```

当該のメソッドのすぐ上にそれぞれ `[\ReturnTypeWillChange]` を追記します。classの上ではないことに注意

```php:vendor/twig/twig/src/Node/Node.php
    /**
     * @return int
     */
    public function count()
    {
        return \count($this->nodes);
    }

    /**
     * @return \Traversable
     */
    public function getIterator()
    {
        return new \ArrayIterator($this->nodes);
    }
```

↓

```php:vendor/twig/twig/src/Node/Node.php
    /**
     * @return int
     */
    #[\ReturnTypeWillChange]
    public function count()
    {
        return \count($this->nodes);
    }

    /**
     * @return \Traversable
     */
    #[\ReturnTypeWillChange]
    public function getIterator()
    {
        return new \ArrayIterator($this->nodes);
    }
```

### 3. Viewテンプレート内で `nl2br(): Passing null to parameter #1 ($string) of type string is deprecated` というエラーが出た

nl2brにnullが入らないようにするべき、というエラーだそうです。
そのため、デフォルト値はnullから空文字に変更します。(`default("")` を追加)

```php:application/views/admin/events/edit.twig.html
{% set setDefaultMessage = old.message | default(item.message) %}

<textarea id="message" name="message" class="form-control textarea">{{ setDefaultMessage | nl2br }}</textarea>
```

↓

```php:application/views/admin/events/edit.twig.html
{% set setDefaultMessage = old.message | default(item.message) | default("") %}

<textarea id="message" name="message" class="form-control textarea">{{ setDefaultMessage | nl2br }}</textarea>
```


# 参考URL

* https://github.com/bcit-ci/CodeIgniter/issues/6192
* https://forum.codeigniter.com/post-345513.html