解析在PHP中使用全局变量的几种方法


Posted in PHP onJune 24, 2013

简介
即使开发一个新的大型PHP程序,你也不可避免的要使用到全局数据,因为有些数据是需要用到你的代码的不同部分的。一些常见的全局数据有:程序设定类、数据库连接类、用户资料等等。有很多方法能够使这些数据成为全局数据,其中最常用的就是使用“global”关键字申明,稍后在文章中我们会具体的讲解到。
使用“global”关键字来申明全局数据的唯一缺点就是它事实上是一种非常差的编程方式,而且经常在其后导致程序中出现更大的问题,因为全局数据把你代码中原本单独的代码段都联系在一起了,这样的后果就是如果你改变其中的某一部分代码,可能就会导致其他部分出错。所以如果你的代码中有很多全局的变量,那么你的整个程序必然是难以维护的。

本文将展示如何通过不同的技术或者设计模式来防止这种全局变量问题。当然,首先让我们看看如何使用“global”关键字来进行全局数据以及它是如何工作的。

使用全局变量和“global”关键字
PHP默认定义了一些“超级全局(Superglobals)”变量,这些变量自动全局化,而且能够在程序的任何地方中调用,比如$_GET和$_REQUEST等等。它们通常都来自数据或者其他外部数据,使用这些变量通常是不会产生问题的,因为他们基本上是不可写的。

但是你可以使用你自己的全局变量。使用关键字“global”你就可以把全局数据导入到一个函数的局部范围内。如果你不明白“变量使用范围”,请你自己参考PHP手册上的相关说明。
下面是一个使用“global”关键字的演示例子:

<?php
$my_var = 'Hello World';
test_global();
function test_global() {
    // Now in local scope
    // the $my_var variable doesn't exist
    // Produces error: "Undefined variable: my_var"
    echo $my_var;
    // Now let's important the variable
    global $my_var;
    // Works:
    echo $my_var;
}
?>

正如你在上面的例子中看到的一样,“global”关键字是用来导入全局变量的。看起来它工作的很好,而且很简单,那么为什么我们还要担心使用“global”关键字来定义全局数据呢?
下面是三个很好的理由:

1、代码重用几乎是不可能的。
如果一个函数依赖于全局变量,那么想在不同的环境中使用这个函数几乎是不可能的。另外一个问题就是你不能提取出这个函数,然后在其他的代码中使用。

2、调试并解决问题是非常困难的。
跟踪一个全局变量比跟踪一个非全局变量困难的多。一个全局变量可能会在一些不明显的包含文件中被重新定义,即使你有一个非常好的程序编辑器(或者IDE)来帮助你,你也得花了几个小时才能发现这个问题所在。

3、理解这些代码将是非常难的事情。
你很难弄清楚一个全局变量是从哪里来得,它是用来做什么的。在开发的过程中,你可能会知道知道每一个全局变量,但大概一年之后,你可能会忘记其中至少一般的全局变量,这个时候你会为自己使用那么多全局变量而懊悔不已。
那么如果我们不使用全局变量,我们该使用什么呢?下面让我们看看一些解决方案。
使用函数参数
停止使用全局变量的一种方法就是简单的把变量作为函数的参数传递过去,如同下面所示:

<?php
$var = 'Hello World';
test ($var);
function test($var) {
    echo $var;
}
?>

如果你仅仅只需要传递一个全局变量,那么这是一种非常优秀甚至可以说是杰出的解决方案,但是如果你要传递很多个值,那该怎么办呢?
比如说,假如我们要使用一个数据库类,一个程序设置类和一个用户类。在我们代码中,这三个类在所有组件中都要用到,所以必须传递给每一个组件。如果我们使用函数参数的方法,我们不得不这样:
   
<?php
$db = new DBConnection;
$settings = new Settings_XML;
$user = new User;
test($db, $settings, $user);
function test(&$db, &$settings, &$user) {
    // Do something
}
?>

显然,这是不值得的,而且一旦我们有新的对象需要加入,我们不得不为每一个函数增加多一个函数参数。因此我们需要用采用另外一种方式来解决。

使用单件(Singletons)解决函数参数问题的一种方法就是采用单件(Singletons)来代替函数参数。单件是一类特殊的对象,它们只能实例化一次,而且含有一个静态方法来返回对象的接口。下面的例子演示了如何构建一个简单的单件:

<?php
// Get instance of DBConnection
$db =& DBConnection::getInstance();
// Set user property on object
$db->user = 'sa';
// Set second variable (which points to the same instance)
$second =& DBConnection::getInstance();
// Should print 'sa'
echo $second->user;
Class DBConnection {
    var $user;
    function &getInstance() {
        static $me;
        if (is_object($me) == true) {
            return $me;
        }
        $me = new DBConnection;
        return $me;
    }
    function connect() {
        // TODO
    }
    function query() {
        // TODO
    }
}
?>

