====== 深入MVC架構 ====== **複習:** 經過[[askeet_2|昨天]]的學習,你已學會如何從一個關係的資料模型產生物件模型,然後產生簡單的物件操作,BTW,昨天產生的源碼可以在 askeet SVN 源碼倉庫得到: http://svn.askeet.com/ =====今天的目標 ===== 是設計一個比較好看一點的外觀,將問題的陳列設為首頁,秀對這個問題有興趣的人,多加入一些測試資料到資料庫,沒有要做很多事,多的是要讀通及理解。讀這個教程,你將會熟悉symfony專案及程式,模組和動作的概念,這也在symfony book的[[http://www.symfony-project.com/content/book/page/controller.html|controller]]章節。 =====The MVC model===== 今天將會深入探討[[http://en.wikipedia.org/wiki/Model-view-controller|MVC 架構]],這是什麼意思?這個源碼是藉由多個不同的檔來產生,有資料維護的代碼一會和網頁是切開的,它應該在模型裏(大多時候是在 askeet/lib/model/)有關於展示的碼,應該在展示層裏,在symfony,展示層會依賴樣板檔。(例如askeet/apps/frontend/modules/question/templates/)和設定檔。最後是把兩者結合起來,把網站的邏輯用 PHP 寫出來是控制層。在 symfony ,會了某一特定網頁的控制層是叫行動 action (行動在askeet/apps/frontend/modules/question/actions/)。 你可以在 symfony book的[[http://www.symfony-project.com/content/book/page/mvc.html|MVC实施]]章節知道更多關於模型在 MVC 的實作。 今天,我們程式的外觀會有一點點的改變,我們將手動調整一大堆不同的檔案,不要覺得痛, 因為分層的檔案和源碼在不同層的分離不久將會變得清楚分明且好用。 ===== 改變外觀 ===== 在 [[http://en.wikipedia.org/wiki/Decorator_pattern|decorator]]這個設計模式的程式中,被動作 action 呼叫的樣板 template 會整合進一個更多的全域樣板 global template ,或外觀 layout 。換句話說,這外觀也包含了界面裏所有的不變的部分,它裝飾了行動的結果。打開預設的外觀 ,在askeet/apps/frontend/templates/layout.php裏,然後按造底下的方式修改
getRaw('sf_content') ?>
#note[**注意:**我們試著保持這些標籤語法是有意義的,把所有的風格相關的語法移到 CSS stylesheets 。這些 stylesheets 不在這裏細說了,因為 CSS 的文法,也不是本教程的目的,它會可以在 SVN 源碼倉庫被下載,我們建立了兩個stylesheets (**main.css and layout.css**).,把它們複製到你的askeet/web/css/目錄下,編輯你的 **frontend/config/view.yml** 來改變自動載入的 stylesheets : stylesheets: [main, layout] ]# 此時外觀仍是輕量的, 它不久後將會重建(約一週後)。在樣板檔裏特別要緊的事是 的部分,最常被產生的, 這個sf_content 變數,是包含行動後的結果,確認一下在外觀修改後,還是可以正常出網頁,你可以在開發環境下 http://askeet/frontend_dev.php/ #sym[{{symfony:askeet:3:congratulations_new.gif|}}]# =====關於環境的一些說明 ===== 假如你對http://askeet/frontend_dev.php/和http://askeet/ 這兩者之間到底差在那裏,覺得好奇,你可以看看 symfony book 的[[http://www.symfony-project.com/content/book/page/configuration.html|configuration]]章節。 目前,你只需要知道他們是指向相同的程式,但在不同的環境下。一個環境是一個唯一的設定,在那裏,框架可以依需求被動作,或不動作 在這個 案例裏,**/frontend_dev.php/**指向開發環境,整個設定會被每一個需求解析, HTML 的快取會被停止,除錯的工具都可以使用(在右上角有一個半透明的工具列)。 **/**就是**/index.php/**,指向產品環境,在這裏設定已經被編譯,為了加速網頁的傳遞,除錯的工具不能使用。 **frontend_dev.php** and **index.php** 這兩個 PHP 指令檔,叫做前端控制器。對程式的所有需求都被他們處理。放在askeet/web/ 。事實上, index.php 應叫做 frontend_prod.php ,但是 frontend 也同時是你的第一個程式, symfony 怕你可能想將它做為預設的程式,就將它命名為**index.php**,這樣你可在產品區只要下**/**就可看到你的程式, 假如你想知道更多,一般而言你可看 symfony book 的[[http://www.symfony-project.com/content/book/page/controller.html|controller]]章節。 一個白痴法則是去察看這個開發環境,直到你充分了解你使用的這些功能,然後切換到產品環境去驗証它的速度和簡單的URLs. #note[**注意:**永遠記住要清掉快取,當你加上一些類別或是當你改變一些設定檔时
symfony cc
]# =====重指定默认主页===== 目前的主页只显示一个恭喜的页面(“Congratulations”)。一個好的想法,將是列出問題(在文件裏是用question/list來參考,程式的架構是,問題的模組,展示的動作)要做到這樣,打開前端程式,路由的設定檔 askeet/apps/frontend/config/routing.yml ,找到homepage:區塊,修改它 homepage: url: / param: { module: question, action: list } ===== 更新開發環境的首頁 ===== (http://askeet/frontend_dev.php/),現在可以展示問題集了 #note[**注意:**如果你是一個嚴謹的人,你會在所有的網頁的字裏行間找'Congratulations'的訊息, 然後你會訝異,在askeet目錄下打不到,事實上, default/index 行動的樣板是定義在 symfony 目錄下,而且和這個專案是獨立不相關的。假如你要修改它 ,你可以在你自已的目錄下建一個預設的模組。]# 在不久的未來,路由系統的可能性將被更細的編程出來。如果你有興趣,你可以讀 symfony book 的[[http://www.symfony-project.com/content/book/page/routing.html|routing]]章節 =====定義測試的數據===== 秀在網頁上的問題還相當少,只有你自已加問題上去,當你開發一個程式,一個好的作法是有一些有代表性的測試資料。用手一筆一筆輸入是件苦差事(或是透過資料的直接界面寫入 )這也是 symfony 可以將文字檔轉入數據庫的原因。 我們將建一個測試數據檔在askeet/data/fixtures/ ,建一個 test_data.yml,有底下的內容 User: anonymous: nickname: anonymous first_name: Anonymous last_name: Coward fabien: nickname: fabpot first_name: Fabien last_name: Potencier francois: nickname: francoisz first_name: François last_name: Zaninotto Question: q1: title: What shall I do tonight with my girlfriend? user_id: fabien 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: francois body: | I have a very swell blog that talks about my class and mates and pets and favorite movies. Interest: i1: { user_id: fabien, question_id: q1 } i2: { user_id: francois, question_id: q1 } i3: { user_id: francois, question_id: q2 } i4: { user_id: fabien, question_id: q2 } 首先,你要了解[[http://www.yaml.org/|YAML]]。假如你不熟悉 symfony ,你可能不知道 YAML 格式是這個框架裏最愛的格式。也不是沒有例外-如果你被XML or .ini 檔綁住,加一個設定檔處理器(handler)讓 symfony 可以讀取也是很容易的。如果有閒和耐性,在 symfony book 的[[http://www.symfony-project.com/content/book/page/configuration_practice.html|practice]]章節可以找到更多有關 symfony 設定檔和 YAML 。現在,就算你不熟YAML文法,你也可以快速開始了,這教程會在很多地方用到,用久了,不懂也懂了,從做中學。 好的,回到測試數據檔,它定義了物件的實體(instances),用一個內部的名稱標示(labeled )。這個標示在連結相關的物件是大大有用的,不用去定義 ids (這是自動加1的欄位,且不允許寫入)。例如,第一個用User類別產生的物件,用 fabien 來標示,第一個問題,用 q1標示。讓由類別產生的實體和相關的物件標示(labels)這件事變成簡單 Interest: i1: user_id: fabien question_id: q1 前面的數據例子是用簡短的YAML來說明同一件事。你可在 symfony book 的[[http://www.symfony-project.com/content/book/page/populate.html|data files]]章節裏有更深的了解。 #note[**注意**:created_at and updated_at 这两列无需指定,symfony会自动识别且建立默认值。]# ===== 整批的寫入數據庫 ===== 下一步是實際上要寫入數據庫,我們希望用一個 PHP 指令來做整批(batch)寫入 ====批次的骨架:==== 在askeet/batch/建一個檔load_data.php ,寫入以下的內容 initialize(); ?> 這段指令目前沒做任何事,或幾乎是 nothing :它定義一段 path ,一枝程式,一個環境, 來讀取一段設定,載入設定,初始化數據庫管理員。這樣已經很多了(呼應前面的nothing,大概是說,常作的整批轉入數據的動作就是這樣,把實際的路徑,程式,環境,設定寫好,就轉入了),意思是,之後寫的CODE會利用類別自動載入 (auto-loading) 自動連接 Propel物件,和 symfony 的根類別。 #note[**注意:**假如你已經測過 symfony's 前端控制器(如askeet/web/index.php), 你會發現這些代碼實在太相似了。那是因為每一個 web 的要求(request)都須要存取同樣的物件和設定,就像一個批次指令做的事一樣。]# ====匯入數據==== 現在,一個整批的指令已經準備好,該要讓它做點事了。這批次檔要做 - 讀 YAML - 建Propel objects的實體(instances) - 匯入記錄(records)到數據庫的表(tables) 也許聽起來有點複雜,但在 symfony ,只要兩行code就做完了。感謝sfPropelData物件。只要把底下的CODE, 加入askeet/batch/load_data.php : $data = new sfPropelData(); $data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures'); 要加在?>之前。 就這樣。建立一個 sfPropelData 物件,然後載入指定目錄下的數據,我們的配件(fixtures)目錄,寫入 databases.yml 設定的資料庫。 #note[**注意:**這個 DIRECTORY_SEPARATOR 常數(constant)是相容於Windows and *nix 平(platforms)]# ====啟動批次檔==== 至少,你可以確認,這幾行文字是否有用。在指令列(command line)鍵入
$ cd /home/sfprojects/askeet/batch
$ php load_data.php
在更新開發區的首頁後,你可以查証一下數據庫有沒有更改到。 http://askeet/frontend_dev.php #sym[{{symfony:askeet:3:fixtures.gif|}}]# 數據進去了! #note[**注意**:一般情況下, sfPropelData 會先刪再轉入新數據。你可以從目前數據後新增數據 $data = new sfPropelData(); $data->setDeleteCurrentData(false); $data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures'); ]# ===== 對模型存取數據 ===== 當要求問題模組(module)列出(list)的動作(action),這個網頁會呈現executeList() (askeet/apps/frontend/modules/question/actions/action.class.php)方法(method)處理後傳送到askeet/apps/frontend/modules/question/templates/listSuccess.php的結果,這是根據 symfony book 的[[http://www.symfony-project.com/content/book/page/controller.html|controller]]章節上面解釋的命名原則(naming convention)。讓我們看一下被執行的代碼: actions.class.php: public function executeList () { $this->questions = QuestionPeer::doSelect(new Criteria()); } listSuccess.php: ... getId(), 'question/show?id='.$question->getId()) ?> getTitle() ?> getBody() ?> getCreatedAt() ?> getUpdatedAt() ?> 一步一步的講解,到底做了什麼: - 這個動作要求 Question 表傳回符合空的WHERE條件(empty criteria)的記錄(records)-就是傳回所有記錄 - 把記錄集(list)放進一個陣列($questions),然後傳入一個樣板 - 這樣板一筆接一筆的把記錄解析出來 - 樣板秀出每一筆記錄的每一個欄位(columns) propel-build-model指令([[askeet_2|昨天]]有用過)會用The ->getId(), ->getTitle(), ->getBody(),etc. 這些方法,把 id,title, body, etc. 欄位(fields) 的值(value)讀取(retrieve)出來。這些是標準的 getters ,在每一個camelCased 欄位名稱前面(prefix)加上 get - Propel 也提供標準的 setters ,前面(prefix)加上 set. Propel文件上也描述了對每一個類別加上 accessors 。然後是神密的QuestionPeer::doSelect(new Criteria()) ,這也是標準的Propel要求(request)。 [[http://propel.phpdb.org/docs/user_guide/|Propel 文献]]會解釋的更仔細 **別焦慮**,假如不懂上面在說些什麼?幾天後,你就懂了。 =====修改 question/list 樣板(template)===== 現在,資料庫裏也有對問題(questions)感興趣的人(interests),要抓出對每一個問題有興趣的人的數目是簡單的。假如你看過aseQuestion.php-由 Propel (askeet/lib/model/om/)產生的類別。你將會注意->getInterests()的方法。 Propel看到question_id是 Interest 表的外鍵(foreign key),然後算出一個問題有幾個感興趣的人。要讓人數秀出很簡單,只要改一下 listSuccess.php 樣板(在askeet/apps/frontend/modules/question/templates/)。我們將刪掉醜醜的 tables (這裏是指HTML的表格,不是數據庫的表)然後用9層 divs 取代:

