symfony 第一案例

哈,你想试试?咱们一起来用一小时建立一个功能完善的网站吧! 随便你怎么叫它。 图书销售程序?好,或者其他什么的。一个博格! 这个不错。开始吧!

我们假设你已经安装了 apache/PHP5 并且在本地 (localhost)启动动了。 你还需要默认编译在 PHP5 内的 SQLite 扩展。 但是, PHP5.1.0之后你需要在 php.ini 中手动将它激活(激活方法请查阅 这里)。

安装 symfony 和初始化项目

为了快捷,我们将使用 symfony 砂箱(sandbox) (你也可以下载 最终源代码)。 它就是一个包含所有需要的库文件的空白 symfony 项目。 砂箱相对于其他安装方法的优势就在于你可以立即体验 symfony。

从这里下载: sf_sandbox.tgz,解压到你的网站根目录。 可以从其中的 readme 文件获得更多的信息。 其文件结构应该是这样的:

   doc/
    lib/
      model/
    log/
    plugins/
    test/
    web/
      css/
      images/
      js/

这是包含一个 frontend 应用sf_sandbox 项目。键入如下 URL 测试砂箱:

http://localhost/sf_sandbox/web/index.php/

你应该可以看到欢迎(Congratulation)页面

Congratulations

你也可以把 symfony 安装在其他文件夹中并在你的网站服务器上设置一个虚拟主机或别名。symfony 权威指南中有详述的章节 symfony 安装symfony 目录结构.

初始化数据模型

博客需要处理帖子(post),而且要求能够加以评注。 在 sf_sandbox/config/ 目录下建立 schema.yml 文件,将如下数据模型复制到该文件:

propel:
  weblog_post:
    _attributes: { phpName: Post }
    id:
    title:       varchar(255)
    excerpt:     longvarchar
    body:        longvarchar
    created_at:
  weblog_comment:
    _attributes: { phpName: Comment }
    id:
    post_id:
    author:      varchar(255)
    email:       varchar(255)
    body:        longvarchar
    created_at:

改配置文件使用 YAML 语法。这是一种类似 XML 以缩进方式表述树状结构的简单语言。 而且,其读写速度超过 XML。 唯一需要注意的是,缩进是有含义的,而且制表符被禁止使用,因此要记住使用空格进行缩进。你将在 configuration chapter中了解更多关于 YAML 配置的内容。

该 schema 描述了 weblog 的两个表的结构。 PostComment 是要生成的两个相关类的名字。 保存这个文件,打开命令行,转到 sf_sandbox/ 目录并键入:

$ php symfony propel-build-model

Note: 在调用 symfony 命令时,应确保是在项目的根目录 (sf_sandbox/)。

sf_sandbox/lib/model/ 目录中一些类被建立。 一些用以确保无需书写 SQL 语句就可以从面向对象代码访问关系数据库的对象关系映射类。 symfony 使用 Propel 类库实现这一点。 我们称这些类为 模型(model) (模型一章中有更进一步的内容)。

现在键入如下命令行:

$ php symfony propel-build-sql

一个 lib.model.schema.sql 文件被新建在 sf_sandbox/data/sql/ 目录中。 该 SQL 语句可以用于相同表结构的数据库初始化。 你可以通过命令行或者网页界面在 MySQL 中新建一个数据库 (在 模型(model)一章中有所描述)。 所幸的是 symfony 砂箱已经配置为使用 SQLite 文件, 所以不需要数据库初始化。默认情况下, sf_sandbox 项目将使用 sf_sandbox/data/ 目录下名为 sandbox.db 的数据库。 基于 SQL 文件建立数据库需要键入如下命令行:

$ php symfony propel-insert-sql

Note: 不要为出现警告而担心,这很正常。 insert-sql 命令在添加你的 lib.model.schema.sql 之前先要删除已有数据库,而此时还没有任何已有数据库。

建立应用脚手架(scaffolding)

网址的基础功能是能够新建(Create)、调用(Retrieve)、更新(Update)和删除(Delete),简称为 CRUD 帖子和评论。鉴于你是 symfony 新手,你不必一行行地书写 symfony 代码 ,而是建立一个脚手架(scaffolding)并在其基础上根据需要进行修改。 symfony 可以翻译数据模型并自动生成 CRUD 界面:

$ php symfony propel-generate-crud frontend post Post
$ php symfony propel-generate-crud frontend comment Comment
$ php symfony clear-cache

