Symfony開發一個尋人平臺

簡介

Symfony2是一個基於PHP語言的Web開發框架,有着開發速度快、性能高等特色。但Symfony2的學習曲線也比較陡峭,沒有經驗的初學者每每須要一些練習才能掌握其特性。php

本文經過一個快速開發尋人平臺的實例向讀者介紹Symfony2框架的一些核心功能和特色。經過閱讀本文,你能夠經過一些具體的例子瞭解Symfony2框架的優秀特性和技術特色,從而體會到使用Symfony2框架支持快速網站開發這一優點。html

適合人羣

  • 本文適用於但願提升PHP語言的開發技術,或者對Symfony2框架有興趣的讀者。
  • 本文也適用於系統架構師和各種技術決策者。

1.前言

在不久前的4月20日,中國四川省雅安地區發生了7.0級地震,累計受災人數達到200多萬。尋人平臺在這樣的狀況下可以起到很大的幫助,並且,尋人平臺越早上線,實用價值就越高。前端

 

Symfony2能夠用來支持大型網站的建設,在中小型網站的快速搭建和開發上也有着很是好的支持。我藉由此次撰文的機會,向你們具體地分享一下我是如何在3個小時內基於Symfony2開發出來一套支持PFIF[^1]格式的網站尋人平臺的,但願讀者可以對Symfony2的各個組件以及功能產生一些瞭解。git

[^1]: People Finder Interchange Format(wiki)是一個被普遍使用的開放的數據結構及標準,災難發生後能夠用該標準在不一樣的組織或網站間交換尋人信息,幫助失去聯繫的人找到彼此。github

2.Bundle的使用

Symfony2框架以及相關社區最大的特色之一就是支持Bundle。什麼是Bundle呢?簡單來講,Bundle就是一種「功能」的抽象。經過把一類具體的問題抽象成一個Bundle,能夠把一個系統的邏輯進行切分:Bundle的開發者能夠專一在某類問題的解決上,而Bundle的使用者則能夠把工做的重心放在本身的業務邏輯上。數據庫

 

在互聯網開發領域,存在着大量能夠被抽象的功能。好比用戶登陸系統,好比新聞評論,好比JS/CSS文件的壓縮和合並等等。舉個具體的例子,好比用戶登陸系統,大部分項目對於用戶系統的需求其實都是差很少的,但每次要開發新產品的時候,都多多少少會去從新造一整個或一部分用戶系統的輪子。而一個專門用來負責管理用戶系統的Bundle的出現則會減輕這些項目的開發壓力,提升項目質量的同時能夠加快項目的總體開發速度。json

Symfony2也支持Bundle。Symfony2的社區有大量由社區進行維護的Bundle,使用這些開源的Bundle可讓咱們的項目直接擁有那部分Bundle所提供的功能。bootstrap

如下列舉了本項目中用到的一些第三方Bundle以及所對應負責的任務。api

Bundle名 功能介紹 在項目中的職責
MopaBootstrapBundle 提供基於Bootstrap的頁面結構和模板 提供頁面的基本HTML架構,樣式
NelmioApiDocBundle 自動生成API的文檔及接口測試工具 生成API文檔以及接口測試工具,並容許工程師及第三方調用者使用工具測試接口是否正常
JMSSerializerBundle 對象進行序列化工具 在接口中,將Doctrine2生成出來的Entity對象轉換爲Json格式

須要安裝一個Bundle,一般只須要兩步:瀏覽器

  1. 使用composer安裝這些Bundle
  2. 對Symfony2進行配置,開啓這些Bundle的支持而且作一些設置工做。

大部分Bundle經過以上兩步就可以被集成進你的項目中,安裝這些Bundle只須要修改一些配置文件而且運行一個系統命令便可。

3.數據庫建表

