PHP 框架中静态方法的替代方案

Rem*_*iDG 3 php coding-style class

最近我一直在尝试创建自己的 PHP 框架,只是为了从中学习(因为我们可能会研究一些更大、更强大的生产框架)。我目前的一个设计理念是,大多数核心类主要处理类内的静态函数。

几天前,我看到了几篇关于“静态方法是可测试性的死亡”的文章。这让我担心..是的..我的类主要包含静态方法..我使用静态方法的主要原因是很多类永远不需要多个实例,并且静态方法很容易在全局范围内访问。现在我意识到静态方法实际上并不是最好的方法,我正在寻找更好的替代方法。

想象一下以下代码来获取配置项:

$testcfg = Config::get("test"); // Gets config from "test"
echo $testcfg->foo; // Would output what "foo" contains ofcourse.

/*
 * We cache the newly created instance of the "test" config,
 * so if we need to use it again anywhere in the application,
 * the Config::get() method simply returns that instance.
 */
Run Code Online (Sandbox Code Playgroud)

这是我目前拥有的一个例子。但根据一些文章,这很糟糕。
现在,我可以按照 CodeIgniter 的方式执行此操作,例如使用:

$testcfg = $this->config->get("test");
echo $testcfg->foo;
Run Code Online (Sandbox Code Playgroud)

就我个人而言,我发现这更难阅读。这就是为什么我更喜欢另一种方式。

简而言之,我想我需要一种更好的方法来上课。我不希望配置类有多个实例,以保持可读性并轻松访问该类。有任何想法吗?

请注意,我正在寻找一些最佳实践或包括代码示例的内容,而不是一些随机的想法。另外,如果我绑定到 $this->class->method 样式模式,那么我会有效地实现它吗?

Eli*_*gem 5

回应 S\xc3\xa9bastien Renauld\ 的评论:这里有一篇关于依赖注入 (DI) 和控制反转 (IoC) 的文章,其中包含一些示例,以及关于好莱坞原则的一些额外说明(当致力于框架)。

\n\n

说你的类只需要一个实例并不意味着静态是必须的。事实上,远非如此。如果您浏览此站点,并阅读处理单例“模式”的 PHP 问题的 PHP 问题,您很快就会发现为什么单例有点禁忌。

\n\n

我不会详细介绍,但测试和单例不能混为一谈。依赖注入绝对值得仔细研究。我暂时就这样吧。

\n\n

