控制器


控制器是一個你創建的php函數,它能夠獲取http請求資訊並建立和返回一個http回應(作為Symfony的Response物件),Response可能是一個html頁面、xml文檔、一個序列化的json數組、圖像、重定向、404錯誤或一些其他你能夠想像的。控制器包含了你應用程式需要渲染頁面的任何邏輯。

看看symfony簡單的控制器。下面控制器將輸出 hello word

use Symfony\Component\HttpFoundation\Response; public function helloAction(){
    return new Response('Hello world!');}

控制器的目標都是相同的:建立並傳回一個Response物件。在這個過程中,它可能會從請求中讀取訊息,載入資料庫資源,發送郵件,在使用者session中設定訊息。但是所有情況下,控制器將最終傳回 Response 物件給客戶端。

沒有什麼神奇的不用擔心還有別的要求!以下是一些常見的範例:

  • 控制器A準備了一個首頁上的Response物件。 -

  • 控制器B從請求中讀取{slug}參數,從資料庫載入一則博文,並建立一個顯示該博文的 Response物件。如果{slug}不能被資料庫中檢索到,那麼控制器將建立並傳回一個帶有404狀態碼的Response物件。

  • 控制器C處理關於聯絡人的表單提交。它從請求中讀取表單訊息,將聯絡人資訊存入資料庫並發送包含聯絡人資訊的電子郵件給網站管理員。最後,它會建立一個Response物件將使用者的瀏覽器重新導向到聯絡人表單的「感謝」頁面。

請求、控制器、回應的生命週期 

#symfony處理的每一個請求都會有相同的生命週期。框架會負責把很多重複的任務用一個控制器最終執行,控制器執行你自訂的應用程式碼:

  1. 每個請求都被單一前端控制器(如app.php生產環境或app_dev.php開發環境)檔案處理,前端控制器負責引導框架;

  2. 前端控制器的唯一工作是去初始化Symfony引擎(呼叫Kernel)並傳入一個Request物件來讓它處理。

  3. Symfony核心要求路由器去檢查這個請求;

  4. #路由查看並匹配請求訊息,並將其指向一個特定的路由,該路由決定呼叫哪個控制器;

  5. 執行控制器,控制器中的程式碼將建立並傳回一個Response物件;

  6. HTTP頭和Response物件的內容將發回客戶端。

建立控制器與建立頁面一樣方便,同時映射一個URI到該控制器。

1466410869_65826_51226_http-xkcd-request.png

雖然名稱相似,但前端控制器與我們在本章節所說的控制器是不同的,前端控制器是你web/目錄中的一個PHP小文件,所有的請求都直接經過它。一個典型的應用程式將有一個用於生產的前端控制器(如app.php)和一個用於開發的前端控制器(如app_dev.php)。你可以永遠不需要去對前端控制器編輯、查看或有所擔心。本章的「控制器類別」用一種方便的方法組織各自的“controllers”,也被稱為actions,它們都在一個類別裡(如,updateAction(), deleteAction( ), 等)。所以,在控制器類別裡一個控制器就是一個方法。它們會持有你所建立的程式碼,並回傳Response回應物件。

一個簡單的控制器 

雖然一個控制器可以是任何的可被呼叫的PHP(函數、物件的方法或Closure),在Symfony,控制器通常是在控制器類別中的一個方法,控制器也常被稱為action:

// src/AppBundle/Controller/HelloController.phpnamespace AppBundle\Controller; use Symfony\Component\HttpFoundation\Response; class HelloController{
    public function indexAction($name)
    {
        return new Response('<html><body>Hello '.$name.'!</body></html>');
    }}

這裡面的控制器是 indexAction方法,它隸屬於一個控制器類別HelloController

