安全的替代方案:PHP 全局变量(良好的编程实践)

10

多年来,我一直在我的应用程序中使用 global $var,$var2,...,$varn 来定义方法。我主要用它们来实现以下两种功能:

获取已设置的类(例如 DB 连接),以及将信息传递给显示在页面上的函数。

示例:

$output['header']['log_out'] = "Log Out";
function showPage(){
     global $db, $output;
     $db = ( isset( $db ) ) ? $db : new Database();
     $output['header']['title'] = $db->getConfig( 'siteTitle' );
     require( 'myHTMLPage.html' );
     exit();
}

然而,这样做会有性能和安全方面的影响。

我可以采用什么替代方法来保持功能并改善设计、性能和/或安全性呢?

这是我在 Stack Overflow 上提出的第一个问题,如果需要澄清,请在评论中指出!


4
如果函数需要一个变量,你就把它传递给它。$foo = some_function($connectionObject); - tereško
1
@BenBrocka,可能有很多原因。例如,OP可能正在实现一个接口,其中方法签名不允许参数。 - Shiplu Mokaddim
@tereško 嗯?我并没有在这里挂着。 - John
@ChaitanyaChandurkar,除非整个核心都是用过程式编写的,而我们又没有经验去实际获得任何好处,否则我不会使用它。“除非你需要它,否则不要使用它”。 - John
好的,那第二种选择是你最好的选择。 - Chaitanya Chandurkar
显示剩余4条评论
6个回答

16

1. 全局变量。可以正常工作。全局变量不受喜爱,这就是我不使用它的原因。

其实全局变量并不只是不受喜爱,有它们被讨厌的原因。如果你没有遇到全局变量带来的问题,那就没关系了。你不需要重构你的代码。

2. 在我的config.php文件中定义一个常量。

这其实就像是一个全局变量,但是用另一个名字。你也不需要使用$,并且在函数开头使用global。WordPress就是这样为它们的配置做的,我认为这比使用全局变量更糟糕。这会让引入seams变得更加复杂。而且你无法将对象赋值给常量。

3. 在函数中包含config文件。

我认为这是一种额外的负担。你将代码库分割成了更小的部分,但收益微乎其微。这里的“全局变量”在其中会变成你所包含的文件的名称。


考虑到你的三个想法和我的评论,我想说:除非你遇到了某些全局变量的实际问题,否则你可以继续使用它们。全局变量可以作为你的服务定位器(配置、数据库)工作。其他方法会做更多事情来完成同样的任务。

如果你遇到了问题(例如,你可能想要开发测试驱动),我建议你逐步对每部分进行测试,然后学习如何避免使用全局变量。

依赖注入

由于在评论中已经明确表明您正在寻找依赖注入,如果您无法编辑函数参数定义,则可以-如果您使用对象-通过构造函数或使用所谓的setter方法注入依赖项。在下面的示例代码中,我将演示如何同时使用这两种方法,仅供演示目的,您可能已经猜到,同时使用两种方法是没有用的:

假设我们希望注入的依赖关系是配置数组。我们将其称为config,并将变量命名为$config。由于它是一个数组,因此我们可以将其类型提示为array。首先在一个包含文件中定义配置,也可以使用parse_ini_file,如果您喜欢ini文件格式,我认为这甚至更快。

config.php

<?php
/**
 * configuration file
 */
return array(
    'db_user' => 'root',
    'db_pass' => '',
);

那个文件可以在应用程序中的任何地方被引用:
$config = require('/path/to/config.php');

因此,在代码中的某个地方,它可以轻松地转换为一个数组变量。到目前为止,这并不是什么惊人的事情,也与依赖注入没有关系。让我们看一个需要在此处配置的示例数据库类,它需要用户名和密码,否则无法连接:

class DBLayer
{
    private $config;

    public function __construct(array $config)
    {
        $this->setConfig($config);
    }

    public function setConfig(array $config)
    {
        $this->config = $config;
    }

