LESS

CSSをシンプルに書くことができるLESS使ってみた
Twitter Bootstrapを使ったサイト構築の流れ (後編)

便利だの。twitter bootstrap3というのは、昔のbootstrapと結構違いますね。LESSというのを使っているので、勉強してみようと思いました。LESSというのはCSSの値をパッケージングできまっす。なのでbootstrapのややこしいcssも簡単にカスタマイズできるようになっているのかもしれません。

http://incident57.com/less/

これがMac用のLessコンパイラーというやつらしいです。Lessを作ってコンパイラーでCssにするそうです。twitter bootstrapは全部Lessでcssを管理しており、bootstrapのgithubにそのLessがあります。これをカスタマイズしてコンパイラーでcssにして、bootstrapのcssを入れ替えればいいそうです。

ただ、どうも、上記コンパイラだとエラーがでる。&:extend(.clearfix all)というやつに対応できていないっぽい。有料版の下記だと問題なくコンパイルできました。

http://incident57.com/codekit/

下記のようにネストもできるようです。関数的なこともできるし、計算もできるようです。いいっすね。

@bgcolor: #09c;
@brand_color: red;

div#hoge .hoge table{
    color: black;
    background-color: @bgcolor;
    
    th{
        color: @brand_color;
    }
}

LESSと同じようなメタ言語として、Sass、Stylusなどがあるらしい。聞いたことはある。

管理画面のメニューを自動で作成する

管理者の権限は色々ありますし、最終的にはメソッド単位で細かく管理できないといけないと思います。
あと、単にそのメソッドを実行できるか否かだけではなく、どのデータを扱うことができるかまで管理できる必要があります。
こういうのはどうもACLというらしいです。

すごく基本的な管理者権限を考えると、管理者のグループを作成して、そのグループがどのモデルに属するかを設定するといいのではないかと思いました。例外処理ができないと実用的ではありませんが、グループがモデルに属するのであれば、アソシエーションの状態から、自動的に実行可能なメソッドと、編集可能なデータを決定できるのではないかと思います。

例えば、roleのグループをrolesというテーブルで管理しつつ、その各グループに属するモデルもデータベース上で設定しておきます。role_idが1のグループは、Shopモデルに属するのであれば、Shopが持っているHasOneとかHasManyとかのテーブルのみ管理できるようにすればよいです。ShopがMovieを持っている場合、Movieのadmin_index,admin_view,admin_edit,admin_deleteへのアクセスが許可されて、admin_indexは自分が持っているMovieのみ表示されるといった感じになります。これをコンポーネント化しておけば、beforeFileterとかで$this->Auth->user(‘role_id’)をチェックして自動的に埋め込みが可能なんではないだろうかと思いました。

cakePHPのプロジェクトを簡単に作成するようにする

cakePHPをやってる人なら少なからずやってると思いますが、僕はやってません。
今考えて仕組みをつくりたいと思います。

今つくっているBakeを使いたいので、BakeのプロジェクトをGithubのリポジトリに入れておいて、それをクローンしてデータベースをつくって、Bakeします。すると作成したいサイトの大方(まだ少し)が出来上がるので、それを別のブランチとしてpushします。で納品時にBakeプラグインなどを簡単に削除できるコマンドを用意しておき、それを実行します。

というのでどうでしょうか?
まあシークレット文字列とかは手動変更が必要だなこれじゃあ。まあ今はいいか。

Bake お気に入り登録機能

お気に入りの登録機能を実装するときにどうしようかなと思っております。
単純に考えると、お気に入りを登録する対象を設定しておき、その対象設定に基づき、モデルを作成します。もしテーブルがなければモデル作成の前にテーブル作成してもいいです。

例えばshopsにお気に入りを登録するという設定がされている場合、shop_likesテーブルを作成して、ShopLikeモデルを作成するわけです。でもshopsにlike_countみたいなお気に入り数を登録したい場合もあるでしょうから、shopsが対象に設定されているのに、like_countがフィールドとしてつくられていない場合は、それも事前に作成してもよいと思います。

しかしながらそうなると、モデル作成関数の実行前にこれら全てを実行する必要がありますので、テーブル作成関数というモデル作成の前段のフェーズをもう少しきちんと考えた方がいいのかなとも思う訳です。おまけにいうと、僕は今Bakeの設定のためのデータベースと、Bakeによって作成するデータベースを同じにしてしまっております。これを分けること自体はもちろん可能だと思うのですが、どうも頭の中でこの2つが混在しているようです。つまりいざ分けようとするときに、あらわけられないじゃんこれじゃあという風になるだろうと思っている訳です。

本来であればというか理想としてはBakeのための設定情報データベースのみから、Bakeにより作成するテーブルを全て新規で作成し、
それに基づきモデルを作成し、、、という流れであれば一番分かりやすいわけです。しかしながら、それはそれで大変でありますし、phpmyadiminを使えばデータベースの作成というのは割とすぐにできるものであり(簡単なものならですが)ますので、そのテーブル情報に基づきBakeするという考え方は、踏襲してもいいのではないかと思っております。

