停止在PHP中使用`global`

48

我有一个config.php文件,它被包含在每个页面中。在配置文件中,我创建了一个类似于以下的数组:

$config = array();
$config['site_name']      = 'Site Name';
$config['base_path']      = '/home/docs/public_html/';
$config['libraries_path'] = $config['base_path'] . '/libraries';
//etc...

我有一个名为function.php的文件,它几乎被包含在每个页面中。我必须使用global $config才能访问它 - 而我希望消除这一点!

如何在代码的其他部分中访问$config而不使用global

有人可以解释一下,为什么我不应该在我的示例中使用global吗?有些人说这是一种不好的做法,还有些人说这不安全?

编辑1:

我使用它的示例:

function conversion($Exec, $Param = array(), $Log = '') {
    global $config;
    $cmd = $config['phppath'] . ' ' . $config['base_path'] . '/' . $Exec;
    foreach ($Param as $s)
    {
        $cmd .= ' ' . $s;
    }
}

编辑2:

Vilx建议的那样将所有内容放在类中,可能很酷,但在这种情况下,我该如何与从数据库提取配置keyvalue的以下循环绑定呢?
我过于简化了分配$config数组的想法,这里是一个例子:

$sql = "SELECT * from settings";
$rsc = $db->Execute($sql);
if ( $rsc ) {
    while(!$rsc->EOF) {
        $field = $rsc->fields['setting_options'];
        $config[$field] = $rsc->fields['setting_values'];
        @$rsc->MoveNext();
    }
}

编辑 3:

此外,我必须从在配置中设置的函数访问其他vars,只有几个,例如:$db$language等。

如果我将它们放入类中,这真的会解决问题吗?如果我使用global,它真正改变了什么?

编辑 4:

我阅读了PHP global in functions,其中Gordon以非常好的方式解释了为什么不应该使用global。 我同意他所说的一切,但在我的情况下,我不使用global重新分配变量,这将导致像他说的那样的<--WTF!!,哈哈,同意,这很疯狂。 但是,如果我只需要通过global $db从函数中访问数据库,在这种情况下有什么问题? 否则,如何在不使用global的情况下完成这项工作?

编辑 5:

在同一篇 PHP global in functions文章中,deceze说:"反对全局的一个主要原因是它意味着函数依赖于另一个范围。这将很快变得混乱。"

但是我在这里谈论的是基本的“INIT”。 我基本上设置了define,但使用vars--从技术上讲,这是错误的。 但是,您的函数并不依赖于任何东西-但是其中一个var的名称$db,您可以记住吗? 需要真正的全局才能使用$db,这里有什么依赖关系,以及如何在不使用global的情况下使用它?

P.S. 我刚想到,我们在这里面临两种不同思维之间的冲突,例如: 我的(尚未完全理解面向对象编程)和那些可能被称为OOP大师(从我的当前观点来看) - 对他们而言显而易见的事情对我来说会产生新的问题。 我认为这就是为什么这个问题一遍又一遍地被问的原因。 就我个人而言,虽然这已经更加清晰明了,但仍有需要澄清的问题。


目前在使用 config 的任何示例中都需要使用全局变量的地方有哪些? - Daedalus
1
require_once 不够用吗? - Marek Sebera
4
如果在函数内调用 $config,就必须使用 global 关键字,否则会创建一个名为 $config 的局部变量。我认为这并不特别不安全,只是很多人看到 "global" 就想到 "红色警戒",而没有实际推理具体问题。 - nico
@Nico:对我来说听起来一样!但是我不想编写一个丑陋的代码,而且在不久的将来我还得重建它。最好现在就处理好。 - Ilia
1
@IliaRostovtsev 这是不好的,因为依赖关系是隐藏的。你期望某个特定名称的变量存在于全局范围内,而不是在调用该函数时将该变量传递给它,这样更加明确和清晰。 - Gordon
显示剩余8条评论
6个回答

59

global变量的缺点是它们将代码耦合得非常紧密。整个代码库都依赖于a)变量名$config和b)该变量的存在。如果您想重命名该变量(出于任何原因),则必须在整个代码库中进行相应更改。您也不能再独立于变量地使用与其相关的任何代码片段。

global变量示例:

require 'SomeClass.php';

$class = new SomeClass;
$class->doSomething();

在上述代码中,由于类或者SomeClass.php中的某些代码隐式依赖于全局变量$config,所以你有可能会遇到错误。然而,仅从类本身看是没有任何迹象表明这一点的。要解决此问题,请按照以下步骤操作:

$config = array(...);

require 'SomeClass.php';

$class = new SomeClass;
$class->doSomething();

如果您没有在$config中设置正确的键,则此代码可能仍然会在某处失败。由于不清楚SomeClass需要或不需要配置数组的哪些部分以及何时需要它们,因此很难重新创建正确的环境以使其正确运行。如果您在想要使用SomeClass的任何地方已经有一个用于其他用途的变量$config,它还会创建冲突。

因此,不要创建隐式的、不可见的依赖项,而是将所有依赖项注入

require 'SomeClass.php';

$arbitraryConfigVariableName = array(...);

$class = new SomeClass($arbitraryConfigVariableName);
$class->doSomething();

通过显式将配置数组作为参数传递,可以解决上述所有问题。就像在应用程序内部“传递所需信息”一样简单。这也使应用程序的结构和流程以及各个部分之间的交互更加清晰。如果您的应用程序目前是一个大混乱,那么达到这种状态可能需要进行一些重构。


您的代码库越大,您就需要将不同部分彼此“解耦”。如果您的代码库中每个部分都依赖于其他每个部分,那么您无法单独测试、使用或重用其中任何一部分。这只会导致混乱。要将部分从彼此分离,请将它们编码为类或函数,并将所有所需数据作为参数传递。这会在代码的不同部分之间创建干净的接口。


试图将您的问题综合成一个例子:

require_once 'Database.php';
require_once 'ConfigManager.php';
require_once 'Log.php';
require_once 'Foo.php';

// establishes a database connection
$db = new Database('localhost', 'user', 'pass');

// loads the configuration from the database,
// the dependency on the database is explicit without `global`
$configManager = new ConfigManager;
$config = $configManager->loadConfigurationFromDatabase($db);

// creates a new logger which logs to the database,
// note that it reuses the same $db as earlier
$log = new Log($db);

// creates a new Foo instance with explicit configuration passed,
// which was loaded from the database (or anywhere else) earlier
$foo = new Foo($config);

// executes the conversion function, which has access to the configuration
// passed at instantiation time, and also the logger which we created earlier
$foo->conversion('foo', array('bar', 'baz'), $log);

我会将各个类的实现留给读者作为练习。当您尝试实现它们时,您会注意到它们非常易于理解和实现,不需要使用单个global。每个函数和类都通过函数参数传递其所有必要的数据。显然,上述组件可以以任何其他组合方式连接在一起,或者依赖项可以轻松替换为其他组件。例如,配置根本不需要来自数据库,或者记录器可以将日志记录到文件而无需Foo::conversion知道任何此类信息。


ConfigManager的示例实现:

class ConfigManager {

    public function loadConfigurationFromDatabase(Database $db) {
        $result = $db->query('SELECT ...');

        $config = array();
        while ($row = $result->fetchRow()) {
            $config[$row['name']] = $row['value'];
        }

        return $config;
    }

}

这是一段非常简单的代码,甚至并没有做太多事情。您可能会问为什么要将其作为面向对象的代码。这样做的关键在于可以使使用此代码变得极其灵活,因为它与其他所有内容完全隔离开来。您输入一个数据库连接,就可以获得一个具有特定语法的数组。输入→输出。清晰的缝隙,清晰的接口,最小化,良好定义的职责。您也可以用一个简单的函数来实现同样的效果。

对象具有的额外优势是进一步解耦了调用loadConfigurationFromDatabase函数的代码和该函数的任何特定实现。如果您只是使用全局的function loadConfigurationFromDatabase(),您基本上又遇到了相同的问题:当您尝试调用它时,需要定义该函数,并且如果您想要替换它,会出现命名冲突。通过使用对象,关键部分的代码移到了这里:

$config = $configManager->loadConfigurationFromDatabase($db);
你可以在此处替换$configManager为任何其他具有方法loadConfigurationFromDatabase的对象。这就是"鸭子类型"。只要它有一个loadConfigurationFromDatabase方法,你不关心$configManager到底是什么。如果它像鸭子一样走路和叫,那么它就是鸭子。或者更准确地说,如果它有一个loadConfigurationFromDatabase方法并返回一个有效的配置数组,它就是某种ConfigManager。你已经将代码从一个特定的变量$config、一个特定的loadConfigurationFromDatabase函数甚至一个特定的ConfigManager中解耦出来了。所有部分都可以被改变、交换、替换和动态加载,因为代码不依赖于任何特定的其他部分。 loadConfigurationFromDatabase方法本身也不依赖于任何一个特定的数据库连接,只要它可以调用query并获取结果即可。传递给它的$db对象完全可以是虚假的,它可以从XML文件或任何其他地方读取数据,只要它的接口仍然表现相同。

