Skip to content

Tutorial.Module Design II.zh_cn

linzongshu edited this page Jul 4, 2013 · 1 revision

English

5 Controller & Action

当然上一步的安装只能将模块的基本信息写入数据库,但是用户访问不到任何页面,这一节里,我们将介绍如何创建一个基本的控制器类和动作,用于响应用户的请求。控制器类文件都放在src/Controller目录下,同时用Front和Admin子目录来区分前后台控制器。这里先以前台控制器为例,如下创建目录结构并在Front文件夹下创建控制器类IndexController.php:

member
|- src
   |- Controller
      |- Front
         |- IndexController.php

需要注意的是,controller的命名规则是有严格要求的,这样可以使框架能够根据解析的参数找到相应的控制器类,其命名规则为{Controller name}Controller,如IndexController,index为控制器名,并且首字母要大写,后面固定跟Controller。在IndexController.php里添加如下代码:

路径:module/member/src/Controller/Front/IndexController.php

Code 2.5.1

<?php
namespace Module\Member\Controller\Front;
use Pi\Mvc\Controller\ActionController;

class IndexController extends ActionController
{
    public function indexAction()
    {
        $this->view()->setTemplate(false);
        $this->view()->assign('content', __('Hello world!'));
    }
}

上面的代码定义了一个简单的controller,其主要由命名空间、类和方法组成。namespace标识了当前文件的命名空间,命名空间实际上与文件的路径相呼应,这个规则也是在框架初始化时定义的,模块的命名空间里只是把路径中小写的src去掉,同时用反斜杠(\)来分隔。

同时类里至少会用到Pi/Mvc/Controller/ActionController类作为IndexController的父类,因此要引用这个命名空间,即use Pi\Mvc\Controller\ActionController。

动作(action)名的定义也必须遵循规范,即{action name}Action,但action name的首字母需要小写。以indexAction为例,这个方法不需要带参数,并且这个方法必须定义为公有(public),这样外部的类就可以访问到这个方法。

**注意:**action的命名也可以使用驼峰形式,如addFormAction,这样addForm就是动作名了,在url请求时可以用'-'来分割不同的字母,如localhost/member/index/add-form。

indexAction里定义了用户请求时需要处理的数据以呈现给用户,例子里调用了ViewModel类的setTemplate()方法设置了indexAction的模板,这里设置为false,即使用Pi默认的模板,我们就不需要再创建模板文件。

在这里先介绍setTemplate()和assign()两个API,因为在后面的介绍中会经常用到。上面介绍一种设置默认模板的方法,另外还可以自定义模板文件,而自定义模板文件又可分为按默认格式定义模板名和自定义模板名两种:

$this->view()->setTemplate('login');

上面的代码指定了这个action使用名为login的模板,将这段代码替换上面的例子,那么模板的路径就是template/front/login.phtml。

如果在action里不使用setTemplate()函数,那么就表明使用默认文件名格式的模板,这个格式为:{controller name}-{action name}.phtml,如上述例子中,去掉setTemplate()代码,其模板文件为:template/front/index-index.phtml。

assign函数定义处理后的数据赋给模板文件的哪个变量,这个API的第一个参数值为字符串,转换成模板里相应的变量,如上述例子中第一个参数值为content,那么模板里就可以使用相应的变量$content。而$content的值就是Hello world!。

当然也可以用数组实现赋值,如:

$this->view()->assign(array(
    'content' => __('Hello world!'),
));

这个代码的结果与上述例子一致。需要注意的是,如果使用默认模板,其变量默认为$content,因此,assign方法里标识变量的字符串只能是content。除非你将layout-front.phtml和layout-admin.phtml里的$content换成自己需要的。

这样我们就有一个基本的Controller/action,这里简单介绍下如何访问indexAction。系统默认的URL访问格式为domain/{section}/{module}/{controller}/{action},section一项如果为空,就对应Front,即系统会去Front文件夹下找controller文件,否则如果为admin则去Admin下找,模块名一项为必须的,而controller和action都可以省略,这样程序就会去找默认的IndexController的indexAction,如我们刚才创建的页面可以这样访问:localhost/course/member或者localhost/course/member/index/index。

fig2-4

图2-4 一个简单的模块页面

对于action内的其他程序逻辑将在后面的章节里介绍。

6 模板

