2日目は設計だけなのでスルーということで。
前回プロジェクトとアプリケーションを作りました。
今回はモジュールを作成します。
がその前に、とりあえずデータベースと接続アカウントを作成します。
CREATE DATABASE `jobeet` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `jobeet_dev` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE USER 'jobeet'@'localhost' IDENTIFIED BY 'jobeetpass';
GRANT ALL PRIVILEGES ON `jobeet` . * TO 'jobeet'@'localhost';
GRANT ALL PRIVILEGES ON `jobeet_dev` . * TO 'jobeet'@'localhost';
次に、O/RマッパをデフォルトのPropelからDoctrineに変更します。
何故Doctrineを選んだかって言われても特に深い意味はありません。
config/ProjectConfiguration.class.phpの
$this->enableAllPluginsExcept(array('sfDoctrinePlugin', 'sfCompat10Plugin'));
となっている行を
$this->enableAllPluginsExcept(array('sfPropelPlugin', 'sfCompat10Plugin'));
に変更します。
見ての通り、sfPropelPluginとsfCompat10Plugin「以外を」使用する、という設定になります。
で、sfCompat10Pluginって何?
Doctrineを使用可能にするために以下のコマンドを実行します。
>php symfony cc
>php symfony plugin:publish-assets
Propelで使用していた以下のディレクトリ、ファイルを削除します。
web/sfDoctrinePlugin/
config/propel.ini
config/schema.yml
config/databases.yml
データベース情報を設定します。
>php symfony configure:database --name=doctrine --class=sfDoctrineDatabase "mysql:host=localhost;dbname=jobeet" jobeet jobeetpass
config/databases.ymlが作成されるので、中身を編集します。
all: doctrine: class: sfDoctrineDatabase param: dsn: 'mysql:host=localhost;dbname=jobeet' username: jobeet password: jobeetpass dev: doctrine: class: sfDoctrineDatabase param: dsn: 'mysql:host=localhost;dbname=jobeet_dev' username: jobeet password: jobeetpass |
このようにすることで、index.phpからアクセスした場合はallの方のDBが、frontend_dev.phpからアクセスしてきた際はjobeet_devのDBが自動的に使われるようになります。
今後拡張子がymlのファイルがよく出てきますが、これはYAMLというデータ形式です。
スペースが意味を持つ形式なので、勝手にタブにしたりスペースを広げたりといったことはできません。
XMLのように面倒なタグなどを考える必要が全くないので、データを突っ込むのに非常に便利な形式となっています。
ということで次にYAMLでスキーマファイルを作成します。
http://www.symfony-project.org/jobeet/1_2/Doctrine/ja/03からconfig/doctrine/schema.ymlをそのままコピペ。
スキーマファイルとは要するにデータベースの設計図です。
これを書いておくと、DBからデータを読み込んだり逆に書き込んだりする際の助けになります。
それどころかDoctrineではスキーマファイルからCREATE TABLE文を作成してテーブルを自動作成したりといったことまで行うことができます。
というわけで一件便利なスキーマファイルですが、データベースの設計変更があった場合の修正が非常に面倒という問題があります。
自動作成コマンドだとテーブルの中身が綺麗さっぱり削除されたりするので運用開始後に設計変更とかが入ってきた場合どうにもならないんですよね。
この点、データベースからスキーマを取得してくるCakePHPとは真逆の思想です。
スキーマファイルの準備ができたら以下のコマンドを実行します。
>php symfony doctrine:build-model
>php symfony doctrine:build-sql
>php symfony doctrine:insert-sql
順にモデルを作成するコマンド、テーブルを作成するSQLを作成するコマンド、SQLを実行してテーブルを実際に作成するコマンドになります。
build-modelを実行すると、lib/model/doctrineディレクトリ内にテーブル毎にモデルファイルが作成されます。
これを編集することで、データの挿入や検索等をカスタマイズすることができるようになります。
最後に
>php symfony cc
としてキャッシュを削除し、作成したモデルを読み込むようにします。
これでスキーマとモデルが完成しました。
次にテストデータを登録します。
直接SQLを作成する必要はなく、スキーマ同様YMLファイルに記述しておき、それをコマンドで突っ込むことになります。
チュートリアルではこれがよい方法とか言っていますが、既存データを残しておきたい場合はどうするんだよといった疑問には答えてくれません。
data/fixtures/categories.ymlおよびdata/fixtures/jobs.ymlをそのままコピペ。
他に追加したいデータがあれば同じ形式で追加します。
といってもモデルオブジェクトとか意味がわかりませんが。
fixturesを保存した後、
>php symfony doctrine:data-load
を実行すると各YMLからデータを読み込んでデータベースに入れてくれます。
同時に該当のテーブルにそれまで蓄積されていたデータは全て消えます。
これだけやってようやくモデルの作成という下準備が終了しました。
モジュールを作成してみます。
>php symfony doctrine:generate-module --with-show --non-verbose-templates frontend job JobeetJob
jobeet_jobテーブルを閲覧、登録、更新、削除するモジュールができました。
ブラウザから
http://symfony.localhost/frontend_dev.php/job
とかそんなかんじでアクセスすると閲覧することができます。
どうなってるの?
さあ?
Symfonyの記事一覧
チュートリアルに従って学習を進めていきます。
いつものように文体が教える形式になっていますが、単にその方が書きやすいというだけで、基本的に自分が学習するために書いてます。
あまり気にしないが吉。
とりあえず真っ先にSymfonyの問題点を挙げておくと、APIの日本語リファレンスが無い。
習得にあたり、SmartyやZFがほぼ完全なリファレンスを提供しており、CakePHPも不完全ながら主要コンポーネント等については一覧を用意しているのに対し、大きなハンデとなっています。
あるのはチュートリアルのようなものばかりです。
http://symfony.xrea.jp/
http://www.symfony-project.org/jobeet/1_2/Doctrine/ja/
英語版は存在しますが、"form_tag"で検索しても何も出てこないなど、何処に何があるのか先に知っておかないと使えないという全く意味のないリファレンスになっています。
単にフォームを表示したいだけなのにオブジェクトがどうとかメソッドがどうとかいった内容をデザイナーに押しつけるという馬鹿げた発想が無くならない限り、積極的に使いたいとは到底思えないんですが。
確かにエスケープやフォームの肩代わりをしてくれるのは楽といえば楽なのですが、そのぶん参入障壁も非常に高くなっているので、入口を整備しないとなかなか入り込めないのですが。
あとフォームやDBの内容を取得すると無駄に馬鹿でかいオブジェクトが入ってくるので、var_dump()等目視によるチェックが行いにくいというのも難点です。
なにやらいきなり文句ばかりになりました。
最終日までに解消されればいいのですが。
インストールそのものは簡単です。
チュートリアルではダウンロードして解凍してといったことが書かれていますが、Pearを利用した方が楽です。
コマンドラインから
> pear channel-discover pear.symfony-project.com
> pear remote-list -c symfony
> pear install symfony/symfony
とするとPEARパッケージとしてインストールされます。
今回はバージョン1.2.9。
次にプロジェクトを作成します。
ディレクトリ構成なんかを簡単に作ってくれるバッチファイルがあります。
プロジェクトを作成したいフォルダに移動し、コマンドを一行打つと完了です。
勿論先にphpとsymfonyにパスを通しておきます。
> cd C:\xampp\htdocs\symfony
> symfony init-project jobeet
> symfony init-app frontend
サブドメインを作成します。
httpd.confを編集。
NameVirtualHost *:80 <VirtualHost *:80> DocumentRoot "C:\xampp\htdocs\symfony\web" DirectoryIndex index.php ServerName symfony.localhost <Directory "C:\xampp\htdocs\symfony\web"> AllowOverride All Allow from All </Directory> </VirtualHost> |
hostsに
127.0.0.1 symfony.localhost
以上を追加することで、
http://symfony.localhost/
でC:\xampp\htdocs\symfony\webがDocumentRootになります。
まあ、元々
http://localhost/
でC:\xampp\htdocsにアクセスできるようにしているのでこのディレクトリ構成だとセキュリティ的には全く意味がありませんが。
根本からディレクトリを分け、プロジェクト作成後に現れるwebディレクトリだけが外部公開され、それ以外のすべては外部から見えないところに隠される、という状態にすべきです。
以上でjobeetプロジェクトと、frontendアプリケーションが作成されました。
同時に開発環境も作成されます。
http://symfony.localhost/index.php
http://symfony.localhost/frontend_dev.php
というURLでそれぞれアクセスできます。
データベース等を開発環境用と本番用で分けたりするといったことができる他に、開発環境では実行したSQLや発生した例外の内容などを見ることができるようになっています。
とりあえずアクセスすると、CSSも画像も認識されず状態になっていました。
開発環境で使用する画像は何故か全然別のフォルダに入っています。
私の場合
C:\php5\pear\data\symfony\web\sf
とかいうところにありました。なんで。
sfディレクトリをwebディレクトリ内にコピペするか、シンボリックリンクで参照できるようすると、インデックスにアクセスできるようになります。
チュートリアルではhttpd.confを設定しろみたいな話になっているのですが、DocumentRootやAllowOverride等は仕方ないとして、たかがWebアプリケーションのためにわざわざhttpd.confでエイリアス設定させる思想ってどうなんですかね?
さて、上記の記事を書いたときには公式のコマンドはinit-projectだったはずなのですが、今見たらいつのまにかgenerate:projectに変わってました。
何故。
どちらを使用しても結果は同じになります。たぶん。
Symfonyの記事一覧
CakePHP1.2には標準でページャが付属しています。
前作ったcake_samplesテーブルでページャを実現してみます。
とりあえず適当にサンプル投入。
cake.php
1
2
3
4
5
6
7
|
$pdo=new PDO('mysql:dbname=cakephp;host=localhost','cakephp','cake');
for($loop=1;$loop<1000;$loop++){
$sql="INSERT INTO cake_samples (title,body,created) VALUES
('タイトル:".md5($loop)."','ボディ:".(int)($loop/50)."',NOW())";
$pdo->exec($sql);
}
|
中身は全く意味はないです。
コントローラを作成。
app/controllers/cake_sample_controller.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/*
CakeSampleコントローラ
*/
class CakeSampleController extends AppController{
//自分の名前
var $name = 'CakeSample';
//CakeSample/form
public function form() {
//入れる
$this->set('cake_sample', $this->paginate('CakeSample'));
}
}
|
コントローラ側の使い方はfind()と概ね同じですが、呼び方は
$this->CakeSample->paginate()
ではなく
$this->paginate('CakeSample')
となります。
$paginateは引数が無かった場合の初期値となります。
ビューは$paginatorを適当に呼び出すだけ。
ページャーのリンクは$paginateから呼び出しますが、実際のデータ自体はコントローラでsetした値から取得することになります。
app/views/cake_sample/search.ctp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
$options=array();
$options['modulus']='10';
$options['update']='Ajax';
$paginator->options($options);
//最初、最後へ
print($paginator->first('最初へ'));
print($paginator->last('最後へ',$options));
//前後へのリンク
print($paginator->prev('前へ',$options,'前はない',$options));
print($paginator->next('次へ',$options,'次はない',$options));
//前後数ページを表示
print($paginator->numbers($options));
//件数取得
print($paginator->counter('
合計 %pages% ページ中の %page% ページ目です。
総レコード %count% のうち、 %start% 行目から %end% 行目までの
%current% 行を表示しています')
);
//チェック
//現在のページ数
$paginator->current();
//10ページ目が存在するか
$paginator->hasPage(10);
//CakeSampleモデルの10ページ目が存在するか
$paginator->hasPage('CakeSample',10);
//前後のページが存在するか
$paginator->hasNext();
$paginator->hasPrev();
//現在のソート状態
$paginator->sortKey(null,$options);
$paginator->sortDir();
//ページャに関する情報を取得
$paginator->params();
//ソートする基準を選択
print($paginator->sort('ID順','id',$options));
print($paginator->sort('CakeSample.title'));
//データ取得
var_dump($cake_sample);
|
大文字だったり小文字だったり_だったりキャメルだったりうっとおしいことこの上ありません。
アクセスすると「前へ」「次へ」「ID順」等のリンクが現れ、クリックするだけで自動的に遷移します。
動作原理が全く分からないので非常に気持ち悪いですが、とりあえずページャが動きました。
では複数のページャーはどうすればいいかとか、検索結果に対してのページャはどうすればいいかとかはわからないままです。
CakePHPの記事一覧
cakePHPのDBアクセスは、基本的にはモデル作って関係を記述してコントローラからfindするだけで自動的に行ってくれます。
が、これだと常に全フィールドをそのまま持っくることしかできません。
"MIN(books.id)"が欲しい、といった場合はfind内に記述することで実現できます。
$this->Book->find('all',array('fields'=>'MIN(Book.id) AS hoge'));
が、"books.created + INTERVAL 1 HOUR"が欲しいんだよ、とばかりに
$this->Book->find('all',array('fields'=>'Book.created + INTERVAL 1 HOUR AS hoge'));
とか書くとSQLエラーになります。
SQL的には何故か勝手に"SELECT `Book`.`created + INTERVAL 1 HOUR` AS `hoge`"と変形されていました。
こういう場合のためにqueryメソッドで直接SQLを書くことが出来ます。
$this->Book->query('SELECT Book.created + INTERVAL 1 HOUR AS hoge FROM books AS Book');
それはまあいいのですが、実はモデルに対して知らないメソッドが来るとそのままSQLに渡されます。
$this->Book->{'SELECT Book.created + INTERVAL 1 HOUR AS hoge FROM books AS Book'}();
両者の結果は同じになります。
あんまり意味がないというか意味がわからない機能だ。
こんな書き方しかできないのかといえばそうでもなく、イレギュラーなSQLは基本的にモデルに追いやります。
これでコントローラからモデルに依存するコードを排除することができます。
/app/controllers/book_controller.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/*
Bookコントローラ
*/
class BookController extends AppController{
//自分の名前
var $name = 'Book';
//Book/indexAction
public function index($id=false) {
//任意のSQLを実行
$this->set('books', $this->Book->getAfter1Hour($id));
}
}
|
実装自体はモデルで地道にSQLを書きます。
本当はこんな書き方ではなく正しい方法があるのかもしれませんが、よくわかりませんでした。
app/models/book.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class Book extends AppModel {
//自分の名前
var $name = 'Book';
//Book::getAfter1Hour
public function getAfter1Hour($books_id=false){
//SQL
$sql=" SELECT books.created + INTERVAL 1 HOUR AS hoge FROM books";
if($books_id){
App::import('Sanitize');
$sql.=" WHERE books.id = '".Sanitize::escape($books_id)."'";
}
//実行
return $this->query($sql);
}
}
|
作成したメソッドを呼び出せば任意のSQLを実行することが出来ます。
また、queryメソッドの第二引数に配列を与えるとバインドしてくれます。
app/models/book.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class Book extends AppModel {
//自分の名前
var $name = 'Book';
//Book::getAfter1Hour
public function getAfter1Hour($books_id=false){
//SQL
$sql=" SELECT books.created + INTERVAL 1 HOUR AS hoge FROM books";
if($books_id){
$sql.=' WHERE books.id = ?';
}
//実行
return $this->query($sql,array($books_id));
}
}
|
http://localhost/Book/index/1'%20OR%201=1
↑というふうに引数を渡すと、SQLは↓というふうにになります。
SELECT books.created + INTERVAL 1 HOUR AS hoge FROM books WHERE books.id = '1\' OR 1=1'
CakePHPの記事一覧
前回のでbooksテーブルと直接繋がっているテーブルは結合できましたが、更に連鎖した先のテーブルを結合することも出来ます。
とりあえず前回のテーブル群にレーベルテーブル追加。
CREATE TABLE `labels` ( `id` int(8) NOT NULL AUTO_INCREMENT, `publisher_id` int(8) NOT NULL, `name` varchar(32) NOT NULL, `comment` text NOT NULL, `created` datetime NOT NULL, `modified` datetime NOT NULL, PRIMARY KEY (`id`), KEY `publisher_id` (`publisher_id`), KEY `publisher_id_2` (`publisher_id`), CONSTRAINT `labels_ibfk_1` FOREIGN KEY (`publisher_id`) REFERENCES `publishers` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
どう考えてもbooks.label_id→labels.publisher_id→publishersが正しい流れですが所詮サンプルなので気にしない。
次はモデルを追加です。
Books→Publishersの関係はBookモデルに書いてありますので、次はPublishers→Labelsの関係を記述する必要があります。
ではPublishersモデルを作成しましょう。
app/models/publisher.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class Publisher extends AppModel {
//自分の名前
var $name = 'Publisher';
//publishers.idはlabels.publisher_idから外部キー
var $hasOne = array(
'Label' =>
array(
'className' => 'Label',
'foreignKey'=> 'publisher_id'
)
);
}
|
毎回全リレーションを呼び出しているとパフォーマンス的に大変なので、デフォルトでは1段階しか呼ばれないようになっています。
Booksを呼び出した場合はPublishersまでしか呼ばれないということです。
これを2段階まで呼び出すようにする必要がありますが、単にコントローラに一行追加すればOKです。
app/controllers/book_controller.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//Bookコントローラ
class BookController extends AppController{
//自分の名前
var $name = 'Book';
//Book/indexAction
public function index() {
//Bookモデルを2階層までキーを追う
$this->Book->recursive=2;
//Bookモデルを取得
$this->set('books', $this->Book->findAll(array('Book.id <>'=>'1')));
}
}
|
このように2段階目まで簡単に取得することができます。
SELECT (略) FROM `books` AS `Book`
LEFT JOIN `authors` AS `Author` ON (`Book`.`author_id` = `Author`.`id`)
LEFT JOIN `publishers` AS `Publisher` ON (`Book`.`publisher_id` = `Publisher`.`id`)
WHERE `Book`.`id` <> 1
前回LIMIT 1が取れない、みたいなことを言いましたが、単にfindが一件取得で、findAllが全件取得だっただけでした。
CakePHPの記事一覧
すぐ忘れてしまうのでメモ。
何故か何故か公式に見あたらず。
http://www.php.net/manual/ja/ini.php
http://www.php.net/manual/ja/function.ini-set.php
と思ったらこんなところにあった。
http://www.php.net/manual/ja/configuration.changes.modes.php
モード | 意味 |
---|---|
PHP_INI_USER | このエントリは、ユーザスクリプトから設定可能 |
PHP_INI_PERDIR | .htaccessから設定可能 |
PHP_INI_SYSTEM | このエントリは、php.ini または httpd.conf で設定可能 |
PHP_INI_ALL | このエントリはどこでも設定可能 |
php.ini only | php.iniのみ |
設定方法 | 設定可能モード |
---|---|
ini_set | PHP_INI_USER,PHP_INI_ALL |
php.ini php.ini only | ,PHP_INI_SYSTEM,PHP_INI_ALL |
.htaccess | PHP_INI_PERDIR,PHP_INI_ALL |
httpd.conf | PHP_INI_SYSTEM,PHP_INI_ALL |
合ってるのかねえ?
ちなみにこれらの定数はget_defined_constants()で出てきません。
ところで公式マニュアルってどれが正式なんですかね?
どれもhttp://www.php.net/とかhttp://jp.php.net/とかhttp://jp2.php.net/とかはどれもエイリアスで、実際はphp.get7.bizとかpobox.packetbusiness.com(jp-php-net.packetbusiness.com)とか胡散臭い名前なんですが。
wwwのy2.php.netが一番まともっぽいかな。
http://www.php.net/manual/ja/
http://jp.php.net/manual/ja/
http://jp2.php.net/manual/ja/
http://y2.php.net/manual/ja/
http://php.get7.biz/manual/ja/
http://pobox.packetbusiness.com/
http://jp-php-net.packetbusiness.com/manual/ja/
http://framework.zend.com/manual/ja/zend.validate.html
いわゆるひとつのバリデータです。
入力形式が整数か、メールアドレスの形式として正しいか、IPアドレスの形式として正しいか、クレカの形式の形式として正しいか、といったチェックを行うことができます。
本来の使い方としてはZend_Validate_Digits、Zend_Validate_EmailAddress等個別のバリデータをインクルードして、といった形なのですが、そんなの面倒でやってられないのでZend_Validateから全て静的に呼び出せるようになっています。
こういう実装は伝説のFactoryMethodパターンなんじゃないかと思うのですがよくわかりません。
それはまあ便利なのですが、この場合は欠点としてバリデータチェインが使用できません。
逆にバリデータチェインを行う場合は、各バリデータを手動でインクルードしないとエラーになってしまいます。
zend_validate.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
//チェックする文字列
$string='abc012';
//Zend_Validate
require_once('Zend/Validate.php');
//静的にバリデート
//必要なファイルは自動的に呼び出してくれる
Zend_Validate::is($string, 'StringLength',array(6,12)); //trueになる
Zend_Validate::is($string, 'Alpha'); //falseになる
//メソッドチェーンでバリデート
//こちらの場合必要なファイルを全部インクルードしないとエラーになる
require_once('Zend/Validate/StringLength.php');
require_once('Zend/Validate/Alpha.php');
//バリデートチェイン
$validator=new Zend_Validate();
$validator
->addValidator(new Zend_Validate_StringLength(6, 12))
->addValidator(new Zend_Validate_Alpha());
$ret=$validator->isValid($string);
//失敗した場合エラーメッセージを取得できる
if(!$ret){
$error_message=$validator->getMessages();
}
|
このように、静的バリデートは使用は簡単ですが'6~12文字かつアルファベット'といった表現に弱く、逆にインスタンスを生成する場合は毎回個別バリデータをインクルードする必要があって面倒です。
毎回インクルードしまくるか、毎回静的に呼び出しまくるかの二択になってしまいます。
もうちょっとどうにかならなかったんでしょうか。
バリデータには通常使われる文字数やメールアドレスといったもの以外にも、16進文字やIBANコードなど珍しいものまでありますが、英語圏出身にふさわしく日本語関連のものはありません。
全角のみ、といったバリデータは自分で実装する必要があります。
また不注意にZend_Validate::is('あ', 'StringLength',array(3,3))とかやってしまうとtrueになってしまったりすることがあるので注意が必要です。
Zend_Validate_StringLengthの文字数比較はiconv_strlen()という見慣れぬ関数で行われており、これのデフォルトがISO-8859-1なので、'あ'がUTF-8だったら3文字と判断されてしまいます。
Zend_Validate_StringLengthを使用する場合は必ず何処かに
iconv_set_encoding('internal_encoding','UTF-8');
と書いておきましょう。
どうでもいいけど住所や電話番号欄に「全角のみ」ってバリデータは百害あって一利なし。
前回マジックメソッド__sleep、__wakeupによるシリアライズを行いましたが、今回はSerializableインターフェイスで同じことを行ってみます。
両者がどう違うかというと、マジックメソッド__sleepはあくまでシリアライズ対象を変更できるだけであったのに対し、Serializableインターフェイスによるシリアライズは出力をわりかし自由にできるという点です。
SerialExample2.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
//適当なクラス
class SerialExample implements Serializable{
//インスタンス変数とか
private $string_serial='';
private $string_no_serial='';
//セッタゲッタ
public function __set($name,$value){
if($name=='string_serial' || $name=='string_no_serial'){
$this->{$name}=$value;
return true;
}else{
return false;
}
}
public function __get($name){
if($name=='string_serial' || $name=='string_no_serial'){
return $this->{$name};
}else{
return false;
}
}
//シリアライズ
public function serialize(){
//シリアライズした文字列を返す
return mcrypt_encrypt(MCRYPT_DES,'hogehoge'
,$this->string_serial,MCRYPT_MODE_ECB);
}
//アンシリアライズ
public function unserialize($serial){
//シリアライズされた文字列を元に戻す
$this->string_serial=mcrypt_decrypt(MCRYPT_DES,'hogehoge'
,$serial,MCRYPT_MODE_ECB);
}
//↓クラスのおわり
}
|
serialize2.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//SerialExample
require_once('SerialExample2.php');
$example=new SerialExample();
//適当
$example->string_serial='aaaaaaaa';
$example->string_no_serial='bbbbbbbb';
//シリアライズ
$serial=serialize($example);
//アンシリアライズ
$desilial=unserialize($serial);
var_dump($serial,$example,$desilial);
|
シリアライズ時にSerialExample::serializeが呼ばれ、シリアライズ後の文字列は、
string 'C:13:"SerialExample":8:{mィ^ョ・シ+}' (length=33)
というふうに暗号化されたなんだかよくわからない文字列となります。
周囲の'C:13'や'SerialExample'等はPHPが必要に応じて自動的に付けるので、実際に変更できるのはその後の{}の中となります。
今回は簡単な暗号化を行いました。
逆にアンシリアライズ時にはSerialExample::unserializeが呼ばれます。
object(SerialExample)[2] private 'string_serial' => string 'aaaaaaaa' (length=8) private 'string_no_serial' => string '' (length=0)
SerialExample::string_serialには無事、復号化後の文字列が収まりました。
serialize()でMDB2のインスタンスを使い回したいとします。
serialize0.php
1
2
3
4
5
6
7
8
|
//MDB2
require_once('MDB2.php');
$db=MDB2::factory('mysqli://testuser:testpass@localhost');
//シリアライズ
$serial=serialize($db);
|
これだけでMDB2のインスタンスがさくっと一つの文字列になり、その後は保存して別の場所で開くなり、APIで他所に渡すなり自由に扱えます。
簡単ですが、この作り方、一つ重大なセキュリティ上の欠陥があります。
コンストラクタで引き渡したユーザ名やパスワードが平文で保存されてしまうのです。
パスワードは見せたくないけど、それ以外のインスタンスはそのまま渡したい、などということはできるでしょうか。
というわけでserialize()の動作を変更する方法があります。
しかも二種類。
http://jp.php.net/manual/ja/language.oop5.magic.php
http://jp.php.net/manual/ja/class.serializable.php
何故二種類もあるのかよくわかりませんが、
http://fabien.potencier.org/article/9/php-serialization-stack-traces-and-exceptions
>@Eric: Because the Serializable interface is much more flexible. In this specific case, we don't use anything fancy, but for consistency, I always use the interface'
だそうです。
まあとりあえずマジックメソッドでやってみます。
SerialExample1.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
//適当なクラス
class SerialExample{
//インスタンス変数とか
private $string_serial='';
private $string_no_serial='';
//セッタゲッタ
public function __set($name,$value){
if($name=='string_serial' || $name=='string_no_serial'){
$this->{$name}=$value;
return true;
}else{
return false;
}
}
public function __get($name){
if($name=='string_serial' || $name=='string_no_serial'){
return $this->{$name};
}else{
return false;
}
}
//シリアライズ
public function __sleep(){
//シリアライズしたい変数名だけ返す
return array('string_serial');
}
//アンシリアライズ
public function __wakeup(){
/*
シリアライズ時に返した変数は自動的に元の場所に復帰する
それ以外の、特に行いたいこと(DBに接続しに行くとか)
がある場合はここに書く
*/
}
//↓クラスのおわり
}
|
serialize1.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//SerialExample
require_once('SerialExample1.php');
$example=new SerialExample();
//適当
$example->string_serial='aaaaa';
$example->string_no_serial='bbbbb';
//シリアライズ
$serial=serialize($example);
//アンシリアライズ
$desilial=unserialize($serial);
|
シリアライズ時に、変数名'string_serial'のみを返しています。
こうすることで指定した'string_serial'のみがシリアライズされ、シリアライズされた文字列は
string 'O:13:"SerialExample":1:{s:28:"�SerialExample�string_serial";s:5:"aaaaa";}'
となります。
シリアライズ前のSerialExampleクラスの中身にはstring_serial及びstring_no_serialが両方とも入っています。
object(SerialExample)[1] public 'string_serial' => string 'aaaaa' (length=5) public 'string_no_serial' => string 'bbbbb' (length=5)
一旦シリアライズ後アンシリアライズしたときには、string_serialだけが元に戻ります。
object(SerialExample)[2] public 'string_serial' => string 'aaaaa' (length=5) public 'string_no_serial' => string '' (length=0)
パスワードのような、平文で送信されると困る部分だけシリアライズ対象から排除することができるようになりました。
めでたし。
全然どうでもいいんですがPHP以外ではデシリアライズです。
何故PHPだけアンシリアライズなんだろう。
Zend_FeedはRSSの読み書きを行うクラスです。
前MagpieRSSでRSSを読み込みましたが、MagpieRSS自体が微妙にバグあり状態で更新が止まってしまっているので、今から使用するのはお薦めできません。
Zend_Feedは扱いがやたら簡単なのでさっくりといじってみましょう。
Zend_Feed::importでRSSから読み込みを行い、Zend_Feed::importFileでローカルのXMLファイルから読み込みを行えます。
驚きなのがZend_Feed::findFeedsで、普通のHTMLファイルの<LINK>タグからRSSを探して読み込んでくれるという超便利メソッド。
サイトトップを指定しておけば、きちんとタグを設定してあるサイトならフィードのURLが変わったとしても一切修正の必要がありません。
とりあえず使用。
zend_feed1.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
//Zend_Feed
require_once('Zend/Feed.php');
//Feedインスタンス
$feed = Zend_Feed::import(
'http://yuubiseiharukana.blog.shinobi.jp/RSS/200/');
//$feed = Zend_Feed::findFeeds(
// 'http://yuubiseiharukana.blog.shinobi.jp/');
//各要素を取得可能
print($feed->title);
print($feed->language());
//イテレータは各エントリに対応
foreach($feed as $val){
print($val->title());
}
//要素の変更
$feed->title='タイトルを修正';
//修正したRSSを出力
$feed->send();
|
取得さえ成功してしまえば各要素には
$feed->title();
$feed->title;
とプロパティでもメソッドでも直接アクセスすることができます。
またフィードの改変を簡単に行うことができます。
$feed->title='タイトルを修正';
というふうに単に値を突っ込むだけで変更することが可能です。
ただ、何故か非対称なことに、メソッドを使って
$feed->title('タイトルを修正')
というふうには書けません。
他所のサイトのRSSフィードを取得して表示したいといった場合、とりあえずHTMLをさっさと表示して、RSSフィード部分をAJAXで自分のサーバにあるRSS取得スクリプトにリクエストを行い、PHPで適当にパースして返すといった方法で簡単にできます。
AJAXによるRSSフィード取得はクロスドメイン問題のせいで難しいので、一旦自サーバに投げてPHPからリクエストした方が楽です。
まあ、貼り付けるだけで簡単取得できるサービスがいっぱいあるんで、それをテンプレに貼り付けたほうがもっと手っ取り早いんですがね。
http://code.google.com/intl/ja/apis/ajaxfeeds/
http://www.moondakota.com/feed/index_s.php
http://zero-hl.ddo.jp/feed2js/build.php