這個控制器非常的簡單:

  • 第2行:Symfony利用php命名空間函數去命名整個控制器類別

  • 第4行:Symfony充分利用了PHP5.3的名稱空間的功能:use關鍵字導入Response類,是我們控制器必須回傳的;

  • 第6行:類別名稱是一個串連的控制器類別名稱(例如hello)加上Controller關鍵字。這是一個約定,為控制器提供一致性,並允許它們引用控制器名稱(例如hello)作為路由配置。

  • 第8行:在控制器類別中的每個action都有著後綴Action,並且這個action名稱(index)被引用到路由設定檔中。在下一節中,我們將使用路由映射一個URI到該action,並展示如何將路由佔位符({name})變成action的參數($name#) ;

  • 第10行:控制器建立並傳回一個Response物件。

將URI對應到控制器 

#我們的新控制器傳回一個簡單的HTML頁。為了能夠在指定的URI中渲染該控制器,我們需要為它建立一個路由。 我們將在路由章節中討論路由元件的細節,但現在我們為我們的控制器建立一個簡單路由:

Annotations:// src/AppBundle/Controller/HelloController.phpnamespace AppBundle\Controller; use Symfony\Component\HttpFoundation\Response;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; class HelloController{
    /**
     * @Route("/hello/{name}", name="hello")
     */
    public function indexAction($name)
    {
        return new Response('<html><body>Hello '.$name.'!</body></html>');
    }}
YAML:# app/config/routing.ymlhello:
    path:      /hello/{name}
    # uses a special syntax to point to the controller - see note below
    defaults:  { _controller: AppBundle:Hello:index }.
XML:<!-- app/config/routing.xml --><?xml version="1.0" encoding="UTF-8" ?><routes xmlns="http://symfony.com/schema/routing"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://symfony.com/schema/routing        http://symfony.com/schema/routing/routing-1.0.xsd">     <route id="hello" path="/hello/{name}">
        <!-- uses a special syntax to point to the controller - see note below -->
        <default key="_controller">AppBundle:Hello:index</default>
    </route></routes>
PHP:// app/config/routing.phpuse Symfony\Component\Routing\Route;use Symfony\Component\Routing\RouteCollection; $collection = new RouteCollection();$collection->add('hello', new Route('/hello/{name}', array(
    // uses a special syntax to point to the controller - see note below
    '_controller' => 'AppBundle:Hello:index',))); return $collection;

現在,你來到/hello/ryan(例如,如果你使用內建的web服務http://localhost:8000/hello/ryan),那麼它就會執行HelloController::indexAction()控制器,並且將ryan賦給$name變數。建立這樣一個頁面就能夠讓路由跟控制器做簡單的關聯。

簡單吧?

AppBundle:Hello:index控制器語法

#如果你是使用YAML或XML格式,你給你的控制器使用的一個特定快速語法稱為邏輯控制器名稱,例如AppBundle:Hello:index。更多關於控制器格式的信息,請閱讀路由器章節的: 控制器命名模式。

把路由參數傳入控制器 ¶

我們現在已經知道路由指向AppBundle中的HelloController::indexAction()方法。還有更有趣的就是控制器方法的參數傳遞:

// src/AppBundle/Controller/HelloController.php// ...use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; /**
 * @Route("/hello/{name}", name="hello")
 */public function indexAction($name){
    // ...}

控制器有個參數$name#,對應所符合路由的{name}參數(如果你訪問/hello/ryan, 在本例中是ryan)。實際上當執行你的控制器時,Symfony在所匹配路由中匹配帶參數控制器中的每個參數。所以這個{name}值被傳入到$name。只需要確保佔位符的名稱和參數名稱一樣就行。

以下是更有趣的例子,這裡的控制器有兩個參數:

Annotations:// src/AppBundle/Controller/HelloController.php// ... use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; class HelloController{
    /**
     * @Route("/hello/{firstName}/{lastName}", name="hello")
     */
    public function indexAction($firstName, $lastName)
    {
        // ...
    }}
YAML:# app/config/routing.ymlhello:
    path:      /hello/{firstName}/{lastName}
    defaults:  { _controller: AppBundle:Hello:index }
XML:<!-- app/config/routing.xml --><?xml version="1.0" encoding="UTF-8" ?><routes xmlns="http://symfony.com/schema/routing"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://symfony.com/schema/routing        http://symfony.com/schema/routing/routing-1.0.xsd">     <route id="hello" path="/hello/{firstName}/{lastName}">
        <default key="_controller">AppBundle:Hello:index</default>
    </route></routes>
PHP:// app/config/routing.phpuse Symfony\Component\Routing\Route;use Symfony\Component\Routing\RouteCollection; $collection = new RouteCollection();$collection->add('hello', new Route('/hello/{firstName}/{lastName}', array(
    '_controller' => 'AppBundle:Hello:index',))); return $collection;

