【问题标题】:Safe alternatives to PHP Globals (Good Coding Practices)PHP Globals 的安全替代品(良好的编码实践)
【发布时间】:2011-09-03 03:33:54
【问题描述】:

多年来,我一直在我的应用程序中使用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();
}

但是,这样做会对性能和安全性产生影响。

我可以使用哪些替代做法来保持我的功能,同时改进设计、性能和/或安全性?

这是我在 SO 上提出的第一个问题,所以如果您需要澄清,请发表评论!

【问题讨论】:

  • PHP 没有真正的全局变量。安全神话源于与register_globals 的混淆。 $db 全局是一种非常常见的做法。您的 $output 变量可能不太可取(无法从简短的 sn-p 中看出)。一般来说:与全局范围共享访问是可行的,而将其用于信令不应该普遍。
  • 感谢您的反馈!输出可能是所需全局的最佳示例,在我的情况下也是最不危险的。输出数组仅包含要直接输出到浏览器的内容。因此,这种情况几乎总是良性的,除非用户可以恶意向其中注入暴露凭据的代码。如果在 register_globals 关闭的情况下可以这样做,请告诉我!
  • 如果函数需要一个变量,你将它传递给它。 $foo = some_function($connectionObject);
  • 为什么不能只将变量传递给函数,必要时通过引用?
  • @BenBrocka 可能有很多原因。例如,OP 可能正在实现一个方法签名不允许参数的接口。

标签: php performance security


【解决方案1】:

1。全球人。奇迹般有效。 Globals 很讨厌,因此我不想使用它。

嗯,不只是讨厌全局变量。他们被讨厌是有原因的。如果您还没有遇到全局变量导致的问题,那很好。您无需重构代码。

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

这实际上就像一个全局变量,但有另一个名称。您也可以省略$,并在函数的开头使用global。 Wordpress 为他们的配置做了这个,我想说这比使用全局变量更糟糕。这使得引入接缝变得更加复杂。您也不能将对象分配给常量。

3。在函数中包含配置文件。

我认为这是开销。您对代码库进行分段并没有太大收获。此处的“全局”将成为您包含的文件的名称 btw..


考虑到您和我的 cmets 对他们的这三个想法,我会说:除非您遇到一些全局变量的实际问题,否则您可以坚持使用它们。然后全局作为您的服务定位器(配置、数据库)。其他人做更多的事情来创造同样的东西。

如果您遇到问题(例如,您可能想开发测试驱动),我建议您先将一个部分放在另一个待测部分,然后学习如何避免全局变量。

依赖注入

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

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

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方法

public function setConfig(array $config)

这允许稍后添加依赖项,但某些方法可能需要在完成工作之前检查可用的东西。例如。如果您可以在不提供配置的情况下创建 DBLayer 对象,则可以在该对象没有配置的情况下调用 oneICanNotChange 方法,并且应该必须处理它(本示例中未显示)。

服务定位器

由于您可能需要动态集成代码,并且希望通过依赖注入以及所有使我们的生活更轻松的方式对新代码进行测试,因此您可能需要将其与您的古老/遗留代码放在一起。我认为那部分很难。它自己的依赖注入很容易,但是将它与旧代码放在一起并不是那么简单。

我在这里可以建议的是,您创建一个全局变量,即所谓的服务定位器。它包含一个从中获取对象(甚至是像您的$config 这样的数组)的中心点。然后可以使用它,并且契约是那个单一的变量名。因此,要删除全局变量,我们使用全局变量。听起来有点适得其反,即使您的新代码也过多地使用它也是如此。但是,您需要一些工具将新旧结合在一起。所以这是迄今为止我能想象到的最简单的 PHP 服务定位器实现。

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

第一个例子:假设函数oneICanNotChange 不是对象的一部分,而只是全局命名空间中的一个简单函数。我们将无法注入 config 依赖项。这就是Services Service Locator 对象的用武之地:

$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 解决方案,需要相当多的经验,但它仍然是最佳解决方案。
  • 是的,我应该提到依赖注入作为替代方案,但我对这里给出的信息细节没有信心(“变量不能通过参数传递,因为我无法从那里访问它,也不应该需要它。”) - 所以我什至不知道通过构造函数注入是否会起作用。如果不是,这又接近一个全局变量。所以我把这部分排除在外,因为这可能会变成一场非生产性的讨论,直到 OP 更清楚他想要什么以及他愿意为此做什么。
  • 哇。我从来没有遇到过“依赖注入”这个词。 (我首先是系统管理员,后来是未经培训的程序员)。但事实证明,自 1999 年左右以来,我一直在自己的 PHP 项目中使用依赖注入。@rdlowrey - 感谢教我一个“新”术语。 :-D
  • 我添加了一个依赖注入和一个服务定位器示例,希望这很有用。我试图在这里保持轻松,以便您可以采用。
  • 您还需要实现三个额外的方法来使用 ArrayAccess 接口。
