cakephp雑記帳 ITかあさん

ITかあさん

CakePHP SSL強制リダイレクト

CakePHP3からCakePHP1.3対応SSL強制リダイレクト

CakePHP2 Helper
わーい、久々のCakePHPネタ。
相当昔に開発をして、たまに保守作業をしていたCakePHP1.3環境。SSL対応する必要が出てきて一通りの設定が完了したのち、非SSLアクセスを強制的にHTTPSにリダイレクトさせる必要が出てきました。
調べたところ、CakePHP3からCakePHP1.3 (それ以前のバージョンについては確認していませんが) Securityコンポーネントを使うという点で全く同じです。

CakePHP2以下


if文でactionを指定することで、指定されたaction でのみSSL強制が動作可能です。例はAppControllerですが、個別のControllerで動作もちろん可能です。

CakePHP3

CakePHPではコンポーネントの呼び出しの仕方が少し変わっているだけで、強制リダイレクト用の関数を呼び出しの内容など、ほぼ差はない形になっています。

CakePHP Paginator sort カスタマイズ

CakePHP Paginator sort で、簡単テーブルソートを実現!

スクリーンショット 2015-07-11 11.51.01

大量データの一覧画面で データのソートを簡単に実現できたらなあ!

とかいいつつ、CakePHPでModel~Viewまでをbakeで焼き上げるとちゃんとソートが出来上がっているんだ。
※かあさんのサンプルでは、Bootstrapソートですが、Paginator-<とするとBootstrapを使っていないケースでも動作させることができます。

bakeで焼き上がった ソート用リンク

