在PHP中扩展静态类 - 避免在扩展类中为多个类共享变量

3

我似乎在扩展PHP中的静态类上遇到了麻烦。

PHP代码:

<?php
    class InstanceModule {
        public static $className = 'None';
        public static function PrintClassName() {
            echo self::$className . ' (' . __CLASS__ . ')<br />';
        }
    }

    class A extends InstanceModule {
        public static function Construct() {
            self::$className = "A";
        }
    }

    class B extends InstanceModule {
        public static function Construct() {
            self::$className = "B";
        }
    }
?>

我的呼叫代码和期望:

<?php
    //PHP Version 5.3.14

    A::PrintClassName(); //Expected 'None' - actual result: 'None'
    B::PrintClassName(); //Expected 'None' - actual result: 'None'

    A::Construct();

    A::PrintClassName(); //Expected 'A' - actual result: 'A'
    B::PrintClassName(); //Expected 'None' - actual result: 'A'

    B::Construct();

    A::PrintClassName(); //Expected 'A' - actual result: 'B'
    B::PrintClassName(); //Expected 'B' - actual result: 'B'

    A::Construct();

    A::PrintClassName(); //Expected 'A' - actual result: 'A'
    B::PrintClassName(); //Expected 'B' - actual result: 'A'
?>

实际的完整输出:

None (InstanceModule)
None (InstanceModule)
A (InstanceModule)
A (InstanceModule)
B (InstanceModule)
B (InstanceModule)
A (InstanceModule)
A (InstanceModule)

从看起来的情况来看,当我在任一扩展类中设置self::$className时,它会覆盖另一个类的变量。我猜这是因为我使用静态类,只能有一个InstanceModule类,而不是将其简单复制到AB,这是我以前对extends的理解。我尝试使用关键字static::$className,但似乎没有任何区别。

如果有人能指导我正确的方向,告诉我我在这里做错了什么,以及如何解决这个问题,那就太好了。

编辑: 澄清一下,这段代码做到了我想要的效果,但显然是一个可怕的解决方法,因为它会破坏继承和重用函数的整个思路:

<?php
    class A {
        public static $className = 'None';
        public static function PrintClassName() {
            echo self::$className . ' (' . __CLASS__ . ')<br />';
        }
        public static function Construct() {
            self::$className = "A";
        }
    }

    class B {
        public static $className = 'None';
        public static function PrintClassName() {
            echo self::$className . ' (' . __CLASS__ . ')<br />';
        }
        public static function Construct() {
            self::$className = "B";
        }
    }
?>
4个回答

4

由于$className是静态的且位于父类中,当您在A或B中设置className时,它将更改父类中的变量,并在读取变量时执行相同操作。除非您在扩展类中覆盖className,否则您将从最初在InstanceModule中定义的相同内存位置存储和检索信息。

如果您在A/B中重新定义className,则可以使用parent::或self::从InstanceModule或A/B中访问className。根据您想要做什么,抽象类也可能发挥重要作用。

请参见PHP5手册上的Static KeywordClass Abstraction


1
类抽象对我似乎没有什么影响(虽然,我可能做错了)。如OP中所述,我已经尝试使用static关键字,并且最后,在两个扩展类中重新定义classNamepublic static $className ='None')实际上导致PrintClassName()始终输出None,即使self::$className仍在被定义。 - h2ooooooo
这确实帮助我理解了使用继承的public static。 - Steve Bauman

3

看起来在php 7.3中仍然存在此问题(在此处发布原始问题后的7年)。

使用__callStatic的答案似乎可能有效,但对于应该简单的东西而言太过复杂。另外两个答案似乎没有提供解决问题的工作方案,因此我将我的解决方法作为答案包含在这里。

您可以将静态变量替换为静态数组:

<?php
    class InstanceModule {
        public static $className = [];
        public static function PrintClassName() {
            $calledClass = get_called_class();
            if(empty(self::$className[$calledClass])){
              $thisClassName = "none";
            }else{
              $thisClassName = self::$className[$calledClass];
            }
            echo $thisClassName . ' (' . __CLASS__ . ')<br />';
        }
    }

    class A extends InstanceModule {
        public static function Construct() {
            $calledClass = get_called_class();
            self::$className[$calledClass] = "A";
        }
    }

    class B extends InstanceModule {
        public static function Construct() {
            $calledClass = get_called_class();
            self::$className[$calledClass] = "B";
        }
    }