なので、最終的に納品するような状態になった場合に限り、設定情報に関連するテーブルを削除するという考え方で進めればいいのだなと今思いました。となるとまあ今のまま進めていいのか。

defaultデータベースにお気に入り設定に関するテーブルを作成しまして、そこに対象となるモデルを登録するようにします。
Bakeの前段でテーブル作成関数を実行して、そこでお気に入りに関する未作成のテーブルを作成するようにします。
となると、画像登録(Filebinder)に関する未作成テーブルもそこで作成することになります。なにしろ、機能毎に別個でテーブル、モデル、コントローラー。。。という風に作成していくと、不整合が生じてしまいますので、テーブル、モデル、コント、、という流れの中で全機能をつつがなく実装していく必要があるわけであります。

Bake改造 あたまの整理

Bakeを改造しているわけですが、結局何がしたいのか不明瞭なままつくっているとたこ足配線みたいになってきますので、目的をしぼってリファクタリングしていきます。最終的にはWEB上で設定した内容に基づいてぽこっとサイトを生むようなものを想定しておりますので、Bakeのコンソール上で設定を入力させるような機能はいらないわけで、むしろサイトの設定内容に基づき一から完全なサイトを瞬間的に一気に作成することが必要なわけですから、どの設定をいじっても確実のその設定が反映されたサイトが自動作成されるようにする必要があり、裏側のBakeコマンドは1つでいいわけです。その一つに全てを集約させていきます。

データベースがあり、そこに設定情報があります。その設定情報に基づき、モデルを作成し、コントローラーを作成し、ビューを作成します。認証情報にソーシャルを連動させるか否か、お気に入り機能を搭載するか否か、各モデルにおいて画像を登録させるか否かなどの情報はモデル作成段階、コントローラー作成段階、ビュー作成段階でそれぞれチェックし、設定内容に応じたモデル、コントローラー、ビューを作成していく必要があります。

あと最終的には、特定のディレクトリに新しいプロジェクトを作成し、シークレット文字列?とかもランダムに変えつつ、今回作成するBakeプラグインなどを排除したものにし、データベースからも設定情報自体は削除(分離)するようにしたいです。自動作成するための情報は、自動作成後(納品後)は基本不要だからであります。まあこの辺りは特段問題ないですので、後で考えるとして、やはり設定情報を全てDB上で表現できるようにし、その情報に基づいてターミナルからBakeすることで一発サイト作成が出来る状態を早々につくっていきたいと思います。

cakePHP2.3 Bakeの改造

coreのbakeをコピーしてプラグイン化することによりオリジナルbakeを作成します。

bakeに、下記機能を追加していきます。
・twitter bootstrapを使ったviewに変更する
・多言語化機能を使った効率的な日本語化を可能にする
・管理画面のメニューもDBに応じて自動作成する
・認証関連も自動実装する
・問い合わせ機能も自動実装する
・userの権限に応じた細かなアクセス管理(表示・編集内容の限定)を自動実装する
・ユーザのマイページのメニューも自動作成する
・お気に入り、口コミ機能なども自動実装を可能にする
・ソーシャル連動も自動実装する
・サイト名、各ページのMETA情報の登録などの管理画面も自動作成する
・デザインはTempleteの入れ替えで柔軟に変更できるものにする
・画面構成などは順次パターンを増加させて、なるべく柔軟な画面構成、遷移、デザインへの対応を実現していく
・上記の機能をWEB上で設定〜サイト自動作成までを可能にする
・本システムを利用するユーザ毎に作成したサイトの一覧などを保存できるようにする
・主要サイトパターン毎に個別の設定画面を儲けてより的を得たサイトを簡単に自動作成できるようにする

cakePHP プラグイン内でモデルを使う

プラグインを追加して、プラグインのコントローラーから、プラグインではないモデルにアクセスする方法がよく分からない。
というか、User認証に関するロジックをプラグイン化しつつ、モデルは既存のUserモデルを使おうとするときに、プラグインのユーザ登録画面で、Userモデルに記載しているバリデーションがなぜか中途半端に機能しない。notemptyは機能するが、それ以外のvalidationロジックがまったくきかない。なぜだ??確かにプラグインの各種連動の仕組みをしっかり分かっていない。だから変なことになっているのかもしれない。

プラグイン内のコントローラー名は、UserControllerにしていたのだが、これだとどうもうまくいかないらしい。
HogeUserControllerにして、public $uses = array(‘User);としつつ、$this->User->save($this->request->data)とするとうまくバリデーションが効いた。中途半端に効くというのは一番困るわー。

cakePHP2.3 多言語化

app/Consoleに移動して、下記を起動する

./cake i18n

