Controller 的介紹與基本使用
基本 Controller
創建一個 Controller
Symfony 提供了一個能夠直接產生一個空的 Controller 的指令,在以下的範例中,我們將這個 Controller 命名為 HelloController
。
~$ php bin/console make:controller
Choose a name for your controller class (e.g. BraveElephantController):
> HelloController
created: src/Controller/HelloController.php
created: templates/hello/index.html.twig
Success!
Next: Open your new controller class and add some pages!
這個指令執行結束後會產生兩個檔案,分為:src/Controller/HelloController.php
以及 template/hello/index.html.twig
,前者為這章要提到的部份,後者為模板文件,會在之後的章節做詳細解釋。
Controller 的基本結構
首先我們先打開剛才透過指令產生的 src/Controller/HelloController.php
接著我們就會看到如下的程式碼:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
// extends AbstractController,
// 所有 Controller 都必須是 AbstractController 的子類別,
// 且所有 Controller 的命名都必須以 Controller 這個字詞做結尾。
class HelloController extends AbstractController
{
/**
* 透過 @Route 定義該 Controller 函數對應的 URL,
* 並給予一個 name,這個 name 可以不設定,
* 但是在 MVC 架構的網站中,加上 name 能在開發上更為方便。
*
* 此外像以下 index() 這種處理請求的函數都稱為 Action ,
* 在本系列的文章中,都會不斷使用這個詞彙。
*
* @Route("/hello", name="hello")
*/
public function index()
{
// 返回 Response ,
// 所有用於 Controller 函數的回傳值都必須是 Symfony\Component\HttpFoundation\Response 類別的子類別,
// 如 JsonResponse, RedirectResponse 等,
// 而這行中的 $this->render() 就是會找到對應的 twig template,
// 並渲染成 HTML 文件之後,再封裝成 Response 返回。
return $this->render('hello/index.html.twig', [
'controller_name' => 'HelloController',
]);
}
}
接著我們開啟伺服器
~$ symfony server:start
接著打開瀏覽器訪問 http://localhost:8000/hello,就會看到如下的畫面:
這樣就完成了第一個 Controller 的建構。
__invoke()
在一般的 Controller 中我們可以定義很多個 Controller 函數, 來對應多個 Route,但是當我們將一個 Controller Class 視為一個獨立功能時, 我們就可以透過__invoke
來撰寫 Controller 中唯一用於處理請求的函數。 如:在撰寫 Restful API 時我們創建了一個可以取得用戶實體的 API Controller。
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class GetUserController extends AbstractController
{
/**
* 這個 Action 將會成為 Controller 的唯一 Action
* @Route("/api/v1/users/{uuid}", name="get_user", methods={"GET"})
*/
public function __invoke(string $uuid)
{
}
}
這種 Controller Class 中僅有一個 Action 的開發模式,稱為 ADR Pattern (Action-Domain-Responder),更多的 ADR Pattern 的解釋可以參照這篇文章:Action-Domain-Responder 。
透過指令創建一個指定 Namespace 的 Controller
在執行指令php bin/console make:controller
時,若我們需要創建一個帶有自定義 namespace 的 Controller ,我們可以這樣打:
~$ php bin/console make:controller
Choose a name for your controller class (e.g. GentleJellybeanController):
> Api\HelloController
created: src/Controller/Api/HelloController.php
created: templates/api/hello/index.html.twig
Success!
Next: Open your new controller class and add some pages!
這樣就產生一個檔案src/Controller/Api/HelloController.php
, 並且這個 Controller 將被歸屬在 App\Controller\Api
這個 namespace 底下。
Request 的取得與處理
參考文章: https://symfony.com/doc/current/components/http_foundation.html
要在Controller
中處理Request
,我們可以直接透過這樣的寫法來取得當前的Request
。
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class HelloController extends AbstractController
{
/**
* 我們只要在函數的 Parameter 中帶上 $request 變數並將參數型態定為 Symfony\Component\HttpFoundation\Request ,
* 這樣就可以取得當前請求的資料了。
*
* @param Request $request
* @return Response
* @Route("/hello", name="hello")
*/
public function index(Request $request)
{
return $this->render('hello/index.html.twig', [
'controller_name' => 'HelloController',
]);
}
}
Request Header
要取得 Request Header 可以透過以下的寫法
public function index(Request $request)
{
// 嘗試取得 User-Agent 這個 HTTP Header,若 Header 不存在則會回傳 NULL 。
$userAgent = $request->headers->get("User-Agent");
// 若 User-Agent 的 HTTP Header 不存在時,
// 想要賦予一個預設值,讓回傳的值不為 NULL 的話,
// 可以傳入第二個參數,這個參數代表的就是預設值。
$userAgent = $request->headers->get("User-Agent", "Default Value");
}
若想知道更多函數的應用,可以參照 HeaderBag 的 原始碼。
Cookies
取得 Cookie 的方法
public function index(Request $request)
{
// 假設 trace_id 為 "1234",
// 可以從 cookies 中透過 get() 函數以及 key 來取得 cookie 的值,
// 並且 get() 函數的第二個參數也同樣是預設值。
$strTraceId = $request->cookies->get("trace_id"); // "1234"
// 在 Cookie 中,所有資料都以字串形式儲存,
// 但是如果我們能夠確定某一個值必為整數,
// 如 "1234",且我們需要使用整數值,那麼我們可以直接使用 getInt() 函數。
$intTraceId = $request->cookies->getInt("trace_id"); // 1234
}
Query String
取得 Query String 的方法
public function index(Request $request)
{
// Query String 與 Cookies 的在函數操作上完全一樣,
// 因為這兩個都是 InputBag 型態,若想知道更多函數的細節,
// 可以到 GitHub 觀看原始碼(ParameterBag 以及 InputBag)或者詳讀本段開頭的 Ref。
$strPage = $request->query->get("page");
$intPage = $request->query->getInt("page");
}
Path Parameter
我們常會需要能在 Route 中帶入變數,如 /users/userid_1234
, 來分辨需要取得的資源, 而我們可以透過 Route 來定義變數路由, 詳細的路由設定在將在之後的章節做詳細說明, 這裡僅講解如何讓 Action 接受到 Route 中的變數。
/**
* 我們透過 Route 來定義一個 {userid} 作為變數,
* 告訴框架這個部份可以是任何值 (wildcard route)。
*
* 在函數的參數中帶上 string $userid,
* 來告訴框架要將路由中的變數帶到這裡,
* 這樣框架在呼叫這個 Action 的時候就會自動帶上這個參數。
*
* @param Request $request
* @param string $userid
* @return Response
* @Route("/users/{userid}", name="hello")
*/
public function index(Request $request, string $userid)
{
return new Response($userid);
}
對於第 7 行也許你會對參數的順序有所疑問,實際上參數的順序並不影響運行,若寫成:
public function index(string $userid, Request $request) {}
也不會有任何影響,這歸功於 Symfony 的 Autowiring 的機制,對於更詳細的 Autowiring 機制我們會在後續的文章中詳細講解。
接著我們打開開發用伺服器,並訪問http://localhost:8000/users/userid_1234就會得到如下的結果:
這樣就成功取得 Route 中的變數了。
Request Body
以下的寫法將可以取得 Request 的內容
public function index(Request $request)
{
// 取得請求的內容。
$requestContent = $request->getContent();
// 我們假設這個請求是一個 Restful API,
// 並且會在請求中帶上 JSON 格式的內容,
// 那麼我們就可以使用 json_decode() 來解析請求的內容。
if($request->headers->get("Content-Type") === "application/json")
$jsonContent = json_decode($requestContent, true);
}
Response
基礎 Response
Response
是用於回傳純文字的 Response 類別,因此你可以透過傳入一個字串來將這個字串直接傳回前端
use Symfony\Component\HttpFoundation\Response;
public function index()
{
return new Response("plaintext");
}
你也可以將 HTML 作為純文字傳回並手動設定 Header:
use Symfony\Component\HttpFoundation\Response;
public function index()
{
return new Response(
// 要回傳給用戶端的 Response Body。
"<html><body><h1>Hello World</h1></body></html>",
// Response 類別提供的 const 可以讓你更快的找到你想要使用的 HTTP Status Code,
// 如:
// Response::HTTP_FORBIDDEN
// Response::HTTP_NOT_FOUND 等
// 可以參照 Response 的原始碼
Response::HTTP_OK,
// Response Header 設定
array(
"Content-Type" => "text/html"
)
);
}
而回傳 JSON 也可以透過類似的手法,當然我並不推薦你這麼做, 上面的這個寫法可以透過 $this->render()
來渲染 twig 並返回 HTML, 而後者也可以透過接下來要講的JsonResponse
完成。
$this->render()
在剛才生成HelloController
時,隨之生成的還有template/hello/index.html.twig
這個文件, 如果要透過$this->render()
來達成上面同樣的效果的話,我們可以在index.html.twig
這個文件中寫入:
<!-- template/hello/index.html.twig -->
<html>
<body>
<h1>Hello World</h1>
</body>
</html>
接著我們的 Action 要改成
use Symfony\Component\HttpFoundation\Response;
public function index()
{
return $this->render("template/hello/index.html.twig");
}
這樣就可以輕鬆的達成渲染 HTML 的目標, 而這個功能是透過 Twig Template Engine 達成的, 會在之後的章節中做詳盡的講解。
JsonResponse
如同前面所說,如果想要回傳 JSON 格式的資料給客戶端,那麼可以透過JsonResponse
來達成這個目標
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
public function index()
{
$data = array(
"key1" => "value1",
"key2" => "value2"
);
return new JsonResponse($data, Response::HTTP_OK);
}
上面這段程式碼返回的 JSON 內容為:
{
"key1": "value1",
"key2": "value2"
}
RedirectResponse
要在 Symfony 中實現由後端驅動的網頁轉跳,則可以使用RedirectResponse
,寫法如下:
use Symfony\Component\HttpFoundation\RedirectResponse;
public function index()
{
return new RedirectResponse("/redirect/target");
}
在這篇文章的開頭有提到,在配置@Route
時,設定name
會對開發更容易, 這裡就是一個很好的例子,當今天我們想要轉跳至應用程式中的指定路由時,我們以這樣寫:
/**
* @Route("/redirect/target", name="redirect.target")
*/
public function redirectTarget()
{
// do something
}
/**
* @Route("/", name="index")
*/
public function index()
{
// 將轉跳至 /redirect/target
return $this->redirectToRoute("redirect.target");
}
而當今天轉跳的目標,是一個 Wildcard Route 的話也可以透過這種方法做轉跳:
/**
* @Route("/redirect/target/{id}", name="redirect.target")
*/
public function redirectTarget(string $id)
{
// do something
}
/**
* @Route("/", name="index")
*/
public function index()
{
// 將轉跳至 /redirect/target/1
return $this->redirectToRoute("redirect.target", array("id" => 1));
}