简体   繁体   English

PHP使用数组作为名称设置魔术方法

[英]PHP set magic method with array as names

I am creating a class which I will use to store and load some settings. 我正在创建一个类,我将用它来存储和加载一些设置。 Inside the class all settings are stored in an array. 在课堂内,所有设置都存储在一个数组中。 The settings can be nested, so the settings array is a multidimensional array. 设置可以嵌套,因此设置数组是一个多维数组。 I want to store and load the settings using the magic methods __get and __set, so the settings can act as class members. 我想使用魔术方法__get和__set存储和加载设置,因此设置可以充当类成员。 However, since I'm using nested methods, I can't get the __set method to work when I try to access a nested setting. 但是,由于我使用的是嵌套方法,因此在尝试访问嵌套设置时无法使__set方法起作用。

The class is like this: 这个班是这样的:

class settings
{
    private $_settings = array();

    //some functions to fill the array

    public function __set($name, $value)
    {
        echo 'inside the __set method';
        //do some stuff
    }
}

And the code to use this class: 以及使用此类的代码:

$foo = new settings();
//do some stuff with the class, so the internal settings array is as followed:
//array(
//    somename => somevalue
//    bar => array (
//               baz = someothervalue
//               qux = 42
//                 )
//     )
$foo->somename = something; //this works, __set method is called correctly
$foo->bar['baz'] = somethingelse; //Doesn't work, __set method isn't called at all

How can I get this last line to work? 我怎样才能让最后一行工作?

When accessing an array using this method, it actually goes through __get instead. 使用此方法访问数组时,它实际上会通过__get。 In order to set a parameter on that array that was returned it needs to be returned as a reference: &__get($name) 为了在返回的数组上设置一个参数,需要将其作为引用返回: &__get($name)

Unless, what you mean is that you want each item that is returned as an array to act the same way as the parent object, in which case you should take a look at Zend Framework's Zend_Config object source for a good way to do that. 除非,您的意思是您希望作为数组返回的每个项目的行为方式与父对象相同,在这种情况下,您应该查看Zend Framework的Zend_Config对象源,以获得这样做的好方法。 (It returns a new instance of itself with the sub-array as the parameter). (它以子数组作为参数返回自身的新实例)。

This would work: 这可行:

$settings = new Settings();
$settings->foo = 'foo';
$settings->bar = array('bar');

But, there is no point in using magic methods or the internal array at all. 但是,根本没有使用魔术方法或内部数组。 When you are allowing getting and setting of random members anyway, then you can just as well make them all public. 无论如何,当你允许获取和设置随机成员时,你也可以将它们全部公开。

Edit after comments (not answer to question above) 评论后编辑(不回答上述问题)

Like I already said in the comments I think your design is flawed. 就像我在评论中已经说过的那样,我认为你的设计是有缺陷的。 Let's tackle this step by step and see if we can improve it. 让我们一步一步解决这个问题,看看我们是否可以改进它。 Here is what you said about the Settings class requirements: 以下是您对Settings类要求的说明:

  • settings can be saved to a file or a database 设置可以保存到文件或数据库中
  • settings might need to update other parts of the application 设置可能需要更新应用程序的其他部分
  • settings need to be validated before they are changed 设置需要在更改之前进行验证
  • should use $setting->foo[subsetting] over $setting->data[foo[subsetting]] 应该使用$setting->foo[subsetting]不是$setting->data[foo[subsetting]]
  • settings class needs to give access to the settings data for other classes settings类需要提供对其他类的设置数据的访问权限
  • first time an instance is made, the settings need to be loaded from a file 第一次创建实例时,需要从文件加载设置

Now, that is quite a lot of things to do for a single class. 现在,对于单个班级来说,这是很多事情要做。 Judging by the requirements you are trying to build a self-persisting Singleton Registry , which on a scale of 1 (bad) to 10 (apocalyptic) is a level 11 idea in my book. 根据你要求建立一个自我持久的 Singleton 注册表的要求来判断,在我的书中,这个注册表在1(坏)到10(世界末日)的范围内是11级的想法。

