php phalconをv3からv4にアップグレードする

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/6468bf26-8c5c-b73a-d57e-ccd7fa48ce2e.png" alt="phalcon" width="220">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/54d37327-66f1-2933-8975-34dd22349ebd.png" alt="php" width="80">

`php:7.2-fpm-alpine3.12`コンテナイメージが、`vscode-v1.77`以降のDevContainersでvscode-serverの起動エラーでコンテナに接続出来なくなったので、`alpine-v3.16`以降の
コンテナイメージへのアップグレードの必要に迫られた。

[[VsCode v1.77以降のDevContainers拡張でalpine v3.12コンテナに接続できない]]

php公式イメージで要件を満たすもっとも世代の近いイメージ`php:7.4-fpm-alpine3.16`で`php-v7.4`に対応する`phalcon`のバージョン要件が`v4`以降のため、phalconを`v3`から`v4`にメジャーアップデートする際の対応録。

## イメージビルド編

### ベースイメージの変更

```dockerfile:Dockerfile
# FROM php:7.2-fpm-alpine
FROM php:7.4-fpm-alpine3.16
```

### oniguruma

`docker-php-ext-install`ツールで`mbstring`をインストールしようとすると依存ライブラリの`oniguruma`がないといってビルドに失敗する。

が、`php:7.4-fpm-alpine3.16`にはそもそも`oniguruma`非依存の`mbstring`がセットアップ済みのため、追加インストールなど不要だったのでシンプルに`docker-php-ext-install`から`mbstring`を削除して解決。

```dockerfile:Dockerfile
FROM php:7.4-fpm-alpine3.16
RUN docker-php-ext-install pdo_mysql mysqli mbstring < これ
```

```plaintext
#5 15.29 configure: error: Package requirements (oniguruma) were not met:
#5 15.29 
#5 15.29 Package 'oniguruma', required by 'virtual:world', not found
#5 15.29 
#5 15.29 Consider adjusting the PKG_CONFIG_PATH environment variable if you
#5 15.29 installed software in a non-standard prefix.
#5 15.29 
#5 15.29 Alternatively, you may set the environment variables ONIG_CFLAGS
#5 15.29 and ONIG_LIBS to avoid the need to call pkg-config.
#5 15.29 See the pkg-config man page for more details.
```

備忘録として

