2019年3月1日 星期五

Symfony4.2入門教學【第四篇】控制器(Controllers)


所謂控制器是一個PHP函式,用來讀取Request物件中的資訊,並建立和回傳一個Response物件,這個回應(response)可以是HTML頁面、JSON、XML、供下載的檔案、重新定向(redirect)、404錯誤等等,控制器根據應用的需求,執行任何可能的邏輯去呈現畫面。

如果你還沒建立第一個頁面,請查看文章『建立第一個頁面』之後再回來吧!


簡易的控制器

雖然控制器可以是任何可呼叫的(函式、物件中的方法或Closure),但大多會是一個控制器類內的方法(a method inside a controller class):
// src/Controller/LuckyController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class LuckyController
{
    /**
     * @Route("/lucky/number/{max}", name="app_lucky_number")
     */
    public function number($max)
    {
        $number = random_int(0, $max);

        return new Response(
            'Lucky number: '.$number.''
        );
    }
}
控制器就是方法number(),被放在名為LuckyController的控制器類。

這個控制器非常容易理解:
  • line 2: Symfony利用PHP的命名空間(namespace)功能來命名整個控制器類。
  • line 4: Symfony也是利用PHP的命名空間功能來命名整個控制器類:關鍵字use輸入(import)一個控制器必要回傳的類(class)Response
  • line 7: 實作上,類名可隨便取,但按慣例會用Controller當作後綴字。
  • line 12: 由於有個{max}通配符在路由,操作方法(action method)會有一個$max的參數。
  • line 16: 控制器建立和回傳一個Response物件

對應URL到控制器
想要看到控制器呈現的結果,你必須透過路由將URL與其對應,這邊的設定是透過註解式路由,寫成@Route("/lucky/number/{max}")來達成對應。

想看頁面,請用瀏覽器到這個URL:
欲見更多關於路由資訊,請看路由



基礎的控制器和服務

Symfony為了讓大家輕鬆點,有一個基礎控制器AbstractController可以選擇使用,你可以延伸了解一些輔助方式(helper methods)。

在控制器類的上方加上use的語句,並修改LuckyController以擴展(extend)它:
// src/Controller/LuckyController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class LuckyController
class LuckyController extends AbstractController
{
    // ...
}
就是這樣!你現在已經接觸了像$this->render()這樣的方法,接下來還有很多其他的方法你會慢慢學到。

生成URL
generateUrl()方法是一個輔助方法,可以針對給定的路由產生URL:
$url = $this->generateUrl('app_lucky_number', ['max' => 10]);

重新導向(Redirecting)
如果你像要將使用者重新導向其他頁面,可以使用redirectToRoute()redirect()兩個方法:
use Symfony\Component\HttpFoundation\RedirectResponse;

// ...
public function index()
{
    // redirects to the "homepage" route
    return $this->redirectToRoute('homepage');

    // redirectToRoute is a shortcut for:
    // return new RedirectResponse($this->generateUrl('homepage'));

    // does a permanent - 301 redirect
    return $this->redirectToRoute('homepage', [], 301);

    // redirect to a route with parameters
    return $this->redirectToRoute('app_lucky_number', ['max' => 10]);

    // redirects to a route and maintains the original query string parameters
    return $this->redirectToRoute('blog_show', $request->query->all());

    // redirects externally
    return $this->redirect('http://symfony.com/doc');
}

redirect()方法並完全不會檢查重新導向的目標,如果你是導向由用戶提供的URL,可能會使得你的應用程式產生未經驗證重新定向的安全漏洞(unvalidated redirects security vulnerability)。

渲染模板(rendering template)
如果你要回傳HTML,你會想要用渲染模板,方法render()會渲染模板,並封裝成Response物件給你:
// renders templates/lucky/number.html.twig
return $this->render('lucky/number.html.twig', ['number' => $number]);
模板和Twig相關的說明,在文章『建立和使用模板』(原文)有更多的說明。

獲取服務(fetching services)
Symfony包含相當多有用的物件,稱為服務(service)。可以用來渲染模板、發送信件、查詢資料庫和任何你想得到的工作。

如果你要在控制器裡使用服務,請用它的類名(或介面(interface))宣告參數,Symfony會自動提供你所需的服務:
use Psr\Log\LoggerInterface;
// ...

/**
 * @Route("/lucky/number/{max}")
 */
public function number($max, LoggerInterface $logger)
{
    $logger->info('We are logging!');
    // ...
}
太棒了!

其他還有哪些服務類型可以使用?如果想知道,請在控制台執行指令debug:autowiring
php bin/console debug:autowiring

如果需要控制參數的確切值,可以使用其名稱綁定(bind)參數:
// 省略程式碼
和其他服務一樣,你可以在控制器中用常規構造函數注入(regular constructor injection)。

想要知道更多關於服務的資訊,請看服務容器(Service Container)這篇文章。


產生控制器

為了節省時間,你可以安裝Symfony Maker,並藉由它產生一個新的控制器類:
php bin/console make:controller BrandNewController

created: src/Controller/BrandNewController.php
如果想要產生針對Doctrine實體(entity)完整的CRUD,執行:
php bin/console make:crud Product

New in version 1.2: MakeBundle 1.2內介紹了指令make:crud


錯誤處理及404頁面

如果找不到東西,應該回傳一個404的回應,請拋(throw)一個特殊形態的例外(exception)來達成:
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

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

        // the above is just a shortcut for:
        // throw new NotFoundHttpException('The product does not exist');
    }

    return $this->render(...);
}
方法createNotFoundException()只是一個NotFoundHttpException特殊物件的捷徑,最終會觸發Symfony內的一個404 HTTP回應。

如果你拋一個延伸的例外或HttpException的實例(instance),Symfony會自帶適當的HTTP狀態碼(status code),否則回應就會是HTTP status code 500。
// this exception ultimately generates a 500 status error
throw new \Exception('Something went wrong!');
每個情況下,錯誤頁面(error page)是顯示給一般使用者看,完整的除錯頁面(full debug error page)則是供開發者(i.e. 當你在除錯模式(Debug mode) - The parameters Key: Parameters (Variables))。

若想要客製化顯示給使用者看的錯誤頁面,請參考如何將錯誤頁面客製化的文章(How to Customize Error Pages article)。


回應物件作為控制器參數

如果想要讀取查詢參數、取得請求的標頭(header)或取得上船的檔案?所有的資訊都存在Symfony的Request物件中,要在你的控制器內取得,只要用請求類名宣告成參數:
use Symfony\Component\HttpFoundation\Request;

public function index(Request $request, $firstName, $lastName)
{
    $page = $request->query->get('page', 1);

    // ...
}
更多資訊請參考Request Object(原文)


管理Session

Symfony提供session的服務,可以讓你從請求中取得更多關於使用者的資訊,預設狀態下就可以使用session,不過只有在讀取或寫入的時候才會啟動。

Session儲存區和其他設定都是由config/packages/framework.yaml檔案內的framework.session configuration管控。

要取得session,請新增一個參數並宣告型態為SessionInterface
use Symfony\Component\HttpFoundation\Session\SessionInterface;

public function index(SessionInterface $session)
{
    // stores an attribute for reuse during a later user request
    $session->set('foo', 'bar');

    // gets the attribute set by another controller in another request
    $foobar = $session->get('foobar');

    // uses a default value if the attribute doesn't exist
    $filters = $session->get('filters', []);
}
儲存的屬性會放在session哩,跟使用者其他的session一起。

想要了解更多,請看Session

Flash Messages
你可以儲存一些特別的訊息在使用者的session,稱為"Flash",設計上,flash message只會使用一次: 一旦你檢索後就會自動從session裡面移除,這個特性讓flash message特別適合存放用戶通知。

例如:想像你需要處理一個表單(form)的遞交:
use Symfony\Component\HttpFoundation\Request;

public function update(Request $request)
{
    // ...

    if ($form->isSubmitted() && $form->isValid()) {
        // do some sort of processing

        $this->addFlash(
            'notice',
            'Your changes were saved!'
        );
        // $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add()

        return $this->redirectToRoute(...);
    }

    return $this->render(...);
}
在處理完這個請求後,控制器會設定flash message到session並重新導向,訊息的鍵(key)(這個例子裡,鍵就是notice)可以任意指定:你可以用這個key來檢索訊息。

在下一頁的模板中(如果放在你的基礎模板中會更好),用app.flashes()來讀取session內任何flash message:
{# templates/base.html.twig #}

{# read and display just one flash message type #}
{% for message in app.flashes('notice') %}
    
{{ message }}
{% endfor %} {# read and display several types of flash messages #} {% for label, messages in app.flashes(['success', 'warning']) %} {% for message in messages %}
{{ message }}
{% endfor %} {% endfor %} {# read and display all flash messages #} {% for label, messages in app.flashes %} {% for message in messages %}
{{ message }}
{% endfor %} {% endfor %}
常見使用noticewarningerror作為不同類型flash message的鍵(key),當然你也可以使用任何符合你需求的鍵。

你可以用方法peek(),替代存起來再檢索訊息。


請求和回應

如同上述,Symfony會傳一個Request物件給任何有宣告參數Request類的控制器:
use Symfony\Component\HttpFoundation\Request;

public function index(Request $request)
{
    $request->isXmlHttpRequest(); // is it an Ajax request?

    $request->getPreferredLanguage(['en', 'fr']);

    // retrieves GET and POST variables respectively
    $request->query->get('page');
    $request->request->get('page');

    // retrieves SERVER variables
    $request->server->get('HTTP_HOST');

    // retrieves an instance of UploadedFile identified by foo
    $request->files->get('foo');

    // retrieves a COOKIE value
    $request->cookies->get('PHPSESSID');

    // retrieves an HTTP request header, with normalized, lowercase keys
    $request->headers->get('host');
    $request->headers->get('content-type');
}
Request類有幾個公開屬性(public properties)和方法,可返回有關請求的所有資訊。

Request一樣,Response物件也有公開屬性headersResponseHeaderBag提供一些好用的方法來設定回應的標頭檔(header),標頭名稱(header names)一般標準使用Content-Type,等同於content-typecontent_type

控制器唯一的條件就是要回傳Response物件:
use Symfony\Component\HttpFoundation\Response;

// creates a simple Response with a 200 status code (the default)
$response = new Response('Hello '.$name, Response::HTTP_OK);

// creates a CSS-response with a 200 status code
$response = new Response('');
$response->headers->set('Content-Type', 'text/css');
有個特殊的類可以讓部分的回應簡單一點,下面會列舉一部份。想要了解更多關於RequestResponse(還有特殊類的Response)的資訊,請查看HttpFoundation component documentation

回傳JSON回應
如果要在控制器內回傳JSON,可以用輔助方法json(),這會回傳一個已經自動編碼(encode)的JsonResponse特殊物件。
// ...
public function index()
{
    // returns '{"username":"jane.doe"}' and sets the proper Content-Type header
    return $this->json(['username' => 'jane.doe']);

    // the shortcut defines three optional arguments
    // return $this->json($data, $status = 200, $headers = [], $context = []);
}
如果能在你的應用裡使用serializer service,可以幫你把資料連載(serialize)成JSON格式,不然也可以用函式json_encode

媒體文件回應(Streaming File Responses)
你可以在控制器內用輔助方式file()操作檔案:
public function download()
{
    // send the file contents and force the browser to download it
    return $this->file('/path/to/some_file.pdf');
}
輔助方式file()提供一些參數設定來決定表現行為:
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

public function download()
{
    // load the file from the filesystem
    $file = new File('/path/to/some_file.pdf');

    return $this->file($file);

    // rename the downloaded file
    return $this->file($file, 'custom_name.pdf');

    // display the file contents in the browser instead of downloading it
    return $this->file('invoice_3241.pdf', 'my_invoice.pdf', ResponseHeaderBag::DISPOSITION_INLINE);
}


結語

當你建立頁面時,你最後會需要幫頁面寫一些邏輯的程式碼,在Symfony稱之為控制器,是一個PHP的函式能讓你執行任何操作,並回應一個Response物件給使用者。

為了簡化一點,你可能會延伸基礎控制器AbstractController的類,因為這樣可以讓你快速取得方法render()redirectToRoute()的捷徑。

在其他文章裡,你會學到如何在控制器裡使用特定的服務,幫助你從資料庫獲取物件、處理表單提交、處理緩存等等。


繼續前進!

接下來,來學習『Symfony4.2入門教學【第五篇】模板(Templates)』(rendering template with Twig)。


文章原文:
https://symfony.com/doc/4.2/controller.html

系列文章:

沒有留言:

張貼留言