またどこかでCTOっぽいことやってる人のブログ

フリーランスを経て、またどこかでCTOっぽいことをやってる人が書いてます。何か色々やってます。

CakePHP3で入力された複数のカラム同士を比較するバリデーションを作ってみた。

今回のネタは合っているのかどうかよくわかっていません。
やりたかったことは大したことではなく

  • 入力されたFrom日付とTo日付の比較

です。
よくあるアレです。入力されたFrom日付がTo日付より未来日の場合はダメというやつです。
最初はvalidationDefaultをごにょごにょやっていたのですが、どうも上手くできず。
もしかしたらできないのかもしれませんが、その辺調べていません。
ですので、buildRulesの方に実装してみました。

以前書いたように、CakePHP3のバリデーションには2種類あります。
ひとつはPOSTデータを送信する際に処理される「validationDefault」、もうひとつはsaveメソッド実行時に処理される「buildRules」です。
ごにょごにょと触った感じ、validationDefaultの方はPOSTデータのカラムをひとつずつ、何らかのバリデーションを実行しているように思えました。
CakePHP2の頃は入力カラム同士でバリデーションできたような気がするのですが、気のせいだったかもしれません。

前置きはともかく、こんな処理を作って実装しました。
昔の自分が書いたコードを雑に修正しただけなんですが。

<?php
 public function buildRules(RulesChecker $rules) {
        
    // 開始日時と終了日時の入力範囲チェック
    $rules->add(
        function ($entity, $options) {
            if ($entity->to_time < $entity->from_time) {
                return false;
            } else {
                // 未入力の場合はtrueで返す
                return true;
            }
        }, [
            'errorField' => 'start_datetime', 
            'message' => '開始日時に設定された日付が、終了日時より未来日として設定されています。'
        ]
    );

    // 処理結果を返す
    return $rules;
 }

この処理はTableに書いてあるのですが、$entityというのはEntity配下に作成するであろう対象テーブルのエンティティです。
エンティティから比較したいカラムを抜いてきて、それを単純に比較しているだけです。
あとはCakePHP3のバリデーションルールに沿って、明示的にtrue / falseを返す。これだけです。

今回は日付のバリデーションなので、Controller側に直接書いたり入力時点でJavaScriptで判定したりとかでもいいと思います。
なんですが、バリデーションはバリデーションとしてまとまってた方がいいかなと。

これもまた個人的な好みです。

WindowsのXAMPPにインストールしたCakePHPからシェルを実行してみた。

とは言っても、ローカルテストにしか使い道がない気がします。
本番運用でWindowsサーバにCakePHP3を載せるのであれば話は別ですが。

CakePHP3になってシェルの配置が変わっています。

CakePHP3インストールディレクトリ\src\Shell

この辺にあります。
インストールすると「ConsoleShell.php」なるものがあるので、とりあえずこれを実行してみます。

Shellの実行ディレクトリは以下です。
あ。タイトルにあるようにXAMPPの場合です。
自分の環境はhtdocs配下にCakePHP3を入れているので

C:\xampp\htdocs\プロジェクト名\bin

ここにありました。

あとはコマンドプロンプトを開いて、上記のShell実行ディレクトリに移動して、CakePHP2の時のように実行すればOKです。

cd /d C:\xampp\htdocs\プロジェクト名\bin
C:\xampp\htdocs\プロジェクト名\bin>cake Console

ローカルテスト用ということで。

CakePHP3でDBに格納した画像データを表示してみた。

2017/09/04 追記
すっかり書いた気でいた画像データ登録記事を追加しました。
本記事と合わせて読んで頂くと、それっぽくできると思います。

tsuralabo.hatenablog.com



画像データはDBにブチ込むタイプです。
サーバにアップロードする方法もありますが、どちらもメリット、デメリットあります。
メリット、デメリットに関してはグーグル先生が詳しいと思います。
案件に応じて柔軟に対応すればいいんじゃないかなと。

さて、いきなり端折ってしまいますが、image_filesというテーブルに画像データが入っていたとします。
いわゆるBLOB型のカラムですね。
テーブル構成はこんな感じで作りました。

