オレオレ MVC を作ってみる(1)

CakePHP の構造が分かったので、簡易 MVC を作ってみます。
理由としては、

  • CakePHP を導入するまでもないかなぁ、という時?
  • 導入前に、そもそも MVC が、どういうものなのかを把握する時?
  • PHP で MVC の繋がりってどう作るんでしょうね?

といったところです。

いずれ、CakePHP に移行するという前提にして、命名規約などは CakePHP に準じます。

いわゆる

まずは view です。

<!-- views/magazine/index.php -->
<h1>雑誌一覧 magazine の表示</h1>
<table>
	<tr>
		<th>Magazine.id</th>
		<th>Magazine.name</th>
		<th>Publisher.name</th>
		<th>State.name</th>
	</tr>
	<?php foreach($Magazines as $item) : ?>
	<tr>
		<td><?php echo $item['Magazine']['id']; ?></td>
		<td><?php echo $item['Magazine']['name']; ?></td>
		<td><?php echo $item['Publisher']['name']; ?></td>
		<td><?php echo $item['State']['name']; ?></td>
	</tr>		
	<?php endforeach; ?>
</table>

CakePHP と同じように、$Magazines でモデルを参照できるようにします。

お次は、コントローラー

<?php
include 'models/magazine.php';
class MagazineController {
	var $name = 'MagazineController';
	function index() {
		// view に渡す変数
		global $Magazines;
		// モデルを作成して
		$magazine = new Magazine();
		// 検索
		$Magazines = $magazine->findAll();
		include('views/magazine/index.php');
	}
}
?>

モデルクラスを include して、index メソッド内では最後にビューを include して表示、って感じです。

で、肝心のモデルはどうなっているかというと、

<?php
class Magazine {
	var $name = 'Magazine';
	var $cn ;
	function open() {
		$this->cn = new PDO(&quot;mysql:host=127.0.0.1;dbname=moonmile_cake&quot;,
			&quot;moonmile_cake&quot;,&quot;cakephp&quot;);
	}
	function close() {
		$this->cn = null;
	}

	function findAll() {
		$this->open();
		$sql = <<< HERE
select
	Magazine.id 	`Magazine.id`,
	Magazine.name 	`Magazine.name`,
	Publisher.id	`Publisher.id`,
	Publisher.name	`Publisher.name`,
	State.id		`State.id`,
	State.name		`State.name`
from 
	magazines as Magazine
	inner join publishers as Publisher
		on ( Magazine.publisher_id = Publisher.id ) 
	inner join states as State
		on ( Publisher.state_id = State.id )
	order by Magazine.id;
HERE;
		$stmt = $this->cn->query($sql);
		foreach($stmt as $item) {
			$row['id'] = $item['Magazine.id'];
			$row['name'] = $item['Magazine.name'];
			$table['Magazine'] = $row;
			$row['id'] = $item['Publisher.id'];
			$row['name'] = $item['Publisher.name'];
			$table['Publisher'] = $row;
			$row['id'] = $item['State.id'];
			$row['name'] = $item['State.name'];
			$table['State'] = $row;
			$items[] = $table; 
		}
		$this->close();
		return $items;
	}
}
?>

データベースからクエリで読み込んで、コントローラーから findAll メソッドの呼び出しでデータを設定しています。
クエリ文は慣れるとすらすら書けるでしょうし、データへの再設定もそれなりに…なのかなぁ。

さて、これらを動かすために、http://servername/magazine で表示させたい訳ですが、mod_rewirte を有効にして URL の書き換えを利用するために、次のように index.php を細工します。

<?php
$url = $_GET['url'];

$url = strtolower($url);
$pos = strpos($url,&quot;/&quot;);
if ( $pos === false ) {
	$base = $url;
	$method = &quot;index&quot;;
} else {
	$base = substr($url,0,$pos);
	$method = substr($url,$pos+1);
}

$classfile = $base.&quot;_controller.php&quot;;
$classname = strtoupper(substr($base,0,1)).substr($base,1).'Controller';

echo &quot;classfile: $classfile<br/>&quot;;
echo &quot;classname: $classname<br/>&quot;;
echo &quot;method: $method<br/>&quot;;

include('controllers/'.$classfile);
$c = new ReflectionClass($classname);
$obj = $c->newInstance();
$method = $c->getMethod($method);
$method->invoke($obj);
?>

指定されたコントローラーのメソッドを呼び出すためにリフレクションを使います。多分、匿名関数の実行なんかを使う方がすっきり書けるのかもしれませんが、ひとまず。
ただし、この場合だと、http://servername/magazine/item/2 のように、雑誌の id が 2 の情報を得る、というような引数を指定することができないので、もう少し手を加える必要がありますね。


追記 2011/01/30
URL 書き換えの index.php ですが、../ 等を使って親ディレクトリのファイルを取れる可能性もあるので、実際に使うときはもう少し調節が必要です。このあたりは、パラメータを取得できるときにもで書き換えてみます(できるのか?)。

カテゴリー: 開発, CakePHP | オレオレ MVC を作ってみる(1) はコメントを受け付けていません

CakePHP を開発する環境を探る

PHP 専用エディタってのを使っていないので、アレなのですが、手元には、

  • newQX エディタ
  • Visual Studio 2010
  • Microsoft Visual Web Developer 2010 Express
  • WebMatrix
  • Microsoft Express Web 4

