“AI 技术+人才”如何成为企业增长新引擎?戳此了解>>> 了解详情
写点什么

深入浅出 Symfony2 - 如何在三小时内开发一个寻人平台

  • 2013-05-08
  • 本文字数:8149 字

    阅读完需:约 27 分钟

简介

Symfony2 是一个基于 PHP 语言的 Web 开发框架,有着开发速度快、性能高等特点。但 Symfony2 的学习曲线也比较陡峭,没有经验的初学者往往需要一些练习才能掌握其特性。

本文通过一个快速开发寻人平台的实例向读者介绍 Symfony2 框架的一些核心功能和特点。通过阅读本文,你可以通过一些具体的例子了解 Symfony2 框架的优秀特性和技术特点,从而体会到使用 Symfony2 框架支持快速网站开发这一优势。

适合人群

  • 本文适用于希望提高 PHP 语言的开发技术,或者对 Symfony2 框架有兴趣的读者。
  • 本文也适用于系统架构师和各类技术决策者。

1. 前言

在不久前的 4 月 20 日,中国四川省雅安地区发生了 7.0 级地震,累计受灾人数达到 200 多万。寻人平台在这样的情况下能够起到很大的帮助,而且,寻人平台越早上线,实用价值就越高。

Symfony2 可以用来支持大型网站的建设,在中小型网站的快速搭建和开发上也有着非常好的支持。我借由这次撰文的机会,向大家具体地分享一下我是如何在 3 个小时内基于 Symfony2 开发出来一套支持 PFIF[^1] 格式的网站寻人平台的,希望读者能够对 Symfony2 的各个组件以及功能产生一些了解。

[^1]: People Finder Interchange Format( wiki )是一个被广泛使用的开放的数据结构及标准,灾难发生后可以用该标准在不同的组织或网站间交换寻人信息,帮助失去联系的人找到彼此。

2.Bundle 的使用

Symfony2 框架以及相关社区最大的特点之一就是支持 Bundle。什么是 Bundle 呢?简单来说,Bundle 就是一种“功能”的抽象。通过把一类具体的问题抽象成一个 Bundle,可以把一个系统的逻辑进行切分:Bundle 的开发者可以专注在某类问题的解决上,而 Bundle 的使用者则可以把工作的重心放在自己的业务逻辑上。

在互联网开发领域,存在着大量可以被抽象的功能。比如用户登录系统,比如新闻评论,比如 JS/CSS 文件的压缩和合并等等。举个具体的例子,比如用户登录系统,大部分项目对于用户系统的需求其实都是差不多的,但每次要开发新产品的时候,都多多少少会去重新造一整个或一部分用户系统的轮子。而一个专门用来负责管理用户系统的 Bundle 的出现则会减轻这些项目的开发压力,提高项目质量的同时可以加快项目的整体开发速度。

Symfony2 也支持 Bundle。Symfony2 的社区有大量由社区进行维护的 Bundle,使用这些开源的 Bundle 可以让我们的项目直接拥有那部分 Bundle 所提供的功能。

以下列举了本项目中用到的一些第三方 Bundle 以及所对应负责的任务。

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/get_person_by_person_id 这个接口。而文档的内容就是我们刚才在 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 )关注我们,并与我们的编辑和其他读者朋友交流。

2013-05-08 10:129977

评论

发布
暂无评论
发现更多内容

安卓rxjava使用,现在做Android开发有前途吗?附面试题答案

欢喜学安卓

android 程序员 面试 移动开发

1000道最新整理的Java 技术考题及解答,抢先直通TMDBATJW拿高薪

钟奕礼

Java 编程 程序员 架构 面试

PHPStorm 安装Xdebug插件开启单步调试

慢慢de

win10 Xdebug PHPStorm

如何使用iMazing将iPhone的数据迁移到iPad

懒得勤快

iphone ipad 苹果 数据迁移 数据备份

app启动速度优化,分享一点面试小经验,最全的BAT大厂面试题整理

欢喜学安卓

android 程序员 面试 移动开发

Java 面试题目最全集合1000+ 大放送,能答对70%就去BATJTMD

钟奕礼

Java 编程 程序员 架构 面试

anyRTC 实时音视频打造安全合规壁垒

anyRTC开发者

网络安全 WebRTC RTC

2021金三银四面试必备?体系化带你学习:分布式进阶技术手册

比伯

Java 架构 程序人生 编程语言 技术宅

C统计量/ C statistic

Geek_Goldensikaiqi

「 最具技术影响力企业号 TOP10 」—— InfoQ 写作平台【 1 周年盛典 】

InfoQ写作社区官方

1 周年盛典 热门活动

GopherChina 2021 定了,干货满满的来了

GoCN技术社区

Go 语言 GopherChina

Kotlin @inline内联函数

季浩田 🍙

kotlin inline

区块链“数据上链”管理系统

电微13828808271

Mokito 单元测试与 Spring-Boot 集成测试

Zhang

Java 单元测试 集成测试 Mokito Spring boot starter test

上次挂在了京东(Java岗)二面不服气,这次终于拿下offer,皇天不负有心人了也是!

钟奕礼

Java 编程 程序员 架构 面试

区块链结合农业产业,平台全程溯源

电微13828808271

java中三种内存溢出错误的处理方法

Sakura

4月日更

MySQL数据库函数、DCL详解(及备份恢复操作)

若尘

MySQL 数据库 备份 DCL

区块链商品溯源平台--全流程捍卫食品安全

13530558032

解读金融高频交易不出错的金手指:分布式事务管理

华为云开发者联盟

微服务 事务 华为云 数据一致性 分布式事务管理

源中瑞区块链Baas平台--助力区块链应用落地

13530558032

什么是自然语言处理(NLP)?

澳鹏Appen

人工智能 自然语言处理 聊天机器人 nlp 自然语言

工业机器视觉系统相机如何选型?

不脱发的程序猿

工业物联网 4月日更 LabVIEW 工业视觉 工业机器视觉

架构师训练营 模块2作业

eoeoeo

架构实战营

项目管理之相关方管理

Geek_XOXO

项目管理 复盘 相关方管理

翻译:《实用的Python编程》09_00_Overview

codists

Python

MemVerge CEO表示基于大内存的基础架构将取代性能层级存储

Steven Xu

内存 存储 基础框架 傲腾

全网下载量过亿!12万字阿里内部Java面试手册有多强?

Java架构追梦

Java 架构 面试 成长笔记 阿里巴巴内部资料

DNS原理及其应用

赖猫

c++ 后台开发 网络编程 DNS 服务器开发

Kubenav: 使用手机管理你的 K8S 集群

郭旭东

Kubernetes k8s多集群管理

币安智能链智能合约Dapp系统开发技术

薇電13242772558

智能合约

深入浅出Symfony2 - 如何在三小时内开发一个寻人平台_Web框架_洪涛_InfoQ精选文章