<th><?php echo $this->BootstrapPaginator->sort(\'number_of_purchase\');?></th>
<th><?php echo $this->BootstrapPaginator->sort(\'purchase_price\');?></th>
<th><?php echo $this->BootstrapPaginator->sort(\'selling_price\');?></th>
<th><?php echo $this->BootstrapPaginator->sort(\'number_of_selling\');?></th>
でも、ソートの名称が日本語名じゃなくて、Modelのフィールド名になっているのが嫌・・

Paginator sortを日本語にする

<th><?php echo $this->BootstrapPaginator->sort(\'number_of_purchase\',\'件数\');?></th>
<th><?php echo $this->BootstrapPaginator->sort(\'purchase_price\',\'買取価格\');?></th>
<th><?php echo $this->BootstrapPaginator->sort(\'selling_price\',\'販売価格\');?></th>
<th><?php echo $this->BootstrapPaginator->sort(\'number_of_selling\',\'件数\');?></th>
第二引数に値を入れると、ソートのリンクが変更されます

Paginatorの降順・昇順(DESC,ASC)によって、記号を出現させる

<a href=\"/courses/index/sort:Price.man_member_sell/direction:desc?stat=\" class=\"asc\">件数</a>
ソート実行中は、なんと、asc,descのクラスがあたっていました!

ですから、ソート実行したら、それが分かるように記号を出現させるにはjQueryなどJavaScriptを使って文字を挿入してあげます。

  $(document).ready(function(){
    $(\".asc\").append(\"▲\");
    $(\".desc\").append(\"▼\");
  });

検索実行時のパラメータもソート条件に含める

実はこれがけっこう悩みました。

<?php if(isset($this->request->query)):?>
<?php $this->BootstrapPaginator->options(array(\'url\' => array(\'?\' => $this->request->query))); ?>
<?php endif;?>
urlオプションに対して、request queryをセット。(検索実行時のパラメータ)

これは、通常のpagenateの場合でも一緒。

以上となります

CakePHP Searchプラグインで 複数Modelを検索

CakePHP Searchプラグインで 複数Modelを検索

CakePHP Behavior

CakePHP Searchプラグインでアソシエーションされている方のデータを検索したいの!

ああ!Searchプラグイン!君無しでは検索できない!

もうね、便利すぎて検索フォームとか自作する気ないわ。何かもうかあさんの人生にSQLが必要無いわ(エンジニアとしてそれはどうかと。。

検索がとっても便利なSearchプラグインの基本的な使い方はこちら

基本的な使い方は上記サイトがとてもきれいにまとめてあるのでそちらを参照してください。
かあさんの方は複数モデルを扱っているケースで、アソシエーションされている方のModelのデータをSearchプラグインを使って検索する方法です。
例として、ブログ的なPostModelとUserModelがあったとして、PostModelからも著者であるUserModelも両方ともSearchプラグインの検索対象としたい場合を想定します。

Controller

※Search Compornentは各自呼んでおくようにしてくださいな。

	public function index() {
        $this->paginate = array(
            'conditions' => $this->Post->parseCriteria($this->passedArgs),
            'limit' => 10,
        );
		$this->set('courses', $this->Paginator->paginate());
	}

Controllerは特に変わったことはありません。要するに、Searchプラグインで、Postにアソシエーションされているデータを検索したいのです。

Model

複数Modelに股がって検索したい時は、アソシエーションされている方のModelではなく、アソシエーションしている方、今回だとメインはPostModelで、アソシエーションしているのがUserModelとします。

 public $actsAs = array('Search.Searchable');
	public $filterArgs = array(
        // search rules
        array('name' => 'title', 'type' => 'like', 'field' => 'Post.title'),
        array('name' => 'name', 'type' => 'like', 'field' => 'User.name'),
    );

fieldというオプションがあると知って、入れてみたら簡単に動きました。
nameとは、検索のgetパラメータにあたる部分。titleの検索対象はPostModelのtitleフィールドにするよ、という意味になります。
name属性とfield名は必ずしも同じ名前である必要はないの。ですから、titleのパラメータがあったら、PostModelのtitleフィールドを検索する、という意味になります。

view

viewはなんだか不思議な感じになります。

Form->create('Post');?>
Form->input('title', array('div' => false, 'required' => false, 'placeholder' => 'title入力'));?>
Form->input('name', array('div' => false, 'required' => false, 'placeholder' => '著者名入力'));?>
Form->submit(__('Submit'));?>

すると出力されるHTMLは当然のことながらPostが主体になってしまいます。



でも、さきほどModelに、getパラメータのnameに 「name」が来たら、User.nameを検索してね!と記載したので大丈夫なんです。
もしも違うModelに同じフィールド名があって、それを検索対象としたい場合は、$filterArgsに対して都度Model名も書いてあげればよいわけです。分かると実に簡単!
Searchプラグインがますます大好きになりました

CakePHP2 database.phpを本番と開発環境で違うDBを設定るす

cakephp Config

DBは開発・ステージ・本番で違うのよ!

ひさびさにCakePHPをお届けします。今更感満載ですが、CakePHPの開発と本番とでDBが違う時一緒のdatabase.phpを使いたいなあ!そんな時はdatabase.phpに複数のDBを設定しましょう。

config/database.php

AppControllerにも書く方法があるようですが、database.phpだけ書けばよいので間違いにくいので個人的にはこれが一番好き。

class DATABASE_CONFIG {

	public $default = array(
		'datasource' => 'Database/Mysql',
		'persistent' => false,
		'host' => 'localhost',
		'login' => 'root',
		'password' => 'password',
		'database' => 'databaseName',
		'prefix' => '',
		//'encoding' => 'utf8',
	);

	public $production = array(
		'datasource' => 'Database/Mysql',
		'persistent' => false,
		'host' => 'localhost',
		'login' => 'user',
		'password' => 'password',
		'database' => 'test_database_name',
		'prefix' => '',
		//'encoding' => 'utf8',
	);
	public function __construct() {
        if (env('SERVER_NAME') == 'HostNameHere') {
            $this->default = $this->default;
        } else {
            $this->default = $this->production;
        }
    }
}

こんな感じで、ホスト名によって切り替えればよいですね。

生まれて初めて、CakePHPのHABTMを書いてみたの♪

生まれて初めて、CakePHPのHABTMを書いてみたの♪

生まれて初めて、CakePHPのHABTMを書いてみたの♪
Bakeに頼らず自分でHABTAMのアソシエーションを書いたのは初めてだったかも。
正直わかりにくいのでどうしても避けてきたのですが、比較的仕事に余裕があったので、ゆっくりやってみて、
今日やっとこさ理解することができました

HABTMの構造

HABTAMとはHasAndBelongToMany(HABTM..ハビタムで呼び方あってる?)の略で、多対多のデータ構造です。

多対多のデータ構造の例

多対多のデータ構造の典型的な例はブログですかね。記事はたくさんあって、必ずどれかのカテゴリーに入っているけれど、一つの記事は複数のカテゴリーに分類されるケースもある。
そういうのが多対多のHABTMです。

・ブログの記事(Post 多のデータ)とカテゴリー(Category 多のデータ)とその中間テーブル(CategoryPost 中間テーブルのデータ)

HABTMアソシエーションをPostModelに書いてみる

Post.php

class Post extends AppModel {
	public $name = 'Post';

	public $hasAndBelongsToMany = array(
		'Category' => array(
			'className' => 'Category',
				'join_table' => 'categories_posts',//中間テーブルの名称(Model名ではなくテーブル名)
				'foreignKey' => 'post_id',//categories_postsの中のCategoryとのforeignKey
				'associationForeignKey' => 'category_id',//アソシエーションしている中のforeignKey.ここではCategoryPostのcategory_id
 			),
		);
}
categories_postsという中間テーブル、当初はposts_categoriesでした。でもなぜかこれだとcategories_postsっていうテーブルが無いよ!というエラーが発生してしまい、
結局中間テーブルの名称そのものをposts_categories→categories_postsに変更しました。ナゼ?why?

recursiveで深度を設定していなくても中間データまで一発で取得できているところが非常にナイスです!

PostsControllerで、Postデータをfind Allした結果

hasAndBelongsToManyのアソシエーションをPostModelにしただけで、投稿した記事(Post)が、どのカテゴリーに属しているか、さらにその中間データであるCategoriesPostのデータも取得ができました
PostModelにちょっと書いただけで、複雑なアソシエーションをしてくれるなんて、ちょっとうれしい。。。

array(
	(int) 0 => array(
		'Post' => array(
			'id' => '9e8c4ef7-33d0-11e4-a724-3417eba238af',
			'title' => '記事タイトル',
			'text' => '記事本文',
			'created' => '2014-09-04 10:12:45',
			'modified' => '2014-09-04 10:12:47'
		),
		'Category' => array(
			(int) 0 => array(
				'id' => '4b056148-33d0-11e4-a724-3417eba238af',
				'slug' => 'category_1',
				'name' => 'カテゴリー1',
				'created' => '2014-09-04 10:10:29',
				'modified' => '2014-09-04 10:10:32',
				'CategoriesPost' => array(
					'id' => 'e6dd311a-33f3-11e4-a724-3417eba238af',
					'post_id' => '9e8c4ef7-33d0-11e4-a724-3417eba238af',
					'category_id' => '4b056148-33d0-11e4-a724-3417eba238af',
					'created' => '2014-09-04 14:25:32',
					'modified' => '2014-09-04 14:25:35'
				)
			),
			(int) 1 => array(
				'id' => '4b055c33-33d0-11e4-a724-3417eba238af',
				'slug' => 'data_input',
				'name' => 'category_2',
				'created' => '2014-09-04 10:10:24',
				'modified' => '2014-09-04 10:10:27',
				'CategoriesPost' => array(
					'id' => 'fd4f31a0-33d0-11e4-a724-3417eba238af',
					'post_id' => '9e8c4ef7-33d0-11e4-a724-3417eba238af',
					'category_id' => '4b055c33-33d0-11e4-a724-3417eba238af',
					'created' => '2014-09-04 10:15:36',
					'modified' => '2014-09-04 10:15:38'
				)
			)
		)
	),

今度は逆に、もう一方の多のデータであるCategoryModelをHABTMしてみる

foreignKeyとassociationForeignKeyが逆になっているだけで後は全く同じことに注目してください。

class Category extends AppModel {
	public $hasAndBelongsToMany = array(
		'Post' => array(
			'className' => 'Post',
				'join_table' => 'categories_posts',
				'foreignKey' => 'category_id',//categories_postsの中のPostとのforeignKey
				'associationForeignKey' => 'post_id',//アソシエーションしている中のforeignKey.ここではCategoryPostのpost_id
 			),
		);
}

Categoryデータをfind Allした結果

先ほどと同様に、PostModelの下にCategoriesPost(中間データ)がアソシエーションされたデータが返ってきました。

array(
	(int) 0 => array(
		'Category' => array(
			'id' => '4b055c33-33d0-11e4-a724-3417eba238af',
			'slug' => 'data_input',
			'name' => 'データ入力',
			'created' => '2014-09-04 10:10:24',
			'modified' => '2014-09-04 10:10:27'
		),
		'Post' => array(
			(int) 0 => array(
				'id' => '9e8c4ef7-33d0-11e4-a724-3417eba238af',
				'title' => '記事タイトル',
				'text' => '記事本文',
				'created' => '2014-09-04 10:12:45',
				'modified' => '2014-09-04 10:12:47',
				'CategoriesPost' => array(
					'id' => 'fd4f31a0-33d0-11e4-a724-3417eba238af',
					'post_id' => '9e8c4ef7-33d0-11e4-a724-3417eba238af',
					'category_id' => '4b055c33-33d0-11e4-a724-3417eba238af',
					'created' => '2014-09-04 10:15:36',
					'modified' => '2014-09-04 10:15:38'
				)
			),
			(int) 1 => array(
				'id' => 'c672c872-33d0-11e4-a724-3417eba238af',
				'title' => '記事タイトル',
				'text' => '記事本文',
				'created' => '2014-09-04 10:13:52',
				'modified' => '2014-09-04 10:13:54',
				'CategoriesPost' => array(
					'id' => 'fd4f3512-33d0-11e4-a724-3417eba238af',
					'post_id' => 'c672c872-33d0-11e4-a724-3417eba238af',
					'category_id' => '4b055c33-33d0-11e4-a724-3417eba238af',
					'created' => null,
					'modified' => null
				)
			)
		)
	),
	(int) 1 => array(
		'Category' => array(
			'id' => '4b056148-33d0-11e4-a724-3417eba238af',
			'slug' => 'category_2',
			'name' => 'カテゴリー2',
			'created' => '2014-09-04 10:10:29',
			'modified' => '2014-09-04 10:10:32'
		),
		'Post' => array(
			(int) 0 => array(
				'id' => '9e8c4ef7-33d0-11e4-a724-3417eba238af',
				'title' => '記事タイトル',
				'text' => '記事本文',
				'link' => 'http://www.atsoho.com/jobinfo/detail/no-59201.html',
				'created' => '2014-09-04 10:12:45',
				'modified' => '2014-09-04 10:12:47',
				'CategoriesPost' => array(
					'id' => 'e6dd311a-33f3-11e4-a724-3417eba238af',
					'post_id' => '9e8c4ef7-33d0-11e4-a724-3417eba238af',
					'category_id' => '4b056148-33d0-11e4-a724-3417eba238af',
					'created' => '2014-09-04 14:25:32',
					'modified' => '2014-09-04 14:25:35'
				)
			)
		)
	)
)

わかり難いからいやだとずっと避けて別々にデータを取得していたのですが、recursiveを設定しなくても、一発で中間データまで取ってきてくれるのはいいですね。
ちょっとわかり難い部分もありましたが、今後は使って生きたいと思います。

CakePHP2.x Ajax PostとAjax pagenationとか

CakePHP Ajax Postのお話

jQueryの$.ajax()と組み合わせると結構簡単に出来ます。

Ajax Post

jQuery

	$("#post_bt").click(function () {
			$.ajax({
				type: "POST",
				url: "/posts/add/",
			        data: {text : 'テキストテキスト'},
				dataType: "json",
				success: function(data) {
					location.reload();
				},
				error: function(data){
					//失敗した時の処理
			}
		});
	});

PostsController.php

    function add() {
        if ($this->request->is('post')) {
            if ($this->Post->save($this->request->data)) {
                //saveが完了した後の処理
            }
	}
    }

Ajax Pagination

AjaxPaginationもそれほど難しくありません。

PostsController.php

ポイントはRequestHandler->isAjax()のところ。
layoutもnullにしておいて、paginationの中身だけ表示されるようにします。

        $this->set('plans', $this->paginate('Plan'));
     if($this->RequestHandler->isAjax()) {
            $this->layout = null;
            $this->render();
        }

Ajaxとview

viewの中身はpaginationの中身を、普通に書けばよいです。
Ajaxもそれほど難しくなく、pagenateのリンクをjQueryのload()で指定した箇所に呼び出しています。
2回目以降にAjax Pagenateのリンクがクリックされた時に2ページ目以降の内容が入るdivのボックスを追加しているのです。

<script>
  $(function(){
  $('.more:last a').load(function(){
  // ページの読み込みが完了した後に実行するコード
  });
  $('.more:last a').click(function() {
  $(this).parent().remove();
  var url = $(this).attr("href");
  $('.posts:last').load(url);
  $(".posts").after('<div class="posts"></div>');
  return false;
  });
  $('.more:not(:has(a))').css('display','none');
  });
  </script>
  <div class="posts"></div>
  <?php echo $this->Paginator->next('もっと見る', array('class' => 'more','tag' => 'p'));?>

1ページ目だけにしか表示したくない内容があれば。

Ajaxページ読み込みするに当たって、読み込んだ際に余計なコンテンツを読み込んでしまう場合もあるかもしれません。

もしも1ページ目だけにしか表示させたくない内容があれば、以下のように書くと現在表示中のページ番号が取得出来ます。これはAjax Pagenationしていても同様に取得出来ます。

$this->Paginator->counter(‘{:page}’)
if($this->Paginator->counter('{:page}') == 1) {
}

これで1ページ目だけ表示させたい場合は対応出来ます。

CakePHP2.x的Componentの作り方

CakePHP2.x的Componentの作り方

Componentとは、Controllerの共通処理に使います。

奥様!Controller下にComponentフォルダがございませんわ!

CakePHP2.xにはデフォルトでComponentフォルダはないので、自分でControllerフォルダ下にComponentフォルダを作ります。

FooComponentを作る

FooComponent.phpをComponentフォルダ下に作ります。

ファイル名はコンポーネント名+Component.phpです。

中身

class FooComponent extends Component
{
public function hoge()
{
// ここにコードを書く
}
}

Componentの呼び出し

class MPlanController extends AppController {
//Componentの呼び出し

public $components = [‘Foo’];

後は使いたいControllerのアクションのところで
$this->Foo->hoge();
と、呼び出して下さい。

CakePHP Modelで大量にbind(アソシエーション)したらContainable Behavior使うといいらしい。

CakePHP Modelで大量にbind(アソシエーション)したらContainable Behavior使うといいらしい。

CakePHPのModelのアソシエーションが本当にひどいのよ。もう少しシンプルにならないかしら?主婦も忙しいのにModel呼び出す度に必要のないModelをunBindeするなんてやってられないわ!

うい。私は常にControllerでアソシエーションするんですが(必要な時に必要な分だけ取り出す)Modelに既にアソシエーション大量にしてあって、うわー!unbindeめんどいよー!な時に使えるのが

Containable Behavior

Containable Behaviorの使い方

Model

class Model extends AppModel {
var $actsAs = [‘Containable’];

// 後はお好みのアソシエーションを記述

Controller

// HogeModelだけがアソシエーションとして取り出せる

$conditions = [
‘contain’ => [
‘HogeModel’
],
];
$this->Model->find(‘all’,$conditions);

Pagenateの場合も、$this->paginate(‘Model’)でPagenateでデータ取得前にcontainしておけばOK!

CakePHP2 生まれて初めてHelper作ってみるの〜♬

CakePHP2 生まれて初めて~Helper作ってみるの〜♬

CakePHP2 Helper

ごめんなさい、全然生まれて初めてじゃないです。

CakePHPのhelperは言わずもがな、View側の共通処理を記載します。(Controllerの共通処理はComponentsですね)

CakePHPでUserAgentによる表示切り替えをHelperで実装してみる

よくある、スマホでアクセスしたらどうする、PCでアクセスがあればどうするといった UserAgentによる表示切り替えをHelperで実装してみます。

参考

UserAgentによる切り替えのサンプルはこちらが参考になりそうなので、これをCakePHPのHelper化したいと思います。

STEP:1 View/HelperディレクトリにUserAgentHelper.phpを作る

HelperディレクトリにHelperファイルを作ります。

ファイル名は必ず○○Helper.php

STEP2:Helperの中身を書く

class ○○Helper extends AppHelper の書き出しです。
App::uses('AppHelper', 'View/Helper');
 
class UserAgentHelper extends AppHelper {
 
    private $ua;
    private $device;

    public function deviceCheck(){
         
        //ユーザーエージェント取得
        $this->ua = $_SERVER['HTTP_USER_AGENT'];
 
        if(strpos($this->ua,'iPhone') !== false){
            //iPhone
            $this->device = 'iphone';
        }
        elseif(strpos($this->ua,'iPad') !== false){
            //iPad
            $this->device = 'ipad';
        }
        elseif((strpos($this->ua,'Android') !== false) && (strpos($this->ua, 'Mobile') !== false)){
            //Android
            $this->device = 'android_m';
        }
        elseif(strpos($this->ua,'Android') !== false){
            //Android
            $this->device = 'android_t';
        }
        else{
            $this->device = 'pc';
        }
        return $this->device;
    }
}

STEP3:Controllerから作ったHelperを呼び出し

public $helpers = [‘UserAgent’];
App::uses('AppController', 'Controller');
class ExampleController extends AppController {
    public $helpers = ['UserAgent'];
//以下省略

STEP4:後はViewで良きほどに使う!

$this->UserAgent->deviceCheck();

以上!おしまい!

CakePHP2的 Sitemapの作り方

今更なんですが、CakePHP2的sitemapの作り方を。。。

Sitemapを作るなら、プラグインでさくっと作れるに決まっているわ!そうよ!うふふ

と思っていたら、返って柔軟に作りにくいことが分かり、Bakeryの基本的な手順にのっとって作ったほうが後々楽だったのでそのメモを。

なお、Bakeryは上記URLの手順はCakePHP1系ですが、基本的なことはほぼ一緒ですし、以下記載するコードは2.4で動作チェック済みです。

ではでは、CakePHP2的sitemap。

基本的にはControllerを作って、そのControllerに乗っ取ってview作ってルーティングでhttp://yourhost/sitemapにアクセス出来るようにするだけ。

SitemapsController.phpの作成

class SitemapsController extends AppController{
    public $layout = 'xml/default';
    public $uses = ['Post']; 
    public $helpers = ['Time']; 
       public $components = ['RequestHandler'];

    function index (){     
        $this->set('posts', $this->Post->find('all', [ 'conditions' => ['is_published'=>1,'is_public'=>'1'] ]));
        Configure::write ('debug', 0);
        $this->RequestHandler->respondAs('xml');
    } 
} 
エラーが万が一出力されないために、debugは0にしておきましょう。findするデータは任意でどんどん増やして行って下さい。

layoutファイルは、CakePHPにデフォルトでLayout/xml/default.ctpを使うことにしたので、layoutファイルの定義はxml/defaultになりました。
もちろん、layoutファイルは自分で作って設定しても大丈夫です。
RequestHandlerを使ってXMLとして表示させます。これが無いとxmlとして出力されず、だ〜っと文字列だけ出てします。

View/Sitemaps/index.ctpを作成

はい、後はもうお分かりですね、View/Sitemaps/index.ctpにfindしたデータを記載します。

<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url> 
<loc><?php echo Router::url('/',true); ?></loc> 
<changefreq>daily</changefreq> 
<priority>1.0</priority> 
</url> 
<!-- posts--> 
<?php foreach ($posts as $post):?> 
<url> 
<loc><?php echo Router::url(array('controller'=>'posts','action'=>'view','id'=>$post['Post']['id']),true); ?></loc> 
<lastmod><?php echo $this->time->toAtom($post['Post']['date_modified']); ?></lastmod> 
<priority>0.8</priority> 
</url> 
<?php endforeach; ?> 
</urlset>

app/Confit/routes.php ルーティングを記述

Router::connect('/sitemap', array('controller' => 'sitemaps', 'action' => 'index')); 
これでhttp://yourhost/sitemapにアクセス出来ますね。

ほんと、プラグインに頼らず最初から自分で書いたら15分以内に出来たわ。