Caution: You are browsing the legacy symfony 1.x part of this website.

第三天:数据模型

前情提要

迫不及待的要操起编辑器编写PHP的一些同志今天一定会非常高兴,今天起我们的教程终于 要做开始一些开发工作了了。我们要定义 Jobeet 的数据模型,使用 ORM 和数据库打交道 并创建应用程序的第一个模块。 但是因为 symfony 已经为我们做了不少工作,我们不需 要编写多少代码就可以拥有一个完善工作的web模块了。

关系模型

昨天我们编写的用户故事提到了我么项目的几个主要对象: 工作、推广者和分类。下面是 相应的实体关系图:

Entity relationship diagram

除在故事中提到的字段外,我们还为一些表加入了created_at 字段。 Symfony 会自动识 别这样的字段并在插入记录时会设置其值为系统当前时间。updated_at 字段也一样,将在记录 更新的时候设置其值为系统当前时间。

表结构

显然我们需要关系型数据库来存储工作、推广者和分类等对象。

但是 symfony 作为一个一个面向对象的框架, 我们希望在尽可能的地方都可以操作对象。例 如,我们更希望使用对象操作而不是编写SQL语句来获取数据库记录。

关系数据库的信息必须被映射到对象模型上。这可以通过ORM 工具做到。 谢天谢地, Symfony 绑定了其中两个:PropelDoctrine. 在本教程中,我们使用 Propel。

ORM 需要数据表信息及其相互关系来创建相关的类。有两种方式来描述 schema: 窥探 已有数据库或者手动创建。

note

