连载:走进Zend Framework框架编程【6-10章】

走进Zend Framework框架编程(六):视图(第一部分)

本部分内容包括:视图,模板,视图帮助类等。

6.0视图介绍
在Zendframework的MVC编程模型中,视图(View)是在控制器的控制和指挥下,用来对程序逻辑进行呈现(Render)的。呈现的结果,就是我们在浏览器里看到的文字、图片、表单等各种网页元素及其字体、颜色、样式等各种效果。
Zend_View Class就是负责视图工作的类,它有效地完成了视图与程序逻辑的分离。它提供了视图帮助、输出过滤和变量转义等功能。
Zend_View还是一个模板系统,我们可以用PHP作为我们的模板语言。当然ZF还可以在View脚本里使用其他第三方的模板系统,比如PHPLib和Smarty等。
使用Zend_View时主要分两步,首先声明一个Zend_View实例,把变量等赋给它,然后使用控制脚本,根据视图脚本呈现出结果。
例如:
控制脚本(在控制器文件的action里,例如IndexController.php中的函数dispdataAction()):
……
Function dispdataAction()
{
……
$data = 'to view'; //数据变量
Zend_Loader::loadClass('Zend_View');
$view = new Zend_View(); //实例化
$view->books = $data;  //赋值
echo $view->render('view.php');
}
视图脚本转义输出语句(在视图脚本文件里,本例是view.php):
<?
php echo $this->escape($this->data
?>

6.1引导文件中setParam('noViewRenderer', <bool>)语句的再解释
Index文件中$fc->setParam('noViewRenderer', <bool>);语句和视图的某性特性有很大关系,比如视图存放的路径等一些默认属性都受该开关属性的影响。
false 是noViewRenderer的默认值。也就是说,如果没有该语句,则认为noViewRenderer参数被设置为false。
noViewRenderer参数被设置为false:意味着控制器使用ZF默认的视图特性,比如视图文件默认必须存放于views\script\<action>\文件夹下,视图文件必须存在,而且其名字必须为<action>.phtml。.phtml是ZF使用的PHP脚本文件,和普通PHP文件没有本质区别。同时,视图对象默认被实例化为$view变量,在控制脚本中使用$view的形式为:$this->view->…。
noViewRenderer参数被设置为true:意味着控制器不使用ZF默认的视图特性,而是通过显式的实例化Zend_view对象,通过我们自己的代码来设置视图对象的属性和方法。本部分“视图介绍”中就是显式声明和使用视图对象的例子。为了程序的灵活性和可控行,我们自然建议把noViewRenderer 设置为 true,这也是比较通常的做法。

6.2视图对象的Options
视图对象的Options选项进一步规定了视图脚本呈现过程中的一些细节。这些选项可以通过在声明视图对象时指定,在构造函数里设置,也可以通过set……()方法来指定。
—basePath基本路径
设置方法:setBasePath(), addBasePath()
例如目录结构:
base/path/
      helpers/
      filters/
      scripts/
     用$view->setBasePath(”base/ path/”);语句设置基本路径后,在没有$view->setScriptPath('……');语句直接指定脚本路径时,就会自动在base/path/scripts/下搜索视图脚本文件,如果使用了视图助手和过滤器,就会分别自动在helpers/和 filters/文件夹下搜索。
—encoding字符编码
用来在使用htmlentities()、htmlspecialchars()或其他操作时,指定字符编码。
设置方法:setEncoding()
默认编码是ISO-8859-1 (latin1)。
—escape回调函数
用于在视图呈现时调用该函数。后边有示例。
设置方法:setEscape()
—filter过滤器
用于在视图呈现后调用过滤方法。
设置方法:setFilter(), addFilter(),
—strictVars
当视图视图发送一个未初始化的变量时,用该选项指定ZF收购给出一个提示或警告信息:
Notice: Key "xxx" does not exist ……
设置方法:strictVars(true)

6.3视图对象的一些属性存取方法
—getVars() 得到所有赋予的变量
—clearVars()清除所有赋予的变量
—getScriptPath($script) 得到给定脚本的路径
—getScriptPaths()得到所有脚本的路径
—getHelperPath($helper)得到某个指定助手类的路径
—getHelperPaths()得到所有助手的路径
—getFilterPath($filter) 得到某个指定过滤器类的路径
—getFilterPaths()得到所有过滤器的路径

6.4视图的路径:

6.4.1视图脚本的搜寻路径
如果引导文件中$fc->setParam('noViewRenderer', false);
则默认指定视图文件views/scripts/[controller_name]/[action].phtml
在实际的程序代码中,为了获得可定制的灵活性,都在控制器中实际指定了视图文件的路径:
指定路径例句:$view->setScriptPath('…/views');或
$view->addScriptPath('…/views');
这个时候,引导文件必须有$fc->setParam('noViewRenderer', true);语句,即设置noViewRenderer为true。
6.4.2视图脚本的搜寻的优先顺序
$view = new Zend_View();
$view->addtScriptPath('…/views1');
$view->addScriptPath('…/views2');
$view->addScriptPath('…/views3');
Zend的手册是这样说的:“如果没有指定任何搜素路径,则在控制器文件下搜索视图文件。”但是通过实际环境测试,发现这时会报告错误:
“no view script directory set; unable to determine location for view script”
看起来最少需要指定一个搜索路径。
如果指定了多条搜素路径,则最后的搜索路径优先。也就是说,如果所有的搜索路径下有相同的视图文件,则最后路径下的起作用,它覆盖了前边路径下的视图文件。

6.5视图控制脚本及其变量传递
ZF的控制器是实例化和设置Zend_View的地方。在这里,我们给视图赋值并告诉它用指定的视图脚本去呈现它们。
6.5.1给视图对象赋值
示例:
$view->Variable=”……”;的形式
    function assign1Action()
    {
      $view = new Zend_View();
      $view->setScriptPath('views');
      $view->strictVars(true);
      $view->a = "Hay";
      $view->b = "Bee";
      $view->c = "Sea";
      $view->d;
      echo $view->render('tp_abc.php');
   }
示例:
$view-> assign(' Variable ', "……");的形式
    function assign2Action()
    {
      $view = new Zend_View();
      $view->setScriptPath('views');
      $view->assign('a', "Hay");
      $view->assign('b', "Bee");
      $view->assign('c', "Sea");
      echo $view->render('tp_abc.php');
   }
示例:数组
   function assign3Action()
    {
      $view = new Zend_View();
      $view->setScriptPath('views');
      $array = array(
        'a' => "Hay",
        'b' => "Bee",
        'c' => "Sea",
      );
      $view->assign($array);
      echo $view->render('tp_abc.php');
   }
示例:对象
    function assign4Action()
    {
      $view = new Zend_View();
      $view->setScriptPath('views');
      $obj = new StdClass;
      $obj->a = "Hay";
      $obj->b = "Bee";
      $obj->c = "Sea";
      $view->assign((array) $obj);
      echo $view->render('tp_abc.php');
   }
示例:
    使用回调函数。
    function myhtmlentityAction()
    {
      $view = new Zend_View();
      $view->setScriptPath('views');
//自定义类,在/models文件夹下的myclass.php文件中定义
      $mycls = new myClass();
//调用$mycls类的myHtmlEntity方法
      $view->setEscape(array($mycls, 'myHtmlEntity'));
      $obj = new StdClass;
      $obj->a = "the words hay bee sea ";
      $obj->b = "this is bee";
      $obj->c = " this is sea";
      $view->assign((array) $obj);
      echo $view->render('tp_abc.php');
   }
tp_abc.php视图脚本模板文件内容:
<?php
  echo 'a = ' . $this->escape($this->a).'<br>';
  echo 'b = ' . $this->escape($this->b).'<br>';
  echo 'c = ' . $this->escape($this->c).'<br>';   
?>
myclass.php文件内容:
<?php
  class myClass
  {
    public function __construct($options = null)
    {
    }
    function myHtmlEntity($val)
    {//把所有单词首字母变为大写
      return ucwords($val);
    }
  }
?>
输出结果:
a = The Words Hay Bee Sea
b = This Is Bee
c = This Is Sea

走进Zend Framework框架编程(六):视图( 第二部分)

6.6视图脚本的变量转义输出(escaping output)
视图脚本得到变量以后,需要通过转义进行输出,变成页面可以显示的Html代码。
输出语句的格式:
echo $this->escape($this->variable);
$variable变量是在视图脚本里用render方法传递过来的。
一般情况下,传递的变量是通过PHP的 htmlspecialchars()函数转义的。而我们也可以实现我们自己的转义函数。请参考以上“使用回调函数”示例。

6.7视图脚本的模板系统—操作PHPLib类型的模板
模板系统进一步完美的实现了视图与程序逻辑的分离。视图脚本可以完美的操作PHPLib等类型的模板。

6.7.1PHPlib的安装和调用
为了测试下面的示例,我们必须安装PHPLib模板系统到我们的环境中。从网上下载到phplib-7.4.ZIP安装压缩包,解压到安装ZEND的library文件夹下,就完成了安装。
为了在ZF的视图脚本里调用得到模板类文件,必须在引导文件Index.php的set_include_path部分添加PHPLib模板类库文件夹phplib-7.4/php到搜索路径中。以下示例同时包含了Smarty模板引擎的类库文件的搜索路径:
set_include_path('.' .
  PATH_SEPARATOR . '../library/'.
  PATH_SEPARATOR . '../library/phplib-7.4/php/'.
  PATH_SEPARATOR . '../library/Smarty-2.6.19/libs/'.
  PATH_SEPARATOR . 'models/'.
  PATH_SEPARATOR . get_include_path()
);
注意,所有路径都是以引导文件所在文件夹作为参照的。尽管视图文件里所在文件夹不是引导文件所在根目录,但在视图文件里包含PHPLib类库文件的语句include_once 'template.inc';仍然是以引导文件所在目录作为参照的。

6.7.2在视图文件里调用PHPLib模板
首先包含PHPLib类库文件,然后声明模板类的一个实例。使用模板类,首先需要指定一些属性,比如指定模板所在路径,指定模板文件等,然后用set_var传递模板变量,最后用parse方法调用模板文件。PHPLib模板系统的详细用法请参考其帮助文档。
示例:
<?php
  include_once 'template.inc';
  $tpl = new Template();
  $tpl->set_root('views');
  if ($this->books)
  {
    $tpl->set_file(array(
        "booklist" => "booklist.tpl",
        "eachbook" => "eachbook.tpl",
    ));
    foreach ($this->books as $key => $val)
    {
      $tpl->set_var('author', $this->escape($val['author']));
      $tpl->set_var('title', $this->escape($val['title']));
      $tpl->parse("books", "eachbook", true);
    }
    $tpl->pparse("output", "booklist");
  }
  else
  {
    $tpl->setFile("nobooks", "nobooks.tpl");
    $tpl->pparse("output", "nobooks");
  }
?>
booklist.tpl文件内容:
<?php
  if ($this->books):
?>
  <table border=1>
  <tr>
    <th>作者</th>
    <th>书名</th>
  </tr>
  <?php
    foreach ($this->books as $key => $val):
  ?>
  <tr>
    <td><?php echo $this->escape($val['author']) ?></td>
    <td><?php echo $this->escape($val['title']) ?></td>
  </tr>
  <?php endforeach; ?>
  </table>
<?php
  else:
?>
  <p>There are no books to display.</p>
<?php
  endif;
eachbook.tpl文件内容:
<!-- eachbook.tpl -->
<tr>
  <td>{author}</td>
  <td>{title}</td>
</tr>

6.8视图脚本的模板系统—使用 Zend_View_Interface调用第三方模板引擎
我们还可以通过实现Zend_View_Interface接口,来得到一个可用的模板系统。
ZF对Zend_View_Interface接口的原始定义:
/** Return the actual template engine object */
public function getEngine();
/* Set the path to view scripts/templates */
public function setScriptPath($path);
/* Set a base path to all view resources */
public function setBasePath($path, $prefix = 'Zend_View');
/* Add an additional base path to view resources */
public function addBasePath($path, $prefix = 'Zend_View');
/* Retrieve the current script paths */
public function getScriptPaths();
/* Overloading methods for assigning template variables as object properties */
public function __set($key, $value);
public function __get($key);
public function __isset($key);
public function __unset($key);
/* Manual assignment of template variables, or ability to assign multiple
  variables en masse.*/
public function assign($spec, $value = null);
/* Unset all assigned template variables */
public function clearVars();
/* Render the template named $name */
public function render($name);
使用该接口,我们可以很容易的把第三方的模板引擎,比如Smarty,包装成Zend_View兼容的模板类。

6.8.1Smarty的安装
下载Smarty软件包,解压到ZEND的library文件夹下,就完成了安装。
为了在ZF的视图脚本里调用得到模板类文件,必须在引导文件Index.php的set_include_path部分添加Smarty模板类库文件夹Smarty-2.6.19/libs到搜索路径中,参看前面PHPlib的安装说明部分。
Smarty模板引擎需要建立template_dir和compile_dir文件夹才能工作。ZF手册里的示例因为缺少这些设置而无法运行,正确的代码片段如下:
    public function setScriptPath($path)
    {
        if (is_readable($path))
{
            $this->_smarty->template_dir = $path;
            $this->_smarty->compile_dir = $path;  //必须加语句:设置编译路径
            $this->_smarty->cache_dir = $path;    //设置缓存路径
return;
         }
……
我们把对Zend_View_Interface接口的实现的类,放在models文件夹下的ZendViewSmarty.php文件中,该文件的内容如下:
<?php
require_once 'Zend/View/Interface.php';
require_once 'Smarty.class.php';
class ZendViewSmarty implements Zend_View_Interface
{
    /**
     * Smarty object
     * @var Smarty
     */
    protected $_smarty;
    /**
     * Constructor
     * @param string $tmplPath
     * @param array $extraParams
     * @return void
     */
    public function __construct($tmplPath = null, $extraParams = array())
    {
        $this->_smarty = new Smarty;
        if (null !== $tmplPath) {
            $this->setScriptPath($tmplPath);
        }
        foreach ($extraParams as $key => $value) {
            $this->_smarty->$key = $value;
        }
    }
    /**
     * Return the template engine object
     * @return Smarty
     */
    public function getEngine()
    {
        return $this->_smarty;
    }
    /**
     * Set the path to the templates
     * @param string $path The directory to set as the path.
     * @return void
     */
    public function setScriptPath($path)
    {
        if (is_readable($path)) {
            $this->_smarty->template_dir = $path;
            $this->_smarty->compile_dir = $path;
            $this->_smarty->cache_dir = $path;
            return;
        }
        throw new Exception('Invalid path provided');
    }
    /**
     * Retrieve the current template directory
     * @return string
     */
    public function getScriptPaths()
    {
        return array($this->_smarty->template_dir);
    }
    /**
     * Alias for setScriptPath
     * @param string $path
     * @param string $prefix Unused
     * @return void
     */
    public function setBasePath($path, $prefix = 'Zend_View')
    {
        return $this->setScriptPath($path);
    }
    /**
     * Alias for setScriptPath
     * @param string $path
     * @param string $prefix Unused
     * @return void
     */
    public function addBasePath($path, $prefix = 'Zend_View')
    {
        return $this->setScriptPath($path);
    }
    /**
     * Assign a variable to the template
     * @param string $key The variable name.
     * @param mixed $val The variable value.
     * @return void
     */
    public function __set($key, $val)
    {
        $this->_smarty->assign($key, $val);
    }
    /**
     * Retrieve an assigned variable
     * @param string $key The variable name.
     * @return mixed The variable value.
     */
    public function __get($key)
    {
        return $this->_smarty->get_template_vars($key);
    }
    /**
     * Allows testing with empty() and isset() to work
     * @param string $key
     * @return boolean
     */
    public function __isset($key)
    {
        return (null !== $this->_smarty->get_template_vars($key));
    }
    /**
     * Allows unset() on object properties to work
     * @param string $key
     * @return void
     */
    public function __unset($key)
    {
        $this->_smarty->clear_assign($key);
    }
    /**
     * Assign variables to the template
     * Allows setting a specific key to the specified value, OR passing an array
     * of key => value pairs to set en masse.
     * @see __set()
     * @param string|array $spec The assignment strategy to use (key or array of key
     * => value pairs)
     * @param mixed $value (Optional) If assigning a named variable, use this
     * as the value.
     * @return void
     */
    public function assign($spec, $value = null)
    {
        if (is_array($spec)) {
            $this->_smarty->assign($spec);
            return;
        }
        $this->_smarty->assign($spec, $value);
    }
    /**
     * Clear all assigned variables
     * Clears all variables assigned to Zend_View either via [email={@link]{@link[/email] assign()} or
     * property overloading ([email={@link]{@link[/email] __get()}/{@link __set()}).
     * @return void
     */
    public function clearVars()
    {
        $this->_smarty->clear_all_assign();
    }
    /**
     * Processes a template and returns the output.
     * @param string $name The template to process.
     * @return string The output.
     */
    public function render($name)
    {
        return $this->_smarty->fetch($name);
    }
}
?>
控制脚本中对我们我的模板类的调用代码:
   function smartyAction()
   {
      $view = new ZendViewSmarty(); //实例化新的模板类
      $view->setScriptPath('views'); //设置模板文件路径
      $view->book = 'Enter Zend Framework Programme'; //传递变量给模板引擎
      $view->author = '张庆(网眼)';
      echo $view->render('bookinfo.tpl'); //视图呈现
   }
我们看到,由于Smarty模板引擎的良好特性,除过实现上述接口的代码比较复杂以外,我们这里的控制代码要比应用PHPLib模板简单得多。我们的数据不必像PHPLib引擎那样,要把数据传给视图脚本,再由视图脚本声明PHPLib类,再把数据发送给模板去呈现。这里是在控制脚本里把数据直接传递给Smarty模板去呈现的。这是因为ZendViewSmarty实现的是Zend_View接口,它和Zend_View的用法是一样的。
注意:本例只是使用Smarty引擎的其中一种方法。在ZF中还可以用别的形式来使用Smarty模板引擎,我们会在别的章节里介绍。

PHP 框架 Zend Framework 开发系列专题讲座

PHP 框架 Zend Framework 开发系列专题讲座
    Zend Framework 框架是 PHP 的生产厂家 Zend 公司进军企业级应用开发市场的拳头产品,是一个具有里程碑意义的划时代的产品。它以卓越的设计和严格的规范见长,其开源代码也是我们学习 OOP 设计和开发的良好免费教材。

    PHPChina Zend Framework 讨论版红字加亮推荐帖子:
    走进Zend Framework框架编程(一):开篇
    地址:http://bbs.phpchina.com/thread-64495-1-1.html
    走进Zend Framework框架编程(二):软件安装和环境配置收藏
    走进Zend Framework框架编程(三):运行第一个程序收藏
    地址:
http://bbs.phpchina.com/thread-64495-2-1.html
    走进Zend Framework框架编程(四):Zend_Controller和引导文件收藏
    走进Zend Framework框架编程(五):Zend_Controller进阶收藏
    地址:
http://bbs.phpchina.com/thread-64495-3-1.html
    走进Zend Framework框架编程(六):视图(1)
    走进Zend Framework框架编程(六):视图(2)
    走进Zend Framework框架编程(六):视图(3)
    地址:
http://bbs.phpchina.com/thread-64495-4-1.html

    中国软件网 CSDN 专家专栏:
    走进Zend Framework框架编程(一):开篇
   
http://blog.csdn.net/zhangking/archive/2008/06/02/2501909.aspx
    走进Zend Framework框架编程(二):软件安装和环境配置收藏
   
http://blog.csdn.net/zhangking/archive/2008/06/04/2509575.aspx
    走进Zend Framework框架编程(三):运行第一个程序收藏
   
http://blog.csdn.net/zhangking/archive/2008/06/05/2515175.aspx
    走进Zend Framework框架编程(四):Zend_Controller和引导文件收藏
   
http://blog.csdn.net/zhangking/archive/2008/06/11/2537119.aspx
    走进Zend Framework框架编程(五):Zend_Controller进阶收藏
   
http://blog.csdn.net/zhangking/archive/2008/06/15/2550496.aspx
    走进Zend Framework框架编程(六):视图(1)
   
http://blog.csdn.net/zhangking/archive/2008/06/18/2562462.aspx
    走进Zend Framework框架编程(六):视图(2)
    走进Zend Framework框架编程(六):视图(3)
   
http://blog.csdn.net/zhangking/archive/2008/06/23/2578324.aspx

插播:Zend Framework 中的认证和授权

了解 ZF 的人,应该对 ZF 的权限管理印象深刻。ZF 手册在第一章的简介之后,第二章就拉出 Zend_Acl(访问控制授权)来介绍,而第三章就是Zend_Auth 的内容(认证)。从知识认知的逻辑顺序上,我个人感觉这样安排是不合适的。事实上第二和第三章的内容介绍,牵扯了许多后续章节的内容,我们不得不从第 7 章 Zend_Controller 开始看起。(注:后来了解到,Zend 的手册,是由志愿者翻译的,章节是以字母为顺序的!哈~~除过字典,我还没见过什么书是这么排列内容的!)
    但是不得不承认 ZF 的认证和授权是个优秀的设计。ZF 的认证相对简单,我们可以直接连接数据库表,对用户输入的用户名和密码进行比较,确认当前用户的身份。还提供了摘要式认证等方法。而 ACL(访问控制列表)提供的授权就很强大。
    ACL 提出了资源、角色、动作(访问)三个概念,用这些概念可以轻易构造一个强大的权限管理系统。而且权限管理可以细化到模块下的浏览、添加、删除、修改等细微动作。(ASP.NET 只能控制到页面(模块)级别,更细致的控制需要程序员自己设计和实现)。这里粘一些代码,仅仅是说明一下(在 IIS6.0 + ZF1.51 + Apache2.2 下调试的):
     //4种角色:admin, editor, guest, auditor, poweruser
     //4种资源:channel, item, bbs, blog
     //3种动作:view, edit, revise
     //要求:
     //admin对所有资源有所有权限
     //guest对所有资源只有view权限
     //editor对channel, item, bbs有view, edit权限
     //auditor对channel, item, bbs, blog有revise权限
     //poweruser继承editor, auditor权限
      //定义角色
      $acl = new Zend_Acl();
      $roleAdmin = new Zend_Acl_Role('admin');
      $acl->addRole($roleAdmin);
      $roleEditor = new Zend_Acl_Role('editor');
      $acl->addRole($roleEditor);
      $roleGuest = new Zend_Acl_Role('guest');
      $acl->addRole($roleGuest);
      $roleAuditor = new Zend_Acl_Role('auditor');
      $acl->addRole($roleAuditor);
      $rolePoweruser = new Zend_Acl_Role('poweruser');
      $acl->addRole($rolePoweruser, array('editor', 'auditor'));
      //添加资源
      $acl->add(new Zend_Acl_Resource('channel'));
      $acl->add(new Zend_Acl_Resource('item'));
      $acl->add(new Zend_Acl_Resource('bbs'));
      $acl->add(new Zend_Acl_Resource('blog'));
      //分配权限
      //admin:
      $acl->allow($roleAdmin, null, null);
      //editor:
      $acl->allow('editor', array('channel', 'item', 'bbs'), array('view', 'edit'));
      //guest:
      $acl->allow('guest', null, 'view');
      //auditor:
      $acl->allow('auditor', array('channel', 'item', 'bbs', 'blog'), array('revise'));
      //poweruser(从 editor, auditor 继承权限),可以再叠加另外的权限
      //$acl->allow('poweruser', null, null);
      //给所有角色分配权限(包括 admin)
      //$acl->allow(null, '', '');
      //$acl->deny(null, '', '');
      查看以上代码的最终权限,可以参考一下代码:
      foreach (array('admin', 'editor', 'guest', 'auditor', 'poweruser') as $role)
      {
       echo '<br>' . '角色 ' . $role . '<br>';
       echo '<table border=1>';
       echo '<tr><td align=center>\</td><th>channel</th><th>item</th><th>bbs</th><th>blog</th></tr>';
        foreach (array('view', 'edit', 'revise') as $right)
        {
          echo '<tr>';
          echo '<th>'. $right .'</th>';
          foreach (array('channel', 'item', 'bbs', 'blog') as $resource)
          {
           echo '<td>';
              echo $acl->isAllowed($role, $resource, $right)?"allowed" : "denied";
           echo '</td>';
          }
          echo '</tr>';
        }
        echo '</table>';
      }
    }
    这些资源、角色、动作,在以上代码片段里,是作为硬编码存在的,实际项目中,可以作为数据在数据库里保存。灵活应用就需要程序员的智慧了。
    ZF 的用户,只划分到“角色”这一级,也就是说,不能给某个用户直接分配权限。这肯定是出于简化设计而考虑的,否则设计和实现会复杂的多!这个有个小缺点,就是要为一个用户分配权限,必须首先建一个角色(相当于用户组),再把该用户加入到该角色下,通过角色来给该用户分配权限。而权限设计中,用户、角色、资源、动作的相互包含、交叉,其结果导致问题变得非常复杂!这么复杂的逻辑关系,不借助其他工具(例如计算机),人的大脑大多数情况下是很难想明白的。
    希望西安的 PHP 程序员、教师,一起来推动 PHP 在西安的应用和发展。