基本 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));
}