【问题标题】:Use global variables in a class在类中使用全局变量
【发布时间】:2012-08-09 00:44:55
【问题描述】:

我正在尝试创建一个分页类并使用该类外部的变量。

但它给了我致命错误“调用非对象上的成员函数 query()”。

这是索引文件:

$db = new DB_MySQL("localhost", "root", "", "test"); // connect to the database
include_once("pagi.php");

$pagination = new pagi();
$records = $pagination->get_records("SELECT * FROM `table`");

这是 pagi.php 文件:

class pagi {

    public function get_records($q) {
        $x = $db->query($q);
        return $db->fetch($x);
    }

}

是否可以在类内从类外使用此变量,而无需在类内创建新变量?

【问题讨论】:

  • 您可以在方法内部执行global $db;,但更好的方法是使用单例、依赖注入或数据库工厂模式来消除对全局变量的需要。
  • 最值得注意的是new pagi($db); 可能是可取的。将其从构造函数复制到对象属性中以供以后使用。尽管我个人并不反对单一的全球资源。 (您只需要确保您的应用永远不会超出单个数据库的容量。)
  • @JaredFarrish 谢谢你,效果很好 :) 但是你能不能提供你刚才所说的所有参考资料 :D 因为我实际上对他们一无所知 ^_^
  • @mario 演示的是dependency injection
  • 当然,有一群非常响亮的人认为全局变量是邪恶的、卑鄙的东西(或者只是bad)。正如@mario 所指出的,只要小心,它们不一定是问题,尤其是如果您是代码库的唯一开发人员。实际上,它们只是一种工具,但随着您作为开发人员的进步,您将学会在大多数情况下避免使用它们。

标签: php sql class oop


【解决方案1】:

解决这个问题的正确方法是将数据库对象注入另一个类(dependency injection):

$db = new DB_MySQL("localhost", "root", "", "test"); // connect to the database
include_once("pagi.php");

$pagination = new Paginator($db);
$records = $pagination->get_records("SELECT the, fields, you, want, to retrieve FROM `table`");

class Paginator
{    
    protected $db;

    // Might be better to use some generic db interface as typehint when available
    public function __construct(DB_MySQL $db)
    {
        $this->db = $db;
    }

    public function get_records($q) {
        $x = $this->db->query($q);
        return $this->db->fetch($x);
    }

}

另一种解决方法是将数据库类的实例注入到使用它的方法中:

$db = new DB_MySQL("localhost", "root", "", "test"); // connect to the database
include_once("pagi.php");

$pagination = new Paginator();
$records = $pagination->get_records("SELECT the, fields, you, want, to retrieve FROM `table`", $db);

class Paginator
{
    public function get_records($q, DB_MySQL $db) {
        $x = $db->query($q);
        return $db->fetch($x);
    }

}

您选择哪种方法取决于具体情况。如果只有一个方法需要数据库的实例,您可以将其注入到方法中,否则我会将其注入到类的构造函数中。

另外请注意,我已将您的课程从 pagi 重命名为 Paginator。恕我直言,Paginator 是该类更好的名称,因为其他人(重新)查看您的代码很清楚。另请注意,我已将第一个字母大写。

我所做的另一件事是更改查询以选择您正在使用的字段,而不是使用“通配符”*。这与我更改类名的原因相同:人们(重新)查看您的代码将确切知道将检索哪些字段,而无需检查数据库和/或结果。

更新

因为答案引发了关于为什么我会采用依赖注入路线而不是声明对象 global 的讨论,我想澄清为什么我会在 global 关键字上使用依赖注入:当你有一个方法如:

function get_records($q) {
    global $db;

    $x = $db->query($q);
    return $db->fetch($x);
}

当您在某处使用上述方法时,不清楚使用的类或方法是否依赖于$db。因此,它是一个隐藏的依赖项。上述情况不好的另一个原因是因为您将$db 实例(因此DB_MySQL)类与该方法/类紧密耦合。如果您需要在某个时候使用 2 个数据库怎么办。现在您必须通过所有代码将global $db 更改为global $db2。您永远不需要更改代码来切换到另一个数据库。因此,您不应该这样做:

function get_records($q) {
    $db = new DB_MySQL("localhost", "root", "", "test");

    $x = $db->query($q);
    return $db->fetch($x);
}

同样,这是一个隐藏的依赖关系,将DB_MySQL 类与方法/类紧密耦合。因此,也无法正确地对Paginator 类进行单元测试。您不仅要测试单元(Paginator 类),还可以同时测试 DB_MySQL 类。如果您有多个紧密耦合的依赖项怎么办?现在你突然用你所谓的单元测试来测试几个类。因此,当使用依赖注入时,您可以轻松地切换到另一个数据库类,甚至是用于测试目的的模拟类。除了只测试一个单元的好处(您不必担心由于依赖关系而得到错误的结果),它还可以确保您的测试能够快速完成。