がインストールされています。なので、どれがいいかというと…は自分で判断するとして、参考までにキャプチャを貼りつけておきます。

■newQX エディタ

キーワードに色を付けるだけですが、まぁ、これでも十分なんですが。

■Visual Studio 2010

Visual Studio 2010 だと、PHP は全然ダメですね。xml として認識されてしまう模様。

■Microsoft Visual Web Developer 2010 Express

ちょっと期待したりするのですが、無料で配布されている Web Developer も同じ状況です。

■WebMatrix

最近リリースされた WebMatrix は結構使えます。PHP のコードもこんな風に。

ですが、CakePHP で使われる *.ctp ファイルがこんな感じに駄目なので…

■Microsoft Express Web 4

という訳で、結局デザイナーツールな Express Web を使う羽目に。

ほら、*.ctp もご覧の通り…って、これって CakePHP の拡張子を認識しているわけですよね。

折角、Express Web で CakePHP の view template が認識されているのだから、WebMatrix でも、と思うのは私だけでしょうか?これって、フィードバックで送ればなんとかるかなぁ。
まぁ、WebMatrix の方はデザイナが表示されないので、タグが認識されたところで使いづらいのですが。

カテゴリー: 開発, ツール | 3件のコメント

独自のモデルを作ってマッピングする(queryを使う)

CakePHPの場合、モデルとテーブルが1対1と想定しているので、これ以外のマッピングをする場合は、独自の Model を作ったほうがいいと思います。

例えば、先の記事のように、出版社(Publisher)から必ず都道府県(State)の名前を参照するのであれば、あらかじめそういう Model を作ってしまったほうが利便性が高いかな、と。

このような関係があるとき、雑誌の一覧を次のように取得したいと思う訳です。

  • 雑誌ID
  • 雑誌名
  • 出版社名
  • 出版社の所在地/都道府県

これを通常ならば、データベースのビューを作って、

  • id
  • magazine_name
  • publisher_name
  • publisher_state

のような形で取得しても良いのですが、環境によってはデータベースを変更できなかったり、データ管理は別の会社が構築したものだったりして、データベースとプログラムの間には大きな溝があるのが常だったりします。

# 小さなプロジェクトならばいいんですけどね。
# あと、もの分かり良い(以下略

という訳で http://servername/magazines/lists でアクセスできるようにします。
まずは、ビューを作成して、普通のモデルを使っているように書きます。というか書きたいわけです。

<!-- views/magazines/lists.ctp -->
<h2>雑誌一覧(独自)</h2>
<table>
	<tr>
		<td>id</td>
		<td>name</td>
		<td>Publisher.name</td>
		<td>State.name</td>
	</tr>
<?php foreach($MagazineLists as $item) : ?>
	<tr>
		<td><?php echo $item['Magazine']['id'] ?></td>
		<td><?php echo $item['Magazine']['name'] ?></td>
		<td><?php echo $item['Publisher']['name'] ?></td>
		<td><?php echo $item['State']['name'] ?></td>
	</tr>
<?php endforeach ; ?>
</table>

これに合わせて、コントローラーに lists メソッドを追加します。

<?php
class MagazinesController extends AppController {
	var $name = 'Magazines';
	var $uses = array('Magazine','Publisher','MagazineList');
	function index() {
		$this->set('Magazines',$this->Magazine->find('all'));
		$this->set('Publishers',$this->Publisher->find('all'));
	}
	// 独自の雑誌リスト ★
	function lists() {
		$this->set('MagazineLists',$this->MagazineList->findAll());
	}
}
?>

findAll メソッドを使っているのは、他のモデルと揃えるためです。いわゆる元のメソッドをオーバーライドするという奴です。
モデルのクラス名は「MagazineList」(ファイル名は命名規則で magazine_list.php)になります。

<!-- models/magazine_list.php -->
<?php
class MagazineList extends AppModel
{
	var $name = 'MagazineList';
	var $useTable = false;	// ★テーブルに連結させない

	function findAll()
	{
		$sql = <<< HERE
SELECT
 Magazine.id,
 Magazine.name,
 Publisher.id,
 Publisher.name,
 State.id,
 State.name
FROM
 magazines as Magazine
 inner join publishers as Publisher 
  on ( Magazine.publisher_id = Publisher.id ) 
 inner join states as State
  on ( Publisher.state_id = State.id )
 order by 
  Magazine.id
HERE;
		return $this->query($sql); // クエリを実行する
	}

}
?>

独自にクエリを実行するので、Mode::query メソッドを使います。極悪ですねw。
クエリ文自体は、ヒアドキュメントにしておいて、MySQL Workbench などでテストできるようにしておくのがミソです。

あとは、結果を返す時に、「Magazine.id」のように返しておくと(多分asで別名を定義してもOKかと) $item[‘Magazine’][‘id’] のようにあたかも CakePHP の findAll メソッドを使ったように結果を取得できます。

これを実行すると、こんな感じ。

当然ですが、クエリ文は query メソッドで実行したものが使われています。left join でないので、データベース屋さんとしてはすっきりという感じですね。

私見ですが、そもそもO/Rマッピング(ORM)ってのは、オブジェクト指向とデータ指向を結びつけるというのが目的で、なにもクラス(オブジェクト)とテーブルを1対1にする、というのが目的ではないのですね(初手としては良いのでしょうが)。