有些工具允许你用可视化环境下创建数据库(例如 Fabforce's Dbdesigner) 并直接生成 schema.xml (可用 DB Designer 4 TO Propel Schema Converter).

因为数据库并不存在而且 Jobeet 数据库的结构也暂不是很明朗,让我们通过手动创建 schema 文件吧。 编辑空文件config/schema.yml:

# config/schema.yml
propel:
  jobeet_category:
    id:           ~
    name:         { type: varchar(255), required: true }
 
  jobeet_job:
    id:           ~
    category_id:  { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true }
    type:         { type: varchar(255) }
    company:      { type: varchar(255), required: true }
    logo:         { type: varchar(255) }
    url:          { type: varchar(255) }
    position:     { type: varchar(255), required: true }
    location:     { type: varchar(255), required: true }
    description:  { type: longvarchar, required: true }
    how_to_apply: { type: longvarchar, required: true }
    token:        { type: varchar(255), required: true, index: unique }
    is_public:    { type: boolean, required: true, default: 1 }
    is_activated: { type: boolean, required: true, default: 0 }
    email:        { type: varchar(255), required: true }
    expires_at:   { type: timestamp, required: true }
    created_at:   ~
    updated_at:   ~
 
  jobeet_affiliate:
    id:           ~
    url:          { type: varchar(255), required: true }
    email:        { type: varchar(255), required: true, index: unique }
    token:        { type: varchar(255), required: true }
    is_active:    { type: boolean, required: true, default: 0 }
    created_at:   ~
 
  jobeet_job_affiliate:
    job_id:       { type: integer, foreignTable: jobeet_job, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }
    affiliate_id: { type: integer, foreignTable: jobeet_affiliate, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }

tip

如果你决定通过编写SQL语句来创建表,你可以通过执行propel:build-schema来 生成相应的 schema.yml 配置文件。

schema 是实体关系图的YAML格式直接翻译。

sidebar

YAML 文档格式

根据 YAML 官方网站资料, YAML 的意思是"一种面向所有编程语言的 对人友好的数据序列化标准"

另一方面来说, YAML 是描述数据的简单语言 (strings, integers, dates, arrays, 和 hashes).

YAML 中,通过缩进来表示结构关系,系列元素通过短横线表示,哈希中的键值对通过冒号分割表示。 YAML还有用来描述短短几行的结构的简短语法,数组可表示为 [] 哈希可表示为 {}.

如果你还不熟悉YAML格式,symfony 框架中广泛采用YAML格式来编写配置文件, 后面一定用的上。

schema.yml 文件包含了所有的表和字段的描述信息。每个字段都通过如下信息描述:

  • type: 字段类型 (boolean, tinyint, smallint, integer, bigint, double, float, real, decimal, char, varchar(size), longvarchar, date, time, timestamp, blob, 和 clob)
  • required: 设为 true 来要求必须填写该字段
  • index: 设为 true 来为该字段创建索引,或者设为 unique 在该字段上创建唯一索引。

对于设置为 ~ (id, created_at, 和 updated_at)的字段,symfony会探测最适合的 配置(id是主键字段,created_atupdated_at是时间戳....)

note

onDelete 属性定义了外键的ON DELETE行为。Propel 只是 CASCADE, SETNULL, RESTRICT等几种。例如,删除一条 job 记录后,jobeet_job_affiliate 中所有相关 记录也会自动通过数据库删除。如果底层的数据库引擎不支持该功能,Propel可以做到。

数据库

symfony 框架支持所有支持PDO的数据库 (MySQL, PostgreSQL, SQLite, Oracle, MSSQL, ...). PDO是PHP自带的数据库抽象层。

这个教程我们就以 MySQL 为例吧:

$ mysqladmin -uroot -pmYsEcret create jobeet

note

如果可以请随意选择其他数据库引擎,我们通过ORM编写的代码

我们需要通知symfony使用这个数据库:

$ php symfony configure:database "mysql:host=localhost;dbname=jobeet" root mYsEcret

命令configure:database有三个参数: PDO DSN、访问数据库的用户名和密码。 如果你的开发服务器没有设置密码,可省略密码。

note

命令configure:database 可将数据库配置信息保存到配置文件config/databases.yml里。 捏也可以不运行命令,直接手动修改该文件。

ORM

由于前面我们在 schema.yml 文件中编写了数据库描述,我们可以用 Propel 内置的命令来生成SQL语句创建数据表:

$ php symfony propel:build-sql

命令propel:build-sqldata/sql目录生成SQL语句,并针对我们配置的数据库引擎做了适当优化:

# snippet from data/sql/lib.model.schema.sql
CREATE TABLE `jobeet_category`
(
  `id` INTEGER  NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(255)  NOT NULL,
  PRIMARY KEY (`id`)
)Type=InnoDB;

需要运行propel:insert-sql命令来在数据库中创建表:

$ php symfony propel:insert-sql

As the task drops the current tables before re-creating them, you are required to confirm the operation. You can also add the --no-confirmation option to bypass the question, which is useful if you want to run the task from within a non-interactive batch: 因为该命令会删除原表再重新创建,此操作会提醒你确认后执行。你也可以 用 --no-confirmation 参数来忽略此提醒,当使用非交互的批处理文件中运行该命令时时非常有用:

$ php symfony propel:insert-sql --no-confirmation

tip

和任何命令行工具一样,symfony可以接受参数和选项。每个命令都有内置的帮助信息。你可以通过运行help命令来查看:

$ php symfony help propel:insert-sql

帮助信息列出了所有可能的参数和选项,以及他们的默认值,并给出了一些很有用的示例。

ORM还可生成映射数据表和对象间关系的PHP类:

$ php symfony propel:build-model

propel:build-model 命令 在lib/model目录生成用于和数据库交互的PHP文件

浏览生成的文件,你可能会发现 Propel针对每个表都生成了类,例如 jobeet_job 表:

  • JobeetJob: 此类的一个对象代表jobeet_job表中的一条记录。此类默认为空。
  • BaseJobeetJob: JobeetJob类的父类。每当运行propel:build-model命令时,这个类都会被覆盖。因此,所有的自定义操作都必须在JobeetJob类中编写.
  • JobeetJobPeer: 此类定义了一系列的的静态方法,大多都是返回一系列JobeetJob对象的。此类默认为空。
  • BaseJobeetJobPeer: JobeetJobPeer的父类,每当运行propel:build-model命令时这个类都会被覆盖,因此,所有的自定义操作都必须在JobeetJobPeer类中编写.

The column values of a record can be manipulated with a model object by using some accessors (get*() methods) and mutators (set*() methods): 通过模型对象操作记录时,可以通过访问方法(get*() 类方法)和设值方法(set*() 类方法)来操作字段值。 [php] $job = new JobeetJob(); $job->setPosition('Web developer'); $job->save();

echo $job->getPosition();

$job->delete();

还可以直接通过链接对象来定义外键。

$category = new JobeetCategory();
$category->setName('Programming');
 
$job = new JobeetJob();
$job->setCategory($category);

propel:build-all 命令是本小节及以后都会运行的快捷方式。因此,运行此命令为我们的Jobeet模型类生成表单和验证器吧。

$ php symfony propel:build-all

你会在今天的最后看到验证器,表单则会在第10天的教程中详细讲解其美妙的细节。

tip

propel:build-all-load命令是 propel:build-all命令及其后续命令 propel:data-load 组合而成的快捷方式。

在后面你可以发现,symfony会自动为你加载PHP类文件,这意味着你不再需要在代码中写 require 语句了。 这是symfony自动为开发者做处理的杂事儿中的一点。但这也有不好的一面,每新增一个类都得清空一下symfony的缓存。 由于propel:build-model 命令创建了许多新类,让我们先清理缓存:

 $ php symfony cache:clear

tip

symfony命令由命名空间和任务名称组成。只要不和其他命令冲突,都可以尽可能的简写。因此,下面这条命令等价于 cache:clear:

$ php symfony cc

初始化数据

我们已经在数据库中创建了数据表,但还没有数据。作为网络应用程序,我们有三种类型的数据:

  • 初始化数据: 是应用程序正常运行所必须的数据。例如。Jobeet需要一些初始分类。没有的话就没有人能发布工作。我们还需要一个可以登录到后台管理员用户。
  • 测试数据: 测试数据用于测试应用程序。作为开发者,你需要编写测试用例以确保应用程序按照用户故事里预期的那样正常运行。最好的方式就是编写用例进行自动化测试。每次运行测试用例的时候,都需要清空数据库并加载一些测试数据。
  • 用户数据: 用户数据是应用程序正常运行时用户创建的数据。

每一次symfony创建数据库表结构时,所有的数据都会丢失。要向数据库中填充初始化数据,我们可以编写PHP脚本或者通过mysql 程序运行一些SQL语句。由于要经常用到, symfony里面有更好的方式:在 data/fixtures/目录下创建 YAML 文件,然后用 propel:data-load 命令加载到数据库中: [yml] # data/fixtures/010_categories.yml JobeetCategory: design: { name: Design } programming: { name: Programming } manager: { name: Manager } administrator: { name: Administrator }

# data/fixtures/020_jobs.yml
JobeetJob:
  job_sensio_labs:
    category_id:  programming
    type:         full-time
    company:      Sensio Labs
    logo:         /uploads/jobs/sensio_labs.png
    url:          http://www.sensiolabs.com/
    position:     Web Developer
    location:     Paris, France
    description:  |
      You've already developed websites with symfony and you want to work
      with Open-Source technologies. You have a minimum of 3 years
      experience in web development with PHP or Java and you wish to
      participate to development of Web 2.0 sites using the best
      frameworks available.
    how_to_apply: |
      Send your resume to fabien.potencier [at] sensio.com
    is_public:    true
    is_activated: true
    token:        job_sensio_labs
    email:        [email protected]
    expires_at:   2010-10-10

  job_extreme_sensio:
    category_id:  design
    type:         part-time
    company:      Extreme Sensio
    logo:         /uploads/jobs/extreme_sensio.png
    url:          http://www.extreme-sensio.com/
    position:     Web Designer
    location:     Paris, France
    description:  |
      Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
      eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
      enim ad minim veniam, quis nostrud exercitation ullamco laboris
      nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
      in reprehenderit in.

      Voluptate velit esse cillum dolore eu fugiat nulla pariatur.
      Excepteur sint occaecat cupidatat non proident, sunt in culpa
      qui officia deserunt mollit anim id est laborum.
    how_to_apply: |
      Send your resume to fabien.potencier [at] sensio.com
    is_public:    true
    is_activated: true
    token:        job_extreme_sensio
    email:        [email protected]
    expires_at:   2010-10-10

夹具文件用YAML格式编写,定义了模型对象并用唯一的名字打上了标签。这些标签让我们不必定义主键(这个通常是自增的也没办法预先设定的)就可以链接关联对象。例如job_sensio_labs 工作的分类是 programming,'Programming' 就是一个分类的标记。

一个夹具文件可包含一个或多个模型的对象。

tip

注意文件名前的数字,这可以控制数据加载的先后顺序。因为其间有一些空余的数字可用,如果我们在本项目后期,需要插入一些新的夹具文件就很方便了。

在夹具文件中,你不必定义所有字段的值。如果没有,symfony会自动使用在数据库结构定义的默认值。因为symfony使用Propel加载数据到数据库中,所有内置的行为(包括设置created_atupdated_at 字段的值)或者你自己加入模型类中自定义行为都是可用的。

只需简单的运行propel:data-load 命令就可加载初始化数据到数据中:

$ php symfony propel:data-load

在浏览器中看看实际效果

我们使用了很多的命令行操作,那一点也不让人激动,特别是对于web项目来说。现在我们万事具备只需要创建页面来和数据库交互。

让我们看看怎样显示工作列表,怎样编辑已有工作以及怎样删除工作。和第一天解释的那样,symfony项目由应用程序组成。每个应用程序下面又包括若干个模块。模块可以是实现应用程序功能(如API模块)的一套自包含的PHP代码,也可以使用户针对模型可以做的一系列的的操作的集合(例如工作模块)。

Symfony 能够根据模型自动创建模块,提供基本的操作功能。

$ php symfony propel:generate-module --with-show --non-verbose-templates frontend job JobeetJob

propel:generate-module 命令为工作模型JobeetJobfrontend应用程序中生成了名为 job的模块。和大多数symfony命令一样,它在apps/frontend/modules/job 下创建了一些文件和目录

目录 说明
actions/ 模块的操作
templates/ 模块的模板

actions/actions.class.php 文件定义了 job模块所有可用的操作:

动作名 说明
index 显示数据表中的记录
show 显示指定记录的字段
new 显示一个创建新记录的表单
create 创建一条新的记录
edit 显示修改一条已有记录的表单
update 根据用户提交的数据更新记录
delete 从数据表中删除指定的记录

现在你可以在浏览器中测试工作模块

 http://jobeet.localhost/frontend_dev.php/job

工作模块

如果你试着编辑一个工作,因为symfony需要分类的文字表示,这回触发 一个异常。PHP对象表示方法可以通过定义魔法方法__toString() 来实现。分类的文字表示应该在JobeetCategory模型类文件中定义。

// lib/model/JobeetCategory.php
class JobeetCategory extends BaseJobeetCategory
{
  public function __toString()
  {
    return $this->getName();
  }
}

现在每次symfony需要分类的文字表示时都会调用__toString()方法来返回分类的名称。因为我们差不多在所有的模型类中都要用到文字表示,因此我们为每个类都定义一个__toString()方法:

// lib/model/JobeetJob.php
class JobeetJob extends BaseJobeetJob
{
  public function __toString()
  {
    return sprintf('%s at %s (%s)', $this->getPosition(), $this->getCompany(), $this->getLocation());
  }
}
 
// lib/model/JobeetAffiliate.php
class JobeetAffiliate extends BaseJobeetAffiliate
{
  public function __toString()
  {
    return $this->getUrl();
  }
}

现在你可以创建和修改工作了。试着空缺必填字段或者填错日期看看,对,通过窥探数据库结构,symfony已经创建了基本的验证规则。

validation

明天见

今天就将这么多。在介绍中我就警告过你。今天我们难得写了些PHP代码,工作模型已经有一个可用的模块了,还可以优化和定制。记住。没有PHP代码也就没有BUG!

如果还有精力的话不妨读读针对模块和模型生成的代码,并试图了解他为啥能够生效。如果不行也别担心,好好睡一觉,明天我们就会讨论web框架最常用的原则, MVC 设计模式.

今天编写的代码在 Jobeet SVN 代码仓库中标记为release_day_03 (http://svn.jobeet.org/tags/release_day_03/)。