在 *nix 系统上,你需要更改一些权限:
$ chmod 777 data
$ chmod 777 data/sandbox.db

你现在已经有了两个模块(postcomment),可以用于控制 PostComment 类的对象。 一个 模块(module) 通常由内容相似的一个或者一组页面组成。你的新模块位于 sf_sandbox/apps/frontend/modules/ 目录,可以通过如下 URL 地址访问之:

http://localhost/sf_sandbox/web/frontend_dev.php/post
http://localhost/sf_sandbox/web/frontend_dev.php/comment

请随意向网志中添加帖子,以便不那么空荡荡。

post CRUD

请查阅scaffolding和 symfony 项目结构(project, application, module)了解更多内容。 .

Note: 在上面的 URL 地址中,主脚本在 symfony 中被称为 前台控制器(front controller),是由 index.php 演变为 frontend_dev.php 得来。 两个脚本都用于访问相同的应用 (frontend),但是所应用的环境不同。 使用 frontend_dev.php 是以 开发环境 访问应用,可以获得方便的开发工具的支持,例如查错屏幕右上角(debug)工具栏和实时配置引擎。这也是其页面处理速度比速度方面得到优化的产品环境前台控制器 index.php 慢的原因所在。如果你希望使用产品环境,那么在 URl 中以 index.php/ 替换 frontend_dev.php/,但在出修改后不要忘记清除缓存:

$ php symfony clear-cache

http://localhost/sf_sandbox/web/index.php/

请参阅 环境介绍。

布局更改

为了在两个模块间切换,网志需要一些全局导航。

更改全局模板(global template) sf_sandbox/apps/frontend/templates/layout.php 并将 <body> 内的内容更改为:

[php]
<div id="container" style="width:600px;margin:0 auto;border:1px solid grey;padding:10px">
  <div id="navigation" style="display:inline;float:right">
    <ul>
      <li><?php echo link_to('List of posts', 'post/list') ?></li>
      <li><?php echo link_to('List of comments', 'comment/list') ?></li>
    </ul>
  </div>
  <div id="title">
    <h1><?php echo link_to('My first symfony project', '@homepage') ?></h1>
  </div>

  <div id="content" style="clear:right">
    <?php echo $sf_data->getRaw('sf_content') ?>
  </div>
</div>

请原谅这里低劣的设计和内嵌(inner-tag)CSS 的使用,但一小时毕竟太短了。

改变布局后的 post CRUD

你已经上路啦,你可以更改页面标题。 编辑应用的配置文件(sf_sandbox/apps/frontend/config/view.yml),找到 title 键并做如下修改:

default: http_metas: content-type: text/html

metas:
  title:        The best weblog ever
  robots:       index, follow
  description:  symfony project
  keywords:     symfony, project
  language:     en

主页(home page)也需要更改。 它目前使用的是位于架构(framework)而不在应用之内的默认模块的默认模板。取而代之,你需要建立一个 main 模块:

$ php symfony init-module frontend main

默认情况,index 动作显示默认的欢迎页面。 编辑 sf_sandbox/apps/frontend/modules/main/actions/actions.class.php 删除 executeIndex() 方法中的内容可将其移除:

[php]
public function executeIndex()
{
}

编辑 sf_sandbox/apps/frontend/modules/main/templates/indexSuccess.php 文件显示更合适的欢迎信息:

[php]
<h1>Welcome to my swell weblog</h1>
<p>You are the <?php echo rand(1000,5000) ?>th visitor today.</p>

现在,你需要告诉 symfony 在请求主页时执行哪个动作。 做到这一点,需要编辑 sf_sandbox/apps/frontend/config/routing.yml ,对 homepage 规则作如下修改:

[yml]
homepage:
  url:   /
  param: { module: main, action: index }

请求主页查看结果:

http://localhost/sf_sandbox/web/frontend_dev.php/

New home page

继续,开始使用新的 app:建立一个新的测试帖子,并追加一条测试评论。

请查阅关于 视图和模板得更多内容。

由动作向模板传递数据

这很快,不是吗?现在该将评论帖子结合起来,在每条评论之后显示其评论。

首先,你为帖子模板准备期评论数据。在 symfony 中,根据其逻辑规定应在动作中实现。 编辑动作文件sf_sandbox/apps/frontend/modules/post/actions/actions.class.php,在 executeShow() 方法中添加最后的 4 行:

[php]
public function executeShow()
{
  $this->post = PostPeer::retrieveByPk($this->getRequestParameter('id'));
  $this->forward404Unless($this->post);

  $c = new Criteria();
  $c->add(CommentPeer::POST_ID, $this->getRequestParameter('id'));
  $c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
  $this->comments = CommentPeer::doSelect($c);
}

Criteria-Peer 对象是 Propel 面向对象映射的内容。 简单地讲,这四行将处理一条对 Comment 表的 SQL query,从而获得与当前帖子(通过 URL 中的 id 参数确定)相关的评论。 动作中 $this->comments 行将访问相应模板的 $comments 变量。现在,更改 post 显示模板sf_sandbox/apps/frontend/modules/post/templates/showSuccess.php 在结尾处添加:

[php]
...
<?php use_helper('Text', 'Date') ?>

<hr />
<?php if ($comments) : ?>
  <p><?php echo count($comments) ?> comment<?php if (count($comments) > 1) : ?>s<?php endif; ?> to this post.</p>
  <?php foreach ($comments as $comment): ?>
    <p><em>posted by <?php echo $comment->getAuthor() ?> on <?php echo format_date($comment->getCreatedAt()) ?></em></p>
    <div class="comment" style="margin-bottom:10px;">
      <?php echo simple_format_text($comment->getBody()) ?>
    </div>
  <?php endforeach; ?>
<?php endif; ?>

该也是用了 symfony 提供的新 PHP 函数(format_date()simple_format_text()),它们通常需要更多的时间和代码来帮助你完成一些任务,所以被称作'助手(helpers)' 为你的首个帖子添加评论,然后通过在列表中点击或者直接键入下面的 URL ,查看这第一个帖子:

http://localhost/sf_sandbox/web/frontend_dev.php/post/show?id=1

Comment under post

很好!

请查阅命名约定(conventions)获得更多连接动作和模板的信息。

添加一条与另一个表相关的记录

在添加评论时,你可以选择相关帖子的id。 这个方式不够友好,让我们修改一下吧,并确保添加一条评论后用户回到原贴子。

首先,在刚刚修改过的 modules/post/templates/showSuccess.php 模板最下面添加这样一行:

[php]
<?php echo link_to('Add a comment', 'comment/create?post_id='.$post->getId()) ?>

link_to() 助手在这里建立了一个指向 comment 模块 create行为的超链接,这样你就可以直接通过帖子内容页点击链接添加评论了。 之后,打开 modules/comment/templates/editSuccess.php 页面将如下内容:

[php]
<tr>
  <th>Post:</th>
  <td><?php echo object_select_tag($comment, 'getPostId', array (
  'related_class' => 'Post',
)) ?></td>
</tr>

替换为

[php]
<?php if ($sf_params->has('post_id')): ?>
  <?php echo input_hidden_tag('post_id',$sf_params->get('post_id')) ?> 
<?php else: ?>
  <tr>
    <th>Post*:</th>
    <td><?php echo object_select_tag($comment, 'getPostId', array('related_class' => 'Post')) ?></td>
  </tr>
<?php endif; ?>

comment/create 页面的表单在提交(CRUD的默认行为)时指向 comment/update 动作,该动作将转向到 comment/show对于表单而言意味着,贴子的评论被提交后将先是评论的内容。 但显示带有评论的贴子更好些。 那么就打开 modules/comment/actions/actions.class.php 文件,找到 executeUpdate() 方法。注意 created_at 字段不由动作来定义: symfony 在记录被提交时会自动地将 created_at 设置为系统时间。 动作的最终转向必须被修改。改为:

[php]
public function executeUpdate ()
{
  if (!$this->getRequestParameter('id', 0))
  {
    $comment = new Comment();
  }
  else
  {
    $comment = CommentPeer::retrieveByPk($this->getRequestParameter('id'));
    $this->forward404Unless($comment);
  }

  $comment->setId($this->getRequestParameter('id'));
  $comment->setPostId($this->getRequestParameter('post_id'));
  $comment->setAuthor($this->getRequestParameter('author'));
  $comment->setEmail($this->getRequestParameter('email'));
  $comment->setBody($this->getRequestParameter('body'));

  $comment->save();

  return $this->redirect('post/show?id='.$comment->getPostId());
}

用户现在可以像一个帖子添加评论并在添加后回到这个贴子。 你要网志?你已经有了。

请参阅 动作的进一步介绍。

表单验证

访问者可以添加评论,但如果他们提交的是没有数据的空白表单呢?你的数据库将被搞乱。 为了避免这种情况,在sf_sandbox/apps/frontend/modules/comment/validate/ 目录下建立一个名位 update.yml 的文件并写入:

methods:
  post:           [author, email, body]
  get:            [author, email, body]

fillin:
  enabled:       on

names:
  author:
    required:     Yes
    required_msg: The name field cannot be left blank

  email:
    required:     No
    validators:   emailValidator

  body:
    required:     Yes
    required_msg: The text field cannot be left blank

emailValidator:
  class:          sfEmailValidator
  param:
    email_error:  The email address is not valid.

Note: 注意不要将上面每行之前的4个额外的空格拷贝上,因为 YAML 解析器会读不懂的。 该文件的第一个字母必须是 'methods' 的 'm'。

激活 fillin 可以在表单验证失败而需重新显示表单时将之前输入的值填入其中。 names 声明为表单每个输入设置验证规则。

在不加干涉的情况下,在出错时控制器将自动地转向到 updateError.php 模板,但最好能在出错时显示带有错误信息的表单。 达到这一点需要在modules/comment/actions/actions.class.php 行为文件中添加 handleErrorUpdate 方法:

[php]
public function handleErrorUpdate()
{
  $this->forward('comment', 'create');
}

现在是最后一步,再次打开 modules/comment/templates/editSuccess.php 模板并在其顶部添加:

[php]
<?php if ($sf_request->hasErrors()): ?>  
  <div id="errors" style="padding:10px;">
    Please correct the following errors and resubmit:
    <ul>
    <?php foreach ($sf_request->getErrors() as $error): ?>
      <li><?php echo $error ?></li>
    <?php endforeach; ?>
    </ul>
  </div>
<?php endif; ?>

你现在有了一个完善的表单。

Form validation

请参阅 表单验证中的更多内容。

改变 URL 外观

你注意到 URL 的呈现方式了吗?你可以令他们对于用户和搜索引擎更加友好。 让我们使用贴子标题作为帖子 URL 吧!

现在存在的问题是帖子的标题中可能会包含空格等的特殊字符。 如果你只是 escape 它们, URL 中会显示一些 %20之类的字符,所以最好为 Post 对象添加一个用以获得处理过的、干净的标题的新方法。 实现这一点不要编辑位于 sf_sandbox/lib/model/ 目录的 Post.php 文件,添加如下方法:

[php]
public function getStrippedTitle()
{
  $result = strtolower($this->getTitle());

  // strip all non word chars
  $result = preg_replace('/\W/', ' ', $result);

  // replace all white space sections with a dash
  $result = preg_replace('/\ +/', '-', $result);

  // trim dashes
  $result = preg_replace('/\-$/', '', $result);
  $result = preg_replace('/^\-/', '', $result);

  return $result;
}

现在你可以为 post 模块创建一个 permalink 动作。 将如下方法添加到 modules/post/actions/actions.class.php

[php]
public function executePermalink()
{
  $posts = PostPeer::doSelect(new Criteria());
  $title = $this->getRequestParameter('title');
  foreach ($posts as $post)
  {
    if ($post->getStrippedTitle() == $title)
    {
      break;
    }
  }
  $this->forward404Unless($post);

  $this->getRequest()->setParameter('id', $post->getId());

  $this->forward('post', 'show');
}

帖子列表中每个帖子不必再链接 show 方法,可以改为链接 permalink 方法了。 在 modules/post/templates/listSuccess.php 中删除 id 表头和单元格,并将 Title 单元格由:

[php]
<td><?php echo $post->getTitle() ?></td>

改为:

[php]
<td><?php echo link_to($post->getTitle(), 'post/permalink?title='.$post->getStrippedTitle()) ?></td>

还差一步:编辑位于 sf_sandbox/apps/frontend/config/routing.yml 文件,在顶部添加如下规则:

list_of_posts:
  url:   /latest_posts
  param: { module: post, action: list }

post:
  url:   /weblog/:title
  param: { module: post, action: permalink }

现在重新访问你的应用并观察 URL 的变化。

Routed URLs

请查阅 智能 URLs得更多内容。

清理前台(frontend)

如果这是一个网志,那么每个人都有权发表帖子。 但这并不是你所期望的,对吗? 对,让我们清理一下我们的模板。

modules/post/templates/showSuccess.php 模板中,通过删除下面这行移除 'edit' 链接:

[php]
<?php echo link_to('edit', 'post/edit?id='.$post->getId()) ?>

modules/post/templates/listSuccess.php 模板中作相同的工作,删除:

[php]
<?php echo link_to('create', 'post/create') ?>

你还需要删除 modules/post/actions/actions.class.php 中的如下方法:

* executeCreate
* executeEdit
* executeUpdate
* executeDelete

好了,读者不能发表帖子了。

生成后台(backend)

为了你能书写帖子,我们需要键入下列命令生成后台应用(还是从 sf_sandbox 项目目录运行):

$ php symfony init-app backend
$ php symfony propel-init-admin backend post Post
$ php symfony propel-init-admin backend comment Comment

这次,我们用 管理生成器(admin generator),它比 CRUD 生成器提供了更多的功能和个性化空间。

重复你为前台应用做过的工作,编辑布局 (apps/backend/templates/layout.php),添加全局导航:

<div id="navigation">
  <ul style="list-style:none;">
    <li><?php echo link_to('Manage posts', 'post/list') ?></li>
    <li><?php echo link_to('Manage comments', 'comment/list') ?></li>
  </ul>
</div>
<div id="content">
  <?php echo $sf_data->getRaw('sf_content') ?>
</div>

你可以通过下面的 URL 访问后台应用:

http://localhost/sf_sandbox/web/backend_dev.php/post

basic generated admin

生成的管理系统最大的优势在于,你可以更改配置文件轻松地实现个性化。

backend/modules/post/config/generator.yml 改为:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Post
    theme:            default
    fields:
      title:          { name: Title }
      excerpt:        { name: Exerpt }
      body:           { name: Body }
      nb_comments:    { name: Comments }
      created_at:     { name: Creation date }
    list:
      title:          Post list
      layout:         tabular
      display:        [=title, excerpt, nb_comments, created_at]
      object_actions:
        _edit:        ~
        _delete:      ~
      max_per_page:   5
      filters:        [title, created_at]
    edit:
      title:          Post detail
      fields:
        title:        { type: input_tag, params: size=53 }
        excerpt:      { type: textarea_tag, params: size=50x2 }
        body:         { type: textarea_tag, params: size=50x10 }
        created_at:   { type: input_date_tag, params: rich=on }

注意管理系统在 Post 的现有各列中查找 nb_comments。 现在还没有与之关联的获取器(getter),但可以很简单地将其添加至 sf_sandbox/lib/model/Post.php

[php]
public function getNbComments()
{
  return count($this->getComments());
}

现在刷新帖子管理察看变化:

customized generated admin

限制后台访问

现在每个人都可以访问后台,你必须添加访问限制。

apps/backend/modules/post/config/ 中添加 security.yml 文件,写入如下内容:

all:
  is_secure: on

comment 模块做相同的操作。 现在你不登陆这两模块都不能访问了。

但是登陆动作还没有呢!好的,你可以很轻松地添加。首先创建一个 security 模块的骨架:

$ php symfony init-module backend security

这个新的模块将用于处理登陆请求。 编辑 apps/backend/modules/security/templates/indexSuccess.php 创建登陆表单:

[php]
<h2>Authentication</h2>

<?php if ($sf_request->hasErrors()): ?>
  Identification failed - please try again
<?php endif; ?>

<?php echo form_tag('security/login') ?>
  <label for="login">login:</label>
  <?php echo input_tag('login', $sf_params->get('login')) ?>

  <label for="password">password:</label>
  <?php echo input_password_tag('password') ?>

  <?php echo submit_tag('submit', 'class=default') ?>
</form>

添加由表单向模块 security 调用的 login 动作(在apps/backend/modules/security/actions/actions.class.php 文件中):

[php]
public function executeLogin()
{
  if ($this->getRequestParameter('login') == 'admin' && $this->getRequestParameter('password') == 'password')
  {
    $this->getUser()->setAuthenticated(true);

    return $this->redirect('main/index');
  }
  else
  {
    $this->getRequest()->setError('login', 'incorrect entry');

    return $this->forward('security', 'index');
  }
}

main 模块中,移除 index 动作的默认代码:

[php]
public function executeIndex()
{
}

最后一步工作是设置 security 为处理登陆的默认模块。 打开配置文件 apps/backend/config/settings.yml ,添加:

all:
  .actions:
    login_module:           security
    login_action:           index    

到此为止,如果你试图访问帖子管理,你必须输入用户名和密码:

login form

请查阅更多关于 安全的内容。

结语

好了,一小时过去了。你做到了。现在你可以在产品环境中使用两个应用了:

frontend:   http://localhost/sf_sandbox/web/index.php/
backend:    http://localhost/sf_sandbox/web/backend.php/

此时,如果你遇到错误 (error),可能是因为你在一些动作被缓存(cache) 后修改了模型(开发环境中不使用 cache)。清除 cache,只需要键入:

$ php symfony cc

瞧瞧,应用程序快速地运转自如。 挺酷的吧? 自己试着浏览代码,添加新的模块,修改页面设计。

而且不要忘记在 symfony Wiki 上讲述你的 symfony 应用程序!

  • Bookmark at
  • Bookmark "tutorial:my_first_project:start" at del.icio.us
  • Bookmark "tutorial:my_first_project:start" at Digg
  • Bookmark "tutorial:my_first_project:start" at Furl
  • Bookmark "tutorial:my_first_project:start" at Reddit
  • Bookmark "tutorial:my_first_project:start" at Ask
  • Bookmark "tutorial:my_first_project:start" at BlinkList
  • Bookmark "tutorial:my_first_project:start" at blogmarks
  • Bookmark "tutorial:my_first_project:start" at Blogg-Buzz
  • Bookmark "tutorial:my_first_project:start" at Google
  • Bookmark "tutorial:my_first_project:start" at Ma.gnolia
  • Bookmark "tutorial:my_first_project:start" at Netscape
  • Bookmark "tutorial:my_first_project:start" at ppnow
  • Bookmark "tutorial:my_first_project:start" at Rojo
  • Bookmark "tutorial:my_first_project:start" at Shadows
  • Bookmark "tutorial:my_first_project:start" at Simpy
  • Bookmark "tutorial:my_first_project:start" at Socializer
  • Bookmark "tutorial:my_first_project:start" at Spurl
  • Bookmark "tutorial:my_first_project:start" at StumbleUpon
  • Bookmark "tutorial:my_first_project:start" at Tailrank
  • Bookmark "tutorial:my_first_project:start" at Technorati
  • Bookmark "tutorial:my_first_project:start" at Live Bookmarks
  • Bookmark "tutorial:my_first_project:start" at Wists
  • Bookmark "tutorial:my_first_project:start" at Yahoo! Myweb
  • Bookmark "tutorial:my_first_project:start" at BobrDobr
  • Bookmark "tutorial:my_first_project:start" at Memori
  • Bookmark "tutorial:my_first_project:start" at Faves
  • Bookmark "tutorial:my_first_project:start" at Favorites
  • Bookmark "tutorial:my_first_project:start" at Facebook
  • Bookmark "tutorial:my_first_project:start" at Newsvine
  • Bookmark "tutorial:my_first_project:start" at Yahoo! Bookmarks
  • Bookmark "tutorial:my_first_project:start" at Twitter
  • Bookmark "tutorial:my_first_project:start" at myAOL
  • Bookmark "tutorial:my_first_project:start" at Slashdot
  • Bookmark "tutorial:my_first_project:start" at Fark
  • Bookmark "tutorial:my_first_project:start" at RawSugar
  • Bookmark "tutorial:my_first_project:start" at LinkaGoGo
  • Bookmark "tutorial:my_first_project:start" at Mister Wong
  • Bookmark "tutorial:my_first_project:start" at Wink
  • Bookmark "tutorial:my_first_project:start" at BackFlip
  • Bookmark "tutorial:my_first_project:start" at Diigo
  • Bookmark "tutorial:my_first_project:start" at Segnalo
  • Bookmark "tutorial:my_first_project:start" at Netvouz
  • Bookmark "tutorial:my_first_project:start" at DropJack
  • Bookmark "tutorial:my_first_project:start" at Feed Me Links
  • Bookmark "tutorial:my_first_project:start" at funP
  • Bookmark "tutorial:my_first_project:start" at HEMiDEMi
tutorial/my_first_project/start.txt · 最后更改: 2007/12/03 11:17 由 yujiexie
www.chimeric.de Creative Commons License Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0