CakePHP1.3をPHP7.3へ移行するには

PHP7.3対応版のcakephp-1.3.21

PHP5.6からPHP7.3やPHP7.4に移行すると、処理速度が150%から200%改善するそうです。

ところが、CakePHP1.3はPHP7.xで動きません。

CakePHP1.3にはObjectクラスやStringクラスが定義してあります。ところが、object(PHP 7.2以降)やstring(PHP 7以降)が、PHPの予約語になってしまったからです。

なんと、有志によるPHP7.3対応版のcakephp-1.3.21とsimpletest-1.0.1がgithubで公開されています。

GitHub - littleant/cakephp-1.3.21: CakePHP 1.3.21: The Legacy Rapid Development Framework for PHP7+
CakePHP 1.3.21: The Legacy Rapid Development Framework for PHP7+ - littleant/cakephp-1.3.21
GitHub - littleant/simpletest-1.0.1: Simpletest 1.0.1: Updated for PHP7+
Simpletest 1.0.1: Updated for PHP7+. Contribute to littleant/simpletest-1.0.1 development by creating an account on GitH...

ロードマップ

マルチドメイン、ドメインごとにPHPバージョンを設定できる共用サーバを想定しています。(AWSやGCPでも同じ要領です)

(1)本番環境用のgitリポジトリを作成します。

もしソース管理していなければ、gitリポジトリを作ります。
masterブランチにpushすると、本番環境に自動デプロイするように設定します。
PHP 5.xのdocker環境を作ります。
PHPバージョンの設定が、.htaccessなのか、php.iniなのか、その記述内容を確認し、必要ならgitに追加します。

ローカル共用サーバDBサーバ
(1)masterブランチ
docker
PHP5.x
本番環境
www.example.jp
PHP5.x
本番DB
mysql123.example.jp
production_db

(2)共用サーバで、ステージング環境用のサブドメインを作ります。

サブドメインのPHPバージョンをPHP7.3に設定して、phpinfoを確認します。
PHPバージョンの設定が、.htaccessなのか、php.iniなのか、その記述内容を確認します。

また、ステージングDBを新規作成します。
本番DBからエクスポートして、ステージングDBへインポートします。

ローカル共用サーバDBサーバ
masterブランチ
docker
PHP5.x
本番環境
www.example.jp
PHP5.x
本番DB
mysql123.example.jp
production_db
(2)ステージング環境
stg.example.jp
PHP7.3
(2)ステージングDB
mysql456.example.jp
staging_db

(3)PHP7.3用のstagingブランチを作成します。

(2)で調べたステージング環境のPHPバージョンの設定ファイル(.htaccessやphp.ini)を必要に応じてgitに追加します。

stagingブランチにpushすると、ステージング環境に自動デプロイするように設定します。

PHP7.3のdocker環境を作ります。
PHP7.3対応版のcakephp-1.3.21のcake/をコピーします。
必要な修正をします。

  • ステージング環境で動作確認
  • ローカルのstagingブランチで修正
  • ローカルのdockerで動作確認
  • gitコミット、stagingブランチをpush

を繰り返します。

ローカル共用サーバDBサーバ
masterブランチ
docker
PHP5.x
本番環境
www.example.jp
PHP5.x
本番DB
mysql123.example.jp
production_db
(3)stagingブランチ
docker
PHP7.3
(3)ステージング環境
stg.example.jp
PHP7.3
ステージングDB
mysql456.example.jp
staging_db

(4)stagingブランチからmasterブランチへマージします。

githubコンソールで、Pull Requestを新規作成して、stagingブランチからmasterブランチへマージします。

masterブランチの内容が、本番環境に自動デプロイされます。

ローカル共用サーバDBサーバ
(4)masterブランチ
docker
PHP7.3
(4)本番環境
www.example.jp
PHP7.3
本番DB
mysql123.example.jp
production_db
stagingブランチ
docker
PHP7.3
ステージング環境
stg.example.jp
PHP7.3
ステージングDB
mysql456.example.jp
staging_db

準備

デイリーバックアップの設定

もしデイリーバックアップしていないなら、共用サーバの管理画面にログインして、バックアップオプションをポチっとしましょう。多くの共用サーバで、月額1000円〜2000円です。