上面例子中最重要的部分是函数getInstance()。这个函数通过使用一个静态变量$me来返回这个类的实例,从而确保了只有一个DBConnection类的实例。
使用单件的好处就是我们不需要明确的传递一个对象,而是简单的使用getInstance()方法来获取到这个对象,就好像下面这样:
<?php
function test() {
    $db = DBConnection::getInstance();
    // Do something with the object
}
?>

然而使用单件也存在一系列的不足。首先,如果我们如何在一个类需要全局化多个对象呢?因为我们使用单件,所以这个不可能的(正如它的名字是单件一样)。另外一个问题,单件不能使用个体测试来测试的,而且这也是完全不可能的,除非你引入所有的堆栈,而这显然是你不想看到的。这也是为什么单件不是我们理想中的解决方法的主要原因。

注册模式
让一些对象能够被我们代码中所有的组件使用到(译者注:全局化对象或者数据)的最好的方法就是使用一个中央容器对象,用它来包含我们所有的对象。通常这种容器对象被人们称为一个注册器。它非常的灵活而且也非常的简单。一个简单的注册器对象就如下所示:

<?php
Class Registry {
    var $_objects = array();
    function set($name, &$object) {
        $this->_objects[$name] =& $object;
    }
    function &get($name) {
        return $this->_objects[$name];
    }
}
?>

使用注册器对象的第一步就是使用方法set()来注册一个对象:
<?php
$db = new DBConnection;
$settings = new Settings_XML;
$user = new User;
// Register objects
$registry =& new Registry;
$registry->set ('db', $db);
$registry->set ('settings', $settings);
$registry->set ('user', $user);
?>

现在我们的寄存器对象容纳了我们所有的对象,我们指需要把这个注册器对象传递给一个函数(而不是分别传递三个对象)。看下面的例子:
<?php
function test(&$registry) {
    $db =& $registry->get('db');
    $settings =& $registry->get('settings');
    $user =& $registry->get('user');
    // Do something with the objects
}
?>

注册器相比其他的方法来说,它的一个很大的改进就是当我们需要在我们的代码中新增加一个对象的时候,我们不再需要改变所有的东西(译者注:指程序中所有用到全局对象的代码),我们只需要在注册器里面新注册一个对象,然后它(译者注:新注册的对象)就立即可以在所有的组件中调用。

为了更加容易的使用注册器,我们把它的调用改成单件模式(译者注:不使用前面提到的函数传递)。因为在我们的程序中只需要使用一个注册器,所以单件模式使非常适合这种任务的。在注册器类里面增加一个新的方法,如下所示:

<?
function &getInstance() {
    static $me;
    if (is_object($me) == true) {
        return $me;
    }
    $me = new Registry;
    return $me;
}
?>

这样它就可以作为一个单件来使用,比如:
<?php
$db = new DBConnection;
$settings = new Settings_XML;
$user = new User;
// Register objects
$registry =& Registry::getInstance();
$registry->set ('db', $db);
$registry->set ('settings', $settings);
$registry->set ('user', $user);
function test() {
    $registry =& Registry::getInstance();
    $db =& $registry->get('db');
    $settings =& $registry->get('settings');
    $user =& $registry->get('user');
    // Do something with the objects
}
?>

正如你看到的,我们不需要把私有的东西都传递到一个函数,也不需要使用“global”关键字。所以注册器模式是这个问题的理想解决方案,而且它非常的灵活。

请求封装器
虽然我们的注册器已经使“global”关键字完全多余了,在我们的代码中还是存在一种类型的全局变量:超级全局变量,比如变量$_POST,$_GET。虽然这些变量都非常标准,而且在你使用中也不会出什么问题,但是在某些情况下,你可能同样需要使用注册器来封装它们。
一个简单的解决方法就是写一个类来提供获取这些变量的接口。这通常被称为“请求封装器”,下面是一个简单的例子:

<?php
Class Request {
    var $_request = array();
    function Request() {
        // Get request variables
        $this->_request = $_REQUEST;
    }
    function get($name) {
        return $this->_request[$name];
    }
}
?>

上面的例子是一个简单的演示,当然在请求封装器(request wrapper)里面你还可以做很多其他的事情(比如:自动过滤数据,提供默认值等等)。
下面的代码演示了如何调用一个请求封装器:
<?php
$request = new Request;
// Register object
$registry =& Registry::getInstance();
$registry->set ('request', &$request);
test();
function test() {
    $registry =& Registry::getInstance();
    $request =& $registry->get ('request');
    // Print the 'name' querystring, normally it'd be $_GET['name']
    echo htmlentities($request->get('name'));
}
?>

正如你看到的,现在我们不再依靠任何全局变量了,而且我们完全让这些函数远离了全局变量。