popular questions

getInterests()) ?>

getTitle(), 'question/show?id='.$question->getId()) ?>

getBody(), 200) ?>
在這裏, 和listSuccess.php一樣的 foreach 迴圈。link_to()和truncate_text() 函數是 symfony提供的樣板 helpers 。第一個是新增一個超連結(hyperlink)給同一個模組的另一個行動。第二個是把問題的本文切掉到200個字。 link_to() helper 是自動載入的,但是你必須宣告 Text group of helpers 才能使用truncate_text(). 加油,更新你的開發網頁來試試你的新樣板, http://askeet/frontend_dev.php/ #sym[{{symfony:askeet:3:question_list_day3.gif|}}]# 感興趣的讀者的數目正確的顯示在靠近在每一個問題的位置。要得到上面截取圖(capture),請下載 main.css 並放入askeet/web/css/。 =====清除===== propel-generate-crud會新增後來沒用到的一些動作(actions)和樣本(actions)。該把它們清掉了。 這裏askeet/apps/frontend/modules/question/actions/actions.class.php: 可清除 * executeIndex * executeEdit * executeUpdate * executeCreate * executeDelete 這裏askeet/apps/frontend/modules/question/templates/: 可清除 * editSuccess.php =====明天見===== 今天是往 Model-View-Controller 理論的世界前進一大步。經由手工修改外觀,樣板,和動作,還有 Propel object 模型的物件,你已經玩遍 MVC 的每一層。假如你對這些層之間的橋不是很了解也不用焦慮。未來將會愈來愈清楚。今天看了很多個檔,如果你想要了解在專案裏是如何組織這些檔案(分門別類)。請參考 symfony book 的 file structure 章節。明天更重要,我們將修改展示層(views),建立一個更複雜的選徑(routing)策略,修改模型層(model),深入探討數據維護(manipulation)和數據表的連結。早點睡吧(sleep tight),請自由取用今天教程的源碼 http://svn.askeet.com/tags/release_day_3