[こちら](https://qiita.com/June8715/items/1df5958b95c1ff2d4c1b)さまによると`php-v7.4`らへんの`mbstring`ビルドでは`oniguruma`という荒々しい名の正規表現ライブラリに依存するようになったらしく、`libonig-dev`というパッケージに収納されているとのこと。`apk`では[こちら](https://qiita.com/wolf25/items/74bb98bb3052de4c324d)さまのとおり`oniguruma-dev`というパッケージ名で配布されていた。`oniguruma-dev`のインストール追加でイメージビルドは成功した。

```dockerfile:Dockerfile
# RUN docker-php-ext-install pdo_mysql mysqli mbstring
# docker-php-ext-installからmbstringを削除してapkでoniguruma-devをインストールする
RUN docker-php-ext-install pdo_mysql mysqli mbstring
apk add --no-cache oniguruma-dev < これ
```

### phalcon

phalconのソースをバージョンを4.0系のlatestバージョン`v4.0.6`に変更

```dockerfile:Dockerfile
RUN cd /usr/local \
# phalconのビルドソースを3系から4系に変更
# && git clone https://github.com/phalcon/cphalcon.git -b v3.4.3 \
&& git clone https://github.com/phalcon/cphalcon.git -b v4.0.6 \
&& cd cphalcon/build \
&& sh install \
&& echo "extension=phalcon.so" > /usr/local/etc/php/conf.d/phalcon.ini \
```

### psr

コンテナを起動してhttpアクセスするとphpワーニングで`Uncaught Error: Class 'Phalcon\Config' not found`
え？メジャーアップデートでnamespaceがっつり変わった?と焦るも、公式見ても`Phalcon\Config`は健在だ。

ビルド失敗している？と思ってphpのロードモジュールを確認すると、`psr`モジュールがローディングできてない?からphalconモジュールがローディングできていないようだ。

```sh:sh
php -m | grep phalcon
PHP Warning:  Cannot load module 'phalcon' because required module 'psr' is not loaded in Unknown on line 0
```

`phalcon-v4`から`psr`に対応するためライブラリ依存するようだ。
[Fatal error: Uncaught Error: Class 'Phalcon\Config' not found](https://stackoverflow.com/questions/74405523/fatal-error-uncaught-error-class-phalcon-config-not-found)
[Php can not load module phalcon](https://stackoverflow.com/questions/57638554/php-can-not-load-module-phalcon)
[Installation - Phalcon Documentation](https://docs.phalcon.io/4.2/installation/#psr)

Dockerfileにpecl-psrのインストールコマンドを追加して、phalconモジュールのローディングの前にローディングする
[Phalconをdocker imageに詰め込みたいとき](https://qiita.com/fagai/items/81cd0c7ab1749cf24b33)

```dockerfile:Dockerfile
# xdebugのインストール
RUN pecl install psr \
&& docker-php-ext-enable psr \
&& echo "extension=psr.so" >> /usr/local/etc/php/conf.d/phalcon.ini \
&& echo "extension=phalcon.so" >> /usr/local/etc/php/conf.d/phalcon.ini \
```

### xdebugバージョン

[xdebug3.2.0でphp7がサポート対象外になったのでDockerfileを直した](https://qiita.com/kumanobori/items/49b5515e0922799035ea)

`xdebug-v3.2.0`以降、`php-v7`がサポート対象外になったため、xdebugはバージョン指定でアーカイブDLする

 ```sh:sh
[ 5/17] RUN pecl install xdebug && docker-php-ext-enable xdebug:
> #9 5.238 pecl/xdebug requires PHP (version >= 8.0.0, version <= 8.2.99), installed version is 7.3.29
> #9 5.238 No valid packages found
> #9 5.238 install failed
```

peclでは`xdebug-{バージョン}`でアーカイブがディストリビュートされている

```dockerfile:Dockerfile
RUN pecl install xdebug-3.1.6 && docker-php-ext-enable xdebug
```

## アプリケーション編
[How to upgrade - Phalcon Documentation](https://docs.phalcon.io/4.2/upgrade/)

### Class 'Phalcon\Session\Adapter\Files' not found


サーバーセッションのファイルアダプタクラスはv4で`Phalcon\Session\Adapter\Stream`に置き換えられた
[Phalcon 4.0 setting up Session](https://stackoverflow.com/questions/63783913/phalcon-4-0-setting-up-session)

```php:config/service.php:before
# => Class 'Phalcon\Session\Adapter\Files' not found
$di->setShared('session',function(){
    $session = new \Phalcon\Session\Adapter\Files();
    $session->start();
    return $session;
});
```

```php:config/service.php:after
// 保存先`savePath`を指定する
$di->setShared('session',function(){
    $session = new Phalcon\Session\Manager();
    $files = new Phalcon\Session\Adapter\Stream( [
        'savePath' => '/tmp',
    ]);
    $session->setAdapter($files)->start();
    return $session;
});
```

### Fatal error: Uncaught Error: Call to undefined method 
Phalcon\Logger\Adapter\Stream::log()

v3では`Phalcon\Logger\Adapter\Stream`が出力`log()`と出力先のストリームアダプタを担っていたが、v4から出力は`Phalcon\Logger`に分離されて`Phalcon\Logger\Adapter\Stream`を利用する形に変更となった。ログのインターフェースはpsr-3準拠。
ということでアダプタがlog()など知らんと言っている。

[Fatal error: Uncaught Error: Call to undefined method Phalcon\Logger\Adapter\Stream::log\(\) - Discussion - Phalcon Framework](https://forum.phalcon.io/discussion/20207/fatal-error-uncaught-error-call-to-undefined-method-phalconlogge)
[Phalcon logger - Phalcon Documentation](https://docs.phalcon.io/4.2/api/phalcon_logger/#loggerlogger)
[PSR-3: Logger Interface 読了 #PHP - Qiita](https://qiita.com/tatsuo-iriyama/items/3e531c6922d0e5cae8bd)

```php:config/service.php
$formatter = new Phalcon\Logger\Formatter\Line("[%date%] %message%");
$formatter->setDateFormat('Y/m/d H:i:s');

$adapter = new Phalcon\Logger\Adapter\Stream("php://stdout");
$adapter->setFormatter($formatter);

$logger = new Phalcon\Logger(
	// ロガーに名前をつけられるがマニュアルに書いてないのでとりあえず任意名
	// public function __construct( string $name, array $adapters = [] );
    'hoge', 
    [
		// 任意名のラベル名。Loggerからラベルに対してアダプタを選択したりできる。
        'stdout' => $adapter,
    ]
);

$logger->info('this is log message');
```

### $application->handle(); triggers a "Wrong number of parameters"

フレームワークのエントリポイント`application#handle()で`はリクエストURIをとるようになった
[$application->handle\(\); triggers a "Wrong number of parameters" error after last upgrade - Discussion - Phalcon Framework](https://forum.phalcon.io/discussion/19151/applicationhandle-triggers-a-wrong-number-of-parameters-error-af)

```php:public/index.php:before
// Handle the request
$application = new Phalcon\Mvc\Application();
# $application->handle(); triggers a "Wrong number of parameters"
$response = $application->handle();
$response->send();
```

```php:public/index.php:after
// Handle the request
$application = new Phalcon\Mvc\Application();
$request = new Phalcon\Http\Request();
$response = $application->handle($request->getURI());
$response->send();
```

`Phalcon\Mvc\Router#handle()`も同じく

```php:config/route.php
// triggers a "Wrong number of parameters"
$router->handle();
```

```php:config/route.php
$request = new Phalcon\Http\Request();
$router->handle($request->getURI());
```

### Class 'Phalcon\Mvc\Url' not found

`Phalcon\Mvc\Url`はv4で`Phalcon\Url`になった。そしてv5ではまさかの`Phalcon\Mvc\Url`になった

### VoltEngine#setOptionsのキーが変わった

`compiled`プレフィックスがなくなった
[How to disable volt cache - Discussion - Phalcon Framework](https://forum.phalcon.io/discussion/11299/how-to-disable-volt-cache)

```php:config/service.php:before
$volt = new Phalcon\Mvc\View\Engine\Volt()
$volt->setOptions(
    [
        'compiledPath'      => BASE_PATH . $config->application->cacheDir,
        'compiledExtension' => '.compiled',
        'compiledSeparator' => '_',
        'stat' => true,
        'compiledAlways' => true
    ]
);
```

```php:config/service.php:after
$volt = new Phalcon\Mvc\View\Engine\Volt()
$volt->setOptions(
    [
        'path'      => BASE_PATH . $config->application->cacheDir,
        'extension' => '.compiled',
        'separator' => '_',
        'stat' => true,
        'always' => true
    ]
);
```

### Too few arguments to function {closure}(), 2 passed …

DIに渡すクロージャに設定していた引数がエラーになったので、キャプチャで渡す。変更になったのか元々バグっていてpsrで顕在化したのかな。
[Task Too few arguments to function {closure}\(\), 2 passed and exactly 4 expected · Issue \#4576 · swoole/swoole-src](https://github.com/swoole/swoole-src/issues/4576)

```php:config/service.php
// $di->setShared('voltService', function ($view, $di) use ( $config ){
$di->setShared('voltService', function () use ($config){

		$di = Phalcon\DI::getDefault();
        $volt = new Volt($di->get('view'), $di);
        $volt->setOptions(
            [
                'path'      => BASE_PATH . $config->application->cacheDir,
                'extension' => '.compiled',
                'separator' => '_',
                'stat' => true,
                'always' => true
            ]
        );
        return $volt;
    }
);
```

### Fatal error: Cannot override final method Phalcon\Mvc\Model::getSource()

`Model::getSource()`はオーバーライド不可となったのでイニシャライザに`Model::setSource('テーブル名')`で指定する
[Fatal error: Cannot override final method Phalcon\Mvc\Model::getSource\(\) - Discussion - Phalcon Framework](https://forum.phalcon.io/discussion/19799/fatal-error-cannot-override-final-method-phalconmvcmodelgetsourc)

```php:models/*.php
public function initialize()
{       
    $this->setSchema("phalcon_demo-app");       
    $this->setSource("users");        
}
```

### Fatal error: Uncaught Error: Class 'Phalcon\Validation\Validator' not found

カスタムバリデーションの実装で問題が発生

- `Phalcon\Validation\Validator`はアブストラクトクラス`Phalcon\Validation\Validator`に置き換えられた
- PHP7型宣言の厳格化でオーバーライドに戻り値の宣言が必要

```php:forms/*php
// phalcon v4でのカスタムバリデーションクラス実装
class CustomEmailValidator extends \Phalcon\Validation\AbstractValidator implements \Phalcon\Validation\ValidatorInterface
{
    public function validate(Validation $validation, $attribute): bool { ... }
}
```

### Fatal error: Declaration of { 関数宣言 } must be compatible with { 関数宣言 }: string

PHP7型宣言の厳格化でオーバーライドに戻り値の宣言が必要

### Phalcon\Model::findFirst to return null instead of false if no record was found

Phalcon\Model::findFirstで結果が無い場合に戻り値`false`が`null`になった
変更箇所が多岐にわたるので、モデルクラスでfindFirstをオーバーライドしてnullの場合はfalseを返却して後方互換性を維持する

### Model::saveのホワイトリストが効かない

> The save() method no longer accepts parameters to set data. You can use assign instead.

```php:php
// フィールドnullになる
$model->save($assign_key_val, $whitelist_key);

// assignに変更
$model->assign($assign_key_val);
$model->save();
```