E押したら翻訳ファイルみたいのを作ってくれる。詳細は、ここ。わかりやすい。

/app/Locale/jpn/LC_MESSAGES/default.poに出力したファイルをコピーする。僕の場合は、/app/Locale内にdefault.potというファイルが出来たので、それを上記パスにdefault.poとしてコピーした。potをpoにするのが重要でありんす。そのpoファイルに英語に対応する日本語をせこせこ記述すると、日本語訳してくれるようになった。

AppControllerとかで、beforefilterに下記のように書くと環境に関わらず強制的に言語を変更できる。日本の場合は、ja。

function beforeFilter() {
	Configure::write('Config.language', 'en');
}

YouTube API

cakePHPのviewでYouTubeAPIを初めて使った。divのid=”videoDiv”内に動画が自動で再生される。閲覧中断していた動画は中断したところから再生される。閲覧中断ボタンと、閲覧完了ボタンに対応している。といったようなことをした。

<?php $this->start('script')?>
<script src="//www.google.com/jsapi" type="text/javascript"></script>
<script>
	var ytplayer = null;
	<?php if($watch):?>
		var seek = <?php echo h($watch['Watch']['seek'])?>;
	<?php else:?>
		var seek = 0;
	<?php endif;?>

	google.load("swfobject", "2.1");

	function _run() {
		// The video to load.
		var videoID = "<?php echo h($movie['Movie']['code'])?>";
		// Lets Flash from another domain call JavaScript
		var params = { allowScriptAccess: "always" };
		// The element id of the Flash embed
		var atts = { id: "ytPlayer" };
		// All of the magic handled by SWFObject (http://code.google.com/p/swfobject/)
		swfobject.embedSWF("http://www.youtube.com/v/" + videoID + "?enablejsapi=1&playerapiid=player1",
			"videoDiv", "560", "315", "9", null, null, params, atts);
	}

	google.setOnLoadCallback(_run);

	function onYouTubePlayerReady(playerId) {
		ytplayer = document.getElementById("ytPlayer");
		ytplayer.seekTo(seek, true);
		ytplayer.playVideo();
	}

	//動画閲覧の中断
	function stop(movie_id){
		if(ytplayer){
			var seek = ytplayer.getCurrentTime();
			if(seek > 0){
				location.href = '<?php echo $this->Html->url(array('controller' => 'movies', 'action' => 'stop'))?>' + '/' + movie_id + '/' + seek;
			}
		}
	}

	//動画閲覧の終了
	function finish(movie_id){
		location.href = '<?php echo $this->Html->url(array('controller' => 'movies', 'action' => 'finish'))?>' + '/' + movie_id;
	}
</script>
<?php $this->end()?>

PHP 画像の縮小(リサイズ)

public function resize_image($tmp_file){
	$w = 96;
	$h = 96;

	list($width, $height, $type) = getimagesize($tmp_file);

	if($width > $w || $height > $h){ //はみ出している
		if($width >= $height){
			$new_width = $w;
			$x = 0;
			$new_height = round($h * $height / $width);
			$y = round(($h - $new_height) / 2);
		}else{
			$new_height = $h;
			$y = 0;
			$new_width = round($w * $width / $height);
			$x = round(($w - $new_width) / 2);
		}
	}else{ //はみ出してない
		$new_width = $width;
		$new_height = $height;
		$x = round(($w - $width) / 2);
		$y = round(($h - $height) / 2);
	}

	//背景画像
	$canvas = imagecreatetruecolor($w, $h);
	$bcolor = imagecolorallocate($canvas, 255, 255, 255);
	imagefilledrectangle($canvas,0,0,$w,$h,$bcolor);


	//画像インスタンス生成
	switch($type){
		case 1: //gif
			$image = imagecreatefromgif($tmp_file);
			break;
		case 2: //jpeg
			$image = imagecreatefromjpeg($tmp_file);
			break;
		case 3: //png
			$image = imagecreatefrompng($tmp_file);
	}

	// 背景画像に、画像をコピーする
	imagecopyresampled(
		$canvas,  // 背景画像
		$image,   // コピー元画像
		$x,        // 背景画像のはりつけ位置(x座標)
		$y,        // 背景画像のはりつけ位置(y座標)
		0,        // コピー元画像のコピー開始位置(x座標)
		0,        // コピー元画像のコピー開始位置(y座標)
		$new_width,   // 背景画像のはりつける幅
		$new_height,  // 背景画像のはりつける高さ
		$width, // コピー元画像のコピーする幅
		$height  // コピー元画像のコピーする高さ
	);

	//画像の出力
	switch($type){
		case 1: //gif
			imagejgif($canvas, $tmp_file);
			break;
		case 2: //jpeg
			imagejpeg($canvas, $tmp_file,100);
			break;
		case 3: //png
			imagepng($canvas, $tmp_file,9);
	}
	imagedestroy($canvas);

	return true;
}