Day: January 12, 2009

  • Going for Symfony | 第一天

    最近一段时间比较空一些:受经济危机影响,来访的客户少了;太子期末考试结束,我也不用揪着他复习功课了。所以决定把Symfony看一看。

    Symfony当前的版本是1.2,同时它提供了一个还是比较实用的Step by step教程来创建一个所谓的Job Board(教程的链接在这里——这个教程使用的数据库ORM是Propel,还有一个是基于Doctrine的,链接在这里)。这个教程的目的是在24天内,每天用差不多一个小时,总共24小时,来建立一个供用户发布、搜索、订阅工作机会以及关联网站共享工作机会信息的WEB平台。这个平台包括前后台,全部基于Symfony框架构建。 在整个24课时中,教程包含了很多重要的内容:

    • 项目、应用的创建和数据模型(包括样本数据、测试数据)的建立;
    • MVC结构,路由;
    • 单元测试和功能测试;
    • 表单的定制和测试;
    • 后台管理模块;
    • RSS Feed,Web Service;
    • 基于Lucene的全文搜索;
    • 基于jQuery的AJAX;
    • i18n和l10n,cache,plugins;
    • 发布

    可以说,内容相当广泛,而且对于一个完整的web站点来说,这些功能也是非常基本的。所以,我决定使用Symfony来重新改写我的任氏有无轩。当然,我没有那么大的野心要在24小时内完成,只是希望能通过这个过程去充分掌握Symfony并交出一份比较合理的作业。

    ============

    我的开发平台和实际的运行平台是不同的。先介绍一下这两个平台的相关配置:

    • 开发平台:Windows Vista Business + XAMPP +PHPED + NetBeans IDE + SciTE。之所以要用那么多编辑器是因为我发现各有各的优缺点。有关的比较有空的话我再另文描述;
    • 运行平台:Loongson 2F + LAMP。通过ssh进行远程控制和操作;

    在重新开发过程中,原来的站点还将继续运作,直到开发完成。在这个过程中,我还要完成数据库的迁移(从InterBase到MySQL)。

    这一系列的文章不会成为完整的一个语句接着一个语句的操作,而更注重在整个应用的开发过程。

    ==============

    由于我目前的站点版面设计已经完成,我还想再用这个设计一段时间,而且总体模块也比较固定,所以,整个站点的框架已经基本确定了。我不会太多的改动这些东西。

    好吧,让我开始!

    ==============

    第一步当然是框架的安装。Symfony框架是非常自成体系的。所有的文件都包含在一个目录之中,要发布的话也只要简单拷贝即可。所以,我在开发机上进行了一些设置——具体的设置在教程的第一天中。

    首先我要创建一个项目:

    F:/www/books>php lib/vendor/symfony/data/bin/symfony generate:project books

    然后是创建一个应用。一般而言,总是先创建前台,然后才是后台:

    symfony generate:app --escaping-strategy=on --csrf-secret=123456 frontend

    从这个命令中可以看出,symfony已经内置了防止XSS和CSRF的措施,我们要做的只是激活他们。

    这两个命令会在当前目录下创建一系列的子目录和文件,都是框架本身的要求:

    • /apps: 所有的应用文件都在这里;
    • /cache: 缓存的文件;
    • /config: 项目的配置文件。最重要的是:databases.yml,它保存了各个开发环境下(开发环境和测试环境)的数据库链接参数;schema.yml,它保存了数据库的schema;
    • /lib: 项目的公用库和根据ORM而来的类文件;
    • /log: 日志文件。在调试时会有一定的帮助;
    • /plugins: 插件。这里可以存放插件用来扩展symfony;
    • /test: 进行单元测试和功能测试的地方;
    • /web: 真正应该访问到的web根目录,包含index.php,frontend_dev.php,robots.txt,.htaccess等文件。下面还包括css,images,js,uploads等目录,分别存放对应的支持文件。

    symfony大量的使用了YAML作为其配置文件格式。作为我来说,我只是简单的将其认为是一个XML的简化版本就足够了。

    这时我们已经可以访问localhost了。我们可以直接显示index.php,也可以访问frontend_dev.php:

    [gallery link=file columns=2\]

    这两个界面没有什么大区别,只是frontend_dev界面中多了一个调试工具栏可以让你看到页面执行的一些情况,特别是log和sql部分。

    ==============

    接下来,我要开始编写我的数据库schema。在教程中,这个是第三天的任务。symfony支持两种方式:一种是从编写schema.yml开始,然后用build-sql、insert-sql任务来创建表;另一种是反过来,先在MySQL中定义好表和关联,然后用build-schema来创建schema.yml。这两种方式是等效的,看个人的喜好而定。我比较喜欢后一种。

    于是我用PMA建立了最早期的四个表:book_book(存放书籍基本信息),book_publisher(出版社信息),book_place(买书地方的信息),book_taglist(存放书籍的tag)。这些表的结构是从原来的应用(桌面和WEB)继承过来的,我也不想再更改,否则工程量更大。

    首先我要配置一下databases.yml文件,告诉它我的数据库链接参数:

    symfony config:database mysql:host=localhost;dbname=books root 123456

    然后运行build-schema任务:

    symfony propel:build-schema

    这样,我的databases.yml和schema.yml就更新了:

    #config/databases.yml
    dev:
      propel:
        param:
          classname: DebugPDO
    test:
      propel:
        param:
          classname: DebugPDO
    all:
      propel:
        class: sfPropelDatabase
          param:
            classname: PropelPDO
            dsn: 'mysql:host=localhost;dbname=books'
            username: root
            password: 123456
            encoding: utf8
            persistent: true
            pooling: true
    #config/schema.yml
    propel:
      _attributes:
        package: lib.model
        defaultIdMethod: native
      book_book:
        _attributes: { phpName: BookBook }
        id: { type: CHAR, size: '5', primaryKey: true, required: true }
        title: { type: VARCHAR, size: '200', required: true }
        author: { type: VARCHAR, size: '200', required: true }
        region: { type: VARCHAR, size: '40', required: true }
        copyrighter: { type: VARCHAR, size: '100', required: false }
        translated: { type: TINYINT, size: '1', required: false, defaultValue: '0' }
        place: { type: INTEGER, size: '11', required: false, defaultValue: '-1', foreignTable: book_place, foreignReference: id, onDelete: RESTRICT, onUpdate: CASCADE }
        publisher: { type: INTEGER, size: '11', required: false, defaultValue: '-1', foreignTable: book_publisher, foreignReference: id, onDelete: RESTRICT, onUpdate: CASCADE }
        purchdate: { type: DATE, required: false }
        price: { type: FLOAT, required: false }
        pubdate: { type: DATE, required: false }
        printdate: { type: DATE, required: false }
        ver: { type: CHAR, size: '5', required: false }
        deco: { type: CHAR, size: '6', required: false }
        kword: { type: INTEGER, size: '11', required: false }
        page: { type: INTEGER, size: '11', required: false }
        isbn: { type: CHAR, size: '13', required: false }
        category: { type: VARCHAR, size: '8', required: false }
        location: { type: CHAR, size: '2', required: false }
        intro: { type: LONGVARCHAR, required: false }
        instock: { type: TINYINT, size: '1', required: true, defaultValue: '1' }
        _indexes: { publisher: [publisher], place: [place] }
        _uniques: { uniquebook: [title, author, purchdate, ver] }

    … …

    我只列出了一部分。请注意主键、索引、复合索引的声明。这些,都使用YAML格式写成。

    然后我要用build-model任务来完成ORM的工作:

    ymfony propel:build-model

    symfony会在lib/model下创建对应于这四个表的映射类。以book表为例,有四个文件:

    • BookBook: 该类的一个实例将代表book_book表的一条记录。这个类缺省是空的;
    • BaseBookBook: 是上面类的父类。每次运行propel:build-model时,这个类就会重建,所以所有的定制应该在BookBook中进行;
    • BookBookPeer: 这个类中定义了一些静态方法,主要用来返回BookBook对象的集合。这个类缺省也是空的;
    • BaseBookBookPeer: 是上面类的父类。每次运行propel:build-model时,这个类就会重建,所以所有的定制应该在BookBookPeer中进行;

    进行了这个building-model操作后,symfony创建了很多新的类。每次在symfony中创建新的类后,都要对缓存文件进行手工清理:

    symfony cc

    =============

    接下来是样本数据的填充。symfony使用的方法很灵活,允许我们在yml中使用PHP基本结构,这样我们可以在短时间内创建大量实际有用的数据。例如:

    #data/fixtures/030_book.yml
    BookBook:
    <?php for ($i = 1; $i <= 100; $i++): ?>
      book_<?php echo $i ?>:
        id:  <?php echo $i.n ?>
        title: book_<?php echo $i.n ?>
        author: author_<?php echo $i.n ?>
        region: region_<?php echo $i.n ?>
        place: 1
        publisher: 1
        instock: true
    <?php endfor; ?>
    #data/fixtures/040_taglist.yml
    BookTaglist:
    <?php for ($i = 1; $i <= 100; $i++): ?>
      tag_<?php echo $i ?>:
        id:  book_<?php echo $i ?>
        tag:   tag1_<?php echo $i ?>
    <?php endfor; ?>
    <?php for ($i = 1; $i <= 100; $i++): ?>
      tag2_<?php echo $i ?>:
        id:  book_<?php echo $i ?>
        tag:   tag2_<?php echo $i ?>
    <?php endfor; ?>

    这里我只说明三个要点:

    1. data/fixtures目录下存放了样本数据的yml文件。各个文件的前缀010/020/030/040等代表了加载的顺序。这样我们就可以在填充样本数据的时候满足引用一致性了;
    2. 对于taglist表而言,它和book表是多对一的关系。我用了两个循环来为每本书加入两个tag。其中引用到的id并不是一个简单的数字或字符串,我直接引用了我在030中创建的book对象的示例。这样做的好处是对于那些自动增长的字段,我们可以暂时不关心其父表中的id到底是什么。symfony会自动获得;
    3. 请注意PHP代码的格式,它暂时脱离了缩进。另外,echo后面需要加个回车符号,这个是为了保证yml文件的格式。

    然后我们通过data-load任务来填充数据:

    symfony propel:data-load

    再次进入PMA后,我们可以看到所有数据已经填充完毕。 好了,作为第一天,就到这里吧。