According to the Single Responsibility Principle (the S in SOLID ) a class should have one and only reason to change . 根据单一责任原则SOLID中的S),一个班级应该有唯一的改变理由 If you look at your requirements you will notice that there is definitely more than one reason to change it. 如果你看看你的要求,你会发现改变它的绝对原因不止一个。 And if you look at GRASP you will notice that your class takes on more roles than it should. 如果你看看GRASP,你会注意到你的班级承担的角色超出了应有的范围。

In detail: 详细:

settings can be saved to a file or a database 设置可以保存到文件或数据库中

That is at least two responsibilites: db access and file access. 这至少是两个责任:数据库访问和文件访问。 Some people might want to further distinguish between reading from file and saving to file. 有些人可能希望进一步区分从文件读取和保存到文件。 Let's ignore the DB part for now and just focus on file access and the simplest thing that could possibly work for now. 让我们暂时忽略数据库部分,只关注文件访问和现在可能有用最简单的事情

You already said that your settings array is just a dumb key/value store, which is pretty much what arrays in PHP are. 你已经说过你的设置数组只是一个愚蠢的键/值存储,这几乎就是PHP中的数组。 Also, in PHP you can include arrays from a file when they are written like this: 此外,在PHP中,您可以include文件中的数组,如下所示:

<?php // settings.php
return array(
    'foo' => 'bar'
);

So, technically you dont need to do anything but 所以,从技术上讲,你不需要做任何事情

$settings = include 'settings.php';
echo $settings['foo']; // prints 'bar';

to load and use your Settings array from a file. 从文件加载和使用您的设置数组。 This is so simple that it's barely worth writing an object for it, especially since you will only load those settings once in your bootstrap and distribute them to the classes that need them from there. 这很简单,几乎不值得为它编写一个对象,特别是因为你只需要在引导程序中加载一次这些设置,然后将它们分配到需要它们的类中。

Saving an array as an includable file isnt difficult either thanks to var_export and file_put_contents . 由于var_exportfile_put_contents将数组保存为可包含文件var_export困难。 We can easily create a Service class for that, for example 例如,我们可以轻松地为其创建一个Service类

class ArrayToFileService
{
    public function export($filePath, array $data)
    {
        file_put_contents($filePath, $this->getIncludableArrayString($data));
    }
    protected function getIncludableArrayString($data)
    {
        return sprintf('<?php return %s;', var_export($data, true));
    }
}

Note that I deliberatly did not make the methods static despite the class having no members of it's own to operate on. 请注意,尽管类没有自己的成员可以操作,但我仍然没有使方法保持静态 Usign the class statically will add coupling between the class and any consumer of that class and that is undesirable and unneccessary. 静态地使用该类将在类和该类的任何使用者之间添加耦合,这是不合需要的和不必要的。

All you have to do now to save your settings is 您现在要做的就是保存设置

$arrayToFileService = new ArrayToFileService;
$arrayToFileService->export('settings.php', $settings);

In fact, this is completely generic, so you can reuse it for any arrays you want to persist this way. 实际上,这是完全通用的,因此您可以将其重用于您希望以这种方式持久化的任何数组。

settings might need to update other parts of the application 设置可能需要更新应用程序的其他部分

I am not sure why you would need this. 我不确定你为什么需要这个。 Given that our settings array can hold arbitrary data you cannot know in advance which parts of the application might need updating. 鉴于我们的设置数组可以保存任意数据,您无法预先知道应用程序的哪些部分可能需要更新。 Also, knowing how to update other parts of the application isnt the responsiblity of a data container. 此外,知道如何更新应用程序的其他部分不是数据容器的责任。 What we need is a mechanism that tells the various parts of the application when the array got updated. 我们需要的是一种在阵列更新时告诉应用程序各个部分的机制。 Of course, we cannot do that with a plain old array because its not an object. 当然,我们不能用普通的旧数组做到这一点,因为它不是一个对象。 Fortunately, PHP allows us to access an object like an array by implementing ArrayAccess: 幸运的是,PHP允许我们通过实现ArrayAccess来访问像数组这样的对象:

class HashMap implements ArrayAccess
{
    protected $data;

