学習日記59日目
どうも、enomotoです。
今日から一旦進めるのを休んで復習していきたいと思います。
review askeet Day 1
symfonyインストールは終了しているので省略。プロジェクトのセットアップから行います。
フォルダの作成
今回は/Users/shota/education/enomoto/askeet2/に作ります。
apacheの設定
順番は逆ですが、ブラウザ上でも確認しながらやりたいのでApacheの設定から始めます。
場所: /opt/local/apache2/conf/httpd.conf
NameVirtualHost 127.0.0.1 <VirtualHost 127.0.0.1> ServerName askeet.localhost DocumentRoot "/Users/shota/education/askeet/web" DirectoryIndex index.php Alias /sf /opt/local/lib/php/data/symfony/web/sf <Directory "/Users/shota/education/askeet/web"> AllowOverride All Allow From All </Directory> <Directory "/opt/local/lib/php/data/symfony/web/sf"> AllowOverride All Allow From All </Directory> </VirtualHost> <VirtualHost 127.0.0.1> ServerName review.askeet.localhost DocumentRoot "/Users/shota/education/enomoto/askeet2/web" DirectoryIndex index.php Alias /sf /opt/local/lib/php/data/symfony/web/sf <Directory "/Users/shota/education/enomoto/askeet2/web"> AllowOverride All Allow From All </Directory> <Directory "/opt/local/lib/php/data/symfony/web/sf"> AllowOverride All Allow From All </Directory> </VirtualHost>
/etc/hostsの書換
忘れていたので早速修正。
127.0.0.1 askeet.localhost 127.0.0.1 review.askeet.localhost
プロジェクトの作成
問題なくApacheの設定ができたみたいなのでsymfonyのプロジェクトの作成をする
symfony init-project askeet
これで基本となるものが入るわけですね。
.htaccessのせいか、表示が変わったぞ。
index.phpがないよという404メッセージになった。
subversion(svn)の設定
まずはcacheとlogの中身を空にします。
rm -rf cache/* rm -rf log/*
作業フォルダaskeet2をaddします。
svn add askeet2
次に、cacheとlogはsvnのコミット対象外にします。
svn propedit svn:ignore cache --editor-cmd emacs svn propedit svn:ignore log --editor-cmd emacs
cacheとlogのフォルダのパーミッションを777にする。
chmod 777 log chmod 777 cache
全部終わったら1日目終了
コミットします。
svn up // まずは最新にして svn st // 対象を確認 svn ci -m "[review askeet Day 1] 終了" // ""内はコメント
review askeet Day 2
DBでまず、何が必要かを考える。
askeetというのはデモサイトを見た限り、日本で言うところのOKwaveやY!知恵袋みたいなもの。
ということで必要なものを考えると
- question (質問)
- answer (答え)
- user (ユーザー)
- relevancy (妥当性)
となる…みたい。
schema.ymlを書く。
ということでデータベース設計を書きたいと思います。
前回、askeet通りにしたらえらいことになったのでschema.ymlを書きます。
場所: askeet2/config/schema.yml
propel: _attributes: { noXsd: false, defaultIdMethod: none, package: lib.model } ask_question: _attributes: { phpName: Question, idMethod: native } id: { type: integer, required: true, primaryKey: true, autoIncrement: true } user_id: { type: integer, foreignTable: ask_user, foreignReference: id } title: { type: longvarchar } body: { type: longvarchar } created_at: ~ updated_at: ~ ask_answer: _attributes: { phpName: Answer, idMethod: native } id: { type: integer, required: true, primaryKey: true, autoIncrement: true } question_id: { type: integer, foreignTable: ask_question, foreignReference: id } user_id: { type: integer, foreignTable: ask_user, foreignReference: id } body: { type: longvarchar } created_at: ~ ask_user: _attributes: { phpName: User, idMethod: native } id: { type: integer, required: true, primaryKey: true, autoIncrement: true } nickname: { type: varchar(50), required: true, index: true } first_name: varchar(100) last_name: varchar(100) created_at: ~ ask_interest: _attributes: { phpName: Interest, idMethod: native } question_id: { type: integer, foreignTable: ask_question, foreignReference: id, primaryKey: true } user_id: { type: integer, foreignTable: ask_user, foreignReference: id, primaryKey: true } ask_relevancy: _attributes: { phpName: Relevancy, idMethod: native } answer_id: { type: integer, foreignTable: ask_answer, foreignReference: id, primaryKey: true } user_id: { type: integer, foreignTable: ask_user, foreignReference: id, primaryKey: true } score: { type: integer } created_at: ~
書いているうちにうっすらどういうことか分かってきた感じがします。
MySQLとPropelの設定
InnoDBに対応するように書き換える。ついでにaskeet2というデータベースを使うように設定。
場所: askeet2/config/propel.ini
propel.database.url = mysql://root@localhost/askeet2 propel.mysql.tableType = InnoDB
MySQLにaskeet2というデータベースを作る。
mysqladmin5 -uroot -p create askeet2
databases.ymlを設定する。
前回はここではまったので忘れないように。
場所: askeet/config/databases.yml
all: propel: class: sfPropelDatabase param: dsn: mysql://root@localhost/askeet2
Propelモデルを作る
symfony propel-build-model
SQL文を作る
symfony propel-build-sql
review askeet Day 3
frontendのレイアウトを変更する。
場所: askeet2/apps/frontend/templates/layout.php
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <?php include_http_metas() ?> <?php include_metas() ?> <?php include_title() ?> <link rel="shortcut icon" href="/favicon.ico" /> </head> <body> <div id="header"> <ul> <li><?php echo link_to('about','@homepage') ?></li> </ul> <h1><?php echo link_to(image_tag('askeet_logo.gif','alt=askeet'),'@homepage') ?></h1> </div> <div id="content"> <div id="content_main"> <?php echo $sf_data->getRaw('sf_content') ?> <div class="verticalalign"></div> </div> <div id="content_bar"> <!-- Nothing for the moment --> <div class="verticalalign"></div> </div> </div> </body> </html>
書き換えたらCSSを拾いに行く。
http://svn.askeet.com/tags/release_day_3/web/css/
ここからダウンロードする。askeet2/web/cssに移動して
main.cssを一度削除してからダウンロードする。
cd web/css rm main.css wget http://svn.askeet.com/tags/release_day_3/web/css/main.css wget http://svn.askeet.com/tags/release_day_3/web/css/layout.css
トップページの変更
現在はsymfonyのCongratulations画面なので
questionをトップページにします。
場所: askeet2/apps/frontend/config/routing.yml
# default rules homepage: url: / param: { module: question, action: list }
テストデータを用意する。
そのままテストデータを使ってもおもしろくないので多少弄る。
場所: askeet2/data/fixtures/test_data.yml
User: anonymous: nickname: anonymous first_name: Anonymous last_name: Coward crazyup: nickname: crazyup first_name: Shota last_name: Enomoto dinotaro: nickname: dino first_name: Taro last_name: Dino Question: q1: title: What shall I do tonight with my girlfriend? user_id: crazyup body: | We shall meet in front of the Dunkin'Donuts before dinner, and I haven't the slightest idea of what I can do with her. She's not interested in programming, space opera movies nor insects. She's kinda cute, so I really need to find something that will keep her to my side for another evening. q2: title: What can I offer to my step mother? user_id: anonymous body: | My stepmother has everything a stepmother is usually offered (watch, vacuum cleaner, earrings, del.icio.us account). Her birthday comes next week, I am broke, and I know that if I don't offer her something sweet, my girlfriend won't look at me in the eyes for another month. q3: title: How can I generate traffic to my blog? user_id: dinotaro body: | I have a very swell blog that talks about my class and mates and pets and favorite movies. Interest: i1: { user_id: crazyup, question_id: q1 } i2: { user_id: dinotaro, question_id: q1 } i3: { user_id: dinotaro, question_id: q2 } i4: { user_id: crazyup, question_id: q2 }
テストデータを入れるためのバッチを作る
場所: askeet2/batch/load_data.php
<?php define('SF_ROOT_DIR', realpath(dirname(__FILE__).'/..')); define('SF_APP', 'frontend'); define('SF_ENVIRONMENT', 'dev'); define('SF_DEBUG', true); require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR. SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php'); // データベースマネージャーを初期化する $databaseManager = new sfDatabaseManager(); $databaseManager->initialize(); // fixturesにあるすべてのファイルを読み込みDBにデータを追加 $data = new sfPropelData(); $data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');
あとはこいつをコマンドラインから叩けばテストデータがDBへ。
php batch/load_data.php
テーブルからdivに変更
場所: askeet2/apps/frontend/templates/listSuccess.php
<?php use_helper('Text') ?> <h1>人気の質問</h1> <?php foreach($questions as $question): ?> <div class="question"> <div class="interested_block"> <div class="interested_mark" id="mark_<?php echo $question->getId() ?>"> <?php echo count($question->getInterests()) ?> </div> </div> <h2><?php echo link_to($question->getTitle(),'question/show?id='.$question->getId()) ?></h2> <div class="question_body"> <?php echo truncate_text($question->getBody(),200) ?> </div> </div> <?php endforeach; ?>
link_toはaタグとかを書く必要が無くてすごく便利。
truncate_textはgetBodyを200文字に切り詰めて表示していると。
おーそれっぽい><
使わないコードやファイルを削除する。
あとあと実装するので邪魔なのは削除しておく。
まずはquestionのアクションのお掃除
場所: askeet2/apps/frontend/modules/question/actions/actions.class.php
<?php /** * question actions. * * @package askeet * @subpackage question * @author Your name here * @version SVN: $Id: actions.class.php 3335 2007-01-23 16:19:56Z fabien $ */ class questionActions extends sfActions { public function executeList() { $this->questions = QuestionPeer::doSelect(new Criteria()); } public function executeShow() { $this->question = QuestionPeer::retrieveByPk($this->getRequestParameter('id')); $this->forward404Unless($this->question); } }
あとはeditSuccess.phpを削除
svn rm apps/frontend/modules/question/templates/editSuccess.php
review askeet Day 4
詳細画面をtableからdiv化する。
このテーブルでできたサイトを直す。
場所: askeet2/apps/frontend/modules/question/templates/showSuccess.php
<?php use_helper('Date') ?> <div class="interested_block"> <div class="interested_mark" id="mark_<?php echo $question->getId() ?>"> <?php echo count($question->getInterests()) ?> </div> </div> <h2><?php echo $question->getTitle() ?></h2> <div class="question_body"> <?php echo $question->getBody() ?> </div> <div id="answers"> <?php foreach ($question->getAnswers() as $answer): ?> <div class="answer"> posted by <?php echo $answer->getUser()->getFirstName().' '.$answer->getUser()->getLastName() ?> on <?php echo format_date($answer->getCreatedAt(), 'p') ?> <div> <?php echo $answer->getBody() ?> </div> </div> <?php endforeach; ?> </div>
テストデータの追加
答えと妥当さのデータをテストデータに入れる。
場所: askeet2/data/fixtures/test_data.yml
User: anonymous: nickname: anonymous first_name: Anonymous last_name: Coward crazyup: nickname: crazyup first_name: Shota last_name: Enomoto dinotaro: nickname: dino first_name: Taro last_name: Dino Question: q1: title: What shall I do tonight with my girlfriend? user_id: crazyup body: | We shall meet in front of the Dunkin'Donuts before dinner, and I haven't the slightest idea of what I can do with her. She's not interested in programming, space opera movies nor insects. She's kinda cute, so I really need to find something that will keep her to my side for another evening. q2: title: What can I offer to my step mother? user_id: anonymous body: | My stepmother has everything a stepmother is usually offered (watch, vacuum cleaner, earrings, del.icio.us account). Her birthday comes next week, I am broke, and I know that if I don't offer her something sweet, my girlfriend won't look at me in the eyes for another month. q3: title: How can I generate traffic to my blog? user_id: dinotaro body: | I have a very swell blog that talks about my class and mates and pets and favorite movies. Interest: i1: { user_id: crazyup, question_id: q1 } i2: { user_id: dinotaro, question_id: q1 } i3: { user_id: dinotaro, question_id: q2 } i4: { user_id: crazyup, question_id: q2 } Answer: a1_q1: question_id: q1 user_id: dinotaro body: | You can try to read her poetry. Chicks love that kind of things. a2_q1: question_id: q1 user_id: crazyup body: | Don't bring her to a donuts shop. Ever. Girls don't like to be seen eating with their fingers - although it's nice. a3_q2: question_id: q2 user_id: crazyup body: | The answer is in the question: buy her a step, so she can get some exercise and be grateful for the weight she will lose. a4_q3: question_id: q3 user_id: crazyup body: | Build it with symfony - and people will love it.
で、
php batch/load_data.php
モデルの変更
まずはいちいちgetUserの中のgetFirstNameとか呼び出さなくても
getUserすれば苗字+名前が出るようにする。
現在、askeet2/lib/model/User.phpは空っぽなので
そこに__toStringで書く。
public function __toString() { return $this->getFirstName().' '.$this->getLastName(); }
あとはテンプレートを直すだけ。
さっそく、askeet2/apps/frontend/modules/question/templates/showSuccess.phpを直す。
posted by <?php echo $answer->getUser() ?>
同じことは繰り返さない
listSuccess.phpとshowSuccess.phpには同じコードがある。
同じことは繰り返さないということに反するのでフラグメントにする。
場所: askeet2/apps/frontend/modules/question/templates/_interested_user.php
<div class="interested_mark" id="mark_<?php echo $question->getId() ?>"> <?php echo count($question->getInterests()) ?> </div>
↑が書かれた部分を↓のコードに置き換える。
場所: askeet2/apps/frontend/modules/question/templates/listSuccess.php
場所: askeet2/apps/frontend/modules/question/templates/showSuccess.php
<?php include_partial('interested_user',array('question' => $question)) ?>
にすると共通化ができる。これで同じところを何度も直す必要がなくなる。
オブジェクトモデルに項目を追加
場所: askeet2/config/schema.yml
ask_question: _attributes: { phpName: Question, idMethod: native } id: { type: integer, required: true, primaryKey: true, autoIncrement: true } user_id: { type: integer, foreignTable: ask_user, foreignReference: id } title: { type: longvarchar } body: { type: longvarchar } interested_users: { type: integer, default: 0 } created_at: ~ updated_at: ~
関心の更新をできるようにする
InnoDBなのでトランザクションを使えるように書きます。
場所: askeet2/lib/model/Interest.php
<?php /** * Subclass for representing a row from the 'ask_interest' table. * * * * @package lib.model */ class Interest extends BaseInterest { public function save($con = null) { $con= Propel::getConnection(); try { $con->begin(); $ret = parent::save($con); // interested_usersを更新 $question = $this->getQuestion(); $interested_users = $question->getInterestedUsers(); $question->setInterestedUsers($interested_users + 1); $question->save($con); $con->commit(); return $ret; } catch (Exception $e) { throw $e; } } }
interested_userパーシャルの更新
場所: askeet2/apps/frontend/modules/question/templates/_interested_user.php
<div class="interested_mark" id="mark_<?php echo $question->getId() ?>"> <?php echo $question->getInterestedUsers() ?> </div>
修正したら、テストデータを更新する。
php batch/load_data.php
回答の投票システム
まずはschema.ymlに追加。
場所: askeet2/config/schema.yml
ask_answer: _attributes: { phpName: Answer, idMethod: native } id: { type: integer, required: true, primaryKey: true, autoIncrement: true } question_id: { type: integer, foreignTable: ask_question, foreignReference: id } user_id: { type: integer, foreignTable: ask_user, foreignReference: id } body: { type: longvarchar } relevancy_up: { type: integer, default: 0 } relevancy_down: { type: integer, default: 0 } created_at: ~
できたら、モデルを再設定してDB更新
symfony propel-build-model symfony propel-build-sql symfony propel-insert-sql
Revancyクラスのsaveを書き換える
場所: askeet2/lib/model/Relevancy.php
<?php /** * Subclass for representing a row from the 'ask_relevancy' table. * * * * @package lib.model */ class Relevancy extends BaseRelevancy { public function save($con = null) { $con = Propel::getConnection(); try { $con->begin(); $ret = parent::save(); // relevancy(妥当性)を更新する $answer = $this->getAnswer(); if($this->getScore() == 1) { $answer->setRelevancyUp($answer->getRelevancyUp() + 1); } else { $answer->setRelevancyDown($answer->getRelevancyDown() + 1); } $answer->save($con); $con->commit(); return $ret; } catch(Exception $e) { $con->rollback(); throw $e; } } }
Answerクラスにメソッドを追加
場所: askeet2/lib/model/Answer.php
<?php /** * Subclass for representing a row from the 'ask_answer' table. * * * * @package lib.model */ class Answer extends BaseAnswer { public function getRelevancyUpPercent() { $total = $this->getRelevancyUp() + $this->getRelevancyDown(); return $total ? sprintf('$.0f',$this->getRelevancyUp() * 100 / $total) : 0; } public function getRelevancyDownPercent() { $total = $this->getRelevancyUp() + $this->getRelevancyDown(); return $total ? sprintf('%.0f',$this->getRelevancyDown() * 100 / $total) : 0; } }
テンプレートの変更
場所: askeet2/apps/frontend/modules/question/templates/showSuccess.php
<?php use_helper('Date') ?> <div class="interested_block"> <?php include_partial('interested_user',array('question' => $question)) ?> </div> <h2><?php echo $question->getTitle() ?></h2> <div class="question_body"> <?php echo $question->getBody() ?> </div> <div id="answers"> <?php foreach ($question->getAnswers() as $answer): ?> <div class="answer"> <?php echo $answer->getRelevancyUpPercent() ?>% UP <?php echo $answer->getRelevancyDownPercent() ?>% DOWN posted by <?php echo $answer->getUser() ?> on <?php echo format_date($answer->getCreatedAt(), 'p') ?> <div> <?php echo $answer->getBody() ?> </div> </div> <?php endforeach; ?> </div>
テストデータに追加する。
場所: askeet2/data/fixtures/test_data.yml
User: anonymous: nickname: anonymous first_name: Anonymous last_name: Coward crazyup: nickname: crazyup first_name: Shota last_name: Enomoto dinotaro: nickname: dino first_name: Taro last_name: Dino Question: q1: title: What shall I do tonight with my girlfriend? user_id: crazyup body: | We shall meet in front of the Dunkin'Donuts before dinner, and I haven't the slightest idea of what I can do with her. She's not interested in programming, space opera movies nor insects. She's kinda cute, so I really need to find something that will keep her to my side for another evening. q2: title: What can I offer to my step mother? user_id: anonymous body: | My stepmother has everything a stepmother is usually offered (watch, vacuum cleaner, earrings, del.icio.us account). Her birthday comes next week, I am broke, and I know that if I don't offer her something sweet, my girlfriend won't look at me in the eyes for another month. q3: title: How can I generate traffic to my blog? user_id: dinotaro body: | I have a very swell blog that talks about my class and mates and pets and favorite movies. Interest: i1: { user_id: crazyup, question_id: q1 } i2: { user_id: dinotaro, question_id: q1 } i3: { user_id: dinotaro, question_id: q2 } i4: { user_id: crazyup, question_id: q2 } Answer: a1_q1: question_id: q1 user_id: dinotaro body: | You can try to read her poetry. Chicks love that kind of things. a2_q1: question_id: q1 user_id: crazyup body: | Don't bring her to a donuts shop. Ever. Girls don't like to be seen eating with their fingers - although it's nice. a3_q2: question_id: q2 user_id: crazyup body: | The answer is in the question: buy her a step, so she can get some exercise and be grateful for the weight she will lose. a4_q3: question_id: q3 user_id: crazyup body: | Build it with symfony - and people will love it. Relevancy: rel1: answer_id: a1_q1 user_id: crazyup score: 1 rel2: answer_id: a1_q1 user_id: dinotaro score: -1
書いたらバッチを走らせる。
php batch/load_data.php
ルーティング
http://review.askeet.localhost/question/show/id/1
↓
http://review.askeet.localhost/question/what-shall-i-do-tonight-with-my-girlfriend
↑のようにするためにはQuestionテーブルにwhat-shall-i-do-tonight-with-my-girlfriendを保存すればよい。
と、言うことでschema.ymlを書こう。
場所: askeet2/config/schema.yml
ask_question: _attributes: { phpName: Question, idMethod: native } id: { type: integer, required: true, primaryKey: true, autoIncrement: true } user_id: { type: integer, foreignTable: ask_user, foreignReference: id } title: { type: longvarchar } body: { type: longvarchar } interested_users: { type: integer, default: 0 } stripped_title: varchar(255) _uniques: unique_stripped_title: [stripped_title] created_at: ~ updated_at: ~
モデルを書いたら再設定とデータベースを更新。
symfony propel-build-model symfony propel-build-sql symfony propel-insert-sql
タイトルから生成する。
場所: askeet2/lib/myTools.class.php
<?php class myTools { public static function stripText($text) { $text = strtolower($text); //単語以外をはぎ取る $text = preg_replace('/\W/',' ',$text); //空白文字をハイフンに置き換える $text = preg_replace('/\ +/','-',$text); //ハイフンをトリムする $text = preg_replace('/\-$/','',$text); $text = preg_replace('/^\-/','',$text); return $text; } }
Questionクラスを書く
場所: askeet2/lib/model/Question.php
<?php /** * Subclass for representing a row from the 'ask_question' table. * * * * @package lib.model */ class Question extends BaseQuestion { public function setTitle($v) { parent::setTitle($v); $this->setStrippedTitle(myTools::stripText($v)); } }
テストデータのリロード
キャッシュも捨てておきます。
symfony cc php batch/load_data.php
テンプレートの書き換え
リンク部分を書き換える
場所: askeet2/apps/frontend/modules/question/templates/listSuccess.php
<h2><?php echo link_to($question->getTitle(),'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2>
Questionのshowアクションを直す。
場所: askeet2/apps/frontend/modules/actions/actions.class.php
public function executeShow() { $c = new Criteria(); $c->add(QuestionPeer::STRIPPED_TITLE,$this->getRequestParameter('stripped_title')); $this->question = QuestionPeer::doSelectOne($c); $this->forward404Unless($this->question); }
ただ、今の状態だと
http://review.askeet.localhost/question/show/stripped_title/what-shall-i-do-tonight-with-my-girlfriend
となる。
/show/stripped_title/は要らない。