O/R マッピングをするときに、右のデータは既に正規化されている状態なので、そのままクラスにマッピングするのは不便です。と言いますか、非正規化の状態に戻さないと使えません。RDB 自体がリレーショナルであることを基本としているので、ひとつの現実を表すのに publisher_id や state_id などの identify を使う訳で、これを id のままマッピングするよりも、元の非正規化の状態に戻して、オブジェクト指向の世界へと誘うのがよいかと思っています。

ま、機能と使い方は色々なので、利用する時に合わせるのがベターかと。

カテゴリー: CakePHP | 1件のコメント

CakePHPで3つのテーブルを連結させる。

さて、雑誌(magazines)と出版社(publishers)を Model::$belongsTo で連結させることができましたが、今度は都道府県(states)と出版社を連結させて、3つのテーブルを使います。

実用的な問題として、ひとつのテーブルで収まることはまず無い訳で、ログやら単純なマスターテーブル程度ならばいいのですが、【真面目に】テーブルを正規化していれば、3 つ以上のテーブルが連結するのは当たり前ですッ!!! … だといいなぁ、という希望的観測も含めて(苦笑)。

-- 雑誌テーブル
create table magazines (
 id int not null,
 name varchar(50),
 publisher_id int,
 price int
);
-- 出版社テーブル
create table publishers (
 id int not null,
 name varchar(50),
 state_id int
);
-- 都道府県テーブル
create table states (
 id int not null,
 name varchar(20)
);

# 説明が面倒なので、T100 を magazines に、T200 を publishers に変更しましたw

ER 図はこんな感じで、1対多の関係です。

これを出版社を中心にして(cakephp は left join で連結させるので)、

<h2>出版社一覧</h2>
<table>
	<tr>
		<td>id</td>
		<td>name</td>
		<td>Magazine.name</td>
		<td>State.name</td>
	</tr>
<?php foreach($Publishers as $item) : ?>
	<tr>
		<td><?php echo $item['Publisher']['id'] ?></td>
		<td><?php echo $item['Publisher']['name'] ?></td>
		<td><?php echo $item['Magazine']['name'] ?></td>
		<td><?php echo $item['State']['name'] ?></td>
	</tr>
<?php endforeach ; ?>
</table>

のように、都道府県名(State)を表示させようとすると、Publisher モデルクラスに次のように加筆します。

<!-- models/publisher.php -->
<?php
class Publisher extends AppModel
{
	var $name = 'Publisher ';
    var $belongsTo = array(
    	'Magazine' => array(
			'className' => 'Magazine',
			'conditions' => 'Magazine.publisher_id = Publisher.id',
			'order' => 'Publisher.id ASC',
			'foreignKey' => ''),
        'State' => array (				// ここから追加
	    	'className' => 'State',
			'conditions' => '',
			'order' => 'State.id ASC',
			'foreignKey' => 'state_id')
        );
}
?>

関係が

  • Publisher->Magazine
  • Publisher->State

なので、1段階しか探索をしないため結構簡単にできます。

実行すると素直にデータが取得できます。

SELECT 
 `Publisher`.`id`, 
 `Publisher`.`name`, 
 `Publisher`.`state_id`, 
 `Magazine`.`id`, 
 `Magazine`.`name`, 
 `Magazine`.`publisher_id`, 
 `Magazine`.`price`, 
 `State`.`id`, 
 `State`.`name` 
FROM `publishers` AS `Publisher` 
 LEFT JOIN `magazines` AS `Magazine` 
  ON (`Magazine`.`publisher_id` = `Publisher`.`id`) 
 LEFT JOIN `states` AS `State` 
  ON (`Publisher`.`state_id` = `State`.`id`) 
WHERE 1 = 1 
 ORDER BY `Publisher`.`id` ASC, `State`.`id` ASC 

発行されるクエリを見ると、left join でうまく繋がっています。
難点を云えば、

  • left join でつながっているので、雑誌を持たない出版社も表示されている。この場合、inner join にどうすれば切り替えられるのかは不明です。
  • 全てのカラムを取得しているので、カラム内のデータが多い場合(varchar(5000)など)は、データ転送に時間がかかる。できれば、index.ctp で使っているカラムだけのクエリを作って欲しいのですが、これもどうやるのか不明です。

