前情提要
迫不及待的要操起编辑器编写PHP的一些同志今天一定会非常高兴,今天起我们的教程终于 要做开始一些开发工作了了。我们要定义 Jobeet 的数据模型,使用 ORM 和数据库打交道 并创建应用程序的第一个模块。 但是因为 symfony 已经为我们做了不少工作,我们不需 要编写多少代码就可以拥有一个完善工作的web模块了。
关系模型
昨天我们编写的用户故事提到了我么项目的几个主要对象: 工作、推广者和分类。下面是 相应的实体关系图:
除在故事中提到的字段外,我们还为一些表加入了created_at
字段。 Symfony 会自动识
别这样的字段并在插入记录时会设置其值为系统当前时间。updated_at
字段也一样,将在记录
更新的时候设置其值为系统当前时间。
表结构
显然我们需要关系型数据库来存储工作、推广者和分类等对象。
但是 symfony 作为一个一个面向对象的框架, 我们希望在尽可能的地方都可以操作对象。例 如,我们更希望使用对象操作而不是编写SQL语句来获取数据库记录。
关系数据库的信息必须被映射到对象模型上。这可以通过ORM 工具做到。 谢天谢地, Symfony 绑定了其中两个:Propel 和 Doctrine. 在本教程中,我们使用 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格式直接翻译。
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_at
和 updated_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-sql
在 data/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: job@example.com 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: job@example.com expires_at: 2010-10-10
夹具文件用YAML格式编写,定义了模型对象并用唯一的名字打上了标签。这些标签让我们不必定义主键(这个通常是自增的也没办法预先设定的)就可以链接关联对象。例如job_sensio_labs
工作的分类是 programming
,'Programming' 就是一个分类的标记。
一个夹具文件可包含一个或多个模型的对象。
tip
注意文件名前的数字,这可以控制数据加载的先后顺序。因为其间有一些空余的数字可用,如果我们在本项目后期,需要插入一些新的夹具文件就很方便了。
在夹具文件中,你不必定义所有字段的值。如果没有,symfony会自动使用在数据库结构定义的默认值。因为symfony使用Propel加载数据到数据库中,所有内置的行为(包括设置created_at
和 updated_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
命令为工作模型JobeetJob
在frontend
应用程序中生成了名为 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已经创建了基本的验证规则。
明天见
今天就将这么多。在介绍中我就警告过你。今天我们难得写了些PHP代码,工作模型已经有一个可用的模块了,还可以优化和定制。记住。没有PHP代码也就没有BUG!
如果还有精力的话不妨读读针对模块和模型生成的代码,并试图了解他为啥能够生效。如果不行也别担心,好好睡一觉,明天我们就会讨论web框架最常用的原则, MVC 设计模式.
今天编写的代码在 Jobeet SVN 代码仓库中标记为release_day_03
(http://svn.jobeet.org/tags/release_day_03/
)。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.