    public function __construct(array $initialData = array())
    {
        $this->data = $initialData;
    }
    public function offsetExists($offset)
    {
        return isset($this->data[$offset]);
    }
    public function offsetGet($offset)
    {
        return $this->data[$offset];
    }
    public function offsetSet($offset, $value)
    {
        $this->data[$offset] = $value;
    }
    public function offsetUnset($offset)
    {
        unset($this->data[$offset]);
    }
    public function getArrayCopy()
    {
        return $this->data;
    }
}

The methods starting with offset* are required by the interface. 接口需要以offset*开头的方法。 The method getArrayCopy is there so we can use it with our ArrayToFileService . 方法getArrayCopy就在那里,所以我们可以将它与ArrayToFileService一起使用。 We could also add the IteratorAggregate interface to have the object behave even more like an array but since that isnt a requirement right now, we dont need it . 我们还可以添加IteratorAggregate接口,使对象的行为更像是一个数组,但由于这不是现在的要求,我们不需要它 Now to allow for arbitrary updating, we add a Subject/Observer pattern by implementing SplSubject : 现在为了允许任意更新,我们通过实现SplSubject添加Subject / Observer模式

class ObservableHashMap implements ArrayAccess, SplSubject
…
    protected $observers;

    public function __construct(array $initialData = array())
    {
        $this->data = $initialData;
        $this->observers = new SplObjectStorage;
    }
    public function attach(SplObserver $observer)
    {
        $this->observers->attach($observer);        
    }
    public function detach(SplObserver $observer)
    {
        $this->observers->detach($observer);        
    }
    public function notify()
    {
        foreach ($this->observers as $observers) {
            $observers->update($this);
        }
    }
}

This allows us to register arbitrary objects implementing the SplObserver interface with the ObservableHashMap (renamed from HashMap ) class and notify them about changes. 这允许我们使用ObservableHashMap (从HashMap重命名)类注册实现SplObserver接口的任意对象,并notify它们有关更改。 It would be somewhat prettier to have the Observable part as a standalone class to be able to reuse it for other classes as well. 将Observable部分作为独立类来将其重用于其他类也会更为漂亮。 For this, we could make the Observable part into a Decorator or a Trait . 为此,我们可以将Observable部分变成DecoratorTrait We could also decouple Subject and Observers further by adding an EventDispatcher to mediate between the two, but for now this should suffice. 我们还可以通过添加EventDispatcher来进一步分离主题和观察者,以便在两者之间进行调解,但是现在这应该足够了。

Now to notify an observer, we have to modify all methods of the class that should trigger a notification, for instance 现在要通知观察者,我们必须修改应该触发通知的类的所有方法

public function offsetSet($offset, $value)
{
    $this->data[$offset] = $value;
    $this->notify();
}

Whenever you call offsetSet() or use [] to modify a value in the HashMap , any registered observers will be notified and passed the entire HashMap instance. 每当调用offsetSet()或使用[]修改HashMap的值时,任何已注册的观察者都会收到通知并传递整个HashMap实例。 They can then inspect that instance to see whether something important changed and react as needed, eg let's assume SomeComponent 然后,他们可以检查该实例以查看重要事项是否发生了变化并根据需要做出反应,例如让我们假设SomeComponent

class SomeComponent implements SplObserver
{
    public function update(SplSubject $subject)
    {
        echo 'something changed';
    }
}

And then you just do 然后你就做了

$data = include 'settings.php';
$settings = new ObservableHashMap($data);
$settings->attach(new SomeComponent);
$settings['foo'] = 'foobarbaz'; // will print 'something changed'

This way, your settings class needs no knowledge about what needs to happen when a value changes. 这样,您的设置类不需要知道值更改时需要发生的事情。 You can keep it all where it belongs: in the observers. 你可以将它保留在它所属的所有位置:在观察者中。

settings need to be validated before they are changed 设置需要在更改之前进行验证

That one is easy. 那一个很容易。 You dont do it inside the hashmap/settings object at all. 您根本不在hashmap / settings对象中执行此操作。 Given that the HashMap is just a dumb container holding arbitrary data that is supposed to be used by other classes, you put the validation into those classes that use the data. 鉴于HashMap只是一个容纳任意数据的哑容器,应该由其他类使用,你将验证放入那些使用数据的类中。 Problem solved. 问题解决了。