さらに、共用サーバのバックアップオプションとは別に、独自のデイリーバックアップも設定したほうがいいでしょう。

2012年、ファーストサーバのデータ消失事故がありました。ファーストサーバ側の作業ミスで、バックアップも含めてデータ消失してしまいました。ほとんどのサイトは独自のバックアップをしていなかったので、サイトの復旧は難しかったそうです。

git管理とgithub

もしソース管理していないなら、git管理を始めましょう。ここではgithubで説明しますが、backlogもおすすめです。backlogは日本で広く使われているGit総合サービスです。

自社のgithubアカウントを作り、githubにprivateリポジトリを作ります。ローカルPC内のgitリポジトリとpush/pullして同期します。

もしPHP7.3対応の作業を外注会社に発注する場合、外注会社のgithubアカウント下にリポジトリを作るのはおすすめしません。

githubのリポジトリには、課題管理のIssue、資料やメモを残すwikiがあります。ソースファイルだけでなく、これらも貴重な財産です。

外注会社を変更するときや、外注会社と揉めたりしたとき、リポジトリ譲渡してもらえないと、貴重なIssueやwikiを失ってしまいます。最初から自社のgithubアカウントのリポジトリにしたほうがいいでしょう。

デプロイの自動化

次に、githubからpullして、共用サーバにアップロードする作業を自動化します。

共用サーバへのアップロード方法は、FTPでもSSHでも自動化できます。

デプロイ自動化はシェルスクリプトを書く必要があるので、少しハードルが高いですが、開発者もデザイナーもアップロード作業をする必要がなくなるので、それだけの価値があります。

  • FTP操作ミスがなくなります。
  • 複数人によるファイルの上書き事故や先祖返りがなくなります。
  • FTPアカウントを使いまわす必要がありません。
  • 一人ひとりにFTPアカウントを発行する必要がありません。

自動化のクラウドサービスは、GithubAction、Circle CITravis CIなど、いくつもの選択肢があります。Jenkinsサーバを用意する方法は柔軟性がありますが、難易度も高くなります。

本番サーバへのFTPは何回やっても慣れなかったよ、そのたびに緊張して心臓ドキドキするよね

PHP4.xのdocker環境

PHP4.xの頃からのユーザ向けに、PHP4.xを提供している共用サーバはあるようです。ですので、PHP4.x + CakePHP1.3のWebアプリがないとは言い切れません。

ただし、PHP4.xのdocker環境を作ろうと思っても、いまやPHP4.xはyumでインストールできません。yumリポジトリがPHP4.xの公開を停止しているためです。

そこで、docker環境をCentOS/6 + xamppで用意するか、(docker環境ではなく)Windows + xamppで用意します。

xamppは、Windows版、macOS版、Linux版とあり、PHP4.xからのPHPバージョンをダウンロードできます。

PHP4.xからいきなりPHP7.3へ移行するのではなく、PHP4.x〜PHP5.2〜PHP5.6〜PHP7.3と少しづつ移行とリリースを繰り返したほうがいいかもしれません。

PHP5.xのdocker環境

現在の本番環境のPHPやMySQLのバージョンを調べて、CentOS/6やCentOS/7をベースに、PHP5.x、MySQL/5.xやmariadbを選び、docker環境を用意します。

xdebugも使えるようにしておきます。

RUN yum install -y http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
RUN yum install -y --enablerepo=remi-php56  php
RUN yum install -y --enablerepo=remi-php56  php-pecl-debugCode language: Dockerfile (dockerfile)

一時的に <?php phpinfo(); を書いた phpinfo.phpを置いて、たしかにPHP5.x になっていることを確認します。

本番データベースからデータをエクスポートして、docker環境のMySQLにインポートします。

もしPHP5.1やPHP5.2など古いバージョンの場合、PHP5.1からいきなりPHP7.3へ移行するのではなく、PHP5.2〜PHP5.6〜PHP7.3と少しづつ移行とリリースを繰り返したほうがいいかもしれません。

ユニットテスト、E2Eテスト

simpletestのユニットテスト、Selenium IDEのE2Eテストがあれば、docker環境でテストして、オールグリーンを確認します。