在controller/action一节里,我们提到了模板的设置,这一节主要介绍如何创建模板。模板实际上就是一个phtml文件,这个文件支持PHP与HTML标签混写,它就是用来生成呈现给用户浏览的HTML标签。

创建模板文件的方法很简单,在action里指定了模板后,在template文件夹下就需要创建一个相应的模板。下面以一个例子介绍下模板的创建与使用,我们需要在Front目录下重新创建一个controller,这个controller命名LoginController,之后介绍其他内容时也会用到:

路径:module/member/src/Controller/Front/LoginController.php

Code 2.6.1

<?php
namespace Module\Member\Controller\Front;
use Pi\Mvc\Controller\ActionController;

class LoginController extends ActionController
{
    public function loginAction()
    {
        $this->view()->assign('title', __('Please enter'));
    }
}

上面的例子里指定了当前模板为login-login.phtml,因此需要创建template/front目录及login-login.phtml文件:

member
|- template
   |- front
      |- login-login.phtml

并向login-login.phtml里添加如下代码:

路径:module/member/template/front/login-login.phtml

Code 2.6.2

<h2><?php echo $title ?></h2>
<input name='username' type='text' value='' />

在上面的例子中,我们用PHP和HTML标签混写了模板文件,第一行通过echo语句将$title变量的值输出并包含在<h2></h2>标签里,它相当于:

<h2>Please enter</h2>

第二行用纯HTML标签输出一个text表单。当然Pi Engine提供了通过PHP生成表单的方法,在下一章里将会介绍到。在浏览器请求localhost/course/www/member/login/login就可以看到如下页面:

fig2-5

图2-5 在模板里输出一个表单

至此,一个基本的模板文件就完成了,你也可以根据需求在phtml文件添加其他PHP语句或HTML标签。

7 AJAX action

AJAX也就是异步请求,在做前端交互的时候经常会用到,能够在不加载页面的情况下局部更新页面。Pi Engine也提供了AJAX的调用方式,请求AJAX的方式实际上与请求一个页面类似,这个请求最终也会去执行相应的action,不同的是,这个action不需要设置模板,另外action里需要用echo语句发回结果或直接返回结果给浏览器端的脚本。

下面我们以一个例子来说明如何在Pi Engine里实现AJAX请求。在LoginController里增加一个action并命名为checkUsernameAction,添加如下代码:

路径:module/member/src/Controller/Front/LoginController.php

Code 2.7.1

<?php
...
class LoginController extends ActionController
{
...

    public function checkUsernameAction()
{
    $predefineName = array('admin', 'root');

    $username = $this->params('username');
    if (in_array($username, $predefineName)) {
        return json_encode(array(
            'status'   => false,
            'message' => __('Invalid username, please try again!'),
        ));
    }

    return json_encode(array(
        'status'   => true,
        'message' => __('Success'),
    ));
    }
}

在这个例子里,我们先定义了一个checkUsername的action,用数组指定两个预定义的用户名'admin'和'root',然后调用控制器的plugin -- params()方法,这个方法用于获取通过url传递的参数值,这里获取了key为username的值;接下来判断这个用户名是否在预定义用户的数组里,如果是则返回一个JSON字符串,包含状态和错误信息,否则也同样返回JSON字符串,不过状态为正确。

接下来我们需要在login-login.phtml模板里添加相应的JS脚本,这个脚本在username表单失去焦点时请求checkUsernameAction,并根据返回的结果作相应处理:

路径:module/member/template/front/login-login.phtml

Code 2.7.2

<?php $this->jQuery() ?>
<h2><?php echo $title; ?></h2>

<input name="username" type="text" value="" />
<div id="text-message"></div>

<script>
    $('input[name="username"]').bind('blur', function() {
        var url = '<?php echo $this->url('', array('controller' => 'login', 'action' => 'check-username')) ?>';
        url += 'username-' + $(this).val();
        $.get(url, function(result) {
            result = $.parseJSON(result);
            if (result.status) {
                $('#text-message').css('color', '#00FF00');
            } else {
                $('#text-message').css('color', '#FF0000');
            }
            $('#text-message').html(result.message);
        });
    });
</script>

上面代码中,加粗的为新添加的代码,由于这里的JavaScript使用了JQuery框架,因此需要引用jQuery文件,jQuery()方法是Pi Engine提供的一个helper,这个helper可以直接通过$this调用,这个方法的作用就是在HTML的

