ラベル cakePHP の投稿を表示しています。 すべての投稿を表示
ラベル cakePHP の投稿を表示しています。 すべての投稿を表示

2024年10月28日月曜日

deprecated: str_replace(): passing null to parameter #2 ($replace) of type array|string is deprecated

大量の str_replace のエラー

CentOS もサポート終了してしまったので、数ある Linux プロジェクトの多くを Ubuntu に変更してます。
当然、CakePHP3 のような古いバージョンで構築されているプロジェクトも移植の対象になります。
とあるプロジェクトを Virtual Machine 上に構築した Ubuntu 22.04 LTE 上で動作させようとして、最初に CakePHP home を表示したら、こんなエラーがたくさん出てきまして。

PHP8.1 あたりで仕様が変わったことによる問題なんですけどね。
CakePHP のソースも変更しなきゃならないの?とビビってしまいました。

動作環境

OS:Ubuntu 22.04.01 LTE
MySQL: MariaDB 15.xxx
PHP 7.4.33
phpMyAdmin: 5.2.1
CakePHP 3.10

ということで。
CakePHP3 を動作させるための環境なので、PHP 7.4 で動かそうとしてるんだけど、Cake の homeページを開こうとするときに、PHP 8.x が動いちゃってる、という状況なんですね。

PHP の動作バージョンを変更する

この方法は、あちこちに書いてありますが、alternatives で定義しておき、バージョンを変更します。
sudo update-alternatives --install /usr/bin/php php /usr/bin/php7.3 100
sudo update-alternatives --install /usr/bin/php php /usr/bin/php7.4 110
sudo update-alternatives --install /usr/bin/php php /usr/bin/php8.0 120

sudo update-alternatives --config php
There are 3 choices for the alternative php (providing /usr/bin/php).

Selection Path Priority Status
------------------------------------------------------------
0 /usr/bin/php8.1 130 auto mode
* 1 /usr/bin/php7.4 110 manual mode
2 /usr/bin/php8.1 130 manual mode
3 /usr/bin/php8.2 82 manual mode

Press to keep the current choice[*], or type selection number:
ここで、対象となる PHP をセットしておけばいいわけです。

apache2 の mod を変更する

ちゃんとPHPを7.4に変更したのに、ブラウザで表示させたときのエラーは消えません・・・PHP8 で動いてしまいます
原因は apache2 でした。
# ll mods-enabled/php*
lrwxrwxrwx 1 root root 29 Oct 28 11:06 mods-enabled/php8.1.conf -> ../mods-available/php8.1.conf
lrwxrwxrwx 1 root root 29 Oct 28 11:06 mods-enabled/php8.1.load -> ../mods-available/php8.1.load

# a2dismod php8.1
# a2enmod php7.4
# service apache2 restart

home を開いてみると、無事エラーが消えているのを確認できます。

まとめ

「環境構築」って、なんか、しょっちゅうやってる気がします。
え?コンテナを使えって?w知ってるw

ま、こういった悩みを持ってしまう人も、少なからずいるだろうから、書き留めておきますよっと。
c.f.
https://qiita.com/murakami77/items/a9945f2a8e51ca3791de

2024年5月5日日曜日

PHP の日本語ファイル名処理で文字化け

電子情報保存法に対応

請求書をサーバに保存して電子情報保存法に対応しましょ、という仕組み作りがあちこちで行われているかと思います。
過去の経験から、サーバー上に文書ファイルを保存しようとする場合、日本語のファイル名では保存しない方がいいと思いますよ。
たとえば、濁点、半濁点付きのファイル名「ぱぴぷぺぽ.pdf」ファイルをそのままサーバに保存しておいて、ブラウザでダウンロードできるようにしようとすると
Windows クライアントでは正しく表示できるけど、MacOS では正しく処理できない、といった問題が発生してしまいます。

そこで、サーバに保存するときには、ファイル名を半角英数字のみにして保存するようにしてみました。
ファイル名を拡張子と分離するには、PHP の pathinfo() 関数を使います。
半角英数ファイル名の構造には、BASE64 エンコードと使えない文字を置き換える base64UrlEncode() 関数を使って処理します。

BASE64を使ったエンコード処理

function base64UrlEncode($data) 
{ 
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); 
} 

ファイル名の拡張子以外の部分を base64UrlEncode で構築する処理を用意してみました。
入力フィールドで拡張子を指定しなかった場合は、.pdf ファイルだという処理も入れてみています。