Selenium IDEは、操作を記録して、自動再生します。今からでも、比較的簡単にE2Eテストを用意できます。

E2Eテスト実行後、PHPエラーログを調べて、Notice、Warning、Errorがないか確認します。

デプロイ自動化に組み入れて、テストを合格したら、デプロイするようにすると、テストし忘れがありません。

PHP7.3のdocker環境

PHP 5.xのDockerfileをコピペして、PHP 7.3のdocker環境を作ります。

--enablerepo=remi-php56

--enablerepo=remi-php73
に置換します。

RUN yum install -y http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
RUN yum install -y --enablerepo=remi-php73  php
RUN yum install -y --enablerepo=remi-php73  php-pecl-debugCode language: Dockerfile (dockerfile)

一時的に <?php phpinfo(); を書いた phpinfo.phpを置いて、たしかに PHP7.3 になっていることを確認します。

公式のcakephp-1.3.xとの差分を調べる

CakePHP公式のcake/と、myproject/cake/ の修正箇所(差分)を調べます。

その差分を、PHP7.3対応版のcakephp-1.3.21にも適用します。

CakePHP公式のcake/ +「差分」=myproject/cake/
PHP7.3対応版のcake/ +「差分」=PHP7.3対応版のmyproject/cake/

としたいわけです。

cake/ をまったく修正していなければ、それでいいのですが、意外にも cake/ を修正していることが多いものです。

myproject/当初からgit管理している場合は、git log -p cake で調べることができます。

$ git log -p cake/Code language: Backus–Naur Form (bnf)

git管理していたとしても、CakePHP公式のcake/ と myproject/cake/ の差分を調べたほうがいいでしょう。

まず、CakePHPのバージョンを調べます。myproject/cake/VERSION.txt の最後に記載してあります。

////////////////////////////////////////////////////////////////////////////////////////////////////
// +--------------------------------------------------------------------------------------------+ //
// CakePHP Version
//
略
// @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
// +--------------------------------------------------------------------------------------------+ //
////////////////////////////////////////////////////////////////////////////////////////////////////
1.3.21Code language: plaintext (plaintext)

この例では「1.3.21」です。

次に、githubのCakePHP公式リリースを調べます。
1.3.21なら、
https://github.com/cakephp/cakephp/releases/tag/1.3.21
にアクセスします。

Source code(zip)、Source code(tar.gz)どちらかをダウンロードして、解凍します。

解凍した cakephp-1.3.21/cake/ と、myproject/cake/ の差分を調べます。

myproject/をmyproject_copy/へコピーしておき、cakephp-1.3.21/cake/ と myproject_copy/cake/ の差分を調べると、myproject/のブランチに関係なく、差分を表示できるので便利です。

ターミナルでは、diffコマンドに -Rオプションをつけます。しかし、ターミナルで見ても、わかりづらいです。

GUIのdiffツール、DiffMergeがおすすめです。Windows、macOS、Ubuntu版があります。

また、Ununtuには、meldというGUI diffツールが標準インストールされているので、meldを使ってもいいでしょう。

myproject/cake/ をphpcsで自動整形していないといいね

自動整形した分も「差分」になってしまって、何を修正したのかわからないのよ

ベンチマーク

どれくらい速くなるのか、各環境でトップページの表示時間を測定します。測定は、ab(apache bench)コマンドを使います。

次のような表を用意しておき、各環境の準備ができたら、測定して記録していきます。

環境PHP同時=1
mean
(ミリ秒)
同時=1
median
(ミリ秒)
同時=10
mean
(ミリ秒)
同時=10
median
(ミリ秒)
本番
www.example.jp
5.616997
本番
docker
5.6
ステージング
docker
7.3
ステージング
stg.example.jp
7.3
本番
www.example.jp
7.3

各環境について、同時接続数=1、同時接続数=10の2パターンを測定します。

# 同時接続数=1
$ ab -n 100 -c 1 "https://www.example.jp/"

# 同時接続数=10
$ ab -n 100 -c 10 "https://www.example.jp/"Code language: Bash (bash)

abコマンドの結果の最後のほうに、meanやmedianがあります。

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       28   60 128.2     37    1111
Processing:    35  109 108.6     54     556
Waiting:       35  106 101.2     54     396
Total:         68  169 167.4     97    1200