CREATE TABLE `image_files` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content_type` varchar(64) NOT NULL COMMENT 'コンテンツタイプ',
  `image_data` mediumblob NOT NULL COMMENT '画像バイナリデータ',
  `created` datetime DEFAULT NULL COMMENT 'データ作成日',
  `modified` datetime DEFAULT NULL COMMENT 'データ更新日',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='画像格納テーブル'

ここにブチ込んだ画像バイナリデータをController経由で表示します。
Helperでもいいと思いますが、あんまりHelperを使うのが好きではないのでControllerで。

やりたいことはこれだけです。

・プライマリキーであるimage_files.idを指定する。
・image_files.idに合致するバイナリデータのimage_files.image_dataを取得する。
・画面上に表示する。

なんか簡単っぽいです。
というわけで表示用のControllerを書いてみます。

<?php
  namespace App\Controller;
  use Cake\Event\Event;
  use Cake\ORM\TableRegistry;
  
/**
 * ImageFilesController
 */
class ImageFilesController extends AppController {
  
  public $name = 'ImageFiles';
  
  public function beforeFilter(Event $event) {
    parent::beforeFilter($event);
  }
  
  /**
   * 画像データ取得処理
   * IDに合致する画像データを取得して表示
   * @param type $imageId
   * @throws NotFoundException
   */
  public function content($imageId) {
    
    // Viewをレンダリングしない
    $this->autoRender = false;
    
    // image_filesデータをまるっと取得する
    $imageFile = TableRegistry::get('ImageFiles');
    
    // IDに紐付く画像データを取得
    $fileData = $imageFile->get($imageId);
    
    if (empty($fileData)) {
      // 一応エラー処理
      throw new NotFoundException();
    }
    
    // 画像表示
    $this->response->type($fileData->content_type);
    $this->response->body(stream_get_contents($fileData->image_data));
  }
}

ポイントは特にありません。
強いて言えば、レンダリングしない部分だけじゃないかなと。
あとはよくある画像表示処理です。

このコントローラをView側で呼んであげれば画像が表示されると思います。
View側も一応記載しておきます。

<?php
  <img 
     src="<?php echo $this->Url->build(["controller" => "ImageFiles", "action" => "content", 12345]); ?>" 
     class="img-rounded img-responsive" <!-- ここのClass指定はBootstrap3用 -->
     alt="画像12345です" 
  />

上の例だとimage_files.id = 12345の画像を指定しています。
altは適当に記載していますが、実際はちゃんと入れないとグーグル先生がクローリングしてきた時に叱られます。

出来上がったソースを見ると、CakePHP2の頃とほとんど変わりませんでした。
が、とりあえず画像は表示できたのでよしとします。

CakePHP3でbodyタグ閉じ直前にJavaScriptを読み込むようにしてみた。

datatablesを多用する自分はJQueryやらdatatablesやらのモジュールはheadタグの中で読み込みます。
bodyタグ閉じの直前がセオリーだというのはかろうじて知っているのですが、datatablesモジュール読み込みをbodyタグ閉じ直前に配置すると、デフォルトソート条件なんかが効かないのです。例えばpageLengthとか、orderとかああいうやつです。
いや、自分が書くと効かないだけかもしれませんが、とにかく効かないのです。


で。
datatablesはJQueryやらのモジュールを読み込んだ後じゃないと有効になりませんので、その辺はまとめてheadタグの中にブチ込みます。
良い子の皆様は真似しない方がいいかと思いますが、どうしても動かない時は試しにやってみるといいかもしれません。
とりあえず動かすって大事です。


とは言え、使うJavaScriptを全部headの中で読み込むのは何かモニョっとします。
ですのでView単位で使うJavaScriptはbodyタグ閉じ直前で読み込みたい。
さて、どうしたものか。

普通の方々はCakePHPでプロジェクトを作る方ってレイアウトファイルはどんな風にしてるんでしょう。
自分は至ってシンプルな書き方をしています。

<!-- Layouts/default.ctpだと思って下さい -->
<?php
<!DOCTYPE html>
<html>
  <head>
    <?php echo $this->Html->charset(); ?>
    <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,user-scalable=no">
    <title>
      <?php echo isset($viewTitle) ? $viewTitle : ''; ?>
    </title>
    <!-- metaタグとかが必要ならこの辺りで -->
    <?php echo "CSS読み込んでfetch" ?>
    <?php echo "仕方ないのでJavaScript読み込んでfetch"; ?>
  </head>
  <body>
    <div id="wrap">
      <!-- Elementでヘッダーっぽいのを読み込み -->
      <?php echo $this->Element('Layout/header'); ?>
      <div class="row">
        <div id="contents" class="container-fluid">
          <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
            <div id="loading">
               <!-- ローディング画像 -->
              <?php echo $this->Html->image('loader.gif'); ?>
            </div>
            <!-- Viewの内容 -->
            <?php echo $this->fetch('content'); ?>
          </div>
        </div>
      </div>
    </div>
    <div id="footer">
      <!-- Elementでフッターっぽいのを読み込み -->
      <?php echo $this->Element('Layout/footer'); ?>
    </div>
    <!-- これを予め書いておく -->
    <?php echo $this->fetch('scriptBottom'); ?>
    <script>
      // ローディング画像ごにょごにょ
      $(function() {
        $("#loading").fadeOut(800);
      });
    </script>
  </body>
</html>

Elementで色々分けるのが好きなのです。
それはどうでもいいのですが、「scriptBottom」というfetchを自分はレイアウトファイルに書いてしまいます。
他の上手いやり方あるのかもしれませんが、まぁこの際いいです。


それでViewの方にはこんな風に書きます。

<?php
 <?php echo $this->Html->script('search', ['block' => 'scriptBottom']); ?>
 <!-- 以下はごにょごにょとViewを構成する何かのコード -->


と書くと、上記の例で言うと「js/search.js」がレイアウトファイルで指定したbodyタグ閉じ直前で読み込まれます。
CakePHP2であったinlineみたいなのはなくなってしまっている模様です。よく調べてませんが。
とにかく、HTMLソースを見ると上のレイアウトで言うところの、ローディング画像ごにょごにょ部分の上にsearch.jsが読み込まれていると思います。


今回もまた雑なコードです。

CakePHP3をWindows10にインストールしてみた。

先日、昔中古で買ったWindows7 Pro 32bitのノートPCを滑り込みでWindows10にしてみました。
特に問題もなくアップデートできたのですが、調べてみたところ32bitのWindows10に対して、64bitのWindows10でクリーンインストールするとWindows10 + 64bitになるとか。
試しにやってみたら本当に64bitになりました。Microsoft曰く、ライセンス的にも問題ないそうです。

インストール時につまづいたところは特になかったのですが、クリーンインストールなのでHDDを削除する辺りは少し躊躇しました。
結局はクリーンインストール実行したのですが、元々ノートに入っていたリカバリ領域まで電子の海にドナドナされました。
でも、万が一の場合はまたWindows10をクリーンインストールすればいいんじゃないの? などと楽観的に考えております。
終わったことは仕方がないのです。
ちなみにドライバ等も特に問題なかったです。メモリ増設しようかななどと企んでおります。

さて。
上記のようにノリで出来上がったWindows10なのですが、特に用途がありません。
用途がない時はとりあえず開発環境をインストールすれば暇潰しにはなるかな? と思い、CakePHP3の開発環境を構築してみました。
今回入れたモノは以下の通りです。

・CakePHP3
・XAMPP
・Composer
・SourceTree
・NetBeans8.1
MySQL Workbench

ちゃんと上手くインストールできるかなと身構えていたものの、全く何も起こらずに開発環境ができてしまいました。
昔の自分が書いた記事を見ながらインストールしたのですが、ちゃんと手順通り書いてるじゃんと感心しました。

と、このように全く何も事件発生せずにインストールできてしまったので、記事にするほどではないのですが一応書いてみました。
ちなみにバージョンは「調べるの面倒だからとりあえず最新をブチ込む」派ですので、2016年8月時点での最新版をそれぞれインストールしました。

CakePHP3開発のIDEとしてNetBeansを使うのであれば、バージョンは8.1以上がいいと思います(多分)

FlashメッセージをBootStrap風味にしてみた。

2016年も早いもので8月になりました。
気が付いたら今年も半年以上終わってるんですね。

恐ろしい。

今日は小ネタです。
CakePHPを使っているとエラーメッセージにFlashを使う機会があると思います。
使い方は至って単純で、Controller側でメッセージをセットしてView側でレンダリングするだけです。
CakePHP3だとこんな風に書きます。

<?php
  // Controller側
  public function test() {
    $this->Flash->error('エラーですよ');
  }


例えばこんな感じでエラーメッセージをセットして

<?php
  <!-- ここにメッセージが出ます -->
  <?php echo $this->Flash->render(); ?>


View側でこのようにレンダリングします。
このエラーメッセージはCakePHP3が最初から準備しているテンプレートで、Elementの下にあります。
Element\Flashディレクトリの下ですね。
何もしていなければ「default.ctp」、「error.ctp」、「success.ctp」があると思います。

上の方で「エラーですよ」とメッセージをセットしたテンプレートは今回だと「error.ctp」です。
なので、「$this->Flash->success('成功ですよ');」と書けば「success.ctp」が呼び出されるわけです。

他のテンプレートを使いたかったら「error_special.ctp」なんてのを例えば作成して、メッセージをセットする時に

<?php
  // Controller側
  public function test() {
    $this->Flash->errorSpecial('すごいエラーですよ');
  }

なんて書けばOKです。


で。
個人的な趣味なんでしょうが、自分はBootStrapを使っている時はアラートコンポーネントを使う傾向にあります。
ですのでいつもこんな感じで書き換えています。

<?php
  // 元々のElement\Flash\error.ctp
  <div class="message error" onclick="this.classList.add('hidden');"><?= h($message) ?></div>

これを

  <div class="alert alert-warning" role="alert">
    <p class="text-danger">
      <?php echo h($message); ?>
    </p>
  </div>

例えばこんな風に変更すると、BootStrapのアラートコンポーネントを使ったエラーメッセージエリアができます。

もし、複数のエラーを改行コード入れつつ全部出力したい時は、Controller側で配列をセットしてループさせればいいと思います。
配列セットする時に改行コードを埋め込むか、View側で出力する時に改行コードを出すかはお好みで。
改行コードを埋め込んだ配列をセットした場合だと、error.ctpはこんな感じになると思います。

<?php
  <div class="alert alert-warning" role="alert">
    <p class="text-danger">
      <?php foreach($message as $msg): ?>
        <?php echo $msg; ?>
      <?php endforeach; ?>
    </p>
  </div>

hをつけたままだと改行されなくて頭を抱えるかもしれないので注意してください。
小ネタと冒頭に書きつつ、何でこんなネタを書いた理由は自分が頭を抱えた張本人だからです。

戒めというやつですね。

CakePHP3でbuildRuleに条件分岐を追加してみた。

2017/05/14 追記 ------------------------------------------

昔できていたはずなのに、過去の自分の記事を見ながら作ってみたらエラーに…。
下の方、修正しておきました。
2017/05/14 追記終わり -----------------------------------


2016年の夏はどうも天気がシャキっとしてません(7月現在)
きっと、まだ地球さんが本気出してないんでしょう。

さて、今回はこんなことをやりたくなりました。

「ユーザ登録時にメールアドレスは必須じゃないけど、入力された時は重複不可にしたい」

重複不可だけならbuildRule(saveメソッドコール時に実行されるバリデーション)に書けば終わりです。

<?php
 public function buildRules(RulesChecker $rules) {
   // カラム名はemailと仮定します
   $rules->add($rules->isUnique(['email'], 'メールアドレス重複エラー'));

   // 処理結果を返す
   return $rules;
}

なんですが。
メールアドレスが必須ではないので、入力された時はいいとしても未入力の場合は最初の1件しかデータが入らず、メールアドレス未入力データ2件目以降の場合はしっかりエラーになってしまいます。
要するに「メールアドレスが空のデータはもうありますので!」というわけです。

困りました。
困りましたが、要は「メールアドレスが入力された時だけ、重複チェックすればいいじゃん」というのはわかります。
何か上手い方法はないものかと色々調べたのですが、どうもそれっぽい情報が見当たりません。
で、公式を見ながら実現できたコードが以下です。

<?php
namespace App\Model\Table;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\ORM\RulesChecker;
use Cake\ORM\Rule\isUnique; // これを宣言しておく必要があります

class UsersTable extends Table {

/**
 * POST時のバリデーション
 */
public function validationDefault(Validator $validator) {
  // 今回は省略しますが、何かバリデーションを色々
  // メールアドレスを必須にせずに形式だけチェック
  $validator
    ->allowEmpty('email')
    ->email('email', true, 'メールアドレス形式ではありません。');

  // 処理結果を返す
  return $validator;
}


/**
 * save実行時のバリデーション
 */
public function buildRules(RulesChecker $rules) {
  // カラム名はやっぱりemailと仮定します
  $rules->add(
    function ($entity, $options) {
      if (!empty($entity->email)) {
        // 2017/05/14 ここから修正
        $rule = new isUnique(['email'], $options);
        // 2017/05/14 ここまで修正
        return $rule($entity, $options);
      } else {
        // 未入力の場合はtrueで返す
        return true;
      }
    },
    ['errorField' => 'email', 'message' => 'メールアドレス重複エラー']
  );
  return $rules;
}

$entityからチェックしたいカラムを抜いてきて、その項目に対してemptyチェック。
入力されていたらisUniqueで重複チェックしてます。

isUniqueのコンストラクタ引数が配列指定なので上記のような形式で渡しています


未入力の場合はtrueを返してます。
どうもちゃんと明示的に書かないとダメみたいです。
エラーメッセージ設定は見たままです。

きっともう少しいい方法あるんじゃないかなとは思います。
が、今回emptyチェックしている部分を色々変えれば融通が利くbuildRuleが作れるかなと。

そもそもemailを必須にしない仕様自体がアレとかいう話もありますが。