function buildFileName($filename, $def_ext = 'pdf')
{
    $file_parts = pathinfo($filename);
    $filenode = $file_parts['filename'];
    $ext = $file_parts['extension'];
    if(strlen($ext) == 0)
    {
        $ext = $def_ext;
    }
    $name = $this->AsCommon->base64UrlEncode($filenode) . '.' . $ext;
    return $name;
}

日本語ファイル名が正しく処理されない

$filename = uildFileName("ぱぴぷぺぽ.pdf");
を実行すると、".pdf" という文字列が返ってきてしまいました・・・
またもや日本語処理に別の対応が必要になってしまいます・・・こまったもんだ・・・
調べると、ロケールの設定に依存しているため、というのがわかりまして。
setlocale(LC_ALL, 'ja_JP.UTF-8');
を実行してやると、上手くいくようになりました。

cakePHP での対応

pathinfo() やそのたぐいの関数を使うソースプログラムに、毎回 setlocale を記述するのは無駄を感じますね。
ましてや、プログラムを国際化対応しようとすると、あちこち変更しなければならなくなってしまいます。

cakePHP の config/app.php を見ると、次の記述があります。

'App' => [
    'encoding' => env('APP_ENCODING', 'UTF-8'),
    'defaultLocale' => env('APP_DEFAULT_LOCALE', 'ja_JP'),

これを利用して、config/bootstrap.php の 112 行目あたりに以下を追加しました。

setlocale(LC_ALL, Configure::read('App.defaultLocale') . '.' . Configure::read('App.encoding'));

まとめ

毎度のことながら、日本語対応は問題が多く発生しやすいですね。
国際化対応時に、上記対策でうまくいくのかどうかまでは試しておりません。あしからず。
今回も苦しめられましたが、なんとか事なきを得ました。

めでたしめでたし。

参考になったサイト
https://qiita.com/REAS07/items/3f86a0834d612edaecd6

2024年4月18日木曜日

cakePHP3でメールの受信

XSERVER でも使えるように

cakePHP3を使ったプロジェクトでメールを受信して添付ファイルを保存しようとしましたら。
まず、POP3用のライブラリをインストールしなきゃいけないことがわかりました。
で、VM上の Linux にインストールしてみました。

# pear channel-update pear.php.net
/usr/share/pear/Net にPOP3.php, Socket.php がインストールされました。
# pear install Mail_mimeDecode
/usr/share/pear/Mail にインストールされました。

でも、本番環境の XSERVER では root 権限でインストールなんてできないよね。
と、いうことで。 /usr/share/pear/のファイルを CAKEPHP_DIR /appendix/pear に無理やりコピーして使うことにしました。

require が動かない

 require('Net/POP3.php');
require('Mail/mimeDecode.php');
としなきゃいけないのに、パスが効いてないから読み込めない。
PHP で get_include_path() を呼び出すと、次のようなインクルードパスが設定されていました。
".:/usr/share/pear:/usr/share/php"
そこで、
set_include_path(get_include_path() . PATH_SEPARATOR . CAKEPHP_DIR . '/appendix/pear/');
を呼び出すことで require がうまくいってくれました。

ポート番号995(POP3S)での受信設定

アカウント情報は、とりあえず MailServers データベースにでも置いといて、次のように取り込むようにしました。

  $account = $this->MailServers->find()
    ->select(['pop3_server', 'pop3_port', 'pop3_account', 'pop3_psw'])
    ->first();
実際に POP3サーバに接続してログインする部分のコードは、こんな感じ
  $pop3 = new Net_POP3();
  $pop3->connect($account->pop3_server, $account->pop3_port);
  $pop3->login($account->pop3_account, $account->pop3_psw);
  $message_count = $pop3->numMsg();
$account->pop3_port を 995 にすると、接続ができずに悩みました。
pop3_server にプロトコルヘッダ ssl:// を付けてあげることで接続できました(^^)v

メール受信処理

実際には、message-id をDBに保存しておいて、すでに処理済みのメールかどうかをチェックしたりするのですが
メールの取り込みは次のような感じです

    for($mail_no = 1; $mail_no <= $message_count; $mail_no++)
    {
        $msg = $pop3->getMsg($mail_no);
        $decoder = new Mail_mimeDecode($msg);
        $mime = $decoder->decode([
            'include_bodies' => true,
            'decode_bodies' => true,
            'decode_headers' => true,
        ]);
        $messageid = $mime->headers['message-id'];
        $subject = $mime->headers['subject'];
        $sender = $mime->headers['from'];
        $date = $mime->headers['date'];
        // メール解析
        [$body, $attachments] = $this->analyze($mime);
        // 必要な処理
    }

今回は本文と添付ファイル情報を必要としたので、メールの mime 解析部分は、こんな感じです。

function analyze($mime)
{
    $body = "";
    $attachments = [];

    if(!isset($mime->parts))
    {
        // シングルパート(テキストのみ)
        $body = trim(mb_convert_encoding( $mime->body, "UTF-8", 'ASCII, JIS, UTF-8, SJIS' ));
    }
    else
    {
        // マルチパート
        foreach($mime->parts as $part)
        {
            if((strtolower($part->ctype_primary) == 'text') && (strtolower($part->ctype_secondary) == 'plain'))
            {
                $body .= trim(mb_convert_encoding( $part->body, "UTF-8", 'ASCII, JIS, UTF-8, SJIS' )) . "\n";
            }
            else if((strtolower($part->ctype_primary) == 'application')) 
            {
                $filename = $part->ctype_parameters['name'];
                if(empty($filename))
                {
                    $filename - $part->d_parameters['filename'];
                }
                if(0 < strlen($filename))
                {
                    if(isset($part->body))
                    {
                        $attachment = [];
                        $attachment['filename'] = $filename;
                        $attachment['body'] = $part->body;
                        $attachments[] = $attachment;
                    }
                }
            }
            else if(strtolower($part->ctype_primary) == 'multipart')
            {
                foreach($part->parts as $part2)
                {
                    if(strtolower($part2->ctype_primary) == "text") 
                    {
                        $charset = $part2->ctype_parameters['charset'];
                        $body = mb_convert_encoding($part2->body, 'UTF-8', $charset);
                        break;
                    }
                }
            }
        }
    }  
    return [$body, $attachments];
}

pear の修正

いろいろエラーが出たので、pear 側のソースも変更しました。

■Net/POP3.php 変更点
/* 第3パラメータが非推奨になっていたので削除
define('NET_POP3_STATE_DISCONNECTED',  1, true);
define('NET_POP3_STATE_AUTHORISATION', 2, true);
define('NET_POP3_STATE_TRANSACTION',   4, true);
*/
define('NET_POP3_STATE_DISCONNECTED',  1);
define('NET_POP3_STATE_AUTHORISATION', 2);
define('NET_POP3_STATE_TRANSACTION',   4);

// クラス名と同名のコンストラクタが非推奨なので変更
// function Net_POP3()
function __construct()

■Mail/mimeDecode.php 変更点
// ISO-2022-JPのメールヘッダを正しく解析してくれない。
function _decodeHeader($input, $default_charset=false)
{
    if (!$this->_decode_headers) {
        return $input;
    }
    // iconv_mime_decode を使うように変更。
    $input = iconv_mime_decode($input, ICONV_MIME_DECODE_CONTINUE_ON_ERROR);
    // Remove white space between encoded-words 以下を削除。
    return $input;
}

// はだかのHTMLテキストが渡されてエラーでまくりなので変更
function _quotedPrintableDecode($input)
{
    // Remove soft line breaks
    $input = preg_replace("/=\r?\n/", '', $input);
    $input = quoted_printable_decode($input);
    // Replace encoded characters 以下を削除
    return $input;
}

まとめ

XSERVERにインストールできないとなると、AWS EC2 あたりにサーバー移動しなきゃならないかな?と悩んでいたので、受信できるようになって助かりました。

めでたしめでたし。

2024年2月8日木曜日

CakePHP でアソシエーションを含めたレコードの読込み

containを使え

あちこちのサイトを見ると、contain を使った方法がたくさん書いてありますね。
たとえば、products テーブルに取引先のIDである partner_id が含まれているとき
$product_id で示されるレコードを取得するには、次のように書きます。

  // コントローラの initialize() で
  $this->loadModel('Products');
  // レコードの取り込み処理で
  $record = $this->Products->get($product_id, ['contain' => ['Partners']]);

cakePHP を書きなれている人なら、同じみでしょう。
QueryBuilder を使った find() などでもアソシエーションとともに読み込みできますね。

  $record = $this->Products->find()
    contain(['Partners'])
    ->first();

どんだけ書くねん!

『こう書いておけば動く』から、なんでしょうか・・・
たぶん、世界中の人が、さまざまなプロダクトのテーブル参照を『とっても便利な』QueryBuilder を使って読み込んでいるのでしょうね。

おおきなプロダクトになってくると、それぞれのテーブルごとにレコード参照のコントローラ.phpファイルが増えて、手に負えなくなってしまいますねぇ

簡単にしちゃいましょう

レコードが _id で示されるインデックス指定のフィールドを持っているとき、contain でアソシエーションを『含めずに』読み込む、なんてことは逆に少ないはずです。
少なくとも、私の担当するプロダクトではレコード読込でアソシエーション不要、なんてことは皆無に近いですね。
そこで、こんなコードを共通コンポーネントに書き加えました。

/**
 * 指定テーブルのアソシエーション(BelongsTo/BelongsToMany)を含めて
 * レコードを取得する
 */
function LoadRecordWithBelongs($table, $id)
{
  $record = false;
  $contains = [];
  $associations = $table->associations();
  $items = $associations->getByType(['BelongsTo', 'BelongsToMany']);
  foreach($items as $item)
  {
    $name = $item->getName();
    $contains[] = $name;
  }
  $record = $table->get($id, ['contain' => $contains]);
  return $record;
}
ようするに、belongsTo, belongsToMany のアソシエーション定義されているテーブルなら、そいつをまとめて読み込んでくれよ、というコードです。

まとめ

共通化することで、たくさんのソースを削除することができるようになりますが。
それは、これからやり始めようとしてるところです・・・w

めでたしめでたし。

2023年7月17日月曜日

XServer で cakePHP のシェルを cron で動作させる

XSERVERでcakephpのcronががが

普通に cakePHP3 のプロジェクトで Shell のコントローラを呼び出せるようにしていたスクリプト
XSERVER上で動作させようとすると、動かないじゃん!
ということで、苦労の末、動くようになったので手順をまとめてみる。

PHPのパスを確認

$ pwd
$ ll bin/php
lrwxrwxrwx 1 USERNAME members 23  8月 16  2020 bin/php -> /opt/php-7.3.16/bin/php

cron では、bin/cake ではなく、bin/cake.php を動作させる。

XSERVER での CRON設定画面でいじっていたら、反映されなくなったので(なぜかな?
直接 crontab -e で編集しました。

* */10 * * * /opt/php-7.3.16/bin/php /home/USERNAME/ドメイン/cakephp/bin/cake.php シェルスクリプト > /dev/null 2>&1

まとめ

cronのエラー通知もあまりアテにならず、同じように起動させるシェルスクリプトを書いて動作させたら、requirements.php が PHPバージョンがダメ!みたいなエラーをはいてましたが、なぜ動かなかったのかは、詳しく調査してません。

ま、動くようになったので、めでたしめでたし。

2021年12月6日月曜日

郵便番号から自動的に住所入力させる

cakePHP3 + bootstrap5 プロジェクトで郵便番号入力から自動的に住所欄を入力できるようにしてみました。

ふだん、css/jsはバージョンによって動きが変わったりしないよう、サーバ上にコピーして利用するようにしてますが 市区町村合併などで郵便番号が変化する場合があるので、こちらはgithubのサイトを参照するようにしてます。

[slip.ctp]

≤?php

$this->Html->script("//yubinbango.github.io/yubinbango/yubinbango.js", ['block' => true]);

?>

≤div class="col-md-6 h-adr">
    ≤span class="p-country-name" style="display:none;">Japan≤/span>
    ≤table class="table_client_info as_bd_cl">
        ≤tr>
            ≤th>≤input type="button" class="form-control as_button" value="着住所">≤/th>
            ≤th style="text-align: right;">郵便番号≤/th>
            ≤td>
                ≤input type="text" class="form-control p-postal-code" placeholder="郵便番号を入力" />
            ≤/td>
        ≤/tr>
        ≤tr>
            ≤th>住所1≤/th>
            ≤td colspan="2">
                ≤input type="text" class="form-control p-region p-locality p-street-address" placeholder="住所を入力" />
            ≤/td>
        ≤/tr>
        ≤tr>
            ≤th>住所2≤/th>
            ≤td colspan="2">
                ≤input type="text" class="form-control p-extended-address" placeholder="建物名、部屋番号を入力" />
            ≤/td>
        ≤/tr>

cssクラス部分は、サイズとか罫線を設定してます。
いやー。しかし、こりゃ便利だw
めでたしめでたし。

【追記】
最近になって、郵便番号での住所入力が必要になって、自分のブログを参照にしてみたら、なかなか動いてくれなかった(笑)
原因は、class="h-adr" を付加してなかったからでした。
上記の例では上部の div タグに設定してますね。
ここ、忘れないようにしてくださいw

cakePHP3:javascript 3桁単位でカンマを表示する数字入力

cakePHP3.9 + bootstrap5 の環境で金額入力のために3桁単位で自動で数字を入力できるようにしようとしたんですね。

cleave.js ってのが便利そうだったので使ってみました。

参考
フォームのinput要素に電話番号・日付・時間・金額など、数字のフォーマットを定義できるスクリプト -Cleave.js
https://coliss.com/articles/build-websites/operation/javascript/format-input-text-content-cleave.html

cleave.js
https://github.com/nosir/cleave.js

上記のサイトなどを参考にして、次のようなコードを javascript に記述してました。
[project.js]

    var cleave = new Cleave('.input-decimal', {
      numeral: true,
      numeralThousandsGroupStyle: 'thousand'
    });

[project.css]

    .input-numeral {
        text-align: right;
    }

[slip.ctp]

    ≤div class="row">
        ≤h6 class="col-4 d-flex align-items-center">小計≤/h6>
        ≤div class="col-8">≤input type="text" class="form-control input-numeral input-decimal" value="10,000" />≤/div>
    ≤/div>
    ≤div class="row">
        ≤h6 class="col-4 d-flex align-items-center">消費税≤/h6>
        ≤div class="col-8">≤input type="text" class="form-control input-numeral input-decimal" value="1,000" />≤/div>
    ≤/div>
    ≤div class="row">
        ≤h6 class="col-4 d-flex align-items-center">合計≤/h6>
        ≤div class="col-8">≤input type="text" class="form-control input-numeral input-decimal" value="11,000" />≤/div>
    ≤/div>

困ったことに、数字入力のinputタグが複数あると、上手く動いてくれなかったんです。
chrome の開発者ツールでは、以下のようなメッセージが出てしまいました。

cleave.min.js:8 [cleave.js] Multiple input fields matched, cleave.js will only take the first one.

いやいや、伝票入力画面に数値入力が一か所しかないなんてありえないでしょ!
ということで調べた結果、javascript を次のように変更しました。

[project.js]

    $('.input-decimal').toArray().forEach(function(field){
      new Cleave(field, {
        numeral: true,
        numeralThousandsGroupStyle: 'thousand'
      });
    });

伝票用のフォーム画面では、金額入力フィールドを動的に追加できるようにしましたが、追加の都度 new Cleaveするようにして、ちゃんと動いてくれました。
めでたしめでたし。

PHP ip2longの罠(2) ローカルIPアドレスが '::1'

XAMPPを使った cakePHP3のローカルな開発環境でブラウザ表示させる際に、$_SERVER['REMOTE_ADDR']を使ってIPアドレスを取得すると、localhostでは '::1' という値が返されてきた。

プログラムでは ip2long()関数を使って数値化されたIPアドレス値をデータベースに照合しているので、これじゃ使えない。

  $ip_address = $_SERVER['REMOTE_ADDR'];
  $ip_value = ip2long($ip_address);

実際、$ip_address='::1' の場合に $ip_value=ip2long('::1');を実行すると false が返ってきてしまっている。

WEBを検索すると、次の情報があったのでやってみた。(コントローラ内での呼び出し)

  $ip_address = $this->request->clientIp();

これでも、値は '::1' となる。こまったぞ・・・

よくよく考えると、自分のIPを返せばいいわけで。
次のコードでうまくいくようになった。

if($_SERVER["HTTP_HOST"] == "localhost")
{
	$ip_address = getHostByName(getHostName());
}
$ip_value = ip2long($ip_address);

無事取得できました。
めでたしめでたし。

2021年11月8日月曜日

PHP ip2longの罠

cakePHPのサーバー運用も多くやってますが、もっぱら cakePHP3.9 で作業することが多いです。
担当外の cakePHP1.2で構築されたサーバーくんのトラブルに駆り出されてみたので、そのお話です。

機器との接続がうまく行かないと言われましてですね。
機器は MySQLのテーブルに IPアドレス が記録されていまして、それを読み込んでアクセスしているわけです。

php には、ip2long(), long2ip() という関数が用意されていて、IPアドレスを整数値に変換したり逆に整数値をIPアドレスの文字列に変換できたりするわけで
データベースに文字列でIPアドレスを格納するよりも数値で格納しておく方が効果的なので、よく使われます。

たとえば、192.168.3.20 というIPアドレスを変換してDBに格納する場合は Windows10標準の電卓でHEX入力するといい。
192=C0, 168=A8, 20=14 なので、HEX(16進数)入力で C0A80314 と入力すると、DEC(10進数)部分に 3,232,236,308 が表示される。
DBには、この値 3232236308 を入れてしまえばいいわけ。

しかしだ。
動いてくれないのだよ。
なぜじゃ!?

ローカルに環境を構築する暇なんてなかったので、なかなかデバッグしづらい環境だったけど、原因を突き止めました。


Windows電卓は64bit演算なのですよ。
ターゲットの環境は32bit環境。

2148483647(0x80000000)以上の数値は long なので負数になるべきなのだYO!

ということで、2の補数を使って負数に変換しなきゃいけない。
64bit環境上に、こんなプログラムを作ってみました。

<?php
/*
    ip2long 32bit 処理
*/
$half = ip2long('128.0.0.0');   // 2148483647

$msg = '';
if(isset($_GET['ip']))
{
    $ip = $_GET['ip'];
    $long = ip2long($ip);
    if($half <= $long)
    {
        //MSB が1
        $long = ($long ^ (16 ** 8-1)) + 1;
        $long = 0 - $long;
    }
    $msg .= 'ip:' . $ip . ' value:' . $long;
}
?>

<HTML>
<BODY>
    <DIV>
        <?= $msg ?>
    </DIV>
</BODY>
</HTML>

https://seasoft.co.jp/bin/ip2long32.php で実際に動作できます。
2の補数なんて久しぶりに使ったから、ちょっと時間かかちゃったよ笑


めでたし、めでたし。

2021年10月21日木曜日

Ajax post で WEB-API 呼び出そうとしたらパラメータが渡せなくて少し悩みました。

Javascriptのコード(抜粋)

	var params = [];
	params['date'] = '2021-10-21';
	var url = 'APIのURL';

	$.post({
	      type: 'POST', 
	      url: url,
	      dataType: 'json',
	      data: params,
	      cache: false,
	    }).done(function(response){
	        /*do someting*/
	    }).fail(function(XMLHttpRequest, textStatus, error){
	        /*do someting*/
	    }).always(function(data){
	        /*do someting*/
	    });

受け口となるAPIのコード(cakePHP3.x の controller)

    public function index() 
    {
        $date = $this->request->getData('date');
        ;//以下略

XAMPP+VS Code 環境でブレイクポイントを貼って $date 変数の中身を見ると
からっぽ!
なぜじゃぁぁぁぁ!。

ということで、小一時間悩みました。
わかる人にはすぐわかるのかもしれませんが、この何にも警告を出してくれないトラップは時間かかってしまうことがありますね~。


間違っているのは javascript 抜粋の1行目。

	var params = [];

ここです!

正しくは

	var params = {};

これだけ。 わすれないように、メモ。っと。

2021年9月17日金曜日

cakePHP で開発環境とレンタルサーバでデータベースを日本時間にする

 cakePHP3.9 をXAMPP上で開発しているプロジェクトをレンタルサーバ(XSERVER)上で動作させようとしたら、データベースの日本時間設定がうまくいかなかったので、まとめてみます。

まず、あちこちのサイトに書いてあるこの方法。

config/app.php

'App' => [
   ;
        'defaultTimezone' => env('APP_DEFAULT_TIMEZONE', 'Asia/Tokyo'),
   ;
'Datasources' => [
        'default' => [
                ;
            'timezone' => 'Asia/Tokyo',
                ;

この方法では XAMPP上のプロジェクトはちゃんと動作するけど、XSERVERで実行すると
Unknown or incorrect time zone: 'Asia/Tokyo'
というエラーが発生して、動作しません。

別のWEBサイトでの方法では、'Asia/Tokyo' の代わりに 'JST' を指定すればOK
という情報もありましたが・・・
Unknown or incorrect time zone: 'JST'
XSERVERでは動きませんでしたね。

MySQL の mysql テーブル timezoneをインポートして設定するといいのでしょうけれど
XSERVERでそれをやる方法がわかりませんでした

で、最終的にうまくいった方法を披露しておきます。

'App' => [
   ;
        'defaultTimezone' => env('APP_DEFAULT_TIMEZONE', 'Asia/Tokyo'),
   ;
'Datasources' => [
        'default' => [
                ;
            'timezone' => '+9:00',
                ;

Datasources側だけ、timezoneを '+9:00' に設定するわけです。
これでデータベース内の記録日時とWEBサイトで表示される日時を同期させることができました。
めでたし、めでたし。