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

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

fullcalendar その2 〜表示している月のデータだけ取得してイベント表示してみた〜

前回の続きになります。
前回の記事はこちらです。

tsuralabo.hatenablog.com

カレンダーデータ格納用テーブルを準備

カレンダーデータなので、きっとDBの中に格納されていることでしょう。
ということにして、テーブルはこんな感じで作ります。

CREATE TABLE `schedules` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) DEFAULT NULL COMMENT '予定タイトル',
  `description` varchar(255) DEFAULT NULL COMMENT '予定詳細',
  `calendar_color` varchar(255) DEFAULT NULL COMMENT 'カレンダー背景色',
  `calendar_text_color` varchar(255) DEFAULT NULL COMMENT 'カレンダー文字色',
  `start_date` date DEFAULT NULL COMMENT '開始日',
  `start_time` char(5) DEFAULT NULL COMMENT '開始時刻',
  `end_date` date DEFAULT NULL COMMENT '終了日',
  `end_time` char(5) DEFAULT NULL COMMENT '終了時刻',
  `is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '削除フラグ(0:有効 / 1:削除済み)',
  `created` datetime DEFAULT NULL COMMENT 'データ作成日',
  `created_user_id` int(11) DEFAULT NULL COMMENT 'データ作成者',
  `modified` datetime DEFAULT NULL COMMENT 'データ更新日',
  `modified_user_id` int(11) DEFAULT NULL COMMENT 'データ更新者',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='スケジュール管理テーブル';

他に必要なデータがあればカラム追加なり、いらなければ削除なり。
ただ、fullcalendarを使うのであれば日付と時間はカラム単位で分けておいた方が無難です。

カレンダーデータを取得

Ajax実行用のコントローラを作成して、その中にfunctionを作りました。
あとはそのfunctionに対してパラメータを渡してデータ取得です。
取得したデータは配列に格納し直して、JSON形式で返します。

サンプルなのでこんなfunctionにします。

<?php
  /**
   * 日付範囲に合致するスケジュール一覧を取得
   * @param type $startDate
   * @param type $endDate
   */
  public function getScheduleList($startDate, $endDate) {
    
    // スケジュールデータを取得
    $scheduleModel = TableRegistry::get('Schedules');
    $scheduleData = $scheduleModel->getScheduleList($startDate, $endDate);
    
    // 取得データを整形して配列に再セット
    $scheduleList = [];
    
    foreach ($scheduleData as $schedule) {
      $scheduleList[] = [
          'id' => $schedule->id,
          'title' => $schedule->title,
          'description' => $schedule->description,
          'start' => $schedule->start_date.(!empty($schedule->start_time) ? " ".$schedule->start_time : " 00:00"),
          'end' => $schedule->end_date.(!empty($schedule->end_time) ? " ".$schedule->end_time : " 23:59"),
          'color' => $schedule->calendar_color,
          'textColor' => $schedule->calendar_text_color
      ];
    }
    
    // 処理結果をJSON形式で返す
    $this->response->body(json_encode($scheduleList));
  }

「$scheduleList」の中でデータをセットしていますが、idとかtitleとかそういうのは全部fullcalendarがカレンダー表示時に使うプロパティです。
JSでごにょごにょやるのが得意ではないので、データを返す前に整形しています。


別件なのですが。
Ajax実行用のコントローラ内にfunctionを作って、確実にJS側から呼んでいるはずなのにエラーになる場合は多分CSRF制約に引っかかっています。
上手いやり方があるのかもしれませんが、自分は知らないのでAjax実行する時は対象のfunctionをbeforeFilterで解除するようにしています。
あまり合っている自信ないですが、こんな感じです。

<?php
class ApisController extends AppController {
  
  // CSRF解除アクション
  private $csrfAllowAction = [
    'getScheduleList'
  ];
  
  public function beforeFilter(Event $event) {
    parent::beforeFilter($event);
    
    // Ajax送信時のCSRFを無効
    if (in_array($this->request->action, $this->csrfAllowAction)) {
      $this->eventManager()->off($this->Csrf);
    }
    
    $this->viewBuilder()->layout('ajax');
    $this->autoRender = false;
  }

  /* 以下略 */

Ajaxで取得したデータをfullcalendarのコールバックに設定

ここまで書いてアレなんですが、Ajax実行する箇所を書いてなかったです。
あまり気にせず、まとめて書くことにします。

/**
 * 対象日付スケジュールデータセット処理
 * @param {type} startDate
 * @param {type} endDate
 * @param {type} callback
 * @returns {undefined}
 */
function setCalendarList(startDate, endDate, callback) {
  
  $.ajax({
    type: 'post',
    dataType : "text",
    async: true,
    cache: false,
    url : /* ここに getScheduleList を実行するURLを記載 */
  })
  .then(
    function(data) {
      // JSONパース
      var obj = jQuery.parseJSON(data);
      var events = [];
      
      $.each(obj, function(index, value) {
        events.push({
          // イベント情報をセット
          id: value['id'],
          title: value['title'],
          description: value['description'],
          start: value['start'],
          end: value['end'],
          color: value['color'],
          textColor: value['textColor']
        });  
      });
      
      // コールバック設定
      callback(events);
    }
  );

  return;
}


Ajax側で取ったデータを配列ループさせて、event.pushで登録しています。
最後にcallback設定するのを忘れないようにして下さい。
colorとtextColorはそのままなんですが、カレンダー表示時の背景色と文字色です。
別に設定する必要もないのですが、変更もできますよという例で書いてあります。
スケジュール設定画面にカラーピッカーでも埋め込んで、16進数でデータ登録すればいいと思います。

できたっぽいので呼んでみる

前回作成したはずのView側から、今回作成したJSのfunctionを呼んでAjaxでデータ取得してみます。

<script>
  $(document).ready(function() {
    $('#calendar').fullCalendar({
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,agendaWeek,agendaDay,listMonth'
        },
        timeFormat: 'HH:mm',
        timezone: 'Asia/Tokyo', 
        eventLimit: true, 
        editable: true, 
        slotEventOverlap: true, 
        selectable: true, 
        selectHelper: true, 
        selectMinDistance: 1, 
        events: function(start, end, timezone, callback) {
           // ***** ここでカレンダーデータ取得JSを呼ぶ *****
           setCalendarList(start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD'), callback);
        },
        eventClick: function(calEvent, jsEvent, view) {
           // カレンダーに設定したイベントクリック時のイベント
        },
        dayClick: function(date, jsEvent, view) {
           // カレンダー空白部分クリック時のイベント
        },
        select: function(start, end) {
           // カレンダー空白部分をドラッグして範囲指定した時のイベント
        },
        eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
           // イベントをドラッグして別日に移動させた時のイベント
        }
    });
  });
</script>


こんな感じで呼びます。
startとendには「表示中のカレンダーに該当する月の月初日と月末日」が入っています。
ですので、例えば表示している月が2017年11月であればstartには「2017年11月01日」、endには「2017年11月30日」に相当する値が入っています。
それをフォーマットして渡しています(フォーマット形式はテーブル構成で変わるかもしれません)
callbackはそのまま渡してOKです。

eventsに書いた処理は年月等を変更した時に呼ばれます。
前月とか翌月とか。
上の例で言うとheaderに書いた辺りです。

よくわからなかったらconsole.logを仕込むとかalertを仕込んだりして動かしてみるといいかと思います。

fullcalendar その1 〜とりあえずスケジュール表の外枠だけ作ってみた〜

転職してから4ヶ月が経過しました。
孤独なCTO活動もなかなか慣れてまいりました。
慣れただけであって、平気だというわけではないんですが。


さて。
今回はfullcalendarを使ってスケジュール表を作ってみました。
fullcalendarというものは、どこかの偉い人が作った高機能カレンダーJSライブラリです。

fullcalendar.io

スケジュール表ではないのですが、前職でfullcalendarを使って某機能を作成したことがありました。
が、その時は失敗しました。

datatablesもそうなのですが、JSライブラリなので何も考えないで使うと表示しようとしたデータを最初にまるっと読み込むのです。
なので、データ量が多いと当然動作が重くなったり画面が真っ白になったりします。というかしました。
何も考えないで作ってはいけないということを学んだのです(戒め)

やりたかったこと

・表示している月毎のデータだけを取得したい
・設定済みの予約をクリックしてモーダル表示で詳細を見たい
・設定済みの予約をカレンダー上でドラッグして日付変更したい
・カレンダーの日付をクリックして新規予定を設定したい

編集だとか削除だとか、そういうのはとりあえず置いておきます。

fullcalendarの準備

上にリンクを置いておきましたが、とりあえずfullcalendarをダウンロードして解凍して下さい。
解凍して出てきたディレクトリから、今回使用するのは以下のファイルです。

・JS本体として「fullcalendar.min.js」
・日本語対応として「ja.js」
・fullcalendarの見た目用に「fullcalendar.min.css

この辺を読み込んで下さい。
JQueryは3.1.1を使っています。
fullcalendar.min.jsはJQueryの後の方がいいんじゃないかなと思います、多分。

コントローラ側の記載

何も書きませんでした。

いわゆる管理画面的なものに今回のスケジュール表を載せましたので、CalendarsControllerみたいなものににindexアクションを作った程度です。
データ取得とかそういうのは全部Ajaxでやりました。
まぁAjax嫌いなんですけどね。

View側の記載

カレンダー表示はこんな感じで書きました。
例の如く、今回もBootstrapさんが大活躍です。

<div id="contents" class="container-fluid">
  <div class="row">
    <div class="col-xs-12">
      <div class="panel">
        <div class="panel-heading">
          スケジュール表
        </div>
        <div class="panel-body">
          <!-- メッセージ表示したい時はこの辺に -->
          <div class="row">
            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"
              <div id="calendar"></div> <!-- ここがカレンダー表示部分 -->
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

パネルが好きなのです。
Bootstrap4はパネルなくなってるらしいですが、使ってるのはBootstrap3なのでいいです。
「パネルの中にカレンダーを表示」させるつもりで書いてます。

fullcalendarを着火

着火はJSで実行します。
カレンダーを表示するViewのどこかにJSを書けばOKです。

<script>
  $(document).ready(function() {
    $('#calendar').fullCalendar();
  });
</script>

これでカレンダーが表示されるんじゃないかなと思われます。

fullcalendarのプロパティ設定

上記のコードでカレンダーは表示されるのですが、色々と都合がよろしくないのでプロパティを追加することにします。
プロパティに関しては以下のページを参考にして下さい。

Documentation | FullCalendar

あまりに雑だし英語ばかりなので、サンプルとして今回自分が設定したプロパティを書いておきます。
参考にして下さい。

<script>
  $(document).ready(function() {
    $('#calendar').fullCalendar({
        header: {
          // fullcalendarのヘッダーに配置するボタン
          // 左側には「前月、翌月、今日」のボタン
          // 中央には表示している月
          // 右側には月表示、週表示、日表示、月予定
          left: 'prev,next today',
          center: 'title',
          right: 'month,agendaWeek,agendaDay,listMonth'
        },
        timeFormat: 'HH:mm', // 時間表示フォーマット
        timezone: 'Asia/Tokyo', // タイムゾーン設定
        eventLimit: true, // イベント増えた時にリンクボタン表示
        editable: true, // 編集可能設定
        slotEventOverlap: true, // イベントの見た目を重ねて表示
        selectable: true, // カレンダー空白部分のドラッグ可能
        selectHelper: true, // これをtrueにすると範囲設定できます
        selectMinDistance: 1 // 説明し難いので別途
    });
  });
</script>

「selectMinDistance」だけこちらで説明します。
fullcalendarさんはクリックイベントもドラッグイベントも取れるのですが、「selectMinDistanceに設定した間を超えたらドラッグイベントで扱うね」という設定らしいです。
これを設定しておかないと、クリックとドラッグでそれぞれ上手くイベントが取れなかったり意図しない動きになるので設定しておくといいです。
単位は何だったのか忘れてしまいました。秒じゃないとは思うのですが。
とりあえず「1」にしておけばいいと思います。適当ですいません。

fullcalendarのイベント設定

これは上に書いたプロパティ設定に続けて書くことにします

<script>
  $(document).ready(function() {
    $('#calendar').fullCalendar({
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,agendaWeek,agendaDay,listMonth'
        },
        timeFormat: 'HH:mm',
        timezone: 'Asia/Tokyo', 
        eventLimit: true, 
        editable: true, 
        slotEventOverlap: true, 
        selectable: true, 
        selectHelper: true, 
        selectMinDistance: 1, 
        events: function(start, end, timezone, callback) {
           // ページロード時に表示するカレンダーデータ取得イベント
        },
        eventClick: function(calEvent, jsEvent, view) {
           // カレンダーに設定したイベントクリック時のイベント
        },
        dayClick: function(date, jsEvent, view) {
           // カレンダー空白部分クリック時のイベント
        },
        select: function(start, end) {
           // カレンダー空白部分をドラッグして範囲指定した時のイベント
        },
        eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
           // イベントをドラッグして別日に移動させた時のイベント
        }
    });
  });
</script>

パッと見がそれっぽくなってきました。
上に書いたコメント部分が重要なやつです。
あとはこの部分でAjax呼べばOKです。

何やら長くなってきてしまったので、一旦ここで切ってみます。
続きは次回の記事で書きます。

稼働中のメールサーバ経由で、別サーバからメール送信してみた。

偉そうにサーバ構築なんてやっています。
まぁ常にタコ糸で綱渡りしているのは間違い無いのですが。
カンニングペーパー代わりのEvernoteが成長しています。

さて。
今回は偉そうなことをやってみました。

やりたかったこと

ConoHaで立てた稼働中のメールサーバ経由で、別サーバからメール送信する必要が出て来ました。
別サーバというのはWordpressのサーバなので、プラグイン使ってポイーっとメール送信すればいいのですが、そのサーバ上にランディングページ用の別ディレクトリを切って、そこに配置したお問い合わせフォームからメール送信という仕様です。

え。普通にWordpressに置けばいいじゃん。

とも思ったのですが仕方がありません。
あまり時間もなかったので、お問い合わせフォームはこちらを使わせて頂きました。

www.php-factory.net

どうもありがとうございます。

ちょっと考えてみた

アレです。
要はフォームが載っているサーバから、メールサーバの送信情報を使ってサクっとやればいいのです。
かんたん。かんたーん。

と思ったのが間違いでした。
よく思い出せばよかったのです。自分はインフラが嫌いだということを。
ここ最近、サーバをさわる機会が多かったので勘違いしていました。

以下、このように記載することにします。

・サーバA:Wordpressが載っているサーバ
・サーバB:メールサーバ(固定IPアドレスを振っています)

と、操作はrootでやってます。
OSはCentOS7です。
apacheは2.4です。

とりあえずsendmailをインストールしてみた

既に入っていればいいですが、入ってなかったらインストールしましょう。
CentOSって最初から入ってなかったっけ、などと思いながらyumちゃんにお願いしました。

$ yum install sendmail
$ yum install sendmail-cf

yumちゃんがいい感じにインストールしてくれます。
sendmail-cfの方は後で使うかもしれませんので、まとめて入れてしまいましょう。
ちなみに今回は使ってません。

何となくサーバAのホスト名を変更してみた

何かよくわかりませんが、ホスト名を変更してみました。

$ hostnamectl set-hostname xxxxx.com

上記のコマンドでホスト名が変わります。
変わるのですがこのままだとちょっと都合が悪いので、以下の処理も続けて実施します。

$ vim /etc/cloud/cloud.cfg

# 「- update-hostname」部分をコメントアウト
# - update-hostname

やってやりました。
ホスト名なのですが、自分の場合「メールドメインWordpressサーバドメインが同一」だったので、こちらのホスト名は別の名前に変えてしまいました。
srv.xxxxxxx.comみたいな感じです。

SMTP送信設定をしてみた

いよいよコアな部分です。ドキドキします。
sendmailをインストール後、「/etc/mail/」配下のファイルをごにょごにょします。

ここにsendmailの設定ファイルの数々がいらっしゃいます。
次にSMTP送信設定用の「authinfo」とかいうそれっぽい名前のファイルを開きましょう。
以降のvimコマンドなどは、こちらのディレクトリ内で叩いているという解釈でお願いします。

$ vim /etc/mail/authinfo

開いたらここにSMTP送信設定を記載します。
SMTP送信設定はメールサーバの方にきっとあることでしょう。
2行でセットです。

AuthInfo:SMTPサーバホスト名 "U:メール送信元の対象アドレス" "P:メール送信元の対象アドレスパスワード" "M:PLAIN"
AuthInfo:SMTPサーバホスト名:ポート番号 "U:メール送信元の対象アドレス" "P:メール送信元の対象アドレスパスワード" "M:PLAIN"

SMTPサーバホスト名部分がIPアドレスでもOKなのかは試してないです。
まぁホスト名をブチ込んでおけばいいんじゃないかと。
ConoHaのメールサーバの場合はサーバ詳細の部分に書いてあります。
多分、以下のような感じになるかと思います。

AuthInfo:smtp.xxxxx.aaaaaa.com "U:info@xxxxxx.com" "P:abcdefg12345" "M:PLAIN"
AuthInfo:smtp.xxxxx.aaaaaa.com:587 "U:info@xxxxxx.com" "P:abcdefg12345" "M:PLAIN"

ポート番号は大部分が「587」だとは思いますが、メールサーバに合わせて適宜修正して下さい。

sendmail.mcを修正して、sendmail.cfを作成することにした

ボスの登場です。
こいつが手強かったです。
が、以下のような感じの修正でできると思います。

sendmailの送信にはsendmail.cfを使用するのですが、sendmail.mcファイルを修正してcfファイル(コンフィグファイルらしいです)を生成するのが一般的だそうです。

$ vim /etc/mail/sendmail.mc

## 以下の設定を編集して下さい。
## コメントアウトするものと追記が必要なものは一応書いてみました

define(`confAUTH_OPTIONS', `A')dnl #コメント解除
define(`confAUTH_MECHANISMS', `EXTERNAL GSSAPI DIGEST-MD5 CRAM-MD5 LOGIN PLAIN')dnl #コメント解除
TRUST_AUTH_MECH(`EXTERNAL GSSAPI DIGEST-MD5 CRAM-MD5 LOGIN PLAIN')dnl #追記する
FEATURE(`authinfo', `Hash -o /etc/mail/authinfo')dnl #追記
define(`SMART_HOST', `SMTPサーバホスト名')dnl #コメント解除して編集
define(`RELAY_MAILER_ARGS', `TCP $h 587')dnl #追記
define(`ESMTP_MAILER_ARGS', `TCP $h 587')dnl #追記
dnl DAEMON_OPTIONS(`Port=smtp,Addr=127.0.0.1, Name=MTA')dnl #コメントアウト
DAEMON_OPTIONS(`Port=smtp, Name=MTA')dnl #追記
LOCAL_DOMAIN(`設定したサーバAのホスト名')dnl #コメント解除して編集
MASQUERADE_AS(`メール送信するサーバBのドメイン名')dnl #コメント解除して編集
FEATURE(masquerade_envelope)dnl #コメント解除

雑ですみません。
.mcファイルさんはコメントアウトに「dnl」という謎の文字列を使うそうです。
見慣れませんが仕方がないです。

accessファイルを編集してみる

このファイルはメールのリレー設定に使うファイルです。
否認とかにも使えます。
その辺はGoogle先生がきっと教えてくれます。

今回はサーバAから送信する時、サーバBを使いたいだけなのでリレー設定だけです。
偉そうに書いてる風味ですが、あまりよくわかっていません。

$ vim /etc/mail/access

Connect:サーバBのIPアドレス  RELEY

きっと「指定したIPアドレスにリレーしますよ!」っぽい何かだと思います。
多分。

mailertableとかいうものも編集してみる

これは「送信元のメールドメインはここですよ!」ファイルだと思われます。
そんな感じでいいんじゃないですかね!

$ vim /etc/mail/mailertable

送信元のメールドメイン  smtp:SMTPサーバホスト名

実際の記載例だとこんな風になると思います。

sample.com  smtp:smtp.sample.com

smtp:〜部分はダブルクォーテーションなどで囲わないようにして下さい。
何か上手くいかなかった模様です。

できたっぽい気がするのでsendmailを再起動してみる

上記の設定を反映させるために再起動します。
ひとつひとつの設定からファイルを生成する方法もあるのですが、ボスファイルである「sendmail.mc」の方に再起動したら新しいファイル作ってやるぜ設定が書いてあるので、sendmail再起動でいいかと思います。

$ systemctl restart sendmail

再起動コマンドはOSによって違うと思いますので、その辺はフィーリングで。

ついでにapacheも再起動しておきました。

$ systemctl restart httpd

うまく設定できたか試してみた

と、上記の設定が上手くできていればいいなと祈りながら、以下のコマンドを打ってみましょう。

$ sendmail -bv メールアドレス
# 実際には送信されません
# sendmail -bv aaaaa@sample.com みたいな感じでコマンドを打ちます

これを実行して上手いことできていると、以下のようなログが流れます。

$ sendmail -bv aaaaa@sample.com
aaaaa@sample.com... deliverable: mailer smtp, host smtp.xxxxxx.sample.com, user aaaaa@sample.com

で き て い る

そもそも何故こんなことをしたのか

問い合わせが入った時、管理側と投稿者に「問い合わせきたよー」メールを送信したかったのです。
投稿者の方は問題ないのですが、管理側の方が上手くいきませんで。
よくよく考えたら、ローカルホストに対してローカルホストユーザにメールをするわけなので、そのユーザがいないと「User unknown」と無慈悲なエラーログが表示されてしまいます。

で、今回やってみたのですが果たしてこれでいいのか。
少々不安なのですが、概ねいいでしょう。

またどこかのCTOになってみた。

そう言えば、7月からジョインした会社でCTOになりました。
エンジニアは自分1人なんですけども。

孤独なエンジニアですが結構楽しくやっております。
来年の今頃は4〜5人エンジニアいたらいいなぁなどと期待しています。
アレもコレも作りたいのです。

エンジニアが増えても当然開発やります。
好きな業務はコーディング。
マネジメントなんてフィーリング。組織作りはノリとテンション。
雑談担当からお酒の相手まで幅広くカバー。
こんなCTOがいてもいいんじゃないかと思います。

あ、やる時はちゃんとやるんです、これでも。
いや、ホントに。


というわけなので、またブログタイトルを変えてみました。
内容は特に変わりませんが、宜しくお願い致します。

CakePHP3でunlockFieldとやらを使ってみた。

実はJavaScriptも嫌いです。
嫌いなものばかりなのにエンジニアとか言ってていいんでしょうか。
いいとか悪いとかではなく、ここまで来てしまうと言い続けるしかありません。

さて、本題。
なんですが、今回のネタは合っているのかどうかよくわかりません。
「何かよくわかんないけどできちゃったぞ」的なものなので、その辺りご了承ください。

やりたかったこと

hidden項目として設定したフィールドに対して、AjaxとかJavaScriptとかでごにょごにょした結果を格納してSubmitする。

結果

Securityコンポーネントから怒られた。


困りました。非常に困りました。
困ったのですが、そりゃデフォルトの値が書き換えられてるわけなので怒るのも当たり前です。
だってSecurityコンポーネントですもの。
でも、どうにかしたい。

よくある対応方法

対象のActionが実行された時にSecurityコンポーネントをOFFにすれば動きます。
なんですが、Submitするってことは何かしらの編集処理なわけなので、そのActionをまるっとOFFにするのって引っかかります。
それが正しい処理なのかもしれませんが、個人的にはちょっとアレです。

なのでSecurityコンポーネントをOFFにする書き方は書かないことにします。
気になる方はGoogle先生にお申し立てを。

今回やってみた対応方法

「書き換え対象のフィールドだけSecurityコンポーネント対象外にする」というやり方です。
ここでやっとunlockFieldが登場します。

公式にはこんな風に書いてあります。

セキュリティ - 3.x

unlockedFields
POST バリデーションを解除したいフォームフィールドの一覧をセットします。 このコンポーネントの他にも、 FormHelper::unlockField() でも解除できます。制限が解除されたフィールドは、POST 時に必須ではなくなり、 hidden フィールドの値もチェックされません。

ナニソレ、イミワカンナイ。


この説明で他の方々わかるんですかね。
自分にはさっぱり理解できませんでした。


理解できない時はとりあえず色々アレコレ書いて試すタイプなので、色々試しました。
「あー。もうできないからPOSTする時にモデルからもう一回取得すっかな」などと邪なことも考えながら。

結局こんな風になりました

unlockedFieldsとやらをセットするのはView側です。
こんなソースにしてみました。

  <?php echo $this->Form->input('sample_id', ['type' => 'hidden']); ?>
  <?php echo $this->Form->unlockField('sample_id'); ?>

これだけです。
Ajaxで処理した時に上記ソースのsample_idに何か値がセットされます。
セットされてたら更新、されてなかったら新規登録みたいなことをやりたかったのです。
unlockFieldは何なんでしょうね。
無視しといて。よろしくみたいな感じなんでしょうか?
よくわかりません。

こんな書き方をすると、unlockFieldが有効になりSecurityコンポーネントさんも怒りません。
まぁとりあえずは結果オーライ、大勝利です。

合ってるのかなこれという不安は消えませんが、概ねいいでしょう。
正しい使い方を知ってる方いたら教えてください。

CakePHP3で画像データをDBに保存してみた。

その昔、こんな記事を書きました。
tsuralabo.hatenablog.com


画像データのDB登録記事がないことに1年近く経って気がつきました。
これはいけません。
ということで何事もなかったかの如く、記事を書くことにします。


DBへの画像登録というのは、いわゆるバイナリ登録です。
BLOBデータのアレですね。
昔から「BLOB」と聞くとスライム的な何かを想像してしまうのですが、それはどうでもいいです。
きっと過去にプレイしたRPGの何かなんじゃないかと思いますが、それもよくわかりません。


さて。
画像をDB登録する際はフォームヘルパーのFile属性に対して画像を指定して、それをアップロードするのが多いんじゃないかと。
それ以外もあるのかもしれませんが、自分が見たことないだけかもしれません。
とりあえず、View側の画像アップロード部分はこんな感じで作ります。

  <div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
    <span>画像</span>
    <div class="input-group">
      <span class="input-group-addon">
        <i class="glyphicon glyphicon-paperclip"></i>
      </span>
      <?php echo $this->Form->file('sample_image_id', [
         'class' => 'form-control', 
         'type' => 'file', 
         'div' => false,
         'label'=> false
      ]); ?>
    </div>
  </div>


Bootstrap3使ってます。ちょっと怪しい書き方ですが、その辺は生暖かくお願いします。
さて、ここに指定された画像データがsubmitされた時、以下のようにごにょっとすると登録できます。

<?php
  /**
   * 画像登録処理(アップロードされた画像をそのまま登録)
   * @param type $imageFileModel    image_filesテーブルのモデル
   * @param type $imageFile              登録する画像本体
   */
  public function saveImageFile($imageFileModel, $imageFile) {
    
    // 画像情報を取得
    $imginfo = getimagesize($imageFile['tmp_name']);

    // ファイル拡張子に応じて処理
    if ($imginfo[2] == IMAGETYPE_JPEG) {
      // JPEG画像データを生成
      $createImage = imagecreatefromjpeg($imageFile['tmp_name']);
    } else if ($imginfo[2] == IMAGETYPE_PNG) {
      // PNG画像データを生成
      $createImage = imagecreatefrompng($imageFile['tmp_name']);
    }

    // 画像データをバッファへバイナリ出力
    ob_start();
    imagepng($createImage);
    $imageBinary = ob_get_clean();

    // モデル登録用パラメータ生成
    $imgFileParam = [
      'content_type' => $imageFile['type'],
      'image_data' => $imageBinary
    ];

    // 新規登録用エンティティ生成
    $entity = $imageFileModel->newEntity($imgFileParam);

    // 画像保存処理実行
    $result = $imageFileModel->save($entity);

    // 画像メモリを解放
    ImageDestroy($createImage);

    // データ登録時のIDを返す
    return $result->id;
  }


自分のよくやるパターンだと、上記のfunctionをコンポーネントに入れてます。
で、コントローラの方でそのコンポーネント経由でfunction呼び出してます。
「tmp_nameってなんだよ」ってのはview側からsubmitすればわかります。
「pr($this->request->getData());」とかやってみると出てくると思います。

トランザクションに関しては呼び元のコントローラの方でやっているのですが、環境や仕様に合わせて適宜修正して下さい。
一番最後にIDを返していますが、画像登録用テーブルと実テーブルが別ですのでIDを取得して実テーブルにパラメータセットしているためこんな感じになっています。ここも適宜修正して頂けますと。

getimagesize部分は対象画像のデータが色々取れますので、幅とか高さとかその辺の制限があればここで取得を。
でも登録処理する前にバリデーションは別でやりたいんだよ! という方は、上記functionの前にバリデーションを。
箇条書きっぽくなりましたが、以下を参考にして下さい。

<?php
  // $params['sample_image_id']でFile属性のデータが渡ってくることにします
  // 拡張子判定の場合
  $extension = substr($params['sample_image_id'], strrpos($params['sample_image_id'], '.') + 1);
  
  // $extensionに拡張子が入るので判定。
  // 判定用のリストを作っておくのもいいかもしれません
  private $extensionList = ['jpg', 'png'];

  if (!in_array($extension, $this->extensionList)) {
      $errorMessage[] = '画像の拡張子が不正です。jpgファイル、またはpngファイルを指定して下さい。';
  }


  // サイズ判定の場合
  $fileSize = $params['sample_image_id']['size'];

  // そもそもアップロードされてるかどうか判定
  if ($params['sample_image_id']['error'] == 0) {
    // アップロードされた
  } else {
    // アップロードされてない
    // $params['sample_image_id']['error']が「4」とかだと思います 
  }


こんな感じで画像データをDBにバイナリ登録したり、バリデーション実行したりするといいんじゃないかと思います。
でも、多分もっといい書き方あると思います。

まず動くことが大事。

動いてから色々ごにょごにょするといいかなと。

CakePHP3でリスト取得時のカラムを連結させてみた。

気がついたら8月が終わっていました。
あっという間に2017年もあと4ヶ月です。時間の流れの速さが恐ろしいです。

さておき。
ラジオボタンがあまり好きじゃなくてセレクトボックスの方が好きなタイプなのですが
CakePHP3だとこんな風にしてクエリを投げてView側にセットしています。
多分合ってます。

<?php
  // usersモデルを使うということにします
  $userModel = TableRegistry::get('Users');
  $userList = $userModel->find('list'), [
    'keyField' => 'id',
    'valueField' => 'user_name'
  ])
  ->toArray();

  // ViewにセットしてView側のセレクトボックスのoptionsにセット
  $this->set('userList', $userList);

インデントが様子おかしいかもしれませんが、こんな感じです。
リスト取得部分はモデルに書いた方がすっきりするかもしれません。
その辺はいつものアレですが好みです。

で。
上の例ですと「user_name」を画面上に表示するような雰囲気になりますが、例えばこれが苗字と名前に分かれていたとしたらセレクトボックスの方で連結させたいなーとなるんじゃないかと思います。
カラムだと何でしょう。family_nameとfirst_nameとかになるんですかね。
まぁその辺はどうでもいいですが、今回はそういうことにします。

文字列連結させたら終わり! と思っていたのですがやってみたら上手くいきませんでした。
いや、上手くいくかもしれませんが何だかできなかったのです。
仕方がないのでごにょごにょして実装したソースが以下です。

<?php
  // またusersモデルを使うということにします
  $userModel = TableRegistry::get('Users');
  $userList = $userModel->find('list'), [
    'keyField' => 'id',
    'valueField' => function ($entity) {
       // この部分をEntityから取得する
       // 変数は$enitityじゃなくて構いません。適当に
       return $entity->user_full_name;
    }
  ])
  ->toArray();

  // ViewにセットしてView側のセレクトボックスのoptionsにセット
  $this->set('userList', $userList);
<?php
  // usersモデルのEntity側(Model/Entity/User.php)
  protected function _getUserFullName() {
     // Entity側で苗字と名前を連結させて返す
     return $this->_properties['family_name'].$this->_properties['first_name'];
  }


のような感じで書くとキレイにカラムが連結された状態でセレクトボックスにセットされます。
functionとかなんなの。イミワカンナイという方は、以下のような書き方でもいけます。

<?php
  // これもusersモデルを使うということにします
  $userModel = TableRegistry::get('Users');
  $userList = $userModel->find('list'), [
    'keyField' => 'id',
    'valueField' => function ($entity) {
       return $entity['family_name'].$entity['first_name'];
    }
  ])
  ->toArray();

  // ViewにセットしてView側のセレクトボックスのoptionsにセット
  $this->set('userList', $userList);


ちなみに自分はイミワカンナイ派です。
お好きな方を使うといいんじゃないかなと思います。