Symfony2默認使用Doctrine2做爲其ORM組件,而Doctrine2容許開發者經過定義一個普通的PHP類,再經過這個類生成相應的表結構(而不是像一些ORM會反過來作,先生成表結構才能生成類文件),因此咱們能夠經過熟悉的PHP語法來作建表這件事。而Doctrine2也支持Annotation,因此對於具體字段的定義咱們就能夠放在註釋裏,好比這個尋人項目中的Person表的定義文件Person.php是這樣的:

```
<?php

namespace Scourgen\PersonFinderBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Person
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Scourgen\PersonFinderBundle\Entity\PersonRepository")
 */
class Person
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\OneToMany(targetEntity="Note", mappedBy="person")
     **/
    private $person_records;

    /**
     * @ORM\OneToMany(targetEntity="Note", mappedBy="linked_person")
     **/
    private $linked_person_records;

    /**
     * @ORM\Column(type="datetime",nullable=true)
     */
    private $entry_date;

    /**
     * @ORM\Column(type="datetime",nullable=true)
     */
    private $expiry_date;

    /**
     * @ORM\Column(type="string", length=45,nullable=true)
     */
    private $author_name;

    /**
     * @ORM\Column(type="string", length=45,nullable=true)
     */
    private $author_email;

    ...
```

這是一個典型的php文件,咱們使用了Annotation語法對每一個字段的類型進行了定義。咱們甚至能夠經過Annotation語法定義表之間的外鍵關係(好比咱們在上面的源代碼中定義了person_records字段對Notes表的OneToMany關係,這種關係最終映射在數據庫裏就會體現成爲兩個表之間的外鍵)。

定義完成以後,咱們就能夠經過Doctrine2的一個命令去補全這個類文件的get和set方法:

```
php app/console doctrine:generate:entities
```

自動補全完畢以後,這個類就是一個可使用的ORM對象了,你能夠在項目的任何地方去實例化這個Person對象,而後經過setxxxx和getxxxx系列方法,像操做一個類同樣去操做數據庫裏的一條記錄。

但此時此刻,數據庫自己缺尚未生成,咱們能夠經過下面這條命令把這些類生成相對應的數據庫。

```
php app/console doctrine:schema:create
```

而此時MySQL裏一個實際可用的數據庫表就已經被生成出來了。

而當字段須要變動時,僅須要修改上面那個PHP類(person.php),而後運行doctrine2的update命令,Doctrine2會自動分析現有表結構和目標表結構的不一樣,而後生成相應的update schema語句並執行。

到此時爲止,數據庫定義工做就已經完成了,操做數據庫須要的一些類也已經準備好,咱們下面看一下如何快速把HTML頁面結構搭建起來。

3.頁面結構和layout

要開始進行業務邏輯開發以前,另一件重要的事情就是先要把網站的HTML頁面結構搭建起來。雖然業界也有一些成熟的框架,例如Bootstrap等,但因爲這些前端框架都是單獨的項目,在真實項目的實用中總會有一些誤差,要作一些適配工做,也須要工程師把兩個系統進行整合。而幸運的是,咱們可使用一個Bundle把Symfony2和Bootstrap整合在一塊兒,這個Bundle叫作MopaBootstrapBundle。

咱們看一個MopaBootstrapBundle自帶的layout片斷:

```
{% block body %}
    {% block navbar %}
    {{ mopa_bootstrap_navbar('frontendNavbar') }}
    {% endblock navbar %}

    {% block container %}
    <div class="{% block container_class %}container-fluid{% endblock container_class %}">
        {% block header %}
        {% endblock header %}

        <div class="content">
            {% block page_header %}
            <div class="page-header">
                  <h1>{% block headline %}Mopa Bootstrap Bundle{% endblock headline %}</h1>
            </div>
            {% endblock page_header %}

            {% block flashes %}
            {% if app.session.flashbag.peekAll|length > 0 %}
            <div class="row-fluid">
                <div class="span12">
                {{ session_flash() }}
                </div>
            </div>
            {% endif %}
            {% endblock flashes %}

            {% block content_row %}
            <div class="row-fluid">
                {% block content %}
                <div class="span9">
                    {% block content_content %}
                    <strong>Hier könnte Ihre Werbung stehen ... </strong>
                    {% endblock content_content %}
                </div>
                <div class="span3">
                    {% block content_sidebar %}
                    <h2>Sidebar</h2>
                    {% endblock content_sidebar %}
                </div>
                {% endblock content %}
            </div>
            {% endblock content_row %}
        </div>
```