有些人可能认为单例模式是访问数据库对象的正确方法,但应该清楚,阅读以上所有内容后,单例基本上只是另一种制作方式global。它可能看起来不同,但具有完全相同的特征,因此与global 存在相同的问题。

【讨论】:

  • 我在想同样的事情,认为它可能是一种不同的语言。 Paginate 可能是一个更好的类名,尽管我也想知道这是否是对类结构的适当使用。
  • @PeeHaa,你能解释一下为什么这是正确的方式吗?我已经用一个工厂类解决了这个精确的解决方案,它存储了DB 类的一个实例。然后像这样查询数据库:AppName::Database()->query()。您能否指出您的方法的优势是什么,尤其是与(静态)工厂模式相比?
  • @creativedutchmen 您打算从哪里调用该方法?来自另一个班级?如果您的回答是“是”,我真的很想知道您将如何测试该课程。您不能再模拟数据库类,因为您已将数据库类与分页器类紧密耦合,并且很难告诉您正在使用数据库类(隐藏的依赖项)。另请注意,您现在基本上已经创建了另一个 global 方法。
  • @PeeHaa:啊哈!那么主要区别在于依赖的隐藏性质吗?我不同意你在这个阶段数据库类是紧密耦合的:我什至会说它比你的方法更松散。例如,如果您想稍后更改 DB(例如更改为 postgre),则必须将所有代码从 $db = new DB_MySQL() 更改为 $db = new DB_PostgreSQL();。我只需要让Database 函数返回DB_PostgreSQL 类的实例。当然,App 类有非常紧密的依赖关系,但我并不认为这是一件坏事。
  • 我的意思是你不需要static 方法。除非你能给我提供一个 static 不能被依赖注入替换的用例。
【解决方案2】:

虽然我同意依赖模型很好,但对于数据库,我个人使用静态连接,该连接可用于数据库类的所有实例,并在需要时创建实例进行查询。这是一个例子:

<?php
//define a database class
class DB {
    //the static connection.
    //This is available to all instances of the class as the same connection.
    private static $_conn;

    //store the result available to all methods
    private $result;
    //store the last query available to all methods
    private $lastQuery;

    //static connection function. connects to the database and stores that connection statically.       
    public static function connect($host, $user, $pass, $db){
        self::$_conn = mysqli_connect($host, $user, $pass, $db);
    }

    //standard function for doing queries. uses the static connnection property.
    public function query($query){
        $this->lastQuery = $query;
        $this->result = mysqli_query(self::$_conn, $query);
        //process result, return expected output.
    }
}

//create connection to the database, this connection will be used in all instances of DB class
DB::connect('local', 'DB_USER', 'DB_PASS');

//create instance to query
$test = new DB;
//do query
$test->query("SELECT * FROM TABLE");

//test function
function foo(){
    //create instance to use in this function
    $bar = new DB;
    //do query
    $bar->query("SELECT * FROM OTHER_TABLE");
    //return results
    return $bar->fetchArray();
}

这样我可以在任何函数、方法...等中创建我想要的所有 DB 实例,并使用该类的本地实例来执行我​​的所有查询。所有实例都使用相同的连接。

需要注意的一点是,每个定义的类只允许一个与数据库的连接,但我只使用一个,所以这对我来说不是问题。

【讨论】:

  • 这基本上是单例模式的一个稍微不寻常的变体:您所有的数据库“对象”仅用于包装静态方法,因此您可以同样编写 DB::query("SELECT * FROM OTHER_TABLE"); 或拥有返回的方法 DB::singleton();已连接的对象。它并没有解决耦合问题,因为您仍然有一个“全局”变量 (DB::$_conn) 无法切换出来以进行测试或未来扩展。
【解决方案3】:

到目前为止,其他答案肯定比使用全局更可取,因为这会破坏您的封装(例如,您需要在调用该方法之前定义该对象)。

最好在方法签名中强制执行或不使用类。

【讨论】:

    【解决方案4】:

    您可以将 db-connection ($db) 添加到 get_records 方法的调用中:

    这里只是相关的代码行:

    第一个文件:

    $records = $pagination->get_records("SELECT * FROM `table`", $db);
    

    第二个文件:

    public function get_records($q, $db) {
    

    【讨论】:

    • 虽然不是一个理想的解决方案,但这比静态或全局变量要好,因为您每次调用该方法时都会有效地执行依赖注入。在构造函数中传递它的好处是你只需要做一次,而不是在每个方法上。
    猜你喜欢
    • 2013-04-19
    • 2012-07-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-31
    相关资源
    最近更新 更多