【解决方案2】:

另一种方法称为依赖注入。简而言之,这意味着您将函数/类/对象所需的数据作为参数传递。

function showPage(Database $db, array &$output) {
    ...
}


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

showPage($db, $output);

这更好,原因有很多:

  • 本地化/封装/命名空间功能(函数体不再隐含依赖于外部世界,反之亦然,您现在可以重写任一部分而无需重写另一部分,只要函数调用不变)
  • 允许单元测试,因为您可以单独测试功能而无需设置特定的外部世界
  • 只需查看签名即可清楚地知道函数将对您的代码执行什么操作

【讨论】:

  • 不过,对我来说,将这些作为参数传入的问题是,对于任何给定的函数,除了函数的参数之外,我可能还有可能需要传递的数十个已分配资源已经需要了。然后,每次我需要添加新资源时,我都必须重新访问所有这些函数以对参数进行适当的更改。
  • 我认为这可以追溯到正确构建应用程序的更大问题。如果您需要继续传递更多资源,这意味着这些函数的功能会发生变化(否则它们将不需要资源,对吗?),这意味着您的函数可能具有错误定义的职责。假设有一个合法的需求,你可以传递一个包含所有需要的资源的通用对象或数组(如果你需要传递多个参数,无论如何推荐)。 MVC 控制器对象方法也可以提供帮助。
【解决方案3】:

但是,这样做会对性能和安全性产生影响。

说实话,没有性能或安全后果。使用全局变量是一个更简洁的代码问题,仅此而已。 (好吧,只要你不传递几十兆大小的变量)

因此,您必须首先考虑,替代方案是否会让您的代码更干净

在更简洁的代码方面,如果我在名为 showPage 的函数中看到数据库连接,我会感到害怕。

【讨论】:

  • 感谢您的意见!我很高兴得到保证,这不是一个明显的安全问题。人们告诉我是这样,但我没有得到解释——这导致了我的困惑。
【解决方案4】:

有些人可能不赞成的一个选项是创建一个负责保存应用程序状态的singleton 对象。当您想访问某个共享的“全局”对象时,您可以拨打如下电话:State::get()-&gt;db-&gt;query();$db = State::get()-&gt;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);

【讨论】:

  • 这是一个有趣的方法。您是否介意简要说明一下我目前的方法的一些好处?
  • 谢谢!我将把这种方法与@Col 结合使用。弹片的建议。我可能会继续将 $output 作为全局传递,但单例将有助于减少代码实例化多个数据库连接。
  • 不要在public __clone & __wakeup 中触发错误,而是将它们定义为final private
  • 哦,deceze's answer 更好。由于业务规则或技术限制(例如,只能有一个活动会话),只有当对象只有一个实例时才应该使用单例。在所有其他情况下,单例会导致比它们解决的问题更多。
【解决方案5】:
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,这是要遵循的正确模式。如果依赖注入因为需要传递许多参数而变得繁重,这就是大规模错误架构的迹象。
  • @Jacco 好吧,说实话,我不需要传递这么多参数——我正在使用具有类自动加载功能的 MVC——但如果我决定合并一个新的全局资源,我感觉添加一个额外的参数很麻烦。由于 PHP 并不真正支持类多态性,我觉得进入该行业是冒险的。我的计划是使用我当前的方法,没有全局数据库,而不是单例(保持一个活动连接,除非被覆盖)。如果您发现此计划有错误,请告诉我!!
【解决方案6】:

开始在 OOP 中设计您的代码,然后您可以将配置传递给构造函数。您还可以将所有函数封装到一个类中。

<?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);
}

【讨论】:

  • 我给出三个选择是有原因的。重写核心不是一种选择。
  • 这不是重写。它只是一个你可以添加的新类
  • 这并不比直接将 $config 传递给函数或使用全局变量更好。您只是将一种语法交换为另一种语法......而且您编写的也不完全是 OOP,它看起来更像是一个包装在一个类中的独立函数库。 - 仅使用类不会使代码 OOP。 “设计”必须是 OOP,而不是语法。
  • @Atli 是的,我明白,当 OP 不想重构他的代码时,还有更好的答案吗?
  • @Lawrence Cherone 是的。这都是程序性的,即使是你写的(尽管你使用了类语法),所以你也可以删除类定义,坚持使用函数,然后使用“global”关键字导入 $config。这大致相当于你所做的。 - 虽然,在非 OOP 代码可用的选项中,全局变量将在我的列表中排在第三位。首先将其作为参数传递(OP 排除了),其次将使用常量。 - 无论如何,这都是全球可用的信息,所以你不妨好好做。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-15
  • 2010-10-24
  • 2013-03-07
  • 2012-04-08
  • 1970-01-01
相关资源
最近更新 更多