结论
在本文中,我们演示了如何从根本上移除代码中的全局变量,而相应的用合适的函数和变量来替代。注册模式是我最喜欢的设计模式之一,因为它是非常的灵活,而且它能够防止你的代码变得一塌糊涂。
另外,我推荐使用函数参数而不是单件模式来传递注册器对象。虽然使用单件更加轻松,但是它可能会在以后出现一些问题,而且使用函数参数来传递也更加容易被人理解。
PHP 相关文章推荐
让你同时上传 1000 个文件 (二)
Oct 09 PHP
PHP mcrypt可逆加密算法分析
Jul 19 PHP
php数组函数序列之array_unshift() 在数组开头插入一个或多个元素
Nov 07 PHP
解析csv数据导入mysql的方法
Jul 01 PHP
PHP用星号隐藏部份用户名、身份证、IP、手机号等实例
Apr 08 PHP
PHP简单实现HTTP和HTTPS跨域共享session解决办法
May 27 PHP
php过滤所有的空白字符(空格、全角空格、换行等)
Oct 27 PHP
centos+php+coreseek+sphinx+mysql之一coreseek安装篇
Oct 25 PHP
php使用file函数、fseek函数读取大文件效率对比分析
Nov 04 PHP
PHP+MySQL实现模糊查询员工信息功能示例
Jun 01 PHP
PHP PDOStatement::fetchObject讲解
Feb 01 PHP
解决php用mysql方式连接数据库出现Deprecated报错问题
Dec 25 PHP
探讨:array2xml和xml2array以及xml与array的互相转化
Jun 24 #PHP
解析Ubuntu下crontab命令的用法
Jun 24 #PHP
关于crontab的使用详解
Jun 24 #PHP
解析PHPExcel使用的常用说明以及把PHPExcel整合进CI框架的介绍
Jun 24 #PHP
关于Zend Studio 配色方案插件的介绍
Jun 24 #PHP
解析argc argv在php中的应用
Jun 24 #PHP
解析func_num_args与func_get_args函数的使用
Jun 24 #PHP
You might like
ecshop 批量上传(加入自定义属性)
2012/03/20 PHP
PHP中使用break跳出多重循环代码实例
2015/01/21 PHP
通过php删除xml文档内容的方法
2015/01/23 PHP
php实现的简单美国商品税计算函数
2015/07/13 PHP
Yii框架实现的验证码、登录及退出功能示例
2017/05/20 PHP
javascript父、子页面交互技巧总结
2014/08/08 Javascript
js获取页面传来参数的方法
2014/09/06 Javascript
javascript检查某个元素在数组中的索引值
2016/03/30 Javascript
浅谈Koa服务限流方法实践
2017/10/23 Javascript
使用nvm和nrm优化node.js工作流的方法
2019/01/17 Javascript
使用express来代理服务的方法
2019/06/21 Javascript
前端vue-cli项目中使用img图片和background背景图的几种方法
2019/11/13 Javascript
Bootstrap table 服务器端分页功能实现方法示例
2020/06/01 Javascript
[01:57]2016完美“圣”典风云人物:国士无双专访
2016/12/04 DOTA
[00:32]2018DOTA2亚洲邀请赛iG出场
2018/04/03 DOTA
python读文件逐行处理的示例代码分享
2013/12/27 Python
简单理解Python中的装饰器
2015/07/31 Python
Python和JavaScript间代码转换的4个工具
2016/02/22 Python
开源Web应用框架Django图文教程
2017/03/09 Python
Django学习笔记之为Model添加Action
2019/04/30 Python
CSS3制作圆形滚动进度条动画的示例
2020/11/05 HTML / CSS
adidas旗下高尔夫装备供应商:TaylorMade Golf(泰勒梅高尔夫)
2016/08/28 全球购物
意大利香水和彩妆护肤品购物网站:Ditano
2017/08/13 全球购物
Invicta手表官方商店:百年制表历史的瑞士腕表品牌
2019/09/26 全球购物
Linux文件系统类型
2012/02/15 面试题
机修工岗位职责
2013/11/24 职场文书
办公室文书岗位职责
2013/12/16 职场文书
教师辞职报告范文
2014/01/20 职场文书
计算机系统管理员求职信
2014/06/20 职场文书
党员民主评议总结
2014/10/20 职场文书
画展邀请函
2015/01/31 职场文书
2016年大学校运会广播稿件
2015/12/21 职场文书
python中pandas.read_csv()函数的深入讲解
2021/03/29 Python
Python使用OpenCV和K-Means聚类对毕业照进行图像分割
2021/06/11 Python
python调用ffmpeg命令行工具便捷操作视频示例实现过程
2021/11/01 Python
设置IIS Express并发数
2022/07/07 Servers