將路由參數對應到控制器參數是十分容易且靈活的。在你開發時請遵循以下思路:

1. 控制器參數的順序無關緊要

Symfony可以根據路由參數名稱來匹配控制器方法參數。換句話說,它可以實現last_name參數與$last_name參數的匹配。控制器可以在隨意排列參數的情況下正常工作。

public function indexAction($lastName, $firstName){
    // ...}

2.控制器所需參數必須匹配路由參數

#下面會拋出一個運行時異常(RuntimeException) ,因為在路由定義中沒有foo參數:

public function indexAction($firstName, $lastName, $foo){
    // ...}

如果參數是可選的,那該有多好。下面的範例不會拋出例外:

public function indexAction($firstName, $lastName, $foo = 'bar'){
    // ...}

3.不是所有的路由參數都需要在控制器上有回應參數的

如果,舉個例子,last_name對你控制器不是很重要的話,你可以完全忽略掉它:

public function indexAction($firstName){
    // ...}


##你也可以從你的路由器傳入其他的參數到你的控制器參數。請看 如何從路由向控制器傳遞額外的資訊


Controller基底類別 ¶

出於方便的考慮,Symfony提供了一個可選的Controller基底類別。如果你繼承它,它不會改變你控制器的任何工作原理,而且你還能夠很容易的繼承一些幫助方法和服務容器(可看,下面的訪問其他容器):允許你在系統中訪問每一個有用的對象,類似一個數組對像一樣。這些有用的對像被稱為服務,並且symfony附帶這些服務對象,可以渲染模板,還可以記錄日誌資訊等。

在頂部使用use語句新增Controller類,然後修改HelloController去繼承它。如下所示:

// src/AppBundle/Controller/HelloController.phpnamespace AppBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class HelloController extends Controller{
    // ...}

而無論你是否使用Controller基類,這些幫助方法只是讓你可以方便地使用Symfony的核心功能。其實查看核心功能的最好方式就是看Controller類別本身。

如果你想了解沒有繼承controller基底類別控制器是如何運作的,可以查看 如何把控制器定義為服務。這是可選的,但它可以讓你精確控制的更多的“物件/依賴項",注入到你的控制器。

產生URL ¶

generateUrl()能夠產生一個URL給路由器的輔助方法。

重定向 

如果你想將使用者重新導向到另一個頁面,請使用redirectToRoute() 方法:

public function indexAction(){
    return $this->redirectToRoute('homepage');     // redirectToRoute is equivalent to using redirect() and generateUrl() together:
    // return $this->redirect($this->generateUrl('homepage'));}

預設情況下,redirectToRoute()方法執行302(暫時)重定向。如果要執行301(永久)重定向,請修改第2個參數:

public function indexAction(){
    return $this->redirectToRoute('homepage', array(), 301);}

從定向到外部網站,使用redirect()並傳入外部URL:

public function indexAction(){
    return $this->redirect('http://symfony.com/doc');}

更多細節,請參考框架起步之路由

比建立一個專門從事重定向使用者的Response物件來說redirectToRoute()方法是個簡單的捷徑,它相當於:

use Symfony\Component\HttpFoundation\RedirectResponse; 
public function indexAction()
{
    return new RedirectResponse($this->generateUrl('homepage'));
    }
#

渲染模板 

如果你使用HTML,你應該想要去渲染一個模板。 render()方法可以用來渲染模板並且可以把輸出的內容放到你的Response 物件:

// renders app/Resources/views/hello/index.html.twig
return $this->render('hello/index.html.twig', array('name' => $name));

模板也可以防止在更深層的子目錄。但應該避免創建不必要的深層結構:

// renders app/Resources/views/hello/greetings/index.html.twigreturn $this->render('hello/greetings/index.html.twig', array(
    'name' => $name));

模板可以在任何格式的檔案中以一種通用的方式去渲染內容。雖然在大多數情況下,你使用模板來渲染HTML內容,模板也可以輕鬆地產生JavaScript,CSS,XML或你能想到的任何其他格式。若要了解如何使不同的範本格式,請參考建立並使用範本中的「範本格式」。

模板的命名模式

你也可以把模板放在一個bundle的Resources/views目錄下並引用它們的特殊快捷語法,例如@App/Hello/index.html.twig@App/layout.html.twig。這些將分別存放在bundle的Resources/views/Hello/index.html.twigResources/views/layout.html.twig

訪問其他服務 

Symfony塞了很多有用對象,成為服務。這些服務用於呈現模板,發送電子郵件,查詢資料庫,以及你能夠想到的任何其他的」工作「。當你安裝一個新的bundle,它也可能會帶來更多的服務。

當繼承controller基底類別後,你可以透過get()方法來存取任何Symfony的服務。以下列舉了一些常見服務:

$templating = $this->get('templating'); 
$router = $this->get('router'); 
$mailer = $this->get('mailer');

到底存在哪些服務?我想要看所有的服務,請使用debug:container命令列查看:

$ php bin/console debug:container

更多資訊請看服務容器

管理錯誤和404頁面 ¶

如果有些動作沒找到,將會回傳一個404回應。為此,你需要拋出一個異常。如果你繼承了基礎的Controller類,你可以執行以下操作:

public function indexAction(){
    // retrieve the object from database
    $product = ...;
    if (!$product) {
        throw $this->createNotFoundException('The product does not exist');
    }     return $this->render(...);}

createNotFoundException() 方法建立了一個特殊的NotFoundHttpException對象,來觸發symfony內部的http的404響應。

當然,你也可以自由地拋出你控制器中的任何Exception類,Symfony將自動回傳HTTP回應碼500。

throw new \Exception('Something went wrong!');

在每個範例中,一個帶有格式的錯誤頁被顯示給最終用戶,而一個全是錯誤的調試頁會被顯示給開發者(當在調試模式app_dev.php 查看該頁時- 可查看配置Symfony(和環境))。

這些錯誤頁都是可以自訂的。要知道更多請閱讀“如何自訂錯誤頁面”。

Request物件作為一個控制器參數 ¶

如果你需要取得查詢參數,抓取請求頭或取得一個上傳檔案?這些資訊都儲存在Symfony的Request物件。在你的控制器裡取得它們,只需要加入Request物件作為一個參數並強制類型為Request類別:

use Symfony\Component\HttpFoundation\Request;
public function indexAction($firstName, $lastName, Request $request){
$page = $request->query->get('page', 1);     // ...}

管理Session 

Symfony提供了一個好用的Session對象,它能夠儲存有關使用者的信息(它可以是使用瀏覽器的人、bot或Web服務)之間的請求。預設情況下,Symfony透過使用PHP的原生Session來保存cookie中的屬性。

去取得這個session,需要呼叫Request 物件的getSession()方法。這個方法會傳回一個SessionInterface,它用最簡單的方法來儲存和抓取session的資料:

use Symfony\Component\HttpFoundation\Request; public function indexAction(Request $request){
    $session = $request->getSession();     // store an attribute for reuse during a later user request
    $session->set('foo', 'bar');     // get the attribute set by another controller in another request
    $foobar = $session->get('foobar');     // use a default value if the attribute doesn't exist
    $filters = $session->get('filters', array());}

這些屬性將會在該使用者session的有效期限內保留。

Flash Message 

你也可以在使用者session中儲存一些指定的訊息,這個訊息被稱為「flash message」(訊息條子)。證據規定,flash訊息只能夠使用一次:當你取回它們的時候它們會自動的消失。這種特性使得「flash」訊息特別適合儲存使用者通知。

讓我們看看我們處理表單提交的範例:

use Symfony\Component\HttpFoundation\Request; public function updateAction(Request $request){
    $form = $this->createForm(...);     $form->handleRequest($request);     if ($form->isValid()) {
        // do some sort of processing         $this->addFlash(
            'notice',
            'Your changes were saved!'
        );         // $this->addFlash is equivalent to $this->get('session')->getFlashBag()->add         return $this->redirectToRoute(...);
    }     return $this->render(...);}

在處理請求之後,控制器設定了一個名為notice的flash訊息,然後重定向。名稱(notice)並不重要 – 它就是一個確定訊息的識別符。

接下來是模板(或者是更好的,在你的基礎佈局模板),從session中讀取每一條訊息:

XML:{% for flash_message in app.session.flashBag.get('notice') %}    <div class="flash-notice">
        {{ flash_message }}    </div>{% endfor %}
PHP:<?php foreach ($view['session']->getFlash('notice') as $message): ?>
    <div class="flash-notice">        <?php echo "<div class='flash-error'>$message</div>" ?>
    </div><?php endforeach ?>

通常使用的 noticewarningerror作為不同類型提示訊息的鍵,但是你可以使用任何你需要的鍵。

你可以使用peek()方法來取得訊息,它可以讓訊息保存住.

要求和回應物件 ¶

如前面所提到的,框架的Request 物件會作為控制器的參數傳入並強制指定資料類型為Request 類別:

use Symfony\Component\HttpFoundation\Request; public function indexAction(Request $request){
    $request->isXmlHttpRequest(); // is it an Ajax request?     $request->getPreferredLanguage(array('en', 'fr'));     // retrieve GET and POST variables respectively
    $request->query->get('page');
    $request->request->get('page');     // retrieve SERVER variables
    $request->server->get('HTTP_HOST');     // retrieves an instance of UploadedFile identified by foo
    $request->files->get('foo');     // retrieve a COOKIE value
    $request->cookies->get('PHPSESSID');     // retrieve an HTTP request header, with normalized, lowercase keys
    $request->headers->get('host');
    $request->headers->get('content_type');}

這個Request類別有一些公共的屬性和方法,它們能夠傳回任何你需要的請求資訊。

就像Request一樣,Response物件也有一個公共的headers屬性。它是一個ResponseHeaderBag它有一些不錯的方法來getting和setting回應頭。頭名的規範化使得 Content-Type等於content-type甚至等於content_type,它們都是一樣的。

對於控制器,唯一的要求就是傳回一個Response物件。 Response類別是一個PHP對於HTTP回應的一個抽象,一個基於文字的訊息填充HTTP頭,其內容傳回客戶端:

use Symfony\Component\HttpFoundation\Response; 
// create a simple Response with a 200 status code (the default)$response = new Response('Hello '.$name, Response::HTTP_OK); 
// create a CSS-response with a 200 status code
$response = new Response('<style> ... </style>');
$response->headers->set('Content-Type', 'text/css');

也有一些特殊的類別能夠簡化某種回應:

  • 對於JSON:這是一個JosnResponse。可查看 建立一個JOSN回應。

  • 對於檔案操作:這是 BinaryFileResponse。可查看 Serving Files。

  • 對於串流回應,有StreamedResponse。請看:流化一個回應

JSON Helper 

3.1##json() helper從symfony3.1開始被引入。

傳回JSON類型在基於API的應用程式中是日益流行的。基於這個原因,控制器基類定義了json()方法,來建立一個JsonResponse 並自動編碼給定的內容:

// ...public function indexAction(){
// returns '{"username":"jane.doe"}' and sets the proper Content-Type header
return $this->json(array('username' => 'jane.doe'));    
// the shortcut defines three optional arguments
// return $this->json($data, $status = 200, $headers = array(), $context = array());}

如果Serializer服務在你的應用程式中啟用,那麼內容傳遞到json()就會自動編碼。否則,你將要使用json_encode函數。

你現在已經了解了Symfony RequestResponse 物件的基礎,你也可以參考HttpFoundation元件以了解更多。

建立靜態頁面 ¶

你可以在沒有控制器的情況下建立一個靜態頁面(只需要一個路由和模板)。參考 不使用自訂控制器時如何渲染範本。

總結 ¶

當你建立了一個頁面,你需要在頁面中寫一些商業邏輯的程式碼。在symfony中,這些就是控制器,它是一個能夠做任何事情的php函數,目的是把最終的Response物件回傳給使用者。

而且你能夠繼承Controller基類,讓工作變得輕鬆。例如,你不用把html程式碼寫到控制器,可以使用render()來渲染模板,並回傳模板的內容。

在其他章節中,你會看到控制器是如何使用從資料庫中讀取物件和進行持久化的,處理表單提交,操作快取以及更多。

Keep Going ¶

接下來,集中學習使用Twig渲染模板。