Phalcon

Phalconで日付の比較バリデーション!開始日と終了日が間違っていないかチェックする方法。

Phalconで日付の比較バリデーションの方法をメモ。

開始日と終了日を入力するフォームがあった場合、開始日が終了日よりも後ろの日付にもしくは、終了日が開始日よりも前の日付になっていないかバリデーション。

ちなみに今回試したPhalconのバージョンはPhalcon5。

Phalconで日付の比較バリデーション

以下は、終了日のフォームにCallbackバリデーションを使用して実現しています。

開始日のフォームでバリデーションしてもOK。

use \Phalcon\Filter\Validation\Validator\Callback;


$endDate = new Date('end_date');
$endDate->setLabel('終了日');
$endDate->addValidators([
    new DateValidator([
        'format' => 'Y-m-d',
        'message' => '終了日に日付を入力してください。'
    ]),
    new Callback([
        'callback' => function ($data) {
            if ($data['start_date'] && $data['end_date']) {
                if (strtotime($data['start_date']) > strtotime($data['end_date'])) {
                    return false;
                }
            }
            return true;
        },
        'message' => '終了日は、開始日以降の日付を入力してください。'
    ])
]);
$this->add($endDate);

バリデーションの解説

new Callback([

Callbackバリデーションを定義しています。

'callback' => function ($data) {

$dataには送信されてきたFormデータがArrayで入っています。。

if ($data['start_date'] && $data['end_date']) {
    if (strtotime($data['start_date']) > strtotime($data['end_date'])) {
        return false;
    }
}
return true;

$data['start_date']が開始日(Y-m-d形式)、$data['end_date']が終了日(Y-m-d形式)。
strtotimeして日付を比較。

バリデーションエラーの場合に「return false」、バリデーションOKの場合は「return true」。

PhalconのUniquenessバリデーションで登録時と更新時でバリデーション変えるには?

phalcon5にはデータが一意か(重複しないか)バリデーションするクラスが用意されています。

そのクラスがUniquenessです。

マニュアルはこちら。
https://docs.phalcon.io/5.0/ja-jp/filter-validation#uniqueness

このUniquenessバリデーションを使うパターンとしては、ログイン情報を登録するテーブルのメールアドレスなどがあります。

この時、ハマったポイントがあったのでメモ。

Uniquenessバリデーションでハマった点

例えば以下のようなフォーム作成したいとします。

  • ログインのアカウント情報を登録するフォームを作成
  • アカウント情報テーブルに登録するメールアドレスは一意とする

この時にメールアドレスが重複していないかUniquenessバリデーションを使って書くと以下のようになります。
今回はバリデーションクラスを作成せず、form生成時にバリデーションを記載とします。

$mail = new Text('mail');
$mail->setLabel('メールアドレス');
$mail->setFilters('email');
$mail->addValidators([
    new PresenceOf([
        'message' => 'メールアドレスを入力してください。',
        'cancelOnFail' => true,
    ]),
    new Email([
        'message' => 'メールアドレスの形式で入力してください。'
    ]),
    new StringLength([
        'max' => 300,
        'messageMaximum' => 'メールアドレスは300文字以内で入力してください。'
    ]),
    // ここが重複チェック(一意か)のバリデーション
    new Uniqueness([
        'model'   => new Account(),
        'convert' => function (array $values) {
            // メールアドレスを暗号化してテーブルに保存している場合は、メアドを暗号化して検索
            // $valuesにはformで送信されてきたメールアドレスが格納されています
            $values['mail'] = crypt($values['mail']);
            return $values;
        },
        'message' => 'すでに登録済みのメールアドレスです。'
    ])
]);
$this->add($mail);

これで、formから送られてきた値をisValid()でバリデーションした際にUniquenessのバリデーションを実行できます。

今回は'model'で設定したAccountクラス(Accountテーブル)のmailカラムに、すでに同じメールアドレスが登録されていないかチェックしています。

で、新規登録時も更新時も同じFormを使用することが多いと思いますが、この時に新規登録の場合はいいのですが、更新時に問題が発生します。

更新時にメールアドレスを変更せずに更新しようとすると、Uniquenessのバリデーションにひっかかって更新できないのです。

例えば、以下のようなデータがあった場合に、

id mail
1 aaaa@bbb.com
2 hoge@hoge.com

id=1のデータを更新する場合に、メールアドレスを変更せずに更新とすると、id=1の「aaaa@bbb.com」と重複しているのでバリデーションエラーになります。

この場合は更新できてほしいです。

ハマりポイントの解決方法

実現したいバリデーションは以下です。

  • 新規登録時はすべてのデータと比較し重複していないかチェック
  • 更新時は自身のデータ以外と比較して重複していないかチェック

上記の方法では、新規登録時は条件を満たしていますが、更新時の条件は満たしていません。

この問題を解決するために'except'オプションを使用します。

'except'オプションは、指定した値を除外して重複チェックしてくれます。

マニュアルはこちらです。
https://docs.phalcon.io/5.0/ja-jp/filter-validation#using-except-for-fields-sql-operation-value-not-in-except

$mail = new Text('mail');
$mail->setLabel('メールアドレス');
$mail->setFilters('email');
$mail->addValidators([
    new PresenceOf([
        'message' => 'メールアドレスを入力してください。',
        'cancelOnFail' => true,
    ]),
    new Email([
        'message' => 'メールアドレスの形式で入力してください。'
    ]),
    new StringLength([
        'max' => 300,
        'messageMaximum' => 'メールアドレスは300文字以内で入力してください。'
    ]),
    // ここが重複チェック(一意か)のバリデーション
    new Uniqueness([
        'model'   => new Account(),
        'convert' => function (array $values) {
            // メールアドレスを暗号化してテーブルに保存している場合は、メアドを暗号化して検索
            // $valuesにはformで送信されてきたメールアドレスが格納されています
            $values['mail'] = crypt($values['mail']);
            return $values;
        },
        'except' => $entity->mail, // ここを追加
        'message' => 'すでに登録済みのメールアドレスです。'
    ])
]);
$this->add($mail);

exceptには更新しようとするデータのメールアドレスを指定します。

$entityはformクラスのconstructで受け渡ししています。

if ($id) {
    // 更新時はテーブルからデータを取得
    $account = Account::findFirst($id);
} else {
    // 新規登録時はデータなし
    $account = new Account();
}
$form = new AccountForm($account);

こうすることで、新規登録時は$entity->mailはnullなので、mailがnull以外のデータと比較してくれ、

更新時は、$entity->mailが更新しようとするデータのmailなので、自身のmail以外のデータと比較してくれます。

id mail
1 aaaa@bbb.com
2 hoge@hoge.com

こちらのデータの例でいうと

  • 新規登録時はすべてのmailと比較
  • id=1のデータ更新時はid=1のmail以外のmailと比較

となり、実現したかったバリデーションとなります。

Phalcon5のトランザクションでハマった!Rollbackできない、トランザクションが始まらない問題。

Phalcon5でDBにデータを登録する際に、トランザクションを使おうと思ってハマったのでメモ。
DBはMariaDB。

マニュアル見てやってもなぜか動かない…

マニュアルはこちら。
https://docs.phalcon.io/5.0/ja-jp/db-models-transactions

上から順番に試してみましたが、なぜかトランザクションが効かない…

Rollbackができるか以下のパターンで検証してみました。

まずは、$this->db->begin()のやつ。

$this->db->begin();

// $customerにデータ設定は省略
$result = $customer->save();

$this->db->rollback();

「PHP message: There is no active transaction」のエラーが発生。
トランザクションが開始されていないようです。

次は、TxManager()のやつ。

use Phalcon\Mvc\Model\Transaction\Manager as TxManager;

$manager = new TxManager();
$transaction = $manager->get();

// $customerにデータ設定は省略
$result = $customer->save();

$transaction->rollback();

Rollbackされず、データが保存されました。

phalcon5でトランザクションを使う方法

参考になるのが、Phalcon5のマニュアルだとisolatedの2つ目の「Transactions can be used to delete a number of records, ensuring that everything is deleted correctly」のところ。
https://docs.phalcon.io/5.0/ja-jp/db-models-transactions#isolated

あとは、Phalcon3.4のマニュアルが参考になります。
https://docs.phalcon.io/3.4/ja-jp/db-models-transactions#isolated-transactions

当環境では、TxManager()でsetTransaction()を使うことでトランザクションが使えました。

use Phalcon\Mvc\Model\Transaction\Manager as TxManager;

$manager = new TxManager();
$transaction = $manager->get();

$customer->setTransaction($transaction);

// $customerにデータ設定は省略
$result = $customer->save();

$transaction->rollback();

これでRollbackされ、データが登録されませんでした。

複数データを登録する場合はこんな感じ。

use Phalcon\Mvc\Model\Transaction\Manager as TxManager;

$manager = new TxManager();
$transaction = $manager->get();

// $customerにデータ設定は省略
$customer->setTransaction($transaction);
$result = $customer->save();

// $robotにデータ設定は省略
$robot->setTransaction($transaction);
$result = $robot->save();

$transaction->rollback();

これでRollbackできたので、登録失敗した場合にrollback()、成功した場合はcommit()するようにすればOK。

ちなみにMariaDB(MySQL)は、RollbackしてもAUTO_INCREMENTの値はRollbackされないので、連番ではなくなります。
2のデータ登録しようとした際にRollbackされたとすると、「1,3,4」みたいにデータができるので要注意。

PHP7からPHP8、Phalcon3からPhalcon5にアップグレードした作業ログ!

PHP7.1からPHP8.1に、Phalcon3.2からPhalcon5.2にアップグレード。

アップグレード前の環境

  • PHP7.1
  • Phalcon3.2
  • Zephir0.10
  • CentOS
  • Nginx

アップグレード後の環境

  • PHP8.1
  • Phalcon5.2
  • Zephir0.17
  • CentOS
  • Nginx

現バージョンの確認

# phpの現パージョン確認
$ php -v
PHP 7.1.33 (cli) (built: Sep 29 2020 09:53:22) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.1.33, Copyright (c) 1999-2018, by Zend Technologies

# phalconの現バージョン確認
$ php -r "echo Phalcon\Version::get();"
3.2.2

PHP7からPHP8へアップグレード

PHP設定ファイルのバックアップ

# php.iniのバックアップ
$ cp /etc/php.ini /etc/php.ini.yyyymmdd

# php-fpm.d/www.confのバックアップ
$ cp /etc/php-fpm.d/www.conf /etc/php-fpm.d/www.conf.yyyymmdd

現在のPHPのパッケージ確認

phpアップデートするときに同じの入れるのでメモっておく。

続きを読む

phalcon3から5にバージョンアップした際のDB関連の対応

phalcon3から5にバージョンアップした際にDB周りで発生したHTTP ERROR 500やDeprecated対応

Service Mysql is not registered

FastCGI sent in stderr: "PHP message: Service Mysql is not registered
#0 [internal function]: Phalcon\Factory\AbstractConfigFactory->getException()
#1 [internal function]: Phalcon\Factory\AbstractFactory->getService()
#2 [internal function]: Phalcon\Db\Adapter\PdoFactory->newInstance()
#3 /web/dev/ttonosaki/juku/app/settings/services.php(28): Phalcon\Db\Adapter\PdoFactory->load()
#4 [internal function]: Closure->{closure}()
#5 [internal function]: Phalcon\Di\Service->resolve()
#6 [internal function]: Phalcon\Di\Di->get()
#7 [internal function]: Phalcon\Di\Di->getShared()
#8 [internal function]: Phalcon\Mvc\Model\Manager->getConnection()
#9 [internal function]: Phalcon\Mvc\Model\Manager->getReadConnection()

DBの設定(config)でadapterの指定がMysqlからmysqlに変わったようなので修正。

-adapter = Mysql
+adapter = mysql

https://docs.phalcon.io/5.0/ja-jp/db-layer#load

SQLSTATE[HY000] [2002] No such file or directory

MySQLに接続できないエラー

FastCGI sent in stderr: "PHP message: SQLSTATE[HY000] [2002] No such file or directory
#0 [internal function]: PDO->__construct()
#1 [internal function]: Phalcon\Db\Adapter\Pdo\AbstractPdo->connect()
#2 [internal function]: Phalcon\Db\Adapter\Pdo\AbstractPdo->__construct()
#3 [internal function]: Phalcon\Db\Adapter\Pdo\Mysql->__construct()
#4 [internal function]: Phalcon\Db\Adapter\PdoFactory->newInstance()
#5 {pathto}/app/settings/services.php(31): Phalcon\Db\Adapter\PdoFactory->load()

PdoFactory->load()に渡すconfigの形式が変わったようです。
iniファイルをそのままconfigとして受けたしてもダメだったので以下のように修正。

-return (new PdoFactory())->load($config->database);
+return (new PdoFactory())->load([
+    'adapter'  => $config->database->adapter,
+    'options' => [
+        'host'     => $config->database->host,
+        'username' => $config->database->username,
+        'password' => $config->database->password,
+        'dbname'   => $config->database->dbname
+    ]
+]);

マニュアルを見る感じではiniファイルで設定した値をそのまま渡してもよさそうな記述でした。
https://docs.phalcon.io/5.0/ja-jp/db-layer#load

githubでコメントを見ると、マニュアルとは違う形式で受け取る内容がコメントに記載してありました。
https://github.com/phalcon/cphalcon/blob/v5.2.1/phalcon/Db/Adapter/PdoFactory.zep#L29-L41

Call to undefined method Criteria::addWhere()

Call to undefined method Phalcon\Mvc\Model\Criteria::addWhere()

phalcon v4でCriteria::addWhere()は削除されました。
https://docs.phalcon.io/4.0/ja-jp/upgrade#mvcmodelcriteria

andWhere()に変更

そのほかのアップグレードで発生したエラー対応