回答你的问题:
\n你的例子(Config::get(\'test\'))意味着你在类的某个地方有一个静态属性Config。现在,如果您已经这样做了,正如您所说,为了方便访问给定的数据,想象一下,如果该值要在某个地方发生更改,那么调试您的代码将是一场噩梦……它是静态的,因此请更改一次之后,到处都变了。找出更改的位置可能比您预期的要困难。即便如此,与使用您的代码的人在相同情况下遇到的问题相比,这不算什么。\n然而,真正的
问题才会开始:如果你想访问给定对象中的实例,该实例已在某个类中实例化,那么有很多方法可以做到这一点(尤其是在框架中):

\n\n
class Application\n{//base class of your framework\n    private $defaulDB = null;\n    public $env = null;\n    public function __construct($env = \'test\')\n    {\n        $this->env = $env;\n    }\n    private function connectDB(PDO $connection = null)\n    {\n        if ($connection === null)\n        {\n            $connection = new PDO();//you know the deal...\n        }\n        $this->defaultDB = $connection;\n    }\n    public function getDB(PDO $conn = null)\n    {//get connection\n        if ($this->defaultDB === null)\n        {\n            $this->connectDB($conn);\n        }\n        return $this->defaultDB;\n    }\n    public function registerController(MyConstroller $controller)\n    {//<== magic!\n         $controller->registerApplication($this);\n         return $this;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

正如您所看到的,该类Application有一个方法将Application实例传递给您的控制器,或者您想要授予对该类范围的访问权限的框架的任何部分Application
\n请注意,我已将该defaultDB属性声明为私有属性,因此我使用了 getter。如果我愿意的话,我可以将连接传递给该吸气剂。当然,您可以利用这种连接做更多的事情,但我懒得编写一个完整的框架来向您展示您可以在这里做的所有事情:)。

\n\n

基本上,所有控制器都会扩展该类MyController,该类可能是一个抽象类,如下所示:

\n\n
abstract class MyController\n{\n    private $app = null;\n    protected $db = null;\n    public function __construct(Application $app = null)\n    {\n        if ($app !== null)\n        {\n            return $this->registerApplication($app);\n        }\n    }\n    public function registerApplication(Application $app)\n    {\n        $this->app = $app;\n        return $this;\n    }\n    public function getApplication()\n    {\n        return $this->app;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

因此,在您的代码中,您可以轻松地执行以下操作:

\n\n
$controller = new MyController($this);//assuming the instance is created in the Application class\n$controller = new MyController();\n$controller->registerApplication($appInstance);\n
Run Code Online (Sandbox Code Playgroud)\n\n

在这两种情况下,您都可以像这样获取单个数据库实例:

\n\n
$controller->getApplication()->getDB();\n
Run Code Online (Sandbox Code Playgroud)\n\n

getDB如果defaultDB在这种情况下尚未设置该属性,您可以通过将不同的数据库连接传递给该方法来轻松测试您的框架。通过一些额外的工作,您可以同时注册多个数据库连接并随意访问这些连接:

\n\n
$controller->getApplication->getDB(new PDO());//pass test connection here...\n
Run Code Online (Sandbox Code Playgroud)\n\n

这绝不是完整的解释,但我想在你最终得到一个巨大的静态(因此无用)代码库之前很快得到这个答案。

\n\n

回应OP的评论:

\n\n

关于我如何处理这Config门课。老实说,我几乎会做与defaultDB上面所示的财产相同的事情。但我可能会允许对哪些类可以访问配置的哪些部分进行更有针对性的控制:

\n\n
class Application\n{\n    private $config = null;\n    public function __construct($env = \'test\', $config = null)\n    {//get default config path or use path passed as argument\n        $this->config = new Config(parse_ini_file($config));\n    }\n    public function registerController(MyController $controller)\n    {\n        $controller->setApplication($this);\n    }\n    public function registerDB(MyDB $wrapper, $connect = true)\n    {//assume MyDB is a wrapper class, that gets the connection data from the config\n        $wrapper->setConfig(new Config($this->config->getSection(\'DB\')));\n        $this->defaultDB = $wrapper;\n        return $this;\n    }\n}\n\nclass MyController\n{\n    private $app = null;\n    public function getApplication()\n    {\n        return $this->app;\n    }\n    public function setApplication(Application $app)\n    {\n        $this->app = $app;\n        return $this;\n    }\n    //Optional:\n    public function getConfig()\n    {\n        return $this->app->getConfig();\n    }\n    public function getDB()\n    {\n        return $this->app->getDB();\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后两个方法并不是真正必需的,您也可以编写如下内容:

\n\n
$controller->getApplication()->getConfig();\n
Run Code Online (Sandbox Code Playgroud)\n\n

同样,这段代码有点混乱和不完整,但它确实向您展示了您可以通过将对该类的引用传递给另一个类来“公开”一个类的某些属性。即使属性是私有的,您仍然可以使用 getter 来访问它们。您还可以使用各种注册方法来控制注册对象可以看到的内容,就像我在代码片段中使用数据库包装器所做的那样。数据库类不应处理视图脚本和命名空间或自动加载器。这就是为什么我只注册配置的数据库部分。

\n\n

基本上,许多主要组件最终都会共享许多方法。换句话说,他们最终将实现给定的接口。对于每个主要组件(假设是经典的 MVC 模式),您将拥有一个抽象基类,以及 1 或 2 级子类的继承链:Abstract Controller>> 。\n同时,所有这些类可能会期望在构造时将另一个实例传递给它们。只需查看任何 ZendFW 项目的:DefaultControllerProjectSpecificController
index.php

\n\n
$application = new Zend_Application(APPLICATION_ENV);\n$application->bootstrap()->run();\n
Run Code Online (Sandbox Code Playgroud)\n\n

这就是您能看到的所有内容,但在应用程序内部,所有其他类都被实例化。这就是为什么您可以从任何地方访问所有内容:所有类都已在另一个类中实例化,如下所示:

\n\n
public function initController(Request $request)\n{\n    $this->currentController = $request->getController();\n    $this->currentController = new $this->currentController($this);\n    return $this->currentController->init($request)\n                                   ->{$request->getAction().\'Action\'}();\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

通过传递$this给控制器​​类的构造函数,该类可以使用各种 getter 和 setter 来获取它需要的任何内容...看看上面的示例,它可以使用getDB, 或getConfig并使用该数据(如果这是它需要的) 。
\n这就是我修改或使用的大多数框架的功能:应用程序启动并确定需要做什么。这就是好莱坞原则,或控制反转:应用程序启动,应用程序决定何时需要哪些类。在我提供的链接中,我认为这与创建自己的客户的商店进行比较:商店被建立,并决定它想要销售什么。为了出售它,它将创造它想要的客户,并为他们提供购买商品所需的手段......

\n\n

而且,在我忘记之前:是的,所有这一切都可以在没有单个静态变量发挥作用的情况下完成,更不用说函数了。我已经构建了自己的框架,并且我从来没有觉得除了“静态化”之外没有其他方法。我一开始确实使用了工厂模式,但很快就放弃了。
\n恕我直言,一个好的框架是模块化的:你应该能够毫无问题地使用它的一部分(比如 Symfony\ 的组件)。使用工厂模式会让你假设太多。您假设X 级可用,但并未给出。
\n注册那些可用的类可以使组件更加可移植。考虑一下:

\n\n
class AssumeFactory\n{\n    private $db = null;\n    public function getDB(PDO $db = null)\n    {\n        if ($db === null)\n        {\n            $config = Factory::getConfig();//assumes Config class\n            $db = new PDO($config->getDBString());\n        }\n        $this->db = $db;\n        return $this->db;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

相对于:

\n\n
class RegisteredApplication\n{//assume this is registered to current Application\n    public function getDB(PDO $fallback = null, $setToApplication = false)\n    {\n        if ($this->getApplication()->getDB() === null)\n        {//defensive\n            if ($setToApplication === true && $fallback !== null)\n            {\n                $this->getApplication()->setDB($fallback);\n                return $fallback;//this is current connection\n            }\n            if ($fallback === null && $this->getApplication()->getConfig() !== null)\n            {//if DB is not set @app, check config:\n                $fallback = $this->getApplication()->getConfig()->getSection(\'DB\');\n                $fallback = new PDO($fallback->connString, $fallback->user, $fallback->pass);\n                return $fallback;\n            }\n            throw new RuntimeException(\'No DB connection set @app, no fallback\');\n        }\n        if ($setToApplication === true && $fallback !== null)\n        {\n            $this->getApplication()->setDB($fallback);\n        }\n        return $this->getApplication()->getDB();\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

尽管后一个版本需要编写更多的工作,但很明显这两个版本中哪一个是更好的选择。第一个版本假设太多,并且不允许安全网。它也相当独裁:假设我已经编写了一个测试,并且我需要将结果发送到另一个数据库。因此,我需要更改整个应用程序的数据库连接(用户输入、错误、统计信息......它们都可能存储在数据库中)。
仅出于这两个原因,第二个片段是更好的候选者:我可以传递另一个数据库连接,它会覆盖应用程序默认值,或者,如果我不想这样做,我可以使用默认连接,或者尝试创建默认连接。存储我刚刚建立的连接,或者不存储...选择完全是我的。如果什么都不起作用,我就会被扔到RuntimeException我身上,但这不是重点。

\n