そんな感じで、クエリ文を直接書いてしまえば良いのでは?と思ったりしますが、PHP は分かるけど、SQL は分からない(C# でも LINQ は分かるけど SQL が分からない、サブクエリが書けない、という方もいらっしゃるので)場合もあるので、これはこれで良いのかなと。

# 余談ですが、Model::$hasMany を使うとクエリ文がたくさん発行されてしまうという…これって、サブクエリを使えばいいんじゃないの?と思うのですが、パフォーマンス的にはどうなんでしょう。CakePHP 2.0 だと違うのかな?

次に雑誌(Magazine)を中心にして、連結をさせます。

  • Magazine->Publiser
  • Magazine->Publiers->State

という形で Magazine から State まで 2段階の探索が必要になるわけですが。

<!-- models/magazine.php -->
<?php
class Magazine extends AppModel
{
	var $name = 'Magazine';
	var $belongsTo = array(
    'Publisher' => array(
			'className' => 'Publisher',
			'conditions' => '',
			'order' => 'Magazine.id ASC',
			'foreignKey' => 'publisher_id' ),
    'State' => array(				// ここから追加
			'className' => 'State',
			'conditions' => 'Publisher.state_id = State.id',
			'order' => 'State.id ASC',
			'foreignKey' => '' ),
            );
}
?>

こんな風に conditions に連結の条件を指定してしまいます。
本当は、Model::$recursive を設定するのでしょうが…

recursive :: Model の属性 :: モデル :: CakePHPによる開発 :: マニュアル :: 1.2コレクション :: The Cookbook
http://book.cakephp.org/ja/view/439/recursive
recursiveの正しい理解CakePHP – CPA-LABテクニカル
http://www.cpa-lab.com/tech/081

なんか思ったように動いてくれません。なので、手っ取り早い方法としては、Model::$belongsTo に設定してしまうのがお手軽のなのかなぁと。

このようにモデルで設定しておくと、次のように $item[‘State’][‘name’] で都道府県名が表示できます。

<h2>雑誌一覧</h2>
<table>
	<tr>
		<td>id</td>
		<td>name</td>
		<td>Publisher.name</td>
		<td>State.name</td>
	</tr>
<?php foreach($Magazines as $item) : ?>
	<tr>
		<td><?php echo $item['Magazine']['id'] ?></td>
		<td><?php echo $item['Magazine']['name'] ?></td>
		<td><?php echo $item['Publisher']['name'] ?></td>
		<td><?php echo $item['State']['name'] ?></td>
	</tr>
<?php endforeach ; ?>
</table>

発行されるクエリは以下の通りです。

SELECT 
 `Magazine`.`id`, 
 `Magazine`.`name`, 
 `Magazine`.`publisher_id`, 
 `Magazine`.`price`, 
 `Publisher`.`id`, 
 `Publisher`.`name`, 
 `Publisher`.`state_id`, 
 `State`.`id`, 
 `State`.`name` 
FROM `magazines` AS `Magazine` 
 LEFT JOIN `publishers` AS `Publisher` 
  ON (`Magazine`.`publisher_id` = `Publisher`.`id`) 
 LEFT JOIN `states` AS `State` 
  ON (`Publisher`.`state_id` = `State`.`id`) 
WHERE 1 = 1 
 ORDER BY `Magazine`.`id` ASC, `State`.`id` ASC 

表示が若干違うのは、left join をしているからですね。

カテゴリー: CakePHP | 3件のコメント

CakePHPで2つのテーブルを連結させる(2)

雑誌(magazines)が出版社(publishers)に属する、という形で magazines のほうから Model::$belongsTo を使って連結できたので、今度は出版社が持っている雑誌の一覧を出してみます。

<h2>出版社一覧</h2>
<table>
	<tr>
		<td>id</td>
		<td>name</td>
		<td>Magazine.name</td>
	</tr>
<?php foreach($Publishers as $item) : ?>
	<tr>
		<td><?php echo $item['Publisher']['id'] ?></td>
		<td><?php echo $item['Publisher']['name'] ?></td>
		<td><?php echo $item['Magazine']['name'] ?></td> ★
	</tr>
<?php endforeach ; ?>
</table>

こんな風に、$Publishers のループの中で、$item[‘Magazine’][‘name’] で参照できれば良いなと。

モデルの Magazine で Model::$belongsTo を設定してあるので、逆向きもできているんじゃないかなぁ、と期待してそのまま動かすと。

なエラーが出ます。逆向きは自動で設定してくれないのですね。。。
なので、Publisher モデルに加筆します。

<!-- models/publisher.php -->
<?php
class Publisher extends AppModel
{
	var $name = 'Publisher ';
	var $useTable = 'T200';
	var $hasMany = array('Magazine' =>
		array(
			'className' => 'Magazine',
			'conditions' => '',
			'order' => 'Magazine.T200id ASC',
            'limit' => 10,
			'foreignKey' => 'T200id',
            'dependent' => true,
            'exclusive' => false,
            'finderQuery' => ''));
}
?>

Mode::$hasMany の詳しい使い方は、マニュアルを見るとして、これを実行するとやっぱりエラーがでます。

「Undefined index: name」というエラーが出ているので、どうやら、Magazine は見つかったらしいけど、name が見つからない。で、print_r($Publishers) で見てみると、Magazine 以降はツリー構造になっているのですね。

Array
(
    [0] => Array
        (
            [Publisher] => Array
                (
                    [id] => 1
                    [name] => 平和システム
                )
 
            [Magazine] => Array
                (
                    [0] => Array
                        (
                            [id] => 1
                            [name] => 逆引き大全F#555
                            [T200id] => 1
                            [price] => 1000
                            [publisher_id] => 1
                        )
 
                    [1] => Array
                        (
                            [id] => 2
                            [name] => 逆引き大全CakePHP555
                            [T200id] => 1
                            [price] => 2000
                            [publisher_id] => 1
                        )
 
                )
 
        )
    [1] => Array
        (
            [Publisher] => Array
                (
                    [id] => 2
                    [name] => 日経BB社
                )
 
            [Magazine] => Array
                (
                    [0] => Array
                        (
                            [id] => 3
                            [name] => ひと目で分かるOracleアプリケーション入門
                            [T200id] => 2
                            [price] => 3000
                            [publisher_id] => 2
                        )
 
                )
 
        )
 

なので、アクセスの仕方としては、$item[‘Magazine’][0][‘name’] のように添え字が間に挟まるわけですが、これだと一覧表には不便です(多分、出版社→その配下の雑誌を表示、のように遷移する場合に使うのかと)。

仕方がないので、Model::$belongsTo を使ってみます。意味的には変なのですが。

<!-- models/publisher.php -->
<?php
class Publisher extends AppModel
{
	var $name = 'Publisher ';
	var $useTable = 'T200';
    var $belongsTo = array('Magazine' =>
		array(
			'className' => 'Magazine',
			'conditions' => 'Magazine.T200id = Publisher.id',
			'order' => 'Publisher.id ASC',
			'foreignKey' => ''));
}
?>

外部キー(foreignKey)を指定することはできないので、join の条件(conditions)を直接指定します。
ここでハマるのは、T100, T200 の元のテーブル名を使うことができなくて(それはそれで良いのですが)、Magazine, Publisher というモデル名を使うところですね。SQL 文で別名定義しているところの制限なので、MySQL だけの制限かもしれません。

これを実行すると、めでたく一覧表が出ます。

がッ!!! 一見大丈夫そうにみえて、ああ、left join を使っているので雑誌を出していない出版社の名前も一覧に出てきます。これは、まぁこれで良いのかと思いますが。

でも left join でループしない場合はどうするんでしょうね?という疑問が(これはまだ解決せず)

カテゴリー: CakePHP | CakePHPで2つのテーブルを連結させる(2) はコメントを受け付けていません

CakePHPで2つのテーブルを連結させる

さて、極悪テーブル命名規則 T100,T200,T300 を magazines, publishers, users に変えることができたので、今度はテーブルの連結をしてみます。

<!-- views/magazines/index.ctp -->
<h2>雑誌一覧</h2>
<table>
	<tr>
		<td>id</td>
		<td>name</td>
		<td>publisher_id</td>
		<td>publisher_id</td>
	</tr>
<?php foreach($Magazines as $item) : ?>
	<tr>
		<td><?php echo $item['Magazine']['id'] ?></td>
		<td><?php echo $item['Magazine']['name'] ?></td>
		<td><?php echo $item['Magazine']['T200id'] ?></td>
		<td><?php echo $item['Magazine']['publisher_id'] ?></td>
	</tr>
<?php endforeach ; ?>
</table>

のように、publisher_id を表示させたって意味がないわけで、出版社名(Publisher.name)を表示させたい訳です。

<!-- views/magazines/index.ctp -->
<h2>雑誌一覧</h2>
<table>
	<tr>
		<td>id</td>
		<td>name</td>
		<td>publisher_id</td>
		<td>publisher_id</td>
	</tr>
<?php foreach($Magazines as $item) : ?>
	<tr>
		<td><?php echo $item['Magazine']['id'] ?></td>
		<td><?php echo $item['Magazine']['name'] ?></td>
		<td><?php echo $item['Magazine']['T200id'] ?></td>
		<td><?php echo $item['Magazine']['publisher_id'] ?></td>
		<td><?php echo $item['Publisher']['name'] ?></td> ★
	</tr>
<?php endforeach ; ?>
</table>

みたいに使えればいいですよね。使えるのかな?

という訳で、ひとつの雑誌(magazine)は、出版社(publisher)に属するということで、Magazine モデルを書き換えます。

<!-- models/magazine.php -->
<?php
class Magazine extends AppModel
{
	var $name = 'Magazine';
	var $useTable = 'T100';
	var $virtualFields = array(
		'publisher_id'=>'T200id' );

	var $belongsTo = array('Publisher' =>
		array(
			'className' => 'Publisher',
			'conditions' => '',
			'order' => 'Magazine.id ASC',
			'foreignKey' => 'T200id' ));
}
?>

この外部キーの書き方はいくつかあるのですが、$belongsTo と $hasMeny は双方向なのでどちらか決めで使えばいいんでしょう。

こうすると、先の index.ctp が動きます。生成される SQL を見ると、

SELECT 
 `Magazine`.`id`, 
 `Magazine`.`name`, 
 `Magazine`.`T200id`, 
 `Magazine`.`price`, 
 (T200id) AS `Magazine__publisher_id`, 
 `Publisher`.`id`, 
 `Publisher`.`name` 
FROM `T100` AS `Magazine` 
 LEFT JOIN `T200` AS `Publisher` ON (`Magazine`.`T200id` = `Publisher`.`id`) 
WHERE 1 = 1 
 ORDER BY `Magazine`.`id` ASC 

となっているので、left join が使われていますね。なるほど。

残念ながら $virtualFields で別名定義をしたカラム名は、$belongsTo では使えないようで、あの不思議な T200id というカラム名が出てきますが、まぁ、良しとしましょう。適当なヘルパー関数なりを作れば、このあたりの設定も楽になると思います。$virtualFields が連想配列なので、逆引きして value から key を取り出せばよいので。

カテゴリー: 雑談, CakePHP | 2件のコメント

CakePHP のモデルでデータベースをリファクタリング

リファクタリングとは言え、いきなりデータベースを変えてしまうのではなくて、database-Model の関係を利用して、「管理のしやすいテーブル名」を、「意味のわかりやすいテーブル名」に変えましょう、という方法です。

# ASP.NET MVC で使う LINQ to Entities や LINQ to SQL なんかは、テーブル名=クラス名が固定(だと思う)なので、この技が使えないのです。

例えば、既存のテーブルが次の 3 つで校正されています。

  • T100 雑誌テーブル
  • T200 出版社テーブル
  • T300 読者テーブル

ああ、なんて【管理しやすい】テーブルなんでしょうかッ!!! おそらく、将来的に雑誌に関係するテーブル名は、「T101」とか「T102」になる、ってことが容易に想像できます。

…が、決して分かり易いテーブル≒プログラミングしやすいテーブルではないですよね。
# こういう管理の仕方は嫌いではないのですが、避けておいたほうがいいというかなんというか。
# ただし、テーブル数が数百を超えると、こんな風に管理せざるを得ない時もありますよね。名前がだぶらないようにとか、名前が長くなりすぎないようにとか、制限があるので。

さて、これをプログラムで扱うときには、次のようにしたいわけです。

  • Magazine 雑誌テーブル
  • Publisher 出版社テーブル
  • User 読者テーブル

ちなみに、CakePHP の命名規則に従えば、

  • magazines 雑誌テーブル
  • publishers 出版社テーブル
  • users 読者テーブル

のように、小文字かつ複数形にするのが良いのですが、あらかじめデータベースができていたり、社内的な命名規則があったりすると、この限りではないのです。

テーブルを次のように作ります。

-- 雑誌テーブル
create table T100 (
 id int,
 name varchar(50),
 T100id int, -- 出版社ID
 price int
);
-- 出版社テーブル
create table T200 (
 id int,
 name varchar(50)
 );
-- 読者テーブル
create table T300 (
 id int,
 name varchar(50),
 age int
 );

な風に、(極悪)テーブル名を付けておいても、Model で書き換えれば OK。

<!-- models/magzine.php -->
<?php
class Magazine extends AppModel
{
	var $name = 'Magazine';
	var $useTable = 'T100';
}
?>

<!-- models/publisher.php -->
<?php
class Publisher extends AppModel
{
	var $name = 'Publisher ';
	var $useTable = 'T200';
}
?>

<!-- models/user.php -->
<?php
class User extends AppModel
{
	var $name = 'User';
	var $useTable = 'T300';
}
?>

これでコントローラーで使うときは、Magazine、Publisher、User が使えるようになります。

<?php
class MagazinesController extends AppController {
	var $name = 'Magazines';
	var $uses = array('Magazine','Publisher');
	function index() {
		$this->set('Magazines',$this->Magazine->find('all'));
		$this->set('Publishers',$this->Publisher->find('all'));
	}
}
?>

まぁ、対応表を作ってちまちま見るよりはいいか、とは思いますけど、結局は「T100」は「magazine」だったしと、言葉の置き換えは必要になるわけですが。

ただし、これでも問題があって、T100.T200id っていう出版社テーブルへのリンクはモロにビューに出てしまうのですよね。

<!-- views/magazines/index.ctp -->
<h2>雑誌一覧</h2>
<table>
	<tr>
		<td>id</td>
		<td>name</td>
		<td>publisher_id</td>
	</tr>
<?php foreach($Magazines as $item) : ?>
	<tr>
		<td><?php echo $item['Magazine']['id'] ?></td>
		<td><?php echo $item['Magazine']['name'] ?></td>
		<td><?php echo $item['Magazine']['T200id'] ?></td>
	</tr>
<?php endforeach ; ?>
</table>
<h2>出版社一覧</h2>
<table>
	<tr>
		<td>id</td>
		<td>name</td>
	</tr>
<?php foreach($Publishers as $item) : ?>
	<tr>
		<td><?php echo $item['Publisher']['id'] ?></td>
		<td><?php echo $item['Publisher']['name'] ?></td>
	</tr>
<?php endforeach ; ?>
</table>

こういうヘンテコな名前「$item[‘Magazine’][‘T200id’]」が残ってしまうので、完全にリファクタリングって訳でもなさそうですなぁ。ま、テーブル名ぐらいは変えられるってことで。

と、思ったらですね。

virtualFields :: Model の属性 :: モデル :: CakePHPによる開発 :: マニュアル :: 1.3コレクション :: The Cookbook
http://book.cakephp.org/ja/view/1588/virtualFields

を見ると CakePHP 1.3 から モデルに仮の名前を付けられるそうです。
テーブル自体を更新できないので、CRUD 全体には使えませんが、ビューで表示するときには便利かと。

<!-- models/magazine.php -->
<?php
class Magazine extends AppModel
{
	var $name = 'Magazine';
	var $useTable = 'T100';
	var $virtualFields = array(
		'publisher_id'=>'T200id' );
}
?>

こんな風に別名を定義しておくと、

<!-- views/magazines/index.ctp -->
<h2>雑誌一覧</h2>
<table>
	<tr>
		<td>id</td>
		<td>name</td>
		<td>publisher_id</td>
		<td>publisher_id</td>
	</tr>
<?php foreach($Magazines as $item) : ?>
	<tr>
		<td><?php echo $item['Magazine']['id'] ?></td>
		<td><?php echo $item['Magazine']['name'] ?></td>
		<td><?php echo $item['Magazine']['T200id'] ?></td>
		<td><?php echo $item['Magazine']['publisher_id'] ?></td>
	</tr>
<?php endforeach ; ?>

のように、再定義した publisher_id で値を取得できます。

すべてのカラムを再定義するのは問題ありでしょうが、こんな風に一時的に変な名前が付いているカラムには適用して良いかと思います。特に *.ctp で表示するときには、「T200id」よりも「publisher_id」のほうが間違いが少なそうですよね。

カテゴリー: CakePHP | CakePHP のモデルでデータベースをリファクタリング はコメントを受け付けていません

CakePHP再入門(2)

覚書きとして。

CakePHP の View-Controller-Model-DataBase の名前の関係がどうなっているかというと。

こんな風になっている。

カテゴリー: CakePHP | 2件のコメント

wordpres に XML-RPC で画像を投稿する

以前、XML-RPC を使って wordpress に投稿するツールを作ったのですが、画像も投稿できないと意味がないですよね…ということで、画像の投稿(というか、ファイルアップロード)のツールを作成しています。

XML-RPC wp ≪ WordPress Codex
http://codex.wordpress.org/XML-RPC_wp#wp.uploadFile

を見ると、データ形式が base64 になっていて、一見 BASE64 でエンコードをしないと駄目なような気がしますが、実は違います。これは XML-RPC 内で作られるフォーマットなので、BASE64 形式の文字列を送るのではなくて、バイナリを BASE64 で送る(CDATA を使う)っていう意味なんですね。

ちょっとハマりました。

ソースは抜粋だけアップしておきます。後で整理して、

  • 記事本体の投稿(WpPost)
  • 画像ファイルの投稿(WpUpFile)

の2つのツールを公開します。

        /// <summary>
        /// 新しい画像を投稿する
        /// </summary>
        /// <param name=&quot;post&quot;>投稿記事</param>
        /// <returns>投稿した記事のID</returns>
        public RetUpFile NewImage(ImageFile post)
        {
            //プロキシクラスのインスタンスを作成
            IBlogger proxy = (IBlogger)
                CookComputing.XmlRpc.XmlRpcProxyGen.Create(
                typeof(IBlogger));

            //URLを指定
            proxy.Url = this.Url;

            // ファイル名を小文字に変換
            post.Filename = post.Filename.ToLower();

            // UpFile を生成
            UpFile upfile = new UpFile();
            upfile.name = post.Filename;
            upfile.type = &quot;image/jpeg&quot;;
            upfile.bits = null;
            upfile.overwrite = true;

            // ファイルを読み込む
            long len = new FileInfo(post.Filename).Length;
            byte[] data = new byte[len];
            BinaryReader rd = new BinaryReader(
                File.OpenRead( post.Filename ));
            rd.Read(data, 0, (int)len);
            rd.Close();
            
            // BASE64に変換しない
            // upfile.bits = Convert.ToBase64String(data);
            upfile.bits = data;
            // ファイルタイプを変更
            if ( upfile.name.EndsWith(&quot;.jpg&quot;) ||
                 upfile.name.EndsWith(&quot;.jpeg&quot;) ) {
                upfile.type = &quot;image/jpeg&quot;;
            } else if ( upfile.name.EndsWith(&quot;.png&quot;) ) {
                upfile.type = &quot;image/png&quot;;
            } else if ( upfile.name.EndsWith(&quot;.gif&quot;)) {
                upfile.type = &quot;image/gif&quot;;
            }

            RetUpFile ret;
            try
            {
                //blogger.getRecentPostsを呼び出す
                ret = proxy.uploadFile(
                    &quot;1&quot;,            // 念のため1にしておく
                    this.Username,
                    this.Password,
                    upfile );
            }
            catch (Exception ex)
            {
                Console.WriteLine(&quot;エラー:&quot; + ex.Message);
                return null;
            }

            return ret;
        }
    }

この手のツールを作っているのは、実は wordpress で 300 ページ位の記事をアップできないか?と考えたわけです。
ブログを適度に書くぐらいであれば、ブラウザ上でもいいのですが、ある程度まとまった記事であればオフラインで書きたい。

そこでオフラインエディタの BlogWrite や Windows Live Writer などを使っていたのですが、どうも記事本体の編集だけに集中できない。様々な装飾が邪魔なんですね。

記事を書くときには、2つのタイプがあって、

  • 記事本体(本文)に集中するためにエディタで書く。
  • 記事のレイアウトを整えるために GUI ベースのツールを使う。

例えば、書籍で云えば、最初のが「著者」、次のが「編集者」あるいは「DTP」ってことです。
BlogWrite なんかのツールは、文章を書くことと、レイアウトと整えることが同時にできる、のが非常に利点なのですが、逆に画像がどうのレイアウトがどうの、と気になってしまったときには「著者」の頭に切り替えづらいという欠点があります。

ま、どのようなタイプの本を書くのかというで別かもしれませんが、BlogWrite を使っているときの問題として、

コードを表示するプラグインのタグ [ code]…[/code] を非常に書きづらい、

という大きな欠点があります。今までブラウザ上でちまちま直したり、Windows Live Writer に切り替えてみたりとしたのですが、本来 HTML タグではないものは(当たり前ですが)オフラインのブログエディタが認識しないので、無理といえば無理な話ですよね。

という訳で、もう一度、エディタに立ち返ってみよう、という感じです。

そんな訳で、

1.QX エディタで文章を書く。
2.perl を使って HTML タグに変換([ code]タグなんかは残す)
3.WpPost, WpUpfile のようなツールで wordpress にアップロード

な流れで現在書いています。
ローテクですが、枯れた技術(テキストエディタ、perl)を使うとそれなりに作れますね。コマンドライン版ですがw

~~

wordpres の独自タグを使わずに、<code>…</code>で記述した後に、wordpress 側で Javascript で判別、なんてことでもいいんですけど、プラグイン作成時のお手軽さが失われるのが残念でして。

wordpress の拡張タグのプラグインは非常に簡単にできて、

function manabook_index($attss) {
	...
	return $links;
}
add_shortcode('manabook_index','manabook_index');

な感じでショートコードを作成して plugins に放り込んでおけば、

記事に [manabook_index] と書いておくだけで、目次に展開してくれるというプラグインが作れます。こっちは別の記事で紹介します。

カテゴリー: 開発, Wordpress | 1件のコメント

CakePHP はマニュアル無しでサイトを作れるのか?

どういう手順でアプリケーションを作成するのか、ってことですね。

CakePHPブログチュートリアル :: 開発例 :: マニュアル :: 1.2コレクション :: The Cookbook
http://book.cakephp.org/ja/view/219/CakePHPブログチュートリアル

最初はチュートリアルに手を付けるわけですが、さて、サイトを作る時はどこから作り始めるのか、というと、CakePHP は面白いことにブラウザに URL を入力するところから始めます。

ひとまず、ここまでデータベースを設定した後で、ユーザーがブラウザで入力するアドレスを入れます。

http://cake.moonmile.net/Products

すると、エラーがでます。当たり前ですが、このエラーを元に書き進めるわけです。

  • ProductsController クラスが無いよ。
  • app/controllers/products_controller.php のファイルに置けばいい。
  • ひとまず、下記を貼りつければ OK。
<?php
class ProductsController extends AppController {

	var $name = 'Products';
}
?>

という訳で、コントローラーを作ります。

再び http://cake.moonmile.net/Products のアドレスに接続すると、エラーが変わります。

  • モデルの Product のための products というテーブルが無いよ。

ってな感じです。

※ ただし、IIS+PHP+MySQL の環境では、ここで 500 のサーバーエラーが出ます。ちょっと、これは不便。何故、500 エラーなのかは不明ですが、Linux 上で PHP+MySQL の組み合わせでは、この通りに動きます。

※ 暫定ですが、/cake/libs/error.php の254 行目ぐらいにある missingTable 関数内の $this->controller->header(“HTTP/1.0 500 Internal Server Error”) の行をコメントアウトすると、テーブルが無いときの 500 エラーを回避できます。

そういう訳で、phpMysqlAdmin などを使って products テーブルを作ります。

create table products (
  id int not null,
  name varchar(50) not null,
  price int not null
)

データも適当に入れておきます。

insert into products values ( 1, 'product 1st', 1000 );
insert into products values ( 2, 'product 2nd', 1000 );
insert into products values ( 3, 'product 3rd', 1000 );

再び http://cake.moonmile.net/Products のアドレスに接続します。

今度は、

  • ProductsController コントローラーに index というメソッドが無い。
  • まあ、だいたいこんな風に書く。
<?php
class ProductsController extends AppController {

	var $name = 'Products';

	function index() {

	}

}
?>

という訳で、index メソッドの中身を作ります。

再び、ブラウザをリロードすると、またエラーが変わります。

今度は ProductsController::index() に対応するビューが無いそうです。
ビューは、app/views/products/index.ctp に作りなさい、ということになっていますね。

ここでビューを適当に作ります。

<p>これは products テーブルのためのビューです。</p>
<?php

?>

再び、ブラウザをリロードすると、

めでたくエラーがなくなります。

…って、テーブルが表示されていないので、コントローラーとビューを直します。

<?php
class ProductsController extends AppController {

	var $name = 'Products';

	function index() {
		$this->set('Products', $this->Product->find('all'));
	}
}
?>

Product モデルから検索して、ビューに Products という名前で引き渡します。

これに合わせて、ビューも変えていきます。

<p>これは products テーブルのためのビューです。</p>

<table>
	<tr>
		<th>id</th>
		<th>name</th>
		<th>price</th>
	</tr>
<?php foreach($Products as $item) : ?>
	<tr>
		<td><?php echo $item['Product']['id'] ?></td>
		<td><?php echo $item['Product']['name'] ?></td>
		<td><?php echo $item['Product']['price'] ?></td>
	</tr>
<?php endforeach ; ?>
</table>

ここまできたら、きちんと表示ができます。

こういう風にひな型を作るって感じでしょうね。

よくできているというか感心するのが、足りない部分を単純なコンパイルエラーの表示ではなくて、足りないファイル、足りないメソッドの内容までを表示しているところです。 ASP.NET MVC の場合は、こんな風にはできていなくて、コンパイルエラーにならないように、どうしても、モデル→コントローラー→ビューの順序で作る必要があります(エラーを見ながらやってもいいんでしょうけど)。

このあたりは、PHP がスクリプト言語である利点を有効に活用しているのかなと。

カテゴリー: CakePHP | CakePHP はマニュアル無しでサイトを作れるのか? はコメントを受け付けていません