创建 Zend 项目

要创建 Zend 项目,首先要下载并解压 Zend Framework。

安装 Zend Framework

下载最新的 Zend Framework 1.12.20 源码包,(我们使用的是 Zend Framework 1,所以下面的内容都是基于 Zend 1)

解压到合适的地方(比如:/home/xxx/bin/).

注: /home/xxx/bin 是我偏爱的目录,如果你设定了自己的目录,就应该替换下面命令中相应的地方

$ tar -xzvf ZendFramework-1.12.20.tar.gz -C /home/xxx/bin

这样 Zend Framework 就安装在了 /home/xxx/bin/ZendFramework-1.12.20

注1:Zend Framework 各个版本下载
注2:$ 符号表示当前环境是 Linux 命令行终端,在等待输入一个人命令。

创建项目

在 Zend 安装目录下的 bin/ 目录下有 zf.sh 脚本文件,你需要在系统path目录下创建该文件的链接,然后就可以在命令行中任何地方使用zf命令创建项目了。

$ sudo ln -s /home/xxx/bin/ZendFramework-1.12.20/bin/zf.sh /usr/local/bin/zf

打开命令行(Ctrl + Alt + T),切换到你想创建项目的位置,假如你想在 ~/demo/ 目录下创建 Zend 项目:cd ~/demo/

执行下面的命令创建 quickstart 项目:

$ zf create project quickstart

这样就创建了 /home/xxx/demo/quickstart,并且在 quickstart 目录下自动创建了 Zend 项目的一些目录结构。其项目结构如下:

quickstart
|-- application
|   |-- Bootstrap.php
|   |-- configs
|   |   `-- application.ini
|   |-- controllers
|   |   |-- ErrorController.php
|   |   `-- IndexController.php
|   |-- models
|   `-- views
|       |-- helpers
|       `-- scripts
|           |-- error
|           |   `-- error.phtml
|           `-- index
|               `-- index.phtml
|-- library
|-- public
|   |-- .htaccess
|   `-- index.php
`-- tests
    |-- application
    |   `-- bootstrap.php
    |-- library
    |   `-- bootstrap.php
    `-- phpunit.xml

此时,只是创建了 Zend 项目结构,你还需要把 Zend Framework 添加到 quickstart 项目中,有两种方式,一中是创建一个链接,把 zend 框架下的 library 目录链接到 quickstart/library ,另一种方式是直接复制 zend 框架下的 library 目录替换掉 quickstart/library。

# Symlink
$ cd library
$ ln -s /home/xxx/bin/ZendFramework-1.12.20/library/Zend .

# or copy
$ cd library
$ cp -r /home/xxx/bin/ZendFramework-1.12.20/library/Zend .

这样就可以看到 quickstart/library/Zend 目录了。

项目启动文件

Bootstrap 类定义了 Zend 项目启动时要初始化的资源和组件。默认地,Zend Framework 初始化了 Front Controller,然后它使用 application/controllers/ 作为寻找 action controller 的默认路径。这个类如下:

// application/Bootstrap.php

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
}

如你所见,这里面没有什么内容。

项目配置文件

默认配置文件放在:application/configs/application.ini,包含一些基本的指令,比如设置php环境,设置启动类,设置 action controller 等。文件内容如下:

; application/configs/application.ini

[production]
phpSettings.display_startup_errors = 0
phpSettings.display_errors = 0
includePaths.library = APPLICATION_PATH "/../library"
bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"
appnamespace = "Application"
resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
resources.frontController.params.displayExceptions = 0

[staging : production]

[testing : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1

[development : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1

关于这个文件有几个要点:
(1) 使用 ini 配置文件时,你可以直接引用常量,APPLICATION_PATH 就是一个常量。
(2) 这个文件被分成几个部分:production,staging,testing 和 development。后面三个包括了 production 的设置。不同的环境其配置分开,这样便于开发和调试。

Action Controller 控制器

应用程序的 action controller 定义了程序流程,把用户请求映射到合适的 model 和 view。

一个 action controller 应包含一个或多个以Action结尾的方法,这些方法可以通过 web 请求访问到。默认地,Zend Framework URLs 遵循 /controller/action 的模式,其中 controller 就映射 action controller 名字(以Controller作为后缀),而 action 就映射到 action 方法(以Action作为后缀)。

通常,需要一个 IndexController,它表示网站首页,和一个 ErrorController,它表示诸如 HTTP404、HTTP500 等错误页面.

初始 IndexController 代码如下:

// application/controllers/IndexController.php

class IndexController extends Zend_Controller_Action
{

    public function init()
    {
        /* Initialize action controller here */
    }

    public function indexAction()
    {
        // action body
    }
}

初始 ErrorController 代码如下:

// application/controllers/ErrorController.php

class ErrorController extends Zend_Controller_Action
{

    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');

        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:

                // 404 error -- controller or action not found
                $this->getResponse()->setHttpResponseCode(404);
                $this->view->message = 'Page not found';
                break;
            default:
                // application error
                $this->getResponse()->setHttpResponseCode(500);
                $this->view->message = 'Application error';
                break;
        }

        $this->view->exception = $errors->exception;
        $this->view->request   = $errors->request;
    }
}

