在PHP中正确地扩展ArrayObject?

7

问题:我试图扩展PHP的ArrayObject,如下所示。不幸的是,在设置多维对象时,我无法使其正常工作,而是会出现错误,因为我在PHP中启用了严格模式。(错误:Strict standards: Creating default object from empty value

问题是:如何修改我的类以自动为我创建不存在的级别?

代码:

$config = new Config;
$config->lvl1_0 = true; // Works
$config->lvl1_1->lvl2 = true; // Throws error as "lvl1" isn't set already

class Config extends ArrayObject
{
    function __construct() {
        parent::__construct(array(), self::ARRAY_AS_PROPS);
    }

    public function offsetSet($k, $v) {
        $v = is_array($v) ? new self($v) : $v;
        return parent::offsetSet($k, $v);
    }
}

你使用的PHP版本是什么? - Farray
你好,工业用户!你真的需要让Config类成为ArrayObject的一个特化实现吗?或者你只是因为它提供的信息存储功能而需要这样做? - nick2083
嗨,尼克!不,我在看了这个问题之后选择了一个ArrayObject实现:https://dev59.com/lVrUa4cB1Zd3GeqPl6n4 - Industrial
3个回答

13
采取面向对象编程的视角,可以创建一个类来模拟多维对象的概念。
我发布的解决方案并不是通过扩展`ArrayObject`来实现您提到的目标。由于您将问题标记为面向对象编程,因此我认为强调将存储对象状态的方式与访问它的方式分开很重要。
希望这将有助于您实现所需的功能!
从您的说法中,多维对象是指:
- 处理多个嵌套信息级别 - 通过属性提供读/写访问信息 - 当访问未定义的属性时表现良好。这意味着,例如,在空实例上执行以下操作:$config->database->host = 'localhost'自动初始化databasehost级别,并在查询时返回'localhost'。 - 理想情况下,应从关联数组中进行初始化(因为您已经可以将配置文件解析为它们)。
建议的解决方案是怎样实现这些特性的?
第二个特性很容易实现:使用PHP的__get__set方法。每当对不可访问属性(即未在对象中定义的属性)进行读取/写入时,就会调用它们。然后需要的技巧是不声明任何属性,并通过这些方法处理属性操作,将被访问的属性名映射为用作存储的关联数组的键。它们基本上提供了访问内部存储信息的接口。
对于第三个特性,我们需要一种方法在读取未声明属性时创建一个新的嵌套级别。关键点是意识到返回的属性值必须是一个多维对象,因此还可以从中创建进一步的嵌套级别:每当我们要求一个在内部数组中不存在名称的属性时,就会将那个名称与MultiDimensionalObject的新实例关联起来并返回它。返回的对象也能够处理已定义或未定义的属性。
当写入未声明属性时,我们只需将其名称与提供的值分配给内部数组即可。
第四个特性很容易(请查看__construct实现)。我们只需要确保在属性的值为数组时创建一个MultiDimensionalObject即可。
最后一个特性:我们处理第二和第三个特性的方式允许我们在任何嵌套级别中读取和写入属性(已声明和未声明)。您可以在空实例上执行像$config->foo->bar->baz = 'hello'这样的操作,然后成功查询$config->foo->bar->baz

重要 请注意,MultiDimensionalObject 不是一个数组,而是由一个数组构成的,可以根据需要更改对象状态的存储方式。

实现

/* Provides an easy to use interface for reading/writing associative array based information */
/* by exposing properties that represents each key of the array */
class MultiDimensionalObject {

    /* Keeps the state of each property  */
    private $properties;
    
    /* Creates a new MultiDimensionalObject instance initialized with $properties */
    public function __construct($properties = array()) {
        $this->properties = array();
        $this->populate($properties);
    }
    
    /* Creates properties for this instance whose names/contents are defined by the keys/values in the $properties associative array */
    private function populate($properties) {
        foreach($properties as $name => $value) {
            $this->create_property($name, $value);
        }
    }
    
    /* Creates a new property or overrides an existing one using $name as property name and $value as its value */
    private function create_property($name, $value) {
        $this->properties[$name] = is_array($value) ? $this->create_complex_property($value)
                                                    : $this->create_simple_property($value);
    }
    
    /* Creates a new complex property. Complex properties are created from arrays and are represented by instances of MultiDimensionalObject */
    private function create_complex_property($value = array()){
        return new MultiDimensionalObject($value);
    }
    
    /* Creates a simple property. Simple properties are the ones that are not arrays: they can be strings, bools, objects, etc. */
    private function create_simple_property($value) {
        return $value;
    }
    
    /* Gets the value of the property named $name */
    /* If $name does not exists, it is initilialized with an empty instance of MultiDimensionalObject before returning it */
    /* By using this technique, we can initialize nested properties even if the path to them don't exist */
    /* I.e.: $config->foo
                    - property doesn't exists, it is initialized to an instance of MultiDimensionalObject and returned
             
             $config->foo->bar = "hello";
                    - as explained before, doesn't exists, it is initialized to an instance of MultiDimensionalObject and returned.
                    - when set to "hello"; bar becomes a string (it is no longer an MultiDimensionalObject instance) */    
    public function __get($name) {
        $this->create_property_if_not_exists($name);
        return $this->properties[$name];
    }
    
    private function create_property_if_not_exists($name) {
        if (array_key_exists($name, $this->properties)) return;
        $this->create_property($name, array());
    }
    
    public function __set($name, $value) {
        $this->create_property($name, $value);
    }
}

演示

代码:

var_dump(new MultiDimensionalObject());

结果:

object(MultiDimensionalObject)[1]
    private 'properties' => 
        array
            empty

代码:

$data = array( 'database' => array ( 'host' => 'localhost' ) );
$config = new MultiDimensionalObject($data);        
var_dump($config->database);

结果:

object(MultiDimensionalObject)[2]
    private 'properties' => 
        array
            'host' => string 'localhost' (length=9)

代码:

$config->database->credentials->username = "admin";
$config->database->credentials->password = "pass";
var_dump($config->database->credentials);

结果:

object(MultiDimensionalObject)[3]
    private 'properties' => 
        array
          'username' => string 'admin' (length=5)
          'password' => string 'pass' (length=4)

代码:

$config->database->credentials->username;

结果:

admin

1

我复制粘贴了你的代码,在我的 PHP 测试环境中(运行 PHP 5.3.6)可以正常工作。虽然它会提到 Strict Standards 警告,但仍然按预期工作。以下是 print_r 的输出:

Config Object
(
    [storage:ArrayObject:private] => Array
        (
            [lvl1_0] => 1
            [lvl1_1] => stdClass Object
                (
                    [lvl2] => 1
                )

        )

)

值得注意的是,PHP文档中有一条与您所尝试的内容相关的指导性评论:

sfinktah at php dot spamtrak dot org 17-Apr-2011 07:27
如果您计划从ArrayObject派生自己的类,并希望维护完整的ArrayObject功能(例如能够转换为数组),则必须使用ArrayObject自己的私有属性“storage”。

详细解释请参见上面的链接,但是除了您已经拥有的offsetSet和xdazz提到的offsetGet之外,您还必须实现offsetExistsoffsetUnset。这与您当前的错误无关,但这是您应该注意的事项。
更新:xdazz的后半部分提供了解决方案。如果您将Config对象作为数组访问,则可以在没有任何错误的情况下运行:
$config = new Config;
$config[ 'lvl1_0' ] = true;
$config[ 'lvl1_1' ][ 'lvl2' ] = true;

你能这样做,还是因为某些原因而受限于对象语法?

嗨Farray!虽然不能以一致的方式读写ArrayObject类有点遗憾,但我可以将其作为数组访问。 - Industrial
@Industrial 很不幸(对于你的情况),它正在按照预期工作。你正在从一个空值创建一个对象,因此它会告诉你这一点。你可以通过使用 ini_set('display_errors', 0); 或关闭严格错误报告 error_reporting(E_ALL); 来隐藏这个问题。这两个选项中有没有一个可行? - Farray

1
实现 offsetGet 方法。如果访问到不存在的属性,可以随意创建一个。
由于你正在扩展ArrayObject,因此应该使用数组方式 [] 进行设置或获取。

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