?>

因此,每个InstanceModule的子类的“静态”值都存储在一个以其来源类名称命名的键下。

对我来说,PHP的一个bug似乎是兄弟类会共享静态属性......那应该永远不会发生,尤其是当父类是抽象的时候。这个解决方法不是很完美,但这是我找到的唯一一个不太复杂的可行方法。

通过这个解决方法,您可以获得所需的结果:

<?php   
    A::PrintClassName(); //Expected 'None' - actual result: 'None'
    B::PrintClassName(); //Expected 'None' - actual result: 'None'

    A::Construct();

    A::PrintClassName(); //Expected 'A' - actual result: 'A'
    B::PrintClassName(); //Expected 'None' - actual result: 'None'

    B::Construct();

    A::PrintClassName(); //Expected 'A' - actual result: 'A'
    B::PrintClassName(); //Expected 'B' - actual result: 'B'

    A::Construct();

    A::PrintClassName(); //Expected 'A' - actual result: 'A'
    B::PrintClassName(); //Expected 'B' - actual result: 'B'
?>

2

我建议将基类保留子类实例的存储库,这样您可以正确地区分属于每个类的数据,而不是从静态基类变量中提取该数据。

您可以在基类上使用__callStatic()魔术方法来调用不存在的子类上的方法,如下所示。不幸的是,由于该魔术方法的可见性,静态存储库变量需要声明为public。

abstract class Base
{
    public static $repo = array();

    public static function __callStatic($name, $args)
    {
        $class = get_called_class();

        if (!isset(self::$repo[$class])) {
                echo "Creating instance of $class\n";
                self::$repo[$class] = new $class();
        }

        return call_user_func_array(array(self::$repo[$class], $name), $args);
    }

        protected function PrintClassName()
        {
                echo __CLASS__, " (", get_called_class(), ")\n";
        }

        protected abstract function Construct($a);
}

class A extends Base
{
        protected function Construct($a)
        {
                echo __CLASS__, ": setting x := $a\n";
        }
}

class B extends Base
{
        protected function Construct($a)
        {
                echo __CLASS__, ": setting y := $a\n";
        }
}

A::PrintClassName();
B::PrintClassName();

A::Construct('X');
B::Construct('Y');

输出:

Creating instance of A
Base (A)
Creating instance of B
Base (B)
A: setting x := X
B: setting y := Y

1

我认为对于你的情况,最好的答案是使用get_called_class()函数代替你当前的$className变量。这将返回后期静态绑定类名,而不是__CLASS__get_class(),它们只会返回当前类名。

如果你将PrintClassName()函数更改为仅输出get_called_class()返回的内容,你的输出将如下所示。现在你只需要合并一个默认值,当然这个默认值将在类之间共享,所以如果你要继续使用静态方法,你必须在两个类中都有这个标志。

A
B
A
B
A
B
A
B

当然,A和B只是展示了正在进行的事情的形式。在我的实际场景中,我处理不同的“extends InstanceModule”类,但它们具有不同的函数(它们的公共函数在“InstanceModule”中)。这意味着当我构造类B并尝试访问“A :: FunctionInAClass”时,我会收到错误提示,因为此函数不存在(因为此时它仅具有“A :: FunctionInBClass”,即使该函数是在B中而不是A中声明的(但是A被B覆盖,就像在我的OP示例中一样)。 - h2ooooooo
看起来最好将这些类从静态类更改为可实例化的对象,这样你就可以获得你要寻找的继承了。 - nickb
事实上,这是我尝试创建更全局的类(和实例)调用方式。当然,我可以调用 $db = DB::GetInstance(); $db->Connect();,但是我在我的代码中尝试做的是使用一个包装器来简单地使用当前实例,当您运行 DB::Connect 时,它会在后台执行此操作。因此出现了这个问题。我想我不得不使用 $GLOBALS(呕吐)或者每次需要调用类时都要获取实例。 - h2ooooooo
你应该研究一下依赖注入或工厂设计模式。两者都能帮助你解决后面的问题(在需要调用类时获取实例)。 - nickb

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