Views 视图

Zend Framework 中的 Views 是用普通 PHP 代码写的。View scripts 在 application/views/scripts/ 下面,它以控制器名字分文件夹组织起来。本例中,我们有一个 IndexController 和 ErrorController,因此相应地,也应在 View scripts 目录下有 index/ 和 error/ 子目录。在这个子目录中,又有每个 view scripts 文件,对应 Controller 中的 Action。本例中,使用 index/index.phtml 和 error/error.phtml。

下面是默认的 index/index.phtml view script:

<!-- application/views/scripts/index/index.phtml -->
<style>

    a:link,
    a:visited
    {
        color: #0398CA;
    }

    span#zf-name
    {
        color: #91BE3F;
    }

    div#welcome
    {
        color: #FFFFFF;
        background-image: url(http://framework.zend.com/images/bkg_header.jpg);
        width:  600px;
        height: 400px;
        border: 2px solid #444444;
        overflow: hidden;
        text-align: center;
    }

    div#more-information
    {
        background-image: url(http://framework.zend.com/images/bkg_body-bottom.gif);
        height: 100%;
    }

</style>
<div id="welcome">
    <h1>Welcome to the <span id="zf-name">Zend Framework!</span><h1 />
    <h3>This is your project's main page<h3 />
    <div id="more-information">
        <p>
            <img src="http://framework.zend.com/images/PoweredBy_ZF_4LightBG.png" />
        </p>

        <p>
            Helpful Links: <br />
            <a href="http://framework.zend.com/">Zend Framework Website</a> |
            <a href="http://framework.zend.com/manual/en/">Zend Framework
                Manual</a>
        </p>
    </div>
</div>

error/error.phtml 稍微复杂一点,其中使用了php条件语句:

<!-- application/views/scripts/error/error.phtml -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN";
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Zend Framework Default Application</title>
</head>
<body>
  <h1>An error occurred</h1>
  <h2><?php echo $this->message ?></h2>

  <?php if ('development' == $this->env): ?>

  <h3>Exception information:</h3>
  <p>
      <b>Message:</b> <?php echo $this->exception->getMessage() ?>
  </p>

  <h3>Stack trace:</h3>
  <pre><?php echo $this->exception->getTraceAsString() ?>
  </pre>

  <h3>Request Parameters:</h3>
  <pre><?php echo var_export($this->request->getParams(), 1) ?>
  </pre>
  <?php endif ?>

</body>
</html>

创建虚拟主机

对于 quickstart 项目,我们把它放在 apache 服务器上运行。

我们需要设置一个虚拟主机,这样就可以在浏览器中输入域名来访问我们的网站了。假设你已经配置好了 php+apache+mysql 运行环境,如果没有,请参考教程

假设我们的项目所映射的域名是:quickstart.local。

进入 apache 配置目录,创建配置文件:

cd /etc/apache2/sites-available/
sudo gedit quickstart.local.conf

然后复制下面的内容到该配置文件,注意:项目路径需要替换成你自己的项目路径:

<VirtualHost *:80>
    ServerName quickstart.local

    ServerAdmin webmaster@localhost
    DocumentRoot /home/xxx/demo/quickstart/public

    <Directory /home/xxx/demo/quickstart/public>
    AllowOverride all
    require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>

保存该文件,并且启用该配置:sudo a2ensite quickstart.local

然后打开hosts文件:sudo gedit /etc/hosts 添加一行:
127.0.0.1 quickstart.local

最后重启一下 apache 服务器:
sudo service apache2 restart

到此为止,这个项目就创建成功并且可运行了。

检验成果

现在就可以访问你的项目网站了,打开浏览器输入http://quickstart.local,就能看到欢迎页了。

可能遇到的问题:

  1. 页面不显示:很有可能时项目目录的权限问题,使用 chmod -R 777 /home/xxx/demo/quickstart 试试看。

创建一个 Layout

你可能注意到,上面的 view scripts 不是完整的 html 页面,这是故意设计的。我们只想让 action 只返回 action 本身要输出的内容,而不是整个应用程序页面。

现在来组成一个完整的 HTML 页面。我们使用一个全局的 layout 来作为网站统一的样式。

注: 下面的命令若无说明,默认是在当前项目根目录下执行的。

开始使用 Zend_Layout,首先让 bootstrap 来加载 Layout 资源。通过下面的命令可实现:

$zf enable layout
# Layouts have been enabled, and a default layout created at
# application/layouts/scripts/layout.phtml
# A layout entry has been added to the application config file.

此时,application/configs/application.ini 内容自动更新了:

; application/configs/application.ini

; Add to [production] section:
resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"

这个指令告诉应用程序去 application/layouts/scripts 去寻找 layout。

我们还要启用 XHTML DocType 声明,为此,在 bootstrap 中加载该资源。在 bootstrap中添加资源的最简单的方法就是创建一个以init开头的方法。此时,我们使用 _initDoctype() 方法来初始化 doctype:

// application/Bootstrap.php

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    protected function _initDoctype()
    {
    }
}

有了这个方法,还需要让 view 使用正确的 doctype,但是 view 从哪里来?最简单的方式就是初始化 View 资源;有了它,就可以把 view 对象放入 bootstrap 并使用它。

添加下行到 aplication/configs/application.ini 来创建 view 资源:

; application/configs/application.ini

; Add to [production] section:
resources.view[] =

然后再来充实一下 _initDoctype() 方法。

// application/Bootstrap.php

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    protected function _initDoctype()
    {
        $this->bootstrap('view');
        $view = $this->getResource('view');
        $view->doctype('XHTML1_STRICT');
    }
}