Totalのmeanとmedianの値を、さきほどの表に記録します。

項目意味計算方法
mean
平均値
最大値も含めた
全体としてのパフォーマンス
全ての接続のレスポンス時間を
合計して、回数で割った値。
最大値の影響を受けやすい。
median
中央値
普通派のパフォーマンス全ての接続のレスポンス時間を
短い順にソートして、その真ん中の値。
個数が99個なら、50番目の値。
mode
最頻値
(abにはない)
多数派のパフォーマンス全ての接続のレスポンス時間を
10〜、20〜と範囲に分けて、カウントして
最も多かった範囲

共用サーバは、曜日や時間帯でアクセス数が違うから、ベンチマークにも影響しそうだよ?

気になるなら、ベンチマークをとる曜日や時間帯をそろえるといいわね

ローカルPCのdocker環境だって、ウィルススキャンやバックアップ処理が、ベンチマークに影響しそうだよ?

気になるなら、ウィルススキャンやバックアップが走っていないときにベンチマークをとればいいのよ

PHP7.3対応版cakephp-1.3.21へアップグレード

こまめに、gitコミット、動作確認を繰り返します。

cake/とsimpletest/

別のディレクトリに、PHP7.3対応版cakephp-1.3.21とsimpletest-1.0.1をダウンロードします。

https://github.com/littleant/cakephp-1.3.21
https://github.com/littleant/simpletest-1.0.1

既存のmyproject/のcake/、app/vendors/simpletest/ を削除します。

同じ位置へ、PHP7.3対応版のcake/、simpletest/ をコピーします。

PHP7.3対応版では、いくつかのファイル名がリネームされました。myproject/のcake/を削除せずに上書きコピーすると、リネーム前の(公式版の)古いファイルも残ってしまいます。myproject/のcake/を削除してからコピーするほうが確実です。

ここで、gitコミットします。

この時点で、PHP5.6では動きませんので、PHP7.3のdockerを動かします。一時的に <?php phpinfo(); を書いた phpinfo.phpを置いて、たしかに PHP7.3.x になっていることを確認します。

app/config/core.phpで、debugを1にします。ErrorやWarnig、Noticeが表示されるようになります。もし、header sent alreadyが表示されてエラー停止するようなら、debugを0も戻します。

Configure::write('debug', 1);Code language: PHP (php)

ここからはふつうのデバグだね。xdebugでトレースできると原因を探りやすいよね

cake/の差分

「公式のcakephp-1.3.xとの差分を調べる」で調べたcake/の差分を適用します。

つまり、IDEやテキストエディタで該当ファイルを開いて、メソッド名を検索して、確認しながら、差分を追加・削除します。

gitコミットします。

クラス名の変更

PHP7.3対応版では、次のクラス名がリネームされました。

app/ の *.phpや*.ctpを検索して、置換します。

CakePHP1.3公式PHP7.3対応版
ObjectCakeObject
StringCakeString

class Foo extends Object だけでなく、

