@blog.justoneplanet.info

日々勉強

CakePHPでモデルを作る

前回に引き続き今更ではあるがメモっておく。

■モデルのファイル生成

bakeする

bakeの前に解説用に以下のテーブルを作る。

CREATE TABLE `users` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `email` varchar(255) NOT NULL,
  `created` datetime NOT NULL,
  `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `is_public` tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

以下のコマンドを実行する。

./cake/console/cake bake

以下のように表示される。

Welcome to CakePHP v1.3.10 Console
---------------------------------------------------------------
App : app
Path: /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app
---------------------------------------------------------------
Interactive Bake Shell
---------------------------------------------------------------
[D]atabase Configuration
[M]odel
[V]iew
[C]ontroller
[P]roject
[F]ixture
[T]est case
[Q]uit
What would you like to Bake? (D/M/V/C/P/F/T/Q) 
> M

Mを選択すると以下の選択肢が表示されるのでdefaultを選択する。

---------------------------------------------------------------
Bake Model
Path: /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/models/
---------------------------------------------------------------
Use Database Config: (default/production) 
[default] > default

この時点でDBにテーブルが存在していないと以下のように表示される。

Your database does not have any tables.

当然だけどデータストアの定義がされていないのにコードを書き始めてはいけない。

テーブルが存在している場合は以下のように表示される。

Possible Models based on your current database:
1. User
Enter a number from the list above,
type in the name of another model, or 'q' to exit 

1番を選択すると以下のように質問される。

Would you like to supply validation criteria 
for the fields in your model? (y/n) 

yを選択すると以下のように表示される。

Field: id
Type: integer
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - uuid
29 - Do not do any validation on this field.

どんな入力値チェックをするかという事ですな。ここで設定した値を元にソースを書きだしてくれる。但し、idはautoincrementで特にinsertするカラムではないので29を選択する。

Field: name
Type: string
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - uuid
29 - Do not do any validation on this field.
... or enter in a valid regex validation string.

次に表示されるのはnameカラムに対するバリデーションだ。必須項目としたいので19を入力すると以下のように表示される。

Would you like to add another validation rule? (y/n) 

1つのカラムに複数のバリデーションを適用できるので必要な場合は番号を入力する。次はパスワードだ。

Field: password
Type: string
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - uuid
29 - Do not do any validation on this field.
... or enter in a valid regex validation string.
  
[19] > 

1と19あたりを設定すると思う。次はemailだ。

Field: email
Type: string
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - uuid
29 - Do not do any validation on this field.
... or enter in a valid regex validation string.
  
[10] > 

なんとカラム名から判断して10番を提示してくれるではないか。Cakeは気がきくのだがこれは時としてお節介にもなる。そんなこんなでバリデーションを設定していくと次に以下のような質問をされる。

Would you like to define model associations
(hasMany, hasOne, belongsTo, etc.)? (y/n) 

関連するテーブルを聞かれているのだが、まだ他のテーブル存在していないのでnを入力する。

---------------------------------------------------------------
The following Model will be created:
---------------------------------------------------------------
Name:       User
DB Table:   `users`
Validation: Array
(
    [name] => Array
        (
            [notempty] => notempty
        )

    [password] => Array
        (
            [notempty] => notempty
        )

    [email] => Array
        (
            [email] => email
        )

    [is_public] => Array
        (
            [boolean] => boolean
        )

)

-------------------------

上述のように生成されるファイルの確認をされるのでyを押す。

ファイルが存在している場合は上書きの確認メッセージが表示される。

Creating file /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/models/user.php
File `/Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/models/user.php` exists, overwrite? (y/n/q) 
[n] >

次に単体テスト用のファイルをbakeするか聞かれる。

SimpleTest is not installed. Do you want to bake unit test files anyway? (y/n)

やっぱちゃんとたりたいのでyを選択する。ちなみにSimpleTestのインストールは後からでも全然構わない。ファイルは「./app/tests/cases/models/user.test.php」に自動的に生成されている。

■生成したファイル

./app/models/user.php

<?php
class User extends AppModel {
    var $name = 'User';
    var $displayField = 'name';
    var $validate = array(
        'name' => array(
            'notempty' => array(
                'rule' => array('notempty'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'password' => array(
            'notempty' => array(
                'rule' => array('notempty'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'email' => array(
            'email' => array(
                'rule' => array('email'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'is_public' => array(
            'boolean' => array(
                'rule' => array('boolean'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
    );
}

コメントアウトされている部分については一番上を参考にする。

./app/tests/cases/models/user.test.php

/* User Test cases generated on: 2011-07-20 02:35:04 : 1311096904*/
App::import('Model', 'User');

class UserTestCase extends CakeTestCase {
	var $fixtures = array('app.user');

	function startTest() {
		$this->User =& ClassRegistry::init('User');
	}

	function endTest() {
		unset($this->User);
		ClassRegistry::flush();
	}

}

しかし、PHP5の文法で書きだして欲しいかつインデントはスペースを使って欲しいものだ。

■SimpleTest

debugモードで動作しているサイトのtest.phpにアクセスするとSimpleTestがダウンロードされていない場合、リンクが表示されるのでそこからダウンロードする。。。のもいいんだが分かりにくいのでコマンドラインから操作する。

cd vendors
wget http://sourceforge.net/projects/simpletest/files/simpletest/simpletest_1.0.1/simpletest_1.0.1.tar.gz/download
tar xvzf simpletest_1.0.1.tar.gz
rm simpletest_1.0.1.tar.gz

上述の操作でSimpleTestが使用できるようになった。テストケースについてはまた別の機会に解説する。

eclipseプラグイン

eclipseプラグインもあるのだがデバッグの構成とかいまいち設定方法が分からん。。。

■メソッドの記述

モデルクラスができたので試しにidからユーザを取得するメソッドgetByIdを書いてみる。

class User extends AppModel {
    public $name = 'User';
    public $displayField = 'name';
    public $validate = array(
        'name' => array(
            'notempty' => array(
                'rule' => array('notempty'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'password' => array(
            'notempty' => array(
                'rule' => array('notempty'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'email' => array(
            'email' => array(
                'rule' => array('email'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'is_public' => array(
            'boolean' => array(
                'rule' => array('boolean'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
    );
    
    /**
     * getById
     * 
     * @param numeric $id
     */
    public function getById($id)
    {
        $id = (int) $id;
        $result = $this->find(
            'first',
            array(
                'conditions' => array(
                    'id' => $id
                )
            )
        );
        return $result;
    }
}

SQLは見当たらない。生のSQLを書かないのが流儀である。まぁ、サブクエリとか書くときは普通に生で書いちゃうんだけど・・・少なくとも通常のCRUDはORMを使うべきだと思う。

理由

Symfonyの記事を参考に読んだ。以下は抜粋である。

データベースはリレーショナルです。一方でPHP 5とsymfonyはオブジェクト指向です。オブジェクト指向のコンテキストでもっとも効果的にデータベースにアクセスするには、オブジェクトをリレーショナルなロジックに変換するインターフェイスが求められます。

リレーショナルから取り出すものは単なるデータの集合に過ぎないが、オブジェクトを取り出せるならコードとの相性もいいよね。

$data = "{'name' : 'pochi' , 'age' : 28}";
$data = new Dog('pochi', 28);
$data->cry();// こんな感じ

抽象化レイヤーの主な利点は、移植性です。これによって、プロジェクトの真っ最中でも、別のデータベースに切り替えることができます。

そう!DBに依存した部分の変換(泥臭い仕事)はフレームワークがこなす。しかし、プロジェクトの途中でDBが変わるなんてあるのか・・・?

詳しくはドキュメントを参照するのが良いんだが、まぁとりあえず書いてみる。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByAge
     * 年齢でユーザを検索する
     * @param numeric $age
     */
    public function getByAge($age)
    {
        $age = (int) $age;
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'age' => $age
                )
            )
        );
        return $result;
    }
}

前述と違うのは結果が複数あるのでallを使用した。条件が複数ある場合は以下のようにする。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByAgeAndGender
     * 年齢と性別でユーザを検索する
     * @param numeric $age
     * @param numeric $gender
     */
    public function getByAgeAndGender($age , $gender)
    {
        $age    = (int) $age;
        $gender = (int) $gender;
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'age'    => $age,
                    'gender' => $gender,
                )
            )
        );
        return $result;
    }
}

上述のように配列の要素を増やすだけで良い。

範囲

日付や数値などで等号ではなく大なり小なりを指定して特定の範囲に含まれるレコードが欲しい場合がある。以下のようにすると実現できる。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByAgeRangeAndGender
     * 年齢の範囲と性別でユーザを検索する
     * @param numeric $minAge
     * @param numeric $maxAge
     * @param numeric $gender
     */
    public function getByAgeRangeAndGender($minAge, $maxAge , $gender)
    {
        $minAge = (int) $minAge;
        $maxAge = (int) $maxAge;
        $gender = (int) $gender;
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'gender' => $gender,
                    'age <' => $maxAge,
                    'age >' => $minAge,
                )
            )
        );
        return $result;
    }
}

若干気持ち悪いが連想配列なのでまぁしょうがない。

OR

conditionsに追加すると全てANDで結合した条件となる。ORにする場合は以下のように記述する。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByAgeRangeAndGender
     * 年齢の範囲と性別でユーザを検索する
     * @param numeric $minAge
     * @param numeric $maxAge
     * @param numeric $gender
     */
    public function getByAgeAndGender($minAge, $maxAge , $gender)
    {
        $minAge = (int) $minAge;
        $maxAge = (int) $maxAge;
        $gender = (int) $gender;
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'gender' => $gender,
                    'or' => array(
                        'age >' => $maxAge,
                        'age <' => $minAge,
                    )
                )
            )
        );
        return $result;
    }
}
NOT

NOTにする場合は以下のように記述する。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getTarget
     * 血液型が入力されているユーザをサービスのターゲットとして検索する
     * @param string $type
     */
    public function getTarget($type)
    {
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'not' => array(
                        'blood_type' => null
                    )
                )
            )
        );
        return $result;
    }
}
順序

以下のようにすることでORDER BYと同じになる。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByAgeAndGender
     * 年齢と性別でユーザを検索する
     * @param numeric $age
     * @param numeric $gender
     */
    public function getByAgeAndGender($age , $gender)
    {
        $age    = (int) $age;
        $gender = (int) $gender;
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'age'    => $age,
                    'gender' => $gender,
                ),
                'order' => array(
                    'age desc',
                    'gender asc',
                )
            )
        );
        return $result;
    }
}
結合

例えば職業テーブルが存在しユーザが職業IDを持つ場合に結合する必要もあるはずだ。以下のようにして結合する。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByOccupation
     * @param string $occupation
     */
    public function getByOccupation($occupation)
    {
        $alias = 'Occupation';
        $result = $this->find(
            'all',
            array(
                'fields' => array(
                    "`{$this->name}`.`id`",
                    "`{$this->name}`.`name`",
                    "`{$alias}`.`name`",
                ),
                'conditions' => array(
                    "`{$this->name}`.`gender`" => $gender,
                ),
                'joins' => array(
                    array(
                        'type'       => 'LEFT',
                        'table'      => 'occupations',
                        'alias'      => $alias,
                        'conditions' => "`{$this->name}`.`occupations_id` = `{$alias}`.`id`",
                    )
                ),
            )
        );
        return $result;
    }
}

fieldsを使用しない場合は結合したテーブルのカラムが取り出せない。結合しないクエリにおいて、fieldsを使用しない場合に発行されるクエリは以下のとおりとなる。

SELECT * FROM `user` WHERE `id` = ?;

fieldsを使用しないとアスタリスクとなり全てのカラムがfetchされるので注意が必要だ。

■保存

CakePHPでは明確にInsertとUpdateが分離されていない。プライマリキーがセットされている場合はUpdateの扱いとなる。

Insert

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * register
     * 登録してIDを返す
     * @param string $name
     * @param numeric $age
     */
    public function register($name, $age)
    {
        $age    = (int) $age;
        $result = $this->save(
            array(
                'name' => $name,
                'age'  => $age,
            )
        );
        return ($result)? $this->getLastInsertID() : $result;
    }
}

Update

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * register
     * 登録してIDを返す
     * @param string $name
     * @param numeric $age
     * @param numeric $id
     */
    public function register($name, $age, $id)
    {
        $id    = (int) $id;
        $result = $this->save(
            array(
                'id'   => $id,
                'name' => $name,
                'age'  => $age,
            )
        );
        return $result;
    }
}

■削除

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * del
     * @param numeric $id
     */
    public function del($id)
    {
        $this->delete((int) $id);
    }
}

第二引数でcascadeも指定できるけど、MySQLで直接定義してるよね・・・。以下のように条件で削除することもできる。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * del
     * @param string $name
     */
    public function del($name)
    {
        $this->deleteAll(array(
            'name' => $name
        ));
    }
}

大体モデルはコンナ感じでいいかな。

コメントはまだありません»

No comments yet.

RSS feed for comments on this post.TrackBack URL

Leave a comment