一個controller是你建立的一個PHP函式,它接收HTTP請求(request)並建立和返回一個HTTP回覆(Response)。回覆物件(Response)可以是一個HTML頁面,一個XML文件,一個序列化的JSON陣列,一個圖片,一個重定向,一個404錯誤或者任何你想要的內容。controller中可以包含任何渲染你頁面內容的所需要的邏輯。
下面是一個controller最簡單的例子,僅僅列印一個Hello world!
use Symfony\Component\HttpFoundation\Response; public function helloAction() { return new Response('Hello world!'); }
Controller的終極目標都是相同的那就是建立並返回一個Response物件。按照這個思路,你可以從request物件讀取資訊,載入資料庫資源,傳送email,或者在使用者的Session中寫入資訊。但是所有情況下,Controller將最終都會返回一個Response物件並被分發會客戶端。
比如如下情況:
Controller A 準備一個Response物件來表現網站homepage內容。
Controller B 從Request中讀取slug引數從資料庫中載入一個blog內容並建立一個Response物件來顯示這個blog。如果slug在資料庫中不存在,它將建立並返回一個帶有404狀態碼的Response物件.
Controller C 處理一個從聯絡表單,它從Request物件中讀取表單資訊,儲存聯絡資訊到資料庫併發郵件給管理員。最後,它建立一個Response物件重定向客戶端瀏覽器到聯絡表單感謝頁面。
Requests,Controller, Response的生命週期
Symfony2專案中處理的每一個Request都是經過了相同的簡單生命週期。框架負責重複的任務,最終執行一個controller,該controller會包含你的應用程式程式碼:
1.每個Request都會被一個統一的前端控制器檔案(比如,app.php,或者app_dev.php)處理,它會啟動應用程式。
2.Router從Request中讀取URI資訊,並找到匹配它的Route,從該Route中讀取_controller引數。
3.匹配成功的route的controller被執行,controller中的程式碼建立並返回一個Response物件。
4.HTTP頭和生成的Response物件內容將會被髮回客戶端。
建立一個頁面跟建立一個controller一樣容易,建立一個路由來對映一個URL到該controller。
注意:儘管從名字上來看,前端控制器和controller差不多,其實它們是不同的。
一個前端控制器是一個存放於web目錄下的PHP檔案,多有的Request都會通過它被重定向。每一個應用程式都會有一個產品前端控制器app.php和一個開發用的前端控制器app_dev.php。你不需要編輯,檢視或者擔心它們。
看一個簡單的Controller: 任何的PHP可呼叫內容(比如函式,物件方法或者一個Closure)都可以成為一個controller。Symfongy2中,一個controller通常為controller物件中一個單一的方法。Controllers通常也被稱為actions。
// src/Acme/HelloBundle/Controller/HelloController.php namespace Acme\HelloBundle\Controller; use Symfony\Component\HttpFoundation\Response; class HelloController { public function indexAction($name) { return new Response('<html><body>Hello '.$name.'!</body></html>'); } }
注意在這個例子中controller是indexAction方法,它存在於controller類(HelloController)中。不要混淆,之所以定義一個controller類(HelloController)只是為了方便組織多個controllers/actions在一起。一般情況下,一個controller類會有多個controllers/actions。
上面例子中的controller相當簡單:
Namespace行是symfony2使用了PHP5.3的名稱空間功能來為整個controller類指定名稱空間。
use關鍵字匯入了Response類,這是我們的controller必須返回的內容。
Controller類名字都是由其名字後面加Controller來定義,但是隻有前面的部分才是其真正名字,為了統一起見,在後面統一新增Controller。 在路由配置時只會取前面部分。
Controller類中每個被用於真正controller的方法都會被新增一個統一的字尾Action,同樣我們在配置其路由時也只會取前面部分而忽略掉Action。把它對映到某個URL。
每個controller方法的最後必然會建立一個Response物件並返回它。
對映一個URL到一個Controller方法:
上面例子中的controller方法返回一個簡單的HTML頁面。如果要在瀏覽器中訪問到該頁面,那麼你需要為它建立一個route,把它對映到一個特定模式的URL上。
# app/config/routing.yml hello: pattern: /hello/{name} defaults: { _controller: AcmeHelloBundle:Hello:index }
XML格式:
<!-- app/config/routing.xml --> <route id="hello" pattern="/hello/{name}"> <default key="_controller">AcmeHelloBundle:Hello:index</default> </route>
PHP程式碼格式:
// app/config/routing.php $collection->add('hello', new Route('/hello/{name}', array( '_controller' => 'AcmeHelloBundle:Hello:index', )));
現在想URL /hello/ryan 將被對映到HelloController::indexAction() controller並將ryan傳遞給$name變數。
建立一個所謂的頁面,其實就是建立一個controller方法和一個相關的route。
注意我們使用的指向controller方法的表示語法:AcmeHelloBundle:Hello:index
Symfony2使用了一個非常靈活的字串宣告來指向不同的controller。它告訴Symfony2在一個名叫AcmeHelloBundle的bundle中去查詢一個叫HelloController的類,並執行它的indexAction()方法。在這個例子中,我們的路由配置直接寫在了app/config/ 目錄下,一個更好的組織方式是把你的路由放到各自的bundle中。
路由引數作為Controller方法參變數
你已經了_controller引數 AcmeHelloBundle:Hello:index指向一個位於AcmeHelloBundle中名叫HelloController::indexAction()的方法。有趣的是路由中引數都會被傳遞給該方法。
<?php // src/Acme/HelloBundle/Controller/HelloController.php namespace Acme\HelloBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class HelloController extends Controller { public function indexAction($name) { // ... } }
上例中controller方法有一個唯一引數,$name, 它對應著route中定義的{name}佔位符名稱。事實上,等你執行你的controller時,Symfony2會匹配controller和route中每一個引數。
如果我修改一下Hello的路由定義:
YAML格式:
# app/config/routing.yml hello: pattern: /hello/{first_name}/{last_name} defaults: { _controller: AcmeHelloBundle:Hello:index, color: green }
XML格式:
<!-- app/config/routing.xml --> <route id="hello" pattern="/hello/{first_name}/{last_name}"> <default key="_controller">AcmeHelloBundle:Hello:index</default> <default key="color">green</default> </route>
PHP程式碼格式:
// app/config/routing.php $collection->add('hello', new Route('/hello/{first_name}/{last_name}', array( '_controller' => 'AcmeHelloBundle:Hello:index', 'color' => 'green', )));
這時候controller中可以獲取這些參變數了:
public function indexAction($first_name, $last_name, $color) { // ... }
注意route定義中無論是佔位符變數還是預設值變數都會被轉化為controller方法的輸入變數。當一個route匹配成功時,它會合並佔位符和defaults到一個陣列傳遞給controller。對映route引數到controller引數非常簡單和靈活。它們從route到controller不匹配順序。Symfony能夠把route中參變數的名字對映到controller方法簽名中的變數名字。比如{last_name} => $last_name,跟排列順序無關。
Controller方法中的引數必須匹配route中定義的引數下面為hello route定義的controller方法將會丟擲異常:
public function indexAction($last_name, $color, $first_name) { // .. }
如果我們把$foo變數變為可選變數,那麼就不會拋異常了。
public function indexAction($first_name, $last_name, $color, $foo) { // .. }
並不是每一個在route中定義的引數都需要在controller中有與之對應的簽名參變數的,比如hello route中定義的{$last_name} 如果對你沒什麼意義的話可以在controller中省略掉它。
public function indexAction($first_name, $color) { // .. }
反之,如果你在Controller簽名中定義了變數,並且不是可選變數,那麼必須在route中有與之對應的引數被定義。
在route定義中有一個特殊引數 _route, 它匹配route的名稱(如上例中的hello)。雖然不常用,但是它也可以作為controller方法的一個參變數使用。
Request作為一個Controller方法簽名變數
為了方便,你可能會讓symfony傳遞你的Request物件作為引數到你的controller方法。這在你處理表單時尤為方便。
use Symfony\Component\HttpFoundation\Request; public function updateAction(Request $request) { $form = $this->createForm(...); $form->bindRequest($request); // ... }
Controller基類
為了方便,Symfony2定義了一個Controller基類,包含了一些常用的controller任務並給了你的controller類訪問任何你需要的資源的途徑。通過繼承該類,你可以獲得許多幫助方法。
// src/Acme/HelloBundle/Controller/HelloController.php namespace Acme\HelloBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; class HelloController extends Controller { public function indexAction($name) { return new Response('<html><body>Hello '.$name.'!</body></html>'); } }
在Symfony中controller並不一定非得繼承Controller基類,因為它內部的幫助方法等都不是必須的。你也可以繼承 Symfony\Component\DependencyInjection\ContainerAware 服務容器物件可以通過container屬性來訪問。同時你也可以把controller定義成service。
通用的Controller任務:
儘管Controller可以幹任何事情,但是大部分的controller還是要重複的幹一些基礎的任務。比如 重定向,跳轉,渲染模板和訪問核心服務等。
重定向(redirecting)
如果你想重定向你的使用者到另一個頁面,可以使用redirect()方法。
public function indexAction() { return $this->redirect($this->generateUrl('homepage')); }
這裡generateUrl()方法是一個幫助函式,用於根據給定的route生成相應的URL。預設情況下,redirect()方法執行一個302重定向。如果要執行301重定向,那麼需要修改第二個引數如下:
public function indexAction() { return $this->redirect($this->generateUrl('homepage'), 301); }
redirect()方法其實是一個簡化寫法,真正的程式碼如下:
use Symfony\Component\HttpFoundation\RedirectResponse; return new RedirectResponse($this->generateUrl('homepage'));
跳轉(Forwarding)
你可以使用forward()方法很容易從一個controller到另一個controller內部。它執行的是一個內部子請求,來呼叫指定的controller,所以不會產生使用者客戶端瀏覽器的重定向。forward()方法返回的Response物件還將從原controller返回。
public function indexAction($name) { $response = $this->forward('AcmeHelloBundle:Hello:fancy', array( 'name' => $name, 'color' => 'green' )); // further modify the response or return it directly return $response; }
這裡forward()方法使用了跟route配置中相同的字串引數。這裡傳入陣列引數會作為目標呼叫controller的引數。當將controller嵌入到模板時,也會使用同樣的介面。目標呼叫的controller方法應該是如下定義:
public function fancyAction($name, $color) { // ... create and return a Response object }
就像為一個route建立一個controller一樣,跟引數的順序沒關係。symfony2 會匹配索引鍵名稱name到方法引數名稱$name,即使順序打亂也沒關係。跟其它Controller基類方法一樣,forward方法也僅僅是一個symfony2核心函式的快捷寫法。一個跳轉可以直接通過http_kernel服務來完成,返回一個Response物件。
$httpKernel = $this->container->get('http_kernel'); $response = $httpKernel->forward('AcmeHelloBundle:Hello:fancy', array( 'name' => $name, 'color' => 'green', ));
渲染模板:
雖然不是必須的,但是大部分controller將最終渲染一個負責生成為controller負責生成HTML的模板。renderView()方法會渲染一個模板並返回它的內容。這個返回內容可以用作建立Response物件,以供controller返回使用。
$content = $this->renderView('AcmeHelloBundle:Hello:index.html.twig', array('name' => $name)); return new Response($content);
上面的程式碼完全可以更進一步的使用下面的程式碼形式來寫:
return $this->render('AcmeHelloBundle:Hello:index.html.twig', array('name' => $name));
這兩種情況下,AcmeHelloBundle中的模板Resources/views/Hello/index.html.twig都會被渲染。
renderview()方法是如下程式碼的快捷寫法:
$templating = $this->get('templating'); $content = $templating->render('AcmeHelloBundle:Hello:index.html.twig', array('name' => $name));
當然也可以在子目錄中渲染模板
$templating->render('AcmeHelloBundle:Hello/Greetings:index.html.twig', array('name' => $name)); // index.html.twig 存放於 Resources/views/Hello/Greetings 目錄.
訪問其它服務
只要是繼承了Controller基類,你就可以通過get()方法訪問symfony2的服務了。比如:
$request = $this->getRequest(); $templating = $this->get('templating'); $router = $this->get('router'); $mailer = $this->get('mailer');
Symfony2中還有無數的可用服務,同時也鼓勵你定義自己的服務。要檢視所有的服務,可以使用container:debug 命令列工具
$ php app/console container:debug
管理錯誤和404頁面
當一些東西沒有找到,你應該重置HTTP協議返回一個404 回覆。要做到這個,你將丟擲一個特殊型別的異常。如果你是繼承了Controller基類,則:
public function indexAction() { $product = // retrieve the object from database if (!$product) { throw $this->createNotFoundException('The product does not exist'); } return $this->render(...); }
createNotFoundException()方法建立一個特定的NotFoundHttpException物件,它最終觸發404 HTTP回覆。當然你從你的controller方法中可以丟擲任何型別的Exception 類,Symfony2會自動返回一個500 HTTP回覆程式碼。
throw new \Exception('Something went wrong!');
管理Session
Symfony2 提供了一個非常好的Session物件,你可以用它來在請求之間存貯有關使用者的資訊。預設情況下,Symfony2 通過PHP本身的Session儲存屬性到cookie。在任何controller中儲存和獲取Session資訊將非常容易:
$session = $this->getRequest()->getSession(); // 為使用者的後一個請求使用儲存一個屬性 $session->set('foo', 'bar'); // 在另一個controller中為另一個請求獲取該屬性 $foo = $session->get('foo'); // 設定使用者的本地化語言 $session->setLocale('fr');
Flash 訊息
你可以為特定的請求儲存少量的訊息到使用者的Session。這在處理一個表單時非常有用,你想重定向和一個特定的資訊顯示在下一個請求中。這種型別的訊息被稱為Flash訊息。比如,假設你處理一個表單提交:
public function updateAction() { $form = $this->createForm(...); $form->bindRequest($this->getRequest()); if ($form->isValid()) { // 做些排序處理 $this->get('session')->setFlash('notice', 'Your changes were saved!'); return $this->redirect($this->generateUrl(...)); } return $this->render(...); }
此例中,在處理完請求後,controller設定了一個notice flash訊息並作了重定向。名字notice沒什麼意義,只是用於標識該訊息。在下一個活動的模板中,下面的程式碼能夠渲染這個notic訊息:
Twig
{% if app.session.hasFlash('notice') %} <div class="flash-notice"> {{ app.session.flash('notice') }} </div> {% endif %}
PHP程式碼:
<?php if ($view['session']->hasFlash('notice')): ?> <div class="flash-notice"> <?php echo $view['session']->getFlash('notice') ?> </div> <?php endif; ?>
這樣設計,flash訊息就能夠為準確的某個請求存在了。他們一般被設計出來就是用於重定向的。
Response物件
作為一個Controller來說,唯一必須做到的是返回一個Response物件。
Response物件是一個PHP程式碼對HTTP Response的抽象。
HTTP Response是一個基於文字的訊息有HTTP headers和 返回給客戶端的內容組成。
//建立一個簡單的Response物件,預設狀態碼為200 $response = new Response('Hello ' .$name, 200); //建立一個基於JSON的Response物件,狀態碼也為200 $response = new Response(json_encode(array('name'=>$name))); $response->headers->set('content-type','application/json');
其中headers屬性是一個HeaderBag物件,內部包含許多有用的方法來讀取和改變Response的頭資訊。頭名字被標準化使用Content-Type 與content-type或者content_type效果等同。
請求物件Request
除了路由佔位符的值以外,如果繼承了Controller基類那麼該controller還可以訪問Request物件。
$request = $this->getRequest(); $request->isXmlHttpRequest(); // 判斷是不是Ajax請求 $request->getPreferredLanguage(array('en','fr')); $request->query->get('page'); // 獲取$_GET 引數 $request->request->get('page'); //獲取$_POST引數
跟Response物件一樣,Request物件的頭也儲存在HeaderBag物件中,可以很方便的被訪問。
總結思考:
無論何時,你建立一個頁面,你最終需要為它寫一些包含邏輯的程式碼。在Symfony中,這叫一個controller, 它是一個PHP的函式,它可以為了最後返回一個Response物件給使用者可以做需要的任何事情。簡單的說,你可以選擇繼承一個Controller基類,它包含了許多執行controller通用任務的快捷方法。比如,你不想把HTML程式碼寫入你的controller, 你可以使用render()方法來渲染並返回一個模板內容。