然后,创建全局的 layout:

<!-- application/layouts/scripts/layout.phtml -->
<?php echo $this->doctype() ?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Zend Framework Quickstart Application</title>
  <?php echo $this->headLink()->appendStylesheet('/css/global.css') ?>
</head>
<body>
<div id="header" style="background-color: #EEEEEE; height: 30px;">
    <div id="header-logo" style="float: left">
        <b>ZF Quickstart Application</b>
    </div>
    <div id="header-navigation" style="float: right">
        <a href="<?php echo $this->url(
            array('controller'=>'guestbook'),
            'default',
            true) ?>">Guestbook</a>
    </div>
</div>

<?php echo $this->layout()->content ?>

</body>
</html>

使用headLink()来生成<link>元素。

现在再打开浏览器并查看源代码,应该能看到 XHTML头部,<head> <title> <body>等部分。


创建一个 Model 和 Database Table

现在考虑一下guestbook的组成,它包含一些记录,每个记录由注释,时间戳,email地址等组成。我们把它存在数据库里,并有一个唯一id。我们希望能保存、获取所有记录。

因此,一个简单的 guestbook 模型 API 就像下面这样:

// application/models/Guestbook.php

class Application_Model_Guestbook
{
    protected $_comment;
    protected $_created;
    protected $_email;
    protected $_id;

    public function __set($name, $value);
    public function __get($name);

    public function setComment($text);
    public function getComment();

    public function setEmail($email);
    public function getEmail();

    public function setCreated($ts);
    public function getCreated();

    public function setId($id);
    public function getId();
}

class Application_Model_GuestbookMapper
{
    public function save(Application_Model_Guestbook $guestbook);
    public function find($id);
    public function fetchAll();
}
}

下面,我们可以开始思考如何设置数据库了。

首先要初始化 Db 资源。使用 zf configure db-adapter 命令:

zf configure db-adapter "adapter=Pdo_MySql&host=localhost&username=root&password=&dbname=guestbook" production

zf configure db-adapter "adapter=Pdo_MySql&host=localhost&username=root&password=&dbname=guestbook-test" testing

zf configure db-adapter "adapter=Pdo_MySql&host=localhost&username=root&password=&dbname=guestbook-dev" development

如果成功执行,那么在 application/configs/application.ini 中就能看到新增的几行配置:

//application/configs/application.ini

resources.db.adapter = "Pdo_MySql"
resources.db.params.host = "localhost"
resources.db.params.username = "root"
resources.db.params.password = ""
resources.db.params.dbname ="guestbook" 

注:要把数据库密码设置成你自己的。

然后创建数据库,打开 phpMyadmin,创建数据库 guestbook,输入下面SQL语句并执行:

CREATE TABLE IF NOT EXISTS `guestbook` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `email` varchar(32) NOT NULL DEFAULT 'noemail@test.com',
    `comment` varchar(200) NOT NULL,
    `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

接下来我们使用 Table Data Gateway 机制来连接数据源。Zend_Db_Table 提供了这个功能。

首先创建一个 Zend_Db_Table 类:

$ zf create db-table Guestbook guestbook

查看项目结构,发现多了一个 application/models/DbTable/ 目录,里面有一个 Guestbook.php 文件。其内容如下:

// application/models/DbTable/Guestbook.php

/**
* This is the DbTable class for the guestbook table.
*/
class Application_Model_DbTable_Guestbook extends Zend_Db_Table_Abstract
{
    /** Table name */
    protected $_name    = 'guestbook';
}

注意 Application_Model_DbTable 类前缀,它对应 application/models/DbTable 目录。

然后创建一个 Data Mapper。Data Mapper 映射对象到数据库。本例中,它将映射 model 到数据源。

$ zf create model GuestbookMapper

然后打开 application/models/GuestbookMapper.php 并输入以下内容:

// application/models/GuestbookMapper.php

class Application_Model_GuestbookMapper
{
    protected $_dbTable;

    public function setDbTable($dbTable)
    {
        if (is_string($dbTable)) {
            $dbTable = new $dbTable();
        }
        if (!$dbTable instanceof Zend_Db_Table_Abstract) {
            throw new Exception('Invalid table data gateway provided');
        }
        $this->_dbTable = $dbTable;
        return $this;
    }

    public function getDbTable()
    {
        if (null === $this->_dbTable) {
            $this->setDbTable('Application_Model_DbTable_Guestbook');
        }
        return $this->_dbTable;
    }

    public function save(Application_Model_Guestbook $guestbook)
    {
        $data = array(
            'email'   => $guestbook->getEmail(),
            'comment' => $guestbook->getComment(),
            'created' => date('Y-m-d H:i:s'),
        );

        if (null === ($id = $guestbook->getId())) {
            unset($data['id']);
            $this->getDbTable()->insert($data);
        } else {
            $this->getDbTable()->update($data, array('id = ?' => $id));
        }
    }

    public function find($id, Application_Model_Guestbook $guestbook)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return;
        }
        $row = $result->current();
        $guestbook->setId($row->id)
                  ->setEmail($row->email)
                  ->setComment($row->comment)
                  ->setCreated($row->created);
    }

    public function fetchAll()
    {
        $resultSet = $this->getDbTable()->fetchAll();
        $entries   = array();
        foreach ($resultSet as $row) {
            $entry = new Application_Model_Guestbook();
            $entry->setId($row->id)
                  ->setEmail($row->email)
                  ->setComment($row->comment)
                  ->setCreated($row->created);
            $entries[] = $entry;
        }
        return $entries;
    }
}

现在,创建 model 类,仍然使用 zf 命令:

$ zf create model Guestbook

打开刚刚创建的模型类: application/models/Guestbook.php 并添加以下代码:

// application/models/Guestbook.php

class Application_Model_Guestbook
{
    protected $_comment;
    protected $_created;
    protected $_email;
    protected $_id;

    public function __construct(array $options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        }
    }

    public function __set($name, $value)
    {
        $method = 'set' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid guestbook property');
        }
        $this->$method($value);
    }

    public function __get($name)
    {
        $method = 'get' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid guestbook property');
        }
        return $this->$method();
    }

    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value) {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            }
        }
        return $this;
    }

    public function setComment($text)
    {
        $this->_comment = (string) $text;
        return $this;
    }

    public function getComment()
    {
        return $this->_comment;
    }

    public function setEmail($email)
    {
        $this->_email = (string) $email;
        return $this;
    }

    public function getEmail()
    {
        return $this->_email;
    }

    public function setCreated($ts)
    {
        $this->_created = $ts;
        return $this;
    }

    public function getCreated()
    {
        return $this->_created;
    }

    public function setId($id)
    {
        $this->_id = (int) $id;
        return $this;
    }

    public function getId()
    {
        return $this->_id;
    }
}

最后,我们创建一个 guestbook controller 查询并显示数据库中的结果:

$ zf create controller Guestbook

这将创建 application/controllers/GuestbookController.php,它有一个 IndexAction 方法,同时,在 application/views/scripts/guestbook 目录中创建了一个 index.phtml 文件。

打开 GuestbookController,在 IndexAction 中添加代码,显示所有的 guestbook 记录:

// application/controllers/GuestbookController.php

class GuestbookController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $guestbook = new Application_Model_GuestbookMapper();
        $this->view->entries = $guestbook->fetchAll();
    }
}

打开 application/views/scripts/guestbook/index.phtml 添加以下内容:

<!-- application/views/scripts/guestbook/index.phtml -->

<p><a href="<?php echo $this->url(
    array(
        'controller' => 'guestbook',
        'action'     => 'sign'
    ),
    'default',
    true) ?>">Sign Our Guestbook</a></p>

Guestbook Entries: <br />
<dl>
    <?php foreach ($this->entries as $entry): ?>
    <dt><?php echo $this->escape($entry->email) ?></dt>
    <dd><?php echo $this->escape($entry->comment) ?></dd>
    <?php endforeach ?>
</dl>

切换到浏览器,打开:http://guestbook.local/guestbook ,你应该看到这些内容:

发现没有数据,那就打开 phpMyadmin,插入一些数据再看看,复制下面的sql语句到phpmyadmin并执行:

INSERT INTO `guestbook`.`guestbook` (`id`, `email`, `comment`, `created`) VALUES (NULL, 'noemail@qq.com', 'good', CURRENT_TIMESTAMP),
(NULL, 'noemail2@qq.com', 'good book', CURRENT_TIMESTAMP),
(NULL, 'noemail3@qq.com', 'good book ok', CURRENT_TIMESTAMP);

刷新浏览器,结果如图所示,成功地从数据库读取数据并显示在页面上:

创建 Form

为了让 guestbook 更有用一些,我们需要一个表单来提交新的评论项。

首先,创建一个 form 类:

$ zf create form Guestbook

打开刚创建的 form 类 *application/forms/Guestbook.php *,添加下列代码:

// application/forms/Guestbook.php

class Application_Form_Guestbook extends Zend_Form
{
    public function init()
    {
        // Set the method for the display form to POST
        $this->setMethod('post');

        // Add an email element
        $this->addElement('text', 'email', array(
            'label'      => 'Your email address:',
            'required'   => true,
            'filters'    => array('StringTrim'),
            'validators' => array(
                'EmailAddress',
            )
        ));

        // Add the comment element
        $this->addElement('textarea', 'comment', array(
            'label'      => 'Please Comment:',
            'required'   => true,
            'validators' => array(
                array('validator' => 'StringLength', 'options' => array(0, 20))
                )
        ));

        // Add a captcha
        $this->addElement('captcha', 'captcha', array(
            'label'      => 'Please enter the 5 letters displayed below:',
            'required'   => true,
            'captcha'    => array(
                'captcha' => 'Figlet',
                'wordLen' => 5,
                'timeout' => 300
            )
        ));

        // Add the submit button
        $this->addElement('submit', 'submit', array(
            'ignore'   => true,
            'label'    => 'Sign Guestbook',
        ));

        // And finally add some CSRF protection
        $this->addElement('hash', 'csrf', array(
            'ignore' => true,
        ));
    }
}

上面代码定义了五个元素:email地址,评论输入框,验证码,提交按钮和CSRF保护字段。

下一步,添加 signActionGuestbookController,它用于处理表单提交页面。使用 zf 命令创建:

$ zf create action sign Guestbook

创建了 signAction 和相应的 view script.

然后添加一些代码到 signAction,首先检查是否有POST或GET请求,然后若没有就简单地显示表单,若有,则验证提交的数据,并保存到数据库中

// application/controllers/GuestbookController.php

class GuestbookController extends Zend_Controller_Action
{
    // snipping indexAction()...

    public function signAction()
    {
        $request = $this->getRequest();
        $form    = new Application_Form_Guestbook();

        if ($this->getRequest()->isPost()) {
            if ($form->isValid($request->getPost())) {
                $comment = new Application_Model_Guestbook($form->getValues());
                $mapper  = new Application_Model_GuestbookMapper();
                $mapper->save($comment);
                return $this->_helper->redirector('index');
            }
        }

        $this->view->form = $form;
    }
}

当然,还需要编辑一下 view application/views/scripts/guestbook/sign.phtml

<!-- application/views/scripts/guestbook/sign.phtml -->

Please use the form below to sign our guestbook!

<?php
$this->form->setAction($this->url());
echo $this->form;

检查效果

现在再打开浏览器:http://quickstart.local/guestbook/sign 看看,效果如下:

填写好表单点击提交,就能看到列表已经更新了:

注: 上面的验证码部分辨认很困难,其实只要复制一下粘贴到txt文档中就能看清楚了。

恭喜你!

你已经成功地使用 Zend Framework 的常用功能建立了一个简单的应用程序。Zend Framework 内置了很多组件,你可以直接用在你的项目中,这些组件包括 web services,搜索,PDF读取和创建,身份认证等等。可以在 文档参考 中找到更多相关的技术细节。

转载请注明来源:http://www.cnblogs.com/feifeifanye/p/6252633.html
英文原文链接:https://framework.zend.com/manual/1.12/en/learning.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注