should use $setting->foo[subsetting] over $setting->data[foo[subsetting]] 应该使用$setting->foo[subsetting]不是$setting->data[foo[subsetting]]

Well, yeah. 嗯,是的 As you probably have guessed already, the above implementation doesnt use this notation. 您可能已经猜到了,上面的实现并没有使用这种表示法。 It uses $settings['foo'] = 'bar' and you cannot use $settings['foo']['bar'] with ArrayAccess (at least to my knowledge). 它使用$settings['foo'] = 'bar' ,你不能在ArrayAccess使用$settings['foo']['bar'] (至少据我所知)。 So that is somewhat of a limitation. 所以这有点局限。

settings class needs to give access to the settings data for other classes settings类需要提供对其他类的设置数据的访问权限

This and the next requirement smell like Singleton to me. 这个和下一个要求对我来说就像辛格尔顿一样。 If so, think again. 如果是这样,请再想一想。 All you ever need is to instantiate the settings class once in your bootstrap. 您所需要的只是在引导程序中实例化一次设置类。 You are creating all the other classes that are required to fulfill the request there, so you can inject all the settings values right there. 您正在创建在那里完成请求所需的所有其他类,因此您可以在那里注入所有设置值。 There is no need for the Settings class to be globally accessible. Settings类不需要全局访问。 Create, inject, discard. 创建,注入,丢弃。

first time an instance is made, the settings need to be loaded from a file 第一次创建实例时,需要从文件加载设置

See above. 往上看。

The part $foo->bar is actually calling __get, this function should (in your case) return an array. 部分$ foo-> bar实际上是调用__get,这个函数应该(在你的情况下)返回一个数组。

returning the right array in the __get would then be your solution. 在__get中返回正确的数组将是您的解决方案。

As has been stated, this is because it is the array stored in $foo->bar that is being modified rather than the class member. 如前所述,这是因为它是存储在$foo->bar中的数组,而不是类成员。 The only way to invoke __set behaviour on an 'array' would be to create a class implementing the ArrayAccess interface and the offsetSet method, however this would defeat the purpose of keeping the settings in the same object. 在'数组'上调用__set行为的唯一方法是创建一个实现ArrayAccess接口和offsetSet方法的类,但是这会使设置保持在同一个对象中的目的失败。

A reasonably neat and common work around is to use dot delimited paths: 一个相当简洁和常见的工作是使用点分隔路径:

class Settings {

  protected $__settings = array();

  // Saves a lot of code duplication in get/set methods.
  protected function get_or_set($key, $value = null) {
    $ref =& $this->__settings;
    $parts = explode('.', $key);

    // Find the last array section
    while(count($parts) > 1) {
      $part = array_shift($parts);
      if(!isset($ref[$part]))
        $ref[$part] = array();
      $ref =& $ref[$part];
    }

    // Perform the appropriate action.
    $part = array_shift($parts);
    if($value)
      $ref[$part] = $value;
    return $ref[$part];
  }

  public function get($key) { return $this->get_or_set($key); }

  public function set($key, $value) { return $this->get_or_set($key, $value); }

  public function dump() { print_r($this->__settings); }
}

$foo = new Settings();
$foo->set('somename', 'something');
$foo->set('bar.baz', 'somethingelse');
$foo->dump();
/*Array
  (
    [somename] => something
    [bar] => Array
      (
        [baz] => somethingelse
      )
  )*/

This also makes it clearer you are not manipulating instance variables, as well as allowing arbitrary keys without fear of conflicts with instance variables. 这也使您更清楚地了解操作实例变量,以及允许任意键而不必担心与实例变量冲突。 Further processing for specific keys can be achieved by simply adding key comparisons to get/set eg 通过简单地将关键比较添加到例如get/set可以实现对特定键的进一步处理

public function set(/* ... */) {
  /* ... */
  if(strpos($key, 'display.theme') == 0)
    /* update the theme */
  /* ... */
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM