跳到內容

Symfony 與原生 PHP

編輯此頁

為什麼 Symfony 比起直接開啟檔案撰寫原生 PHP 更好?

如果您從未使用過 PHP 框架,不熟悉 模型-視圖-控制器 (MVC) 哲學,或者只是想知道 Symfony 熱潮 的原因,本文適合您。您將親眼見證,而不是告訴您 Symfony 如何讓您比使用原生 PHP 更快速、更出色地開發軟體。

在本文中,您將使用原生 PHP 撰寫一個基本應用程式,然後重構它以使其更有條理。您將穿越時空,了解網頁開發在過去幾年中演變成現在這樣的決策歷程。

最後,您將了解 Symfony 如何將您從繁瑣的任務中解放出來,並讓您重新掌控您的程式碼。

使用原生 PHP 建立基本部落格

在本文中,您將僅使用原生 PHP 建立權杖部落格應用程式。首先,建立一個單一頁面,顯示已持久化到資料庫的部落格文章。使用原生 PHP 撰寫程式碼快速又簡陋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
// index.php
$connection = new PDO("mysql:host=localhost;dbname=blog_db", 'myuser', 'mypassword');

$result = $connection->query('SELECT id, title FROM post');
?>

<!DOCTYPE html>
<html>
    <head>
        <title>List of Posts</title>
    </head>
    <body>
        <h1>List of Posts</h1>
        <ul>
            <?php while ($row = $result->fetch(PDO::FETCH_ASSOC)): ?>
            <li>
                <a href="/show.php?id=<?= $row['id'] ?>">
                    <?= $row['title'] ?>
                </a>
            </li>
            <?php endwhile ?>
        </ul>
    </body>
</html>

<?php
$connection = null;
?>

這樣寫起來很快,部署和執行也很快,但隨著您的應用程式成長,將會變得難以維護。有幾個問題需要解決:

  • 沒有錯誤檢查:如果資料庫連線失敗怎麼辦?
  • 組織不良:如果應用程式成長,這個單一檔案將變得越來越難以維護。您應該將程式碼放在哪裡來處理表單提交?您如何驗證資料?傳送電子郵件的程式碼應該放在哪裡?
  • 難以重複使用程式碼:由於所有內容都在一個檔案中,因此無法為部落格的其他「頁面」重複使用應用程式的任何部分。

注意

此處未提及的另一個問題是,資料庫與 MySQL 綁定。雖然這裡沒有涵蓋,但 Symfony 完全整合了 Doctrine,這是一個專用於資料庫抽象和對應的函式庫。

隔離呈現層

程式碼可以立即從將應用程式「邏輯」與準備 HTML「呈現」的程式碼分離中獲益

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// index.php
$connection = new PDO("mysql:host=localhost;dbname=blog_db", 'myuser', 'mypassword');

$result = $connection->query('SELECT id, title FROM post');

$posts = [];
while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
    $posts[] = $row;
}

$connection = null;

// include the HTML presentation code
require 'templates/list.php';

HTML 程式碼現在儲存在一個單獨的檔案 templates/list.php 中,這主要是 HTML 檔案,它使用類似範本的 PHP 語法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- templates/list.php -->
<!DOCTYPE html>
<html>
    <head>
        <title>List of Posts</title>
    </head>
    <body>
        <h1>List of Posts</h1>
        <ul>
            <?php foreach ($posts as $post): ?>
            <li>
                <a href="/show.php?id=<?= $post['id'] ?>">
                    <?= $post['title'] ?>
                </a>
            </li>
            <?php endforeach ?>
        </ul>
    </body>
</html>

依照慣例,包含所有應用程式邏輯的檔案 - index.php - 被稱為「控制器」。無論您使用哪種語言或框架,您都會經常聽到「控制器」這個詞。它指的是您的程式碼中處理使用者輸入並準備回應的區域。

在本例中,控制器準備來自資料庫的資料,然後包含一個範本來呈現該資料。透過隔離控制器,如果您需要以其他格式(例如 JSON 格式的 list.json.php)呈現部落格文章,您可以變更範本檔案。

隔離應用程式(網域)邏輯

到目前為止,應用程式僅包含一個頁面。但是,如果第二個頁面需要使用相同的資料庫連線,甚至相同的部落格文章陣列怎麼辦?重構程式碼,以便應用程式的核心行為和資料存取函式隔離在一個名為 model.php 的新檔案中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// model.php
function open_database_connection()
{
    $connection = new PDO("mysql:host=localhost;dbname=blog_db", 'myuser', 'mypassword');

    return $connection;
}

function close_database_connection(&$connection)
{
    $connection = null;
}

function get_all_posts()
{
    $connection = open_database_connection();

    $result = $connection->query('SELECT id, title FROM post');

    $posts = [];
    while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
        $posts[] = $row;
    }
    close_database_connection($connection);

    return $posts;
}

提示

之所以使用檔案名稱 model.php,是因為應用程式的邏輯和資料存取傳統上被稱為「模型」層。在組織完善的應用程式中,表示您的「商業邏輯」的大部分程式碼應該存在於模型中(而不是存在於控制器中)。而且與本範例不同,只有一部分(或沒有)模型實際上與存取資料庫有關。

控制器 (index.php) 現在只有幾行程式碼

1
2
3
4
5
6
// index.php
require_once 'model.php';

$posts = get_all_posts();

require 'templates/list.php';

現在,控制器的唯一任務是從應用程式的模型層(模型)取得資料,並呼叫範本來呈現該資料。這是模型-視圖-控制器模式的一個非常簡潔的範例。

隔離版型

此時,應用程式已重構為三個不同的部分,提供各種優勢,並有機會在不同頁面上重複使用幾乎所有內容。

唯一無法重複使用的程式碼部分是頁面版型。透過建立新的 templates/layout.php 檔案來修正此問題

1
2
3
4
5
6
7
8
9
10
<!-- templates/layout.php -->
<!DOCTYPE html>
<html>
    <head>
        <title><?= $title ?></title>
    </head>
    <body>
        <?= $content ?>
    </body>
</html>

現在可以簡化範本 templates/list.php 以「擴展」templates/layout.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- templates/list.php -->
<?php $title = 'List of Posts' ?>

<?php ob_start() ?>
    <h1>List of Posts</h1>
    <ul>
        <?php foreach ($posts as $post): ?>
        <li>
            <a href="/show.php?id=<?= $post['id'] ?>">
                <?= $post['title'] ?>
            </a>
        </li>
        <?php endforeach ?>
    </ul>
<?php $content = ob_get_clean() ?>

<?php include 'layout.php' ?>

您現在擁有一個允許您重複使用版型的設定。不幸的是,為了完成此操作,您被迫在範本中使用一些醜陋的 PHP 函式 (ob_start()ob_get_clean())。Symfony 使用 Twig 來解決此問題。您很快就會看到它的實際運作。

新增部落格「顯示」頁面

部落格「列表」頁面現在已經過重構,程式碼更有條理且可重複使用。為了證明這一點,新增一個部落格「顯示」頁面,該頁面顯示由 id 查詢參數識別的個別部落格文章。

首先,在 model.php 檔案中建立一個新函式,該函式根據給定的 id 檢索個別部落格結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// model.php
function get_post_by_id($id)
{
    $connection = open_database_connection();

    $query = 'SELECT created_at, title, body FROM post WHERE id=:id';
    $statement = $connection->prepare($query);
    $statement->bindValue(':id', $id, PDO::PARAM_INT);
    $statement->execute();

    $row = $statement->fetch(PDO::FETCH_ASSOC);

    close_database_connection($connection);

    return $row;
}

接下來,建立一個名為 show.php 的新檔案 - 這個新頁面的控制器

1
2
3
4
5
6
// show.php
require_once 'model.php';

$post = get_post_by_id($_GET['id']);

require 'templates/show.php';

最後,建立新的範本檔案 - templates/show.php - 以呈現個別部落格文章

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- templates/show.php -->
<?php $title = $post['title'] ?>

<?php ob_start() ?>
    <h1><?= $post['title'] ?></h1>

    <div class="date"><?= $post['created_at'] ?></div>
    <div class="body">
        <?= $post['body'] ?>
    </div>
<?php $content = ob_get_clean() ?>

<?php include 'layout.php' ?>

現在建立第二個頁面只需要很少的工作,並且沒有重複的程式碼。儘管如此,這個頁面仍然引入了更多框架可以為您解決的潛在問題。例如,遺失或無效的 id 查詢參數將導致頁面崩潰。如果這導致呈現 404 頁面會更好,但這現在還無法真正完成。

另一個主要問題是,每個個別的控制器檔案都必須包含 model.php 檔案。如果每個控制器檔案突然需要包含額外的檔案或執行一些其他全域任務(例如,強制執行安全性)怎麼辦?就目前情況而言,該程式碼需要新增到每個控制器檔案中。如果您忘記在一個檔案中包含某些內容,希望它與安全性無關...

「前端控制器」救援

解決方案是使用前端控制器:一個單一的 PHP 檔案,透過該檔案處理所有請求。使用前端控制器,應用程式的 URI 會略有變更,但開始變得更靈活

1
2
3
4
5
6
7
Without a front controller
/index.php          => Blog post list page (index.php executed)
/show.php           => Blog post show page (show.php executed)

With index.php as the front controller
/index.php          => Blog post list page (index.php executed)
/index.php/show     => Blog post show page (index.php executed)

提示

透過在您的 網頁伺服器設定 中使用重寫規則,將不再需要 index.php,並且您將擁有美觀、簡潔的 URL(例如 /show)。

當使用前端控制器時,單一 PHP 檔案(在本例中為 index.php)會呈現每個請求。對於部落格文章顯示頁面,/index.php/show 實際上將執行 index.php 檔案,該檔案現在負責根據完整的 URI 在內部路由請求。正如您將看到的,前端控制器是一個非常強大的工具。

建立前端控制器

您即將對應用程式採取重大步驟。透過一個檔案處理所有請求,您可以集中處理安全性處理、組態載入和路由等事項。在這個應用程式中,index.php 現在必須夠聰明,才能根據請求的 URI 呈現部落格文章列表頁面部落格文章顯示頁面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// index.php

// load and initialize any global libraries
require_once 'model.php';
require_once 'controllers.php';

// route the request internally
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ('/index.php' === $uri) {
    list_action();
} elseif ('/index.php/show' === $uri && isset($_GET['id'])) {
    show_action($_GET['id']);
} else {
    header('HTTP/1.1 404 Not Found');
    echo '<html><body><h1>Page Not Found</h1></body></html>';
}

為了組織,兩個控制器(以前的 /index.php/index.php/show)現在都是 PHP 函式,並且每個函式都已移至名為 controllers.php 的單獨檔案中

1
2
3
4
5
6
7
8
9
10
11
12
// controllers.php
function list_action()
{
    $posts = get_all_posts();
    require 'templates/list.php';
}

function show_action($id)
{
    $post = get_post_by_id($id);
    require 'templates/show.php';
}

作為前端控制器,index.php 承擔了全新的角色,其中包括載入核心函式庫和路由應用程式,以便呼叫兩個控制器之一(list_action()show_action() 函式)。實際上,前端控制器開始看起來和行為方式非常像 Symfony 處理和路由請求的方式。

但請注意不要混淆前端控制器控制器這兩個術語。您的應用程式通常只有一個前端控制器,它會啟動您的程式碼。您將擁有許多控制器函式:每個頁面一個。

提示

前端控制器的另一個優勢是彈性的 URL。請注意,透過僅在一個位置變更程式碼,部落格文章顯示頁面的 URL 可以從 /show 變更為 /read。以前,需要重新命名整個檔案。在 Symfony 中,URL 更加靈活。

到目前為止,應用程式已從單一 PHP 檔案演變為有組織且允許程式碼重複使用的結構。您應該更快樂,但遠遠沒有感到滿意。例如,路由系統是反覆無常的,並且無法識別列表頁面 - /index.php - 也應該可以透過 / 存取(如果新增了 Apache 重寫規則)。此外,除了開發部落格之外,還花費了大量時間來處理程式碼的「架構」(例如路由、呼叫控制器、範本等)。將需要花費更多時間來處理表單提交、輸入驗證、記錄和安全性。為什麼您必須重新發明解決所有這些例行問題的方案?

加入一些 Symfony 的元素

Symfony 來救援了。在實際使用 Symfony 之前,您需要下載它。這可以使用 Composer 來完成,它負責下載正確的版本及其所有依賴項,並提供自動載入器。自動載入器是一種工具,可以讓您開始使用 PHP 類別,而無需明確包含包含該類別的檔案。

在您的根目錄中,建立一個包含以下內容的 composer.json 檔案

1
2
3
4
5
6
7
8
{
    "require": {
        "symfony/http-foundation": "^4.0"
    },
    "autoload": {
        "files": ["model.php","controllers.php"]
    }
}

接下來,下載 Composer,然後執行以下命令,這會將 Symfony 下載到 vendor/ 目錄中

1
$ composer install

除了下載您的依賴項之外,Composer 還會產生一個 vendor/autoload.php 檔案,該檔案負責自動載入 Symfony 框架中的所有檔案,以及 composer.json 的自動載入章節中提及的檔案。

Symfony 哲學的核心思想是,應用程式的主要工作是解釋每個請求並傳回回應。為此,Symfony 同時提供了 RequestResponse 類別。這些類別是以物件導向的方式表示正在處理的原始 HTTP 請求以及正在傳回的 HTTP 回應。使用它們來改進部落格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// index.php
require_once 'vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();

$uri = $request->getPathInfo();
if ('/' === $uri) {
    $response = list_action();
} elseif ('/show' === $uri && $request->query->has('id')) {
    $response = show_action($request->query->get('id'));
} else {
    $html = '<html><body><h1>Page Not Found</h1></body></html>';
    $response = new Response($html, Response::HTTP_NOT_FOUND);
}

// echo the headers and send the response
$response->send();

控制器現在負責傳回 Response 物件。為了簡化此操作,您可以新增一個新的 render_template() 函式,順便說一句,它的行為方式很像 Symfony 範本引擎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// controllers.php
use Symfony\Component\HttpFoundation\Response;

function list_action()
{
    $posts = get_all_posts();
    $html = render_template('templates/list.php', ['posts' => $posts]);

    return new Response($html);
}

function show_action($id)
{
    $post = get_post_by_id($id);
    $html = render_template('templates/show.php', ['post' => $post]);

    return new Response($html);
}

// helper function to render templates
function render_template($path, array $args)
{
    extract($args);
    ob_start();
    require $path;
    $html = ob_get_clean();

    return $html;
}

透過引入 Symfony 的一小部分,應用程式變得更靈活、更可靠。Request 提供了一種可靠的方式來存取有關 HTTP 請求的資訊。具體來說,getPathInfo() 方法會傳回已清除的 URI(始終傳回 /show 而絕不會傳回 /index.php/show)。因此,即使使用者前往 /index.php/show,應用程式也夠聰明,可以透過 show_action() 路由請求。

Response 物件在建構 HTTP 回應時提供了彈性,允許透過物件導向介面新增 HTTP 標頭和內容。雖然此應用程式中的回應很簡單,但隨著應用程式的成長,這種彈性將會帶來回報。

Symfony 中的範例應用程式

部落格已經走了很長的路,但對於這樣一個基本應用程式來說,它仍然包含了很多程式碼。一路上,您建立了一個基本的路由系統和一個使用 ob_start()ob_get_clean() 呈現範本的函式。如果由於某種原因,您需要繼續從頭開始建構這個「框架」,那麼您至少可以使用 Symfony 的獨立 路由 組件和 Twig,它們已經解決了這些問題。

您可以讓 Symfony 為您處理常見問題,而不是重新解決這些問題。以下是相同的範例應用程式,現在以 Symfony 建構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Controller/BlogController.php
namespace App\Controller;

use App\Entity\Post;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class BlogController extends AbstractController
{
    public function list(ManagerRegistry $doctrine)
    {
        $posts = $doctrine->getRepository(Post::class)->findAll();

        return $this->render('blog/list.html.twig', ['posts' => $posts]);
    }

    public function show(ManagerRegistry $doctrine, $id)
    {
        $post = $doctrine->getRepository(Post::class)->find($id);

        if (!$post) {
            // cause the 404 page not found to be displayed
            throw $this->createNotFoundException();
        }

        return $this->render('blog/show.html.twig', ['post' => $post]);
    }
}

請注意,兩個控制器函式現在都位於「控制器類別」內。這是一種對相關頁面進行分組的好方法。控制器函式有時也稱為動作

兩個控制器(或動作)仍然很輕量。每個控制器都使用 Doctrine ORM 函式庫 從資料庫檢索物件,並使用 Twig 呈現範本並傳回 Response 物件。list.html.twig 範本現在簡化了很多,並且使用了 Twig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{# templates/blog/list.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}List of Posts{% endblock %}

{% block body %}
<h1>List of Posts</h1>
<ul>
    {% for post in posts %}
    <li>
        <a href="{{ path('blog_show', { id: post.id }) }}">
            {{ post.title }}
        </a>
    </li>
    {% endfor %}
</ul>
{% endblock %}

layout.php 檔案幾乎相同

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- templates/base.html.twig -->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
    </body>
</html>

注意

show.html.twig 範本留給您練習:更新它應該與更新 list.html.twig 範本非常相似。

當 Symfony 的引擎(稱為 Kernel)啟動時,它需要一個地圖,以便它知道根據請求資訊呼叫哪些控制器。路由組態地圖 - config/routes.yaml - 以可讀格式提供此資訊

1
2
3
4
5
6
7
8
# config/routes.yaml
blog_list:
    path:     /blog
    controller: App\Controller\BlogController::list

blog_show:
    path:     /blog/show/{id}
    controller: App\Controller\BlogController::show

現在 Symfony 正在處理所有繁瑣的任務,前端控制器 public/index.php 已簡化為啟動。由於它的功能很少,因此您永遠不必碰它

1
2
3
4
5
6
7
8
// public/index.php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../src/Kernel.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new Kernel('prod', false);
$kernel->handle(Request::createFromGlobals())->send();

前端控制器的唯一工作是初始化 Symfony 的引擎(稱為 Kernel)並將 Request 物件傳遞給它以進行處理。Symfony 核心要求路由器檢查請求。路由器將傳入的 URL 與特定路由匹配,並傳回有關路由的資訊,包括應呼叫的控制器。呼叫來自匹配路由的正確控制器,並且控制器內部的程式碼會建立並傳回適當的 Response 物件。Response 物件的 HTTP 標頭和內容會傳送回用戶端。

這真是太棒了。

Symfony 的優勢

在文件文章的其餘部分中,您將更詳細地了解 Symfony 的每個部分如何運作,以及如何組織您的專案。現在,慶祝將部落格從原生 PHP 遷移到 Symfony 如何改善您的生活

  • 您的應用程式現在具有清晰且一致組織的程式碼(雖然 Symfony 並不強迫您這樣做)。這促進了重複使用性,並讓新開發人員能夠更快地在您的專案中提高生產力;
  • 您撰寫的程式碼 100% 用於您的應用程式。您不需要開發或維護底層實用程式,例如自動載入、路由或呈現 控制器
  • Symfony 讓您存取開放原始碼工具,例如 DoctrineTwig安全性表單驗證器翻譯 組件(僅舉幾例);
  • 現在起,多虧了 Routing 組件,應用程式可以享有 完全彈性的 URL
  • Symfony 以 HTTP 為中心的架構讓您可以使用強大的工具,例如由 Symfony 內部 HTTP 快取 或更強大的工具(如 Varnish)所驅動的 HTTP 快取。這在另一篇關於 快取 的文章中涵蓋。

或許最棒的是,透過使用 Symfony,您現在可以使用由 Symfony 社群開發的一整套高品質開放原始碼工具!在 GitHub 上可以找到精選的 Symfony 社群工具

本作品,包含程式碼範例,以 Creative Commons BY-SA 3.0 授權條款授權。
目錄
    版本