if (get_parent_class($called) == 'Object') {Code language: PHP (php)

のように参照していることもあります。

メソッド名の変更

PHP7.3対応版では、次のメソッド名がリネームされました。

app/ の *.phpや*.ctp を検索して、置換します。

CakePHP1.3公式PHP7.3対応版
JsHelper->value()JsHelper->jsonValue()
JavascriptHelper->value()JavascriptHelper->jsonValue()
JsBaseEngineHelper->value()JsBaseEngineHelper->jsonValue()

ファイル名の変更

PHP7.3対応版では、cake/libs/error.phpが cake/libs/error_handler.phpにリネームされました。

404エラーや500エラーの表示をカスタマイズしている場合、app/app_error.phpが参照しています。

app/ の *.php や *.ctp を検索して、置換します。

CakePHP1.3公式PHP7.3対応版
cake/libs/error.phpcake/libs/error_handler.php
App::import('Core', 'Error')App::import('Core', 'ErrorHandler')

データベースドライバの変更

PHP7.3対応版では、「mysql」と「mssql」が削除されました。

app/config/database.phpで、mysqlを使っている場合は、mysqli にリネームします。

app/models/datasource/db/*.php で、DboMysql をextendsしている場合は、DboMysqli にリネームします。

CakePHP1.3公式PHP7.3対応版
mssql削除
mysqlmysqli
DboMysqlDboMysqli
App::import('Datasource', 'DboMysql');App::import('Datasource', 'DboMysqli');

Security.cipherSeed

PHP標準やCakePHP標準のセッションクッキーには、影響しません。

app/config/core.phpのConfigureのSecurity.cipherSeedが、int型になりました。

以前のように、string型29文字のままでは、Cookieが正しくデコードされません。

17桁以内にするか、サンプルコードのように9桁にするといいでしょう。

// 公式 string 29文字
	Configure::write('Security.cipherSeed', '76859309657453542496749683645');


// php 7.3対応版 64bitでは、最大 9*10^18
	Configure::write('Security.cipherSeed', 768593096);
Code language: PHP (php)

暗号化クッキー

CakePHP1.3の暗号化クッキーに関して重要なことは、PHP5.6からPHP7.3へのアップグレード前後で、Security.cipherSeedに同じ値を設定できたとしても、

CakePHP1.3の暗号化クッキーに関して重要なこと

PHP5.6のサイトの暗号化クッキー
PHP7.3のサイトは復号できません

(暗号化をfalseにして、$this->Cookie->writeしたクッキーには影響しません)

CakePHP1.3はクッキーの暗号化/復号化を処理するとき、rand()のシーケンスを利用します。暗号化で使うrand()のシーケンスと、復号時のrand()のシーケンスは同じである必要があります。

ところが、PHP5.xとPHP7.1で、srandの仕様が変わりました。

後方互換性オプション MT_RAND_PHPがあるので、PHP5.6とPHP7.3のrand()のシーケンスは同じであると、期待できそうですが...

ねんのため、PHP5.6とPHP7.3のrand()のシーケンスを確認しました。srandに同じint値を渡し、後方互換性オプション MT_RAND_PHPを渡しました。

残念ながら、PHP5.6とPHP7.3のrand()のシーケンスは違うものでした。

つまり、PHP5.6のサイトで暗号化したクッキーを、PHP7.3のサイトは復号できません。

// PHP5.6
srand(0);
rand(0, 255);  // 215
rand(0, 255);  // 100
rand(0, 255);  // 200

// PHP7.3
srand(0);
rand(0, 255);  // 172
rand(0, 255);  //  47
rand(0, 255);  // 117

// PHP7.3
srand(0, MT_RAND_PHP);
rand(0, 255);  // 114
rand(0, 255);  // 151
rand(0, 255);  // 183
Code language: PHP (php)

クッキーはブラウザ側で消去できてしまうよね

だから、クッキーに重要なデータを保存していないはずだから、引き継げなくても問題ないと思うけど

もし引き継ぎたいとき、方法はあるの?

暗号化クッキーは、ブラウザ上のjavascriptではクッキーの値を読めないから、ブラウザ側では使っていないはず。つまり、サーバー側でしか利用していない、セッションデータ的なものの可能性が高いわね。だとしたら、PHP7.3に移行する前に、セッションを使うように改修すれば、セッションデータは引き継げると思うわ

セッションに改修せずに、暗号化クッキーを使いたい場合は?

クッキーの暗号処理だけ、PHP5.6で処理できればいいんだけど

えー!そんなことできるの?というか、せっかくPHP7.3に移行するのに残念な感じだね

簡単ではないわよ。
PHP5.6を別サイトとして用意して、クッキーの暗号処理のAPIを用意したり、

共用サーバは、PHPバージョン別にphp-cliを呼べることが多いから、クッキーの暗号処理をコマンラインプログラムとして切り出すとかかしら。

MySQL

sql-modeをONLY_FULL_GROUP_BY無効にする必要があります。CakePHPは、このモードと互換性のないクエリを作成します。

以下、5.1.7 サーバー SQL モード から引用

ONLY_FULL_GROUP_BY

GROUP BY 句で名前が指定されていない非集約カラムを、選択リスト、HAVING 条件、または (MySQL 5.6.5 以降で) ORDER リストが参照するクエリーを拒否します。

ONLY_FULL_GROUP_BY が有効な場合、次のクエリーは無効です。1 番目は、選択リスト内の非集約の address カラムが GROUP BY 句で名前を指定されておらず、2 番目は、HAVING 句の max_ageGROUP BY 句で名前を指定されていないため、ともに無効になります。

mysql> SELECT name, address, MAX(age) FROM t GROUP BY name;
ERROR 1055 (42000): 't.address' isn't in GROUP BYCode language: plaintext (plaintext)
mysql> SELECT name, MAX(age) AS max_age FROM t GROUP BY name
    -> HAVING max_age < 30;
Empty set (0.00 sec)
ERROR 1463 (42000): Non-grouping field 'max_age' is used in HAVING clauseCode language: plaintext (plaintext)

2 番目の例では、HAVING MAX(age) を使用するようにクエリーを書き換えることで、集約関数で名前を指定されているカラムが参照されるようになります。(max_age は集約関数そのものであるため失敗します。)

クエリーに集約関数があって GROUP BY 句がない場合、ONLY_FULL_GROUP_BY が有効なときに、クエリーは選択リストまたは ORDER BY リストに非集約カラムを含めることができません。

mysql> SELECT name, MAX(age) FROM t;
ERROR 1140 (42000): Mixing of GROUP columns (MIN(),MAX(),COUNT(),...)
with no GROUP columns is illegal if there is no GROUP BY clauseCode language: plaintext (plaintext)

新機能: Session.cookie_samesite と CookieComponent

PHP7.3+が必要です。

SameSite-cookie-parameterを設定できるようになりました。

app/config/core.phpやapp/config/bootstrap.phpなどで、

Configure::write('Session.cookie_samesite', 'Lax');Code language: PHP (php)


コントローラで、

$this->Cookie->samesite = "None";Code language: PHP (php)

phpcc7(PHP 7 Compatibility Checker)

GitHub - sstalle/php7cc: PHP 7 Compatibility Checker
PHP 7 Compatibility Checker. Contribute to sstalle/php7cc development by creating an account on GitHub.

コードのPHP7との互換性をチェックします。

splitなどの非推奨となった関数や、PHP4形式のコンストラクタを検出します。

インストール方法

$ composer global require sstalle/php7ccCode language: Bash (bash)

チェック方法

app/ 下をチェックするには、

$ ~/.config/composer/vendor/bin/php7cc --except=vendor --except=foo app/Code language: Bash (bash)

app/vendor/ を除外して、app/ 下をチェックするには、

$ ~/.config/composer/vendor/bin/php7cc --except=vendor  app/Code language: Bash (bash)

その他

各ページにアクセスして、エラー表示がないか確認します。おそらく多数のWarningやErrorがでるので、コツコツと修正します。

Headers already sent

PHP error ini_set(): Headers already sent. You cannot change the session module's ini settings at this time
PHP error session_set_save_handler(): Cannot change save handler when headers already sent

ターミナルからsimpletestを実行すると、headers already sent警告が表示されます。次の箇所を@でエラー抑制します。

// cake/libs/cake_session.php:220付近
// 修正前
        $this->__initSession();

// 修正後 @でエラー抑制
        @$this->__initSession();Code language: PHP (php)

Declaration x should be compatible with y

Warning Declaration of Foo::yyy() should be compatible with Bar::yyy($options = Array)

子クラスのメソッドの引数定義が、親クラスのメソッドと違うという警告です。コピペし忘れの場合が多いので、親クラスからコピペします。

count(): Parameter must be an array

Warning count(): Parameter must be an array or an object that implements Countable

$this->MyModel->find()の返り値は、false|arrayです。falseのときに、count()に渡すと、この警告がでます。is_array()でチェックしてから、count()に渡すようにします。

// 修正前
$data_arr = $this->MyModel->find('all', $params);
if (count($data_arr) > 0) {

// 修正後
$data_arr = $this->MyModel->find('all', $params);
if (is_array($data_arr) && count($data_arr) > 0) {Code language: PHP (php)

ソースを見ていると、リファクタ魂に火がつきそうになるよ

コメントにTODOを残しておくだけにして、PHP7.3で動かすことに集中してね

タイトルとURLをコピーしました