    public function oneICanNotChange($paramFixed1, $paramFixed2)
    {
        $user = $this->config['db_user'];
        $password = $this->config['db_pass'];
        $dsn = 'mysql:dbname=testdb;host=127.0.0.1';
        try {
            $dbh = new PDO($dsn, $user, $password);
        } catch (PDOException $e) {
            throw new DBLayerException('Connection failed: ' . $e->getMessage());
        }

        ...
}

这个例子有点粗糙,但是它展示了两个依赖注入的例子。首先是通过构造函数实现:

public function __construct(array $config)

这是一种常见的方式,所有类需要执行其工作的依赖项都在创建时注入。这也确保了当调用该对象的任何其他方法时,对象将处于可预测的状态-这对系统来说有些重要。

第二个示例是具有公共设置器方法(setter method)

public function setConfig(array $config)

这样可以在稍后添加依赖项,但有些方法可能需要在执行任务之前检查是否有可用的东西。例如,如果你可以创建DBLayer对象而不提供配置,则可以调用oneICanNotChange方法,而该对象没有配置应该处理(此示例未显示)。

服务定位器

由于您可能需要动态集成代码并希望将新代码放入依赖注入的测试中,从而使我们的生活更轻松,因此您可能需要将其与古老/遗留代码结合在一起。我认为那部分很难。仅依赖注入本身非常容易,但是将其与旧代码结合在一起并不那么直截了当。

我在这里建议您制作一个名为“服务定位器”的全局变量。它包含获取对象(甚至是像$config这样的数组)的中心点。然后可以使用它,并且其合同是单个变量名称。因此,为了消除全局变量,我们利用全局变量。如果您的新代码也过多使用它,它甚至会适得其反。但是,您需要一些工具来将旧代码和新代码结合在一起。因此,这是我能想到的迄今为止最简单的PHP服务定位器实现。

它由一个名为Services的对象组成,该对象提供所有服务,例如上面的config。因为在PHP脚本启动时,我们还不知道是否需要服务(例如,我们可能不运行任何数据库查询,因此不需要实例化数据库),因此它还提供了一些延迟初始化功能。这是通过使用工厂脚本来完成的,这些脚本只是设置服务并返回它的PHP文件。

一个示例:假设函数oneICanNotChange不是对象的一部分,而只是全局命名空间中的简单函数。我们将无法注入config依赖项。这就是Services服务定位器对象的用处所在:

$services = new Services(array(
    'config' => '/path/to/config.php',
));

...

function oneICanNotChange($paramFixed1, $paramFixed2)
{
    global $services;
    $user = $services['config']['db_user'];
    $password = $services['config']['db_pass'];

    ...

如示例所示,Services对象确实将字符串'config'映射到定义了$config数组路径的PHP文件上:/path/to/config.php。它使用ArrayAccess接口来在oneICanNotChange函数中公开该服务。
我建议在这里使用ArrayAccess接口,因为它定义清晰,表明我们有一些动态特性。另一方面,它使我们能够进行懒惰初始化:
class Services implements ArrayAccess
{
    private $config;
    private $services;

    public function __construct(array $config)
    {
        $this->config = $config;
    }
    ...
    public function offsetGet($name)
    {
        return @$this->services[$name] ?
            : $this->services[$name] = require($this->config[$name]);
   }
   ...
}

这个示例桩只需要工厂脚本,如果尚未加载,则返回脚本的返回值,例如数组、对象或字符串(但不包括NULL,这没有意义)。

我希望这些示例有所帮助,并且表明在这里不需要太多的代码就可以获得更大的灵活性并且减少代码中的全局变量。但是必须清楚,服务定位器会为您的代码引入全局状态。好处仅在于, 更容易将具体变量名称与其解耦并提供更大的灵活性。也许您能够将代码中使用的对象分成某些组,其中仅有一些需要通过服务定位器可用,并且可以使依赖定位器的代码保持简短。


哦,@hakre,我非常想点赞直到看到结尾...是的,把全局变量留下来可能没问题:代码仍然可以工作。但是OP正在寻求首选解决方案,而首选解决方案是依赖注入。虽然这不是计算机科学101的解决方案,需要相当多的经验,但它仍然是最优解决方案。 - user895378
是的,我应该提到依赖注入作为一种替代方案,但我对这里给出的信息细节不太自信(“由于我无法从那里访问变量并且也不应该需要,因此无法通过参数传递变量。”)- 所以我甚至不知道通过构造函数进行注入是否有效。如果不行,这又接近全局变量了。因此,我省略了这部分,因为这可能会变成一个非生产性的讨论,直到OP更清楚他想要什么以及他愿意为此做些什么。 - hakre
哇。我从未听说过“依赖注入”的术语。(首先我是系统管理员,后来才是无经验的程序员)但事实证明,我自己的PHP项目从1999年以来一直在使用依赖注入。@rdlowrey-感谢您教给我一个“新”的术语。:-D - ghoti
我添加了一个依赖注入和服务定位器的示例,希望对你有所帮助。我尽量让内容简洁轻松,以便你能够方便地采用。 - hakre
您还需要实现三个额外的方法才能使用ArrayAccess接口。 - josmith
@josmith:当然,这就是示例中的...所用的,这是有意为之,以免示例过于冗长。我可能应该编译一个要点摘要并提供链接。 - hakre

9
另一种选择被称为“依赖注入”。简而言之,这意味着您将函数/类/对象所需的数据作为参数传递。
function showPage(Database $db, array &$output) {
    ...
}


$output['header']['log_out'] = "Log Out";
$db = new Database;

showPage($db, $output);

以下是使用函数封装的好处:

  • 本地化/封装/命名空间功能(函数体不再具有与外部世界的隐式依赖关系,反之亦然,只要函数调用不改变,您现在可以重写任一部分而不需要重写其他部分)
  • 允许单元测试,因为您可以在不需要设置特定外部环境的情况下测试函数
  • 通过查看函数签名,清楚地知道函数将要对您的代码执行什么操作

1
对我来说,将它们作为参数传递的问题在于,对于任何给定的函数,我可能已经有了数十个已分配的资源需要传递,除了函数本身需要的参数。然后,每次我需要添加新的资源时,我都必须重新访问所有这些函数,以对参数进行适当的更改。 - ShaneC
4
我认为这涉及到一个更大的问题,即正确地构建你的应用程序。如果你需要不断传递更多的资源,那么这意味着这些函数的功能发生了变化(否则它们就不需要这些资源,对吧?),这意味着你的函数可能有糟糕的定义责任。假设确实有合理的需求,你可以传递一个包含所有所需资源的通用对象或数组(如果你需要传递超过一小撮的参数,这是推荐的)。使用MVC控制器对象方法也可以帮助解决问题。 - deceze

7

然而,像这样做会有性能和安全方面的影响。

实际上,并没有性能或安全问题。使用全局变量只是为了更清晰的代码,仅此而已。(好吧,前提是您不传递数十兆字节大小的变量)

因此,您首先必须考虑替代方案是否对您的代码更加“清晰”。

在编写更清晰的代码时,如果我看到在名为showPage的函数中出现数据库连接,我会感到担忧。


感谢您的输入!我很高兴得到了这个肯定,证明这不是一个严重的安全问题。有些人告诉我这是一个问题,但他们没有给出解释,这让我感到困惑。 - ShaneC

2
一些人可能不太喜欢的选项是创建一个singleton对象来负责保存应用程序状态。当您想要访问某个共享的“全局”对象时,您可以像这样调用:State::get()->db->query();$db = State::get()->db;
我认为这种方法是合理的,因为它避免了在各个地方传递大量的对象。
编辑:
使用这种方法可以帮助简化应用程序的组织和可读性。例如,您的状态类可以调用正确的方法来初始化数据库对象,并将其初始化与showPage函数分离。
class State {
    private static $instance;
    private $_db;

    public function getDB() {
        if(!isset($this->_db)){ 
            // or call your database initialization code or set this in some sort of
            // initialization method for your whole application
            $this->_db = new Database();
        }
        return $this->_db;
    }

    public function getOutput() {
        // do your output stuff here similar to the db
    }

    private function __construct() { }

    public static function get() {
        if (!isset(self::$instance)) {
            $className = __CLASS__;
            self::$instance = new State;
        }
        return self::$instance;
    }

    public function __clone() {
        trigger_error('Clone is not allowed.', E_USER_ERROR);
    }

    public function __wakeup() {
        trigger_error('Unserializing is not allowed.', E_USER_ERROR);
    }
}

你的展示页面功能可以是这样的:
function showPage(){
     $output = State::get()->getOutput();
     $output['header']['title'] = State::get()->getDB()->getConfig( 'siteTitle' );
     require( 'myHTMLPage.html' );
     exit();
}

使用单例对象的一种替代方法是将状态对象传递给各种函数,这样允许您拥有替代的“状态”如果您的应用程序变得复杂,而且您只需要传递一个状态对象。
function showPage($state){
     $output = $state->getOutput();
     $output['header']['title'] = $state->getDB()->getConfig( 'siteTitle' );
     require( 'myHTMLPage.html' );
     exit();
}

$state = new State; // you'll have to remove all the singleton code in my example.
showPage($state);

这是一个有趣的方法。您介意简要澄清一下相比于我当前的方法,它的一些优点吗? - ShaneC
谢谢!我将使用这种方法与@Col. Shrapnel的建议结合使用。我可能会继续将$output作为全局变量传递,但是单例将有助于减少代码实例化多个DB连接。 - ShaneC
不要在 public__clone__wakeup 中触发错误,而是将它们定义为 final private - Jacco
1
哦,而deceze的答案是更好的。单例只应在对象永远只能有一个实例时使用,因为业务规则或技术限制(例如,只能有一个活动会话)。在所有其他情况下,单例将带来比解决更多问题。 - Jacco

0
function showPage(&$output, $db = null){
     $db = is_null( $db )  ? new Database() : $db;
     $output['header']['title'] = $db->getConfig( 'siteTitle' );
     require( 'myHTMLPage.html' );
     exit();
}

并且

$output['header']['log_out'] = "Log Out";
showPage($output);

 $db =new Database();
showPage($output,$db);

我熟悉参数,但我觉得这是一个凌乱的实现。请记住,这必须适用于许多许多函数,并且其中处理的内容通常是动态的。 - ShaneC
@ShaneC,这是正确的模式。如果依赖注入变得繁琐,因为您需要传递太多参数,则表明在更大的范围内架构有问题。 - Jacco
@Jacco,说实话,问题并不是我需要传递太多参数 - 我正在使用具有类自动加载的MVC - 但是如果我决定合并一个新的全局资源,我觉得添加额外的参数会很繁琐。由于PHP并不真正支持类多态性,我觉得涉及这个业务是很棘手的。我的计划是使用当前的方法,放弃全局DB,而选择单例模式(除非被覆盖),以维护一个活动连接。如果您发现此计划存在错误,请告诉我! - ShaneC

0

开始使用面向对象编程设计你的代码,然后可以将配置传递给构造函数。你也可以将所有函数封装到一个类中。

<?php 
class functions{
    function __construct($config){
        $this->config = $config;
    }

    function a(){
        //$this->config is available in all these functions/methods
    }

    function b(){
        $doseSomething = $this->config['someKey'];
    }

    ...

}


$config = array(
'someKey'=>'somevalue'
);

$functions = new functions($config);

$result = $functions->a();
?>

如果您无法重构脚本,可以通过循环配置数组并定义常量来解决问题。

foreach($config as $key=>$value){
    define($key,$value);
}

我给出了三个选择是有原因的。重写核心不是一个选项。 - John
这不是重写。它只是一个你可以添加的新类。 - Shiplu Mokaddim
2
这并不比直接将$config传递给函数或使用全局变量更好。你只是用另一种语法来交换而已...而且你写的也不完全是面向对象编程,看起来更像是一个包含在类中的独立函数库。仅仅使用类并不能使代码成为面向对象编程。"设计"必须是面向对象的,而不是语法。 - Atli
@Atli,是的,我理解,但如果OP不想重构他的代码,有更好的答案吗? - Lawrence Cherone
@Lawrence Cherone 是的。它全部都是过程式的,即使你使用了类语法(尽管如此),所以你可以删除类定义,坚持使用函数,并使用“global”关键字导入$config。这大致相当于你所做的。-虽然,在非OOP代码可用的选项中,全局变量将排在第三位。首先是将其作为参数传递(OP排除了此选项),其次是使用常量。-它所有的信息都是全局可用的,所以最好正确地处理它。 - Atli

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