能夠看到,MopaBootstrapBundle已經幫咱們作好了頁面佈局以及block的定位工做,咱們只須要在頁面中集成它提供的這個layout,而後再經過block複寫,把特定的區塊改爲咱們想要的樣子,就可以很快速的完成一個頁面的佈局工做。

我經過兩個步驟來具體解釋一下這是如何作到的:

1.複寫MopaBootstrapBundle自帶的layout,實現全局統一的導航條及頁腳等信息。

```
{% extends 'MopaBootstrapBundle::base.html.twig' %}

…

{% block header %}
    <div class="navbar">
        <div class="navbar-inner">
            <div class="container">
                <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <div class="nav-collapse collapse">
                    <ul class="nav">
                        <li><a href="/">主頁</a></li>
                        <li class="divider-vertical"></li>
                        <li{% if person_active is defined %} class="active"{% endif %}><a href="{{ path('seek_index') }}">我要找人</a></li>
                        <li class="divider-vertical"></li>
                        <!--li{% if note_active is defined %} class="active"{% endif %}><a href="{{ path('post_new_person') }}">提供線索</a></li-->
                    </ul>
                </div>
            </div>
        </div>
    </div>
{% endblock header %}

```

經過上面的代碼能夠看到,經過繼承並複寫MopaBootstrapBundle的base.html.twig這個layout,咱們從新定義了header這個block,因此其餘頁面均可以經過繼承這個新的layout來顯示公用的導航條。

若是對上述解釋還不太明白的讀者不妨這樣想:一個頁面的基本佈局就是一個類,經過在這個頁面佈局定義block,等因而賦予了這個類許多的方法和屬性。而一個具體的頁面就是繼承了這個類的一個實例,經過對所繼承頁面的block的從新定義,就至關於對這個類的方法和屬性作了從新的定義。而這種頁面佈局上的繼承和被繼承關係是能夠擁有無限多層的。這種繼承頁面的作法,給予了項目在頁面佈局上極大的靈活性。

而若是經過這種作法對頁面佈局層級的合理劃分(好比全站級,頻道級,欄目級,頁面級這種典型的四層劃分方法),每級都會有本身的頁面定義文件,能夠單獨進行樣式的變動和頁面的修改,但又不彼此互相影響和衝突,整個項目的頁面佈局及管理也會層級清晰,開發起來也會很是方便和高效。

2.讓咱們看一下一個最終頁面的源代碼是怎樣的:

```
{% extends 'ScourgenPersonFinderBundle::Layout.html.twig' %}

{% set seek_active=1 %}

{% block headline %}
    我要找人
{% endblock %}

{% block content_content %}
    <form action="{{ path('seek_search') }}" method="post" {{ form_enctype(form) }}>
        <fieldset>
            {{ form_rest(form)  }}
            <input type="submit"/>
        </fieldset>
    </form>
{% endblock %}
```

咱們最終獲得頁面應該是這個樣子的:

5.表單驗證及提交

細心的讀者可能已經發現了,在上一節的最後一段代碼中,咱們用了幾個form_開頭的方法,把表單生成了出來,其實這就是Symfony2表單處理功能的強大之處:支持快速搭建表單系統。

咱們都知道在應用開發過程中,大部分工做都是在處理數據,而大部分數據又都是經過表單進行交互和維護的,而在通常狀況下,表單處理會佔據一個項目至關一部分開發時間。

有沒有辦法解決這個問題呢?答案固然是有的:

既然表單字段和數據庫字段有必定的對應關係,那最理想的狀態應該是有一箇中間層可以根據數據庫表結構自動生成表單系統,容許用戶進行對數據庫的CRUD操做。