2
@Ilia 如果你的意思是想使用静态类属性,即“只需调用Config::$SiteName”,那么请不要这样做。这与global变量相同。每次使用依赖于Config::$SiteName的代码时,您都必须确保已加载该类并具有正确的配置。这与global $config没有区别。您应该使用函数参数。这是将变量分隔到独立作用域的唯一方法。 - deceze
3
这是非常出色的解释,我相信对其他程序员肯定会有所帮助。如果我能为你的答案投两次票,我一定会的!!谢谢你的时间! :-) - Ilia
4
谢谢!最终我明白了依赖注入! :) 现在要弄清楚它的“黑暗面”,以及不适用的情况。 :) - Vilx-
1
@Vilx 依赖注入的黑暗面是对象图。这意味着,决定何时何地实例化您的实际对象以及什么注入到什么中。每当您实例化一个对象(new MyClass),您就将那个代码行与该特定类耦合在一起。为了避免这种情况,通常使用工厂进行注入。但是,该工厂也需要在某个地方实例化...如果您将其推向极端,对象实例化逻辑和依赖管理的复杂性将超过其好处。 - deceze
1
松耦合和紧内聚。逐步细化。如果没有别的,就要做这些事情。学习尽可能多的面向对象编程,然后尽可能多地了解设计模式。注意可见性、抽象类、接口,在PHP中还有traits。如果没有别的,考虑模块化、封装和代码重用。通过努力,每个人都可以写出更好的代码。我一直在努力改进我的代码。永远有更多的东西需要学习,接受这一点是让你的代码不再依赖于像“global”关键字之类的东西的最佳方式。 - Anthony Rutledge
显示剩余4条评论

9

我用一个类来解决这个问题:

class Config
{
    public static $SiteName = 'My Cool Site';
}

function SomeFunction
{
    echo 'Welcome to ' , Config::$SiteName;
}

使用常量的建议是明智的,但我建议给所有常量加上前缀,例如CFG_SITE_NAME,以避免与其他常量意外重名。


2
@IliaRostovtsev - 嗯,什么?在PHP中的一个类基本上和一个数组一样(你一直在使用数组),只是语法有些不同。 - Vilx-
9
类常量/静态值本质上与全局变量相同。它们并未解决任何问题。 - deceze
3
@IliaRostovtsev - 我认为这并没有任何根本性的问题,只是个人偏好不同。我喜欢这种方式是因为我不必每次都写 global 这一行代码。另外,你要求寻找一种替代方案,而我已经提供了一个选择。 - Vilx-
3
这种方式并没有比使用全局变量更好:你仍然与Config类紧密耦合。 - marco-fiset
@Vilx- 不是,但它确实是。在这里,您只是使用另一种形式的全局变量,但没有关键字。 - marco-fiset
显示剩余6条评论

6

针对您的情况,我建议创建一个名为constants.php的文件,其中包含定义(如果您的目的是确保这些“变量”在执行期间不会被更改):

define('SITE_NAME','site name');

define('BASE_PATH','/home/docs/public_html/');

...

在需要使用它的所有文件中,包含这个constants.php文件:

include_once('constants.php');

请正确格式化您的代码。通过在任何代码行之前缩进4个空格来插入代码块。我已经为您格式化了代码,但下次请正确格式化它。如需进一步帮助,请参阅编辑FAQ - Madara's Ghost
我会使用这个,但是从数据库中调用数据。 - Adsy2010
我如何从函数中访问在配置中设置的其他变量?例如 $db(连接到数据库)或 $language(本地化)。 - Ilia
3
常量与“全局”变量相同,它们并不能解决任何问题。 - deceze
1
@deceze他们解决了这个问题,说他想在每个函数的开头使用global $config - Krycke

3

面向对象和过程式方法之间存在着很大的争议(更普遍地说,是声明式和命令式方法),每种方法都有其优缺点。

我使用了一个名为“Config”的类,它是单例模式(全局变量的面向对象版本)。这对我来说效果很好,直到我发现需要在一个应用程序中同时使用几个先前开发的解决方案 - 由于所有配置都是全局的,并且由同一类(在您的情况下是同一变量名)引用,它们会发生冲突,每次从其他子应用程序调用代码时都必须切换到正确的配置。

你有两种方式:

a)要么按照你已经习惯并熟悉的方式设计你的应用程序(这样会更好,因为你已经有了经验,可以预测开发需要多长时间以及可能出现哪些问题或不会出现哪些问题);当你陷入当前方法的限制时,重构以避免全局变量;

b)看看面向对象框架是如何做的(至少看三到四个,例如Cake,CodeIgniter,Zend,Symfony,Flow3),然后借鉴一些东西,或者转向使用一个框架(或许你会更加确信自己做得没错)。


@Sebas,“使用命名空间”在较新的面向对象编程框架中很常见 :) - Andrés Morales

1
我创建了一个简单的小类:
class Config {

    private static $config = array();

    public static function set( $key, $value ) {
        self::$config[$key] = $value;
    }

    public static function get( $key ) {
        return isset( self::$config[$key] ) ? self::$config[$key] : null;
    }
}

Config::set( 'my_config', 'the value' );

echo 'the config value is: ' . Config::get('my_config');

这个可以很容易地重构为一个函数isSet($key)或者一个setAll($array)

编辑:现在语法应该是有效的。

你可以很容易地修改这个类,如下所示:

class Config {

    private static $config = array();

    public static function set( $key, $value ) {
        self::$config[$key] = $value;
    }

    public static function get( $key ) {
        return isset( self::$config[$key] ) ? self::$config[$key] : null;
    }

    public static function setAll( array $array ) {
        self::$config = $array;
    }

    public static function isKeySet( $key ) {
        return isset( self::$config[ $key ] );
    }
}

Config::setAll( array(
    'key' => 'value',
    'key2' => array( 'value',
                    'can be an',
                    'array' ) ) );
Config::set( 'my_config', 'the value' );

if( Config::isKeySet( 'my_config' ) ) {
    echo 'the config value is: ' . Config::get('my_config');
}

您仍然需要在使用配置的任何其他文件中包含该文件,或者使用自动加载程序

编辑2:

这与使用全局变量基本相同,不同之处在于您不需要在每个函数开头声明要使用它。如果您想全局使用Configs,则Configs必须以某种方式是全局的。将某些内容放入全局范围时,您需要考虑是否可以将此信息暴露给其他不应查看此信息的类...默认配置?我认为将其放在全局范围内是安全的,然后您只需要一些易于修改和定制的内容即可。

如果您认为这是危险信息,不应该被其他类访问,那么您可能需要了解依赖注入。使用依赖注入,一个类将在其构造函数中接受一个对象,并将其私有地放置在一个变量中以供使用。这个对象可以是来自配置类的对象,然后您需要创建一个包装器类,首先创建配置对象,然后注入配置的模板对象。这是一种常见的设计模式,例如领域驱动设计中经常看到的。

它包含无效的语法。你能否用例子详细解释一下,可以在CodePad上吗? - Ilia
谢谢你,Krycke!稍后我会仔细看一下并告诉你!;) - Ilia
2
作为面向对象的方法很好,但为什么它比使用 global $config 然后使用 $config['site_name'] 更好呢?如果我理解正确,你的解决方案与 Vilx- 的解决方案是一样的,对于漂亮的面向对象风格的工作示例给予 +1。 - Ilia

0

config.php

<?php
class config {

    private static $config = array();

    public static function set( $key, $value ) {
        self::$config[$key] = $value;
    }

    public static function get( $key ) {
        if( config::isKeySet( $key ) ) {
            return isset( self::$config[$key] ) ? self::$config[$key] : null;
        }
    }

    public static function setAll( array $array ) {
        self::$config = $array;
    }

    public static function isKeySet( $key ) {
        return isset( self::$config[ $key ] );
    }
}

// set valuable values

config::setAll( array(
    'key' => 'value',
    'key2' => array( 'value', 'can be an', 'array' ),
    'database' => array( "username" => "root", "password" => "root")
                     )
    );
config::set( 'my_config', 'the value' );
?>

config.usage.php

<?php
require_once 'config.php';
$database_credentials = config::get('database');
echo 'the config value for username is ' . $database_credentials['username'];
echo '<br> the config value for password is ' . $database_credentials['password'];

function additionalFunctionality($database_credentials)
{
    echo '<br> the config value for password is ' . $database_credentials['password'];
}
?>

config.usage.too.php

<?php
require_once 'config.php'; // put this first
require_once 'config.usage.php'; // include some functionality from another file
$database_credentials = Config::get('database');
echo 'the config value for username is ' . $database_credentials['username'];

additionalFunctionality($database_credentials); // great
?>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接