内生成一条引用jquery文件的script标签,如: <script src="http://localhost/course/www/static/vendor/jquery/jquery.min.js" type="text/javascript"> 这样,在后面的代码里就可以使用JQuery框架提供的方法了。 这里简单介绍下helper,它是Pi\View\Helper目录下的类,继承自AbstractHelper类,这些类里用__invoke()方法完成一定的功能,并且在模板里可以直接用$this调用,如代码里url()方法也是一个helper,这种机制可以方便开发者添加需要的扩展,也利于代码的维护。更多的helper及使用方法可以参考文档。 在input表单后面需要添加一个div标签,用来显示返回的结果信息,我们设置其id为text-message,这样用jQuery的选择器就可以找到这个标签了。 完成上面工作后,就需要用JavaScript实现异步调用了,直接在文件的最后面添加<script></script>标签。在这个标签里,先用jQuery选择器$选择name为username的表单(即:$('input[name= "username"]')),同时绑定失去焦点事件blur,而function(){}内执行的就是失去焦点后需要完成的工作。

首先定义了url变量,并将请求checkUsernameAction的url赋给它,url()方法带两个参数,第一个为路由的类型,这里使用了前台的默认路由,因此设置为空字符串,第二参数用数组标识要请求的module、controller和action,当然还可以带其他参数,书写的形式一样,如'username' => 'admin'表示key为username值为admin的参数。默认路由里module,controller和action的分割符为'/',而自带参数的key和value间的分割符为'-'。上述例子中url()输出的值相当于:

member/login/check-username/

当然我们还需要将用户输入的username的值传递过去,因此需要通过$(this).val()获取表单的当前值,并和username一起组装后写赋给url变量。假设用户输入admin,最终的url形式为:

member/login/check-username/username-admin

这里需要注意下,action名里check和username我们用'-'分割,Pi会自动将其处理成驼峰格式checkUsername。

然后我们需要请求这个url,同样使用jQuery的方法$.get请求url,function里为请求成功后要完成的流程。其参数result表明请求结果保存在result里。通过这个请求,Pi就会去调用checkUsernameAction并执行函数里的代码后输出JSON字符串,这个字符串就保存在result变量里。

通过$.parseJSON方法将JSON字符解析,我们就可以通过result.status和result.message来获取这两个字段的值,若返回false,用$('#test-message')选择id为text-message的元素并设置其内容的颜色为红色,否则为绿色,最后将返回的message填入<div></div>内(即:$('#text-message').html(result.message))。

<div id='text-message'>Success</div>

fig2-6

图2-6 AJAX请求

当我们在表单里输入admin后,在页面空白处点击后,会发现页面没有重新载入,但却出现Invalid username的结果,打开firebug可以看到,这个动作请求了一个页面,同时这个页面返回一些信息。

8 编码规范

在开发过程中,程序员需要遵循一些编码规范,目的是为了能让代码结构更清晰,方便其他程序员阅读,也方便代码维护。在此我们列出了一些编码规范的文档,包括PHP以及前端JavaScript和CSS,希望能对程序员有所帮助。

另外,在开发过程中,为了优化代码,我们也提出一些额外的编程规范:

  • 在action里,赋给模板的变量值,在能不使用类变量的情况下,尽量不要将类变量赋给模板变量。因为使用非类变量能更方便地对其作缓存。
  • 不要在循环里使用数据库查询,因为这可能在请求一个页面时可能执行数十次甚至数百次的数据库查询,对数据库的压力比较大。这种情况多发生在模块间的数据调用。

fig2-7

图2-7 循环里使用sql查询示意图

如图,在模块1里对table1执行了一次查询,找到10条记录,然后对每一条记录分别根据uid调用了模块2的接口,对table执行一次查询,这个过程就执行了11次数据库查询。

解决这个问题的方法就是先找到这10条记录,然后将其uid打包到一个数组里,最后使用IN语句执行一次查询就可以从模块2里获取这所有的10条记录结果。

fig2-8

图2-8 减少sql查询示意图

上述这种方法只需要执行两次数据库查询。

  • 在模板里,输出到页面的变量,需要加转义,也就是使用escape() helper,

如:

$this->escape($title);

这是为防止变量里可能包含HTML标签,若不加转义,直接输出时,页面可能会乱。

Clone this wiki locally