cakePHP4の日時についてFrozenTimeクラスの使い方を簡単に説明

概要

cakePHP4では日時についてFrozenTimeクラスを使います。
(※confi/app.phpで「defaultTimezone」を「Asia/Tokyo」にしておけば、そのタイムゾーンで動作します。)
しかしながらCookBookやAPIリファレンスを参照しても使い方がよくわからないので、自分がよく使うものだけでも簡単なメモを残そうと思います。

前提環境
  • cakePHP: 4.1.1

一応、該当のソースコードの場所について

簡単な説明だけでは不足だ!という方はソースコードを眺めてみてください。
場所がわかりにくく、かつ複数のファイルにまたがっているのでいくつか記載します。
(※下記以外にもあるので必要な方は探してみてくださいー。)

FrozenTimeクラス
/vendor/cakephp/cakephp/src/I18n/FrozenTime.php
親であるChronosクラス
/vendor/cakephp/chronos/src/
ChronosのTraitであるDifferenceTrait
/vendor/cakephp/chronos/src/Traits/DifferenceTrait.php

使い方

DBのdatetime型のカラムの取得と保存

/*
 * itemsテーブルのregistered_timeカラムがdatetime型の場合
 *

// 取得した「$item->registered_time」はFrozenTimeクラスのオブジェクトになっています。
$item = $this->Items->get($id);
echo $item->registered_time;

// 保存する場合はそのまま入れます。
// その1:現在時刻
$datetme = FrozenTime::now();
// その2:日時指定
$datetme = new FrozenTime('2000-01-10 00:00');

$item->registered_time = $datetme;

時間の加算について

// 分数の差分は「addMinutes」メソッドを使います。
// 第1引数はintで分を表します。

$expiration = $expiration->addMinutes(5);

// これ以外にも「add~」メソッドが同様に年、月、日の単位で加算を返します。

時間の減算(差分)について

// 秒数の差分は「diffInSeconds」メソッドを使います。
// 第1引数はFrozenTimeクラスのオブジェクトです。
// この場合は「$item->registered_time」-「$nowTime」になります。
// 第2引数は差分を絶対値とするかどうか。省略はtrueで絶対値扱いです。
// 下記の例ではfalseなので絶対値ではなく、正負の値になります。
$second = $nowTime->diffInSeconds($item->registered_time, false);

// これ以外にも「diffIn~」メソッドが同様に年、月、日の単位で差分を返します。

cakePHP4でログイン時にメールを送信する

概要

不正ログイン防止の一環として、cakePHP4でログイン時にユーザーにメールを送信する方法について解説します。

前提環境
  • Hosting Server: さくらインターネット
  • PHP: 7.4.9
  • cakePHP: 4.1.1

コードのポイント

  • ログイン時の処理なので、UsersControllerのloginメソッドのログイン成功したあと、の箇所に記載します。
  • cakePHP4ではMailerクラスを使用するので、UsersControllerのnamespaneの後に「use Cake\Mailer\Mailer;」を記載してください。
    ※詳しくはcakePHP4のマニュアルのMailerの項を参照してください。

サンプルコード

UsersController

<?php
namespace App\Controller;
use Cake\Mailer\Mailer;  // Mailerを使用する宣言を追加

class UsersController extends AppController
{
    public function login()
    {
        $this->request->allowMethod(['get', 'post']);
        $result = $this->Authentication->getResult();
        if ($result->isValid()) {
            // ログイン成功時にメール送信処理を行います。
            // Authenticationを使ってusersテーブルのユーザー情報を取得
            $user = $this->Authentication->getIdentity();
            $mailer = new Mailer('default');
            $mailer->setEmailFormat('html')
                ->setFrom([FROM_MAIL => FROM_MAIL_NAME]) // Fromのメールアドレス等はconfig/bootstrap.phpで定数宣言しています。
                ->setTo($user->email)
                ->setSubject('ログインのお知らせ')
                ->viewBuilder()
                ->setTemplate('login') //  templates/email/html/login.php
                ->setLayout('default')  // templates/layout/email/html/default.php
                ->setVar('username', $user->username);  // usersテーブルに追加したusernameカラムの情報をテンプレートに適用
            $mailer->deliver();

            $redirect = $this->request->getQuery('redirect', [
                'controller' => 'Pages',
                'action' => 'home',
            ]);
    
            return $this->redirect($redirect);
        }

        // 認証失敗した場合は、エラーを表示します
        if ($this->request->is('post') && !$result->isValid()) {
            $this->Flash->error(__('メールアドレスパまたはパスワードに誤りがあります。'));
        }
    }

// 以下省略

ASP.NET Webフォームで「1 つのページには、1 つのサーバー側 Form タグのみを指定できます」のエラー

概要

ASP.NET Webフォームで「1 つのページには、1 つのサーバー側 Form タグのみを指定できます」のエラーが出た場合の原因と対応方法についてです。

前提環境
  • Windows 10
  • Visual Studio Professional 2019
  • .NET Framework 4.2
  • C#

原因について

単純に「formタグ」が2つ以上設定されているために発生するエラーです。

通常のHTMLであればformタグは2つあっても問題ありませんが、ASP.NetのWebフォームでは1画面にformタグは1つまで、となっています。

なぜならば通常のホームページと異なり、画面上で「ボタンを押した」「値が変わった」など、Windowsアプリ開発で「イベント」と呼ばれているものが発生した場合、WebフォームではサーバーにPost送信され、画面が更新されるためです。これは先程述べた「イベント」駆動(Driven)の開発をWeb上の開発に当てはまめて開発効率を向上させる狙いがあります。

しかしながら、それ故1つの画面に1つのFormとして全てのイベントを集約するようになっています。

対応方法について

対応方法はとても単純で、1画面に1つのformタグとしてください。formタグが2つあればどちらか削除してください。

※マスターページ(テンプレート)を用いている場合、マスターページと個別ページの両方に記載されてしまうことがあるので注意してください。

cakePHP4のインストール方法

概要

cakePHP4系を下記環境にインストールしたときのメモ書きです。

前提環境
  • Hosting Server: さくらインターネット
  • PHP: 7.4.9
  • cakePHP: 4.1.1

composerによるインストール

(注意:composerを使用しないインストールについては、下記「プラグインのインストールについて」に注意点を記載しておりますので、そちらも御覧ください。)

SSH(ターミナル)でサーバーに接続し。公開ディレクトリ(wwwなど)で下記コマンドを入力します。

php composer.phar create-project --prefer-dist "cakephp/app:4.*" my_app_name

※「my_app_name」は自分で作成するアプリ用に適当に変更してください。
※変更したこのアプリ名と同じディレクトリが作成されます。

設定変更

app_local.phpの修正

config/app_local.phpを修正します。※cakePHP3では.envを編集しましたがcakePHP4ではapp_local.phpになります。

  • 「SECURITY_SALT」を任意の文字列に修正します。(キーボードを適当に叩いて「数字」と「アルファベット」のランダムな文字列にします。)
  • 「Datasources」の「default」にDBの設定を記載します。「host」「username」「password」「database」をお使いのDBの設定に変更してください。

app.phpの修正

config/app.phpを修正します。

  • 「App」の「defaultLocale」を「en_US」から「ja-JP」に修正
  • 「App」の「defaultTimezone」を「UTC」から「Asia/Tokyo」に修正

cakePHPインストールにあたっての注意事項

プラグインのインストールについて

旧cakePHPではzipファイルをアップロードして展開するだけでインストールを行うことができました。それはPHP4でも行うことはできますが、1点注意が必要です。問題なのはインストールではなく「その後」なのです。

※zipファイルなどのダウンロードは下記GitHubから行うことができます。
https://github.com/cakephp/cakephp/tags

ログイン画面などを作成するときは認証用のプラグイン(cakePHP4からAuthentication および Authorizationになりました。)を使うなど、各種プラグインを用いると思いますが、このプラグインもcomposerを使用してインストールすることが推奨されています。
これらプラグインはcakePHP本体と異なり、GitHubからソースを持ってきただけでは使用することができません。
composerを使ったときと同じような設定の追加、変更、ファイル配置を行う必要があります。
手動で行うことができないわけではありませんが、その労力を使うならばサーバーを変更したほうが早いです。

※「どうしてもサーバーを変更できない!」という場合は、composerが使用可能なサーバーで一度試して、そのファイル構成などを元のサーバーに模倣したほうが良いでしょう。

ロリポップは実質使用できない

皆さんそれぞれに好きなレンタルサーバーというのはあると思います。しかしcakePHP4ではロリポップは実質使用することができません。

※ロリポップは私自身好きなレンタルサーバーで10年以上使っていますし、このブログもロリポップです。。。しかし、それでもです。

理由は2つあります。
・プラグインのインストールの項で説明しましたが、composerが必須です。しかしロリポップはメモリなどの制約が厳しいため、composerでのインストール中にエラーになります。
・SSH接続のコンソール(ターミナル)の画面で使用するPHPのバージョンが古いです。このためbakeなど各種コマンドを使用する時にPHPのバージョンが古いというエラーになります。

これらの理由が作成するアプリには該当しない、もしくは自力で解決できる、という場合ロリポップを使用することは可能です。
が、そこまでしてロリポップにこだわる必要は無いと思います。

じゃあどこのレンタルサーバーがいいの?と言われるとさくらレンタルサーバーでしょうかね。

cakePHP4のcomposerインストールで「php: No match.」エラーになった場合の対処方法

前提環境
  • Hosting Server: さくらインターネット
  • PHP: 7.4.9
  • cakePHP: 4.1.1

cakePHP4のcomposerインストール方法

php composer.phar create-project --prefer-dist cakephp/app:4.* my_app_name

マニュアルに記載の上記コマンドを入力すると「php: No match.」のエラーになることがあります。
https://book.cakephp.org/4/ja/installation.html


その場合は以下のようにダブルクォートで囲んでください。

php composer.phar create-project --prefer-dist "cakephp/app:4.*" my_app_name

ASP.NET WebフォームでGridViewの特定列をカラム名で非表示にする

概要

ASP.NETのWebフォームの表(GridView)でデータを持っているけれど表示はしたくない列(Column)が存在する場合、RowCreatedイベントでCellsに列番号を指定しますが、カラム名では指定できません。そのため、ひと手間加えてカラム名で非表示にする方法を解説します。

前提環境
  • Windows 10
  • Visual Studio Professional 2019
  • .NET Framework 4.2
  • C#

コードのポイント

  • DataTable.Columns.IndexOfメソッドを用いて該当カラム名の列番号を事前に取得しておきます。
  • GridViewの「AutoGenerateEditButton」プロパティで生成される「編集」や「選択」「削除」などのボタン(リンク)のためのCellがRowデータの最初にあります。そのため、IndexOfで調べた値+1が実際の列番号になります。

サンプルコード

C#

// GridViewで非表示にするセルのindexの配列
を持つprivate変数を用意しておく。
private List<int> hiddenCellList = new List<int>(); 

// GridViewにDataBindするDataTableから列番号を取得するための処理をPage_Loadなどから呼び出す。
private void SetHiddenCellList(DataTable table)
{
    hiddenCellList.Add(table.Columns.IndexOf("隠したい列の列名その1"));
    hiddenCellList.Add(table.Columns.IndexOf("隠したい列の列名その2"));
}

// GridViewの行作成時のRowCreatedイベント。事前に調べておいた列番号を非表示にする。
protected void gridView_RowCreated(object sender, GridViewRowEventArgs e)
{
    If(e.Row.RowType == DataControlRowType.DataRow || e.Row.RowType == DataControlRowType.Header)
    {
        foreach(int i in hiddenCellList)
        {
            e.Row.Cells[i].Visible = false;
        }
    }
}

HTMLの表(Table)の行(row)の上にマウスカーソルが来たときに行のデータを取得するJavaScript

概要

HTMLの表の行の上にマウスカーソルが重なると、非表示になっているセル内にある隠しデータを読み取り、画面内の別の箇所に表示します。

サンプルはこちら

コードのポイント

  • マウスカーソルが表と重なるときのイベントはonmousemove属性を使用します。(cssのhoverやjQueryのmouseoverではありません)
  • マウスカーソルが重なるのはtdやthになるのでparentでも良さそうですが、td内でpやinputなどを使っている場合にparentでは不具合を生じます。そのため祖先のタグを探すclosestメソッドを使いましょう。
  • closestを使うときは「どのID要素から」という指定を忘れないように。なぜなら該当要素が重複する可能性があります。(divのdivのdivなど、入れ子になる可能性を考慮すること)

サンプルコード

HTML

  <div id="tableArea" onmousemove="mouseMove(event, 'tableArea', 'bookComment')">
    <table class="table table-bordered">
      <caption>表:書籍一覧</caption>
      <thead>
        <tr>
          <th>題名</th>
          <th>価格</th>
          <th class="secret">データ</th>
        </tr>
      </thead>
      <tbody>
        <tr id="001">
          <td>魚図鑑</td>
          <td>\25,000</td>
          <td id="001-Comment" class="secret">魚好きの小中学生におすすめ</td>
        </tr>
        <tr id="002">
          <td>機械の仕組み</td>
          <td>\7,000</td>
          <td id="002-Comment" class="secret">これで君も工学博士!</td>
        </tr>
        <tr id="003">
          <td>料理の基本</td>
          <td>\3,000</td>
          <td id="003-Comment" class="secret">自炊始めませんか?</td>
        </tr>
      </tbody>
    </table>
  </div>
  <div id="commnetArea" class="well" style="max-width: 400px;">
    <h3>書籍の備考</h3>
    <p id="bookComment"></p>
  </div>

JavaScript

  function mouseMove(argEnv, tableAreaId, commentViewId) {
    let targetElement = (argEnv.srcElement || argEnv.target);

    let trElement = targetElement.closest("#" + tableAreaId + " table tbody tr");
    if (trElement) {
      let prefixId = trElement.id;
      let hiddenComment = document.getElementById(prefixId + "-Comment");
      document.getElementById(commentViewId).textContent = hiddenComment.textContent;
    }
  }

サンプルはこちら