而Symfony2就可以實現以上這點,咱們經過一個例子來看看如何完成一個表單的開發。

咱們在Controller中作以下的定義:

```
    public function indexAction(Request $request)
    {
        $person = new Person();

        $form = $this->createFormBuilder($person)
            ->add('fullname', 'text')
            ->add('description', 'textarea')
            ->getForm();

        return array('form' => $form->createView());
    }
```

經過這三段代碼咱們作了三件事情:

  1. 聲明瞭一個Person對象。
  2. 將Person傳入建立表單的方法,而且聲明咱們須要用到兩個字段和對應的類型。
  3. 咱們將這個表單的view建立出來後返回給頁面。

而後咱們在頁面樣式文件裏做以下定義:

```
<form action="{{ path('post_new_person') }}" method="post" {{ form_enctype(form) }}>
        {{ form_rest(form)  }}
        <button type="submit" class="btn">提交</button>
</form>
```

而後咱們會獲得以下的頁面:

固然爲了頁面美觀,咱們也能夠對這個表單進行一些調整:增長提示文字、表單報錯信息的警告、優化一些樣式等等。而即便完成了這些優化,最終代碼其實也仍是很是短:

```
{{ form_errors(form) }}
<form action="{{ path('post_new_person') }}" method="post" {{ form_enctype(form) }}>
    <fieldset>
        {{ form_row(form.fullname,{'label':'姓名','attr':{'placeholder':'例如:王小虎'}} ) }}
        <span class="help-block">您輸入的姓名將成爲其餘人尋找的依據,請提供他的正式名字,若是沒法找到,則使用其最經常使用的名字</span>
        {{ form_row(form.description,{'label':'描述','attr':{'placeholder':'例如:在市中心小學見過他,身體健康,正在尋找媽媽。'}} ) }}
        <button type="submit" class="btn">提交</button>
        {{ form_rest(form)  }}
    </fieldset>
</form>
```

下面再來看一下如何實現表單處理的邏輯,咱們對Controller進行一些變動:

```
    /**
     * @Route("/post_new_person",name="post_new_person")
     * @Template()
     */
    public function indexAction(Request $request)
    {
        $person = new Person();

        $form = $this->createFormBuilder($person)
            ->add('fullname', 'text')
            ->add('description', 'textarea')
            ->getForm();

        if ($request->isMethod('POST')) {
            $form->bind($request);
            if ($form->isValid()) {
                $person->setSourceDate(new \DateTime('now',new \DateTimeZone('UTC')));
                $em = $this->getDoctrine()->getManager();
                $em->persist($person);
                $em->flush();
            }
        } else {

        }
        return array('form' => $form->createView());
    }
```

在新增的幾段代碼中,作了以下的事情:

  1. 判斷這個請求是不是一個POST請求,若是是的話則進入表單處理邏輯。
  2. 將表單和提交的數據進行綁定。
  3. 判斷表單是否驗證經過。
  4. 若是驗證經過,則把提交的數據持久化到數據庫裏。

再對代碼修改完以後,咱們嘗試操做一下頁面,會發現這已是一個完整可用的表單了,已經能夠經過操做表單往數據庫添加數據。這時的頁面效果以下圖所示:

那麼到這個時候,一個完整的處理表單邏輯和相關的頁面就已經被開發完畢了,咱們總共只寫了幾十行代碼而已。接下來咱們依樣畫葫蘆,把尋人平臺中的其餘表單和界面也都一一對應,應該很快就能完成。

6.API以及文檔

對於一個尋人平臺或任何一個成熟的系統來講,使用API進行數據的傳遞是必定須要的,否則就會讓網站成爲信息的孤島。在這章裏我將介紹如何使用Symfony2開發API接口,而且完成相應的文檔和API測試工具。

咱們假設須要這麼一個API:容許用戶經過HTTP協議,根據特定的PersonId獲取某個Person的數據,下面看一下實現這些功能的代碼是怎樣的。

```
    /**
     * @Route("/get_person_by_person_id/{person_id}",requirements={"person_id"="\d+"})
     * @Method("GET")
     * @ApiDoc(
     *  resource=true,
     *  description="get person by person_id",
     *  filters={
     *      {"name"="person_id", "dataType"="integer"}
     *  }
     * )
     */
    public function getPersonByPersonIdAction($person_id)
    {
        $odm = $this->getDoctrine()->getManager();
        $serializer = $this->container->get('serializer');

        $person=$odm->getRepository('ScourgenPersonFinderBundle:Person')->find($person_id);
        return new Response($serializer->serialize($person,'json'));
    }
```

經過這十幾行代碼,咱們在Annotation裏完成了如下功能:

  1. 定義了API的URL以及參數,而且限制了傳遞person_id的參數必須爲數字
  2. 限定了這個API只可以經過GET方式調用
  3. 經過ApiDoc定義了這個API的一些使用條件和說明,以便以後能夠對接口進行測試。

而在方法裏咱們則完成了如下功能:

  1. 經過Doctrine2在數據庫裏找到id是$person_id的Person記錄
  2. 獲取到Person記錄後,經過調用serializer這個service,把Person轉換爲JSON格式。
  3. 將JSON格式的數據返回給頁面請求者。

這樣一個接口的開發就已經完成了,但接口畢竟不是一個頁面,去測試一個接口須要配置各類參數,接口的返回值也須要必定的格式化纔可以讓人看得懂。因此在這裏可使用NelmioApiDocBundle去生成API的文檔和測試工具。咱們既然已經在上面的代碼中定義了ApiDoc,那麼就已經能夠直接查看自動生成的API文檔,而且使用測試工具了。

咱們在瀏覽器中打開/apc/doc這個頁面,便可看到自動生成的文檔和測試工具,以下圖所示:

在這個界面中顯示了全部的API接口以及使用方式、參數定義等等,固然也包括咱們剛纔編寫的/api/getpersonbypersonid這個接口。而文檔的內容就是咱們剛纔在Annotation裏定義的。

咱們點擊Sandbox,就可以使用API測試工具對接口進行測試:

在上圖中能夠看到,我經過API測試工具模擬了一個API請求,而且這個頁面工具會自動幫我把返回的JSON格式化,方便查看和調試。

固然讀者也能夠根據上文的例子依樣畫葫蘆去編寫其餘的API接口,因爲有了文檔和工具的支持,即便比較複雜的接口開發起來也不會耗費太多的時間,因此API的開發也算是很快就完成了。

與此同時,整個網站就已經開發完成,一個知足基本需求、界面美觀、支持API調用的尋人平臺就能夠投入使用了。

7.總結

在本項目的開發過程當中,讀者應該能夠體會到Symfony2的這種支持快速建站的特性。使用Symfony2去開發業務邏輯很是複雜的大型網站也並不困難,我將在之後的文章中向讀者一一作介紹。

本項目的源代碼已經在Github上開源,有興趣的朋友能夠直接去Github上查看全部源代碼,也能夠克隆一個項目到本地把玩研究一下。

本項目在Github上的地址是:https://github.com/scourgen/ScourgenPersonFinder

8.關於做者

洪濤,在互聯網、零售、電信領域有多年的從業經驗,曾負責中國電信域名糾錯平臺的開發,也曾爲雅虎、騰訊等大型互聯網站進行架構設計與開發工做,善於使用開源技術解決技術難題。讀者能夠經過他的微博@斯考吉恩或郵件(scourgen at gmail dot com)與他取得聯繫。


感謝陳理捷對本文的審校。

給InfoQ中文站投稿或者參與內容翻譯工做,請郵件至editors@cn.infoq.com。也歡迎你們經過新浪微博(@InfoQ)或者騰訊微博(@InfoQ)關注咱們,並與咱們的編輯和其餘讀者朋友交流。

相關文章
相關標籤/搜索