如何在PHP中实现只读成员变量?

12

尝试更改时,抛出异常。


目前(2020年6月27日)有一份草案RFC,旨在为PHP 8.0添加“readonly”功能:“这是一个早期的草案,目前正在寻求反馈。”作者的电子邮件已列出,您可以通过电子邮件向他们提出建议。 - Reed
7个回答

24

针对类的属性,我想一个解决方案是:

  • 不要定义与您感兴趣的名称相同的属性
  • 使用魔术方法__get访问该属性,使用“虚拟”名称
  • 定义__set方法,以便在尝试设置该属性时抛出异常。
  • 有关魔术方法的更多信息,请参见过载

对于变量,我认为不可能拥有只读变量,当您尝试写入它时,PHP会引发异常。


例如,考虑这个小类:

class MyClass {
    protected $_data = array(
        'myVar' => 'test'
    );

    public function __get($name) {
        if (isset($this->_data[$name])) {
            return $this->_data[$name];
        } else {
            // non-existant property
            // => up to you to decide what to do
        }
    }

    public function __set($name, $value) {
        if ($name === 'myVar') {
            throw new Exception("not allowed : $name");
        } else {
            // => up to you to decide what to do
        }
    }
}

实例化类并尝试读取属性:

$a = new MyClass();
echo $a->myVar . '<br />';

以下代码将会得到你期望的输出:

test

在尝试写入该属性时:

$a->myVar = 10;

使用这段代码将会抛出一个异常:

Exception: not allowed : myVar in /.../temp.php on line 19

好的回答!顺便问一下,__call 有什么用? - user198729
еҪ“жӮЁиҜ•еӣҫи°ғз”Ёзұ»дёӯдёҚеӯҳеңЁзҡ„ж–№жі•ж—¶пјҲдҫӢеҰӮеҪ“жӮЁе°қиҜ•иҜ»еҸ–зұ»дёӯдёҚеӯҳеңЁзҡ„еұһжҖ§ж—¶дјҡи°ғз”Ё__getпјүпјҢе°Ҷи°ғз”Ё__call--иҜ·еҸӮи§Ғhttp://fr2.php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.methodsгҖӮ - Pascal MARTIN
@Pascal MARTIN,谢谢!我也知道你是symfony/doctrine的经验丰富用户,你能看一下这篇文章吗:https://dev59.com/4EzSa4cB1Zd3GeqPkjmM? - user198729
不用谢 :-) ;;; Symfony?呃,我从来没有真正使用过Symfony —— 我可能更适合使用ZF;;;哦,那个问题是关于Doctrine的,我明白了,而不是Symfony;;; 而且我还没用过Doctrine的树形结构——抱歉... - Pascal MARTIN
哦,那很好。那这个怎么样:http://stackoverflow.com/questions/2331723/how-does-the-local-field-for-relation-work-in-doctrine?我真的很难将SQL转换为YAML,特别是在“relation”部分中的“local / foreign”设置。 - user198729
再次检查一下,这不是只读版本。添加方法 public function modify() { $this->_data['myVar'] = 'bla'; }。或者只需扩展 MyClass 并重载 __set() 方法。它与常规受保护的值一样只读 - 这完全取决于类方法的实现。 - chelmertz

15
class test {
   const CANT_CHANGE_ME = 1;
}

你将其称为test::CANT_CHANGE_ME


你如何抛出自定义异常? - user198729
5
你为什么想要抛出自定义异常? - user229044
如果我想要两个具有不同值的test实例,其中包含CANT_CHANGE_ME?这是一个类变量,而不是成员变量,它只存在于一个副本中... - Erk
@Erk,您可以在接口中隐藏值,并在应具有不同值的所有类中实现该接口。这适用于“仅设置一次”的成员(可能是此问题所要求的)或在运行时之前定义的内容。 - chelmertz

1

使用一个常量。关键字const


1
我制作了另一个版本,使用docblock中的@readonly代替private $r_propname。这仍然无法阻止声明类设置属性,但对于公共只读访问将起作用。
示例类:
class Person {
    use Readonly;

    /**
     * @readonly
     */
    protected $name;

    protected $phoneNumber;

    public function __construct($name){
        $this->name = $name;
        $this->phoneNumber = '123-555-1234';
    }
}

只读特质 ReadOnly

trait Readonly {

    public function readonly_getProperty($prop){
        if (!property_exists($this,$prop)){
            //pretty close to the standard error if a protected property is accessed from a public scope
            trigger_error('Undefined property: '.get_class($this).'::\$'.$prop,E_USER_NOTICE);
        }
        
        $refProp = new \ReflectionProperty($this, $prop);
        $docblock = $refProp->getDocComment();
        // a * followed by any number of spaces, followed by @readonly
        $allow_read = preg_match('/\*\s*\@readonly/', $docblock);

        if ($allow_read){
            $actual = $this->$prop;
            return $actual;
        }
        
        throw new \Error("Cannot access non-public property '{$prop}' of class '".get_class($this)."'");
    }

    public function __get($prop){
        return $this->readonly_getProperty($prop);
    }
    
}

查看源代码并测试,请前往我的GitLab


1

我也使用了一个特质来编写一个版本。

虽然在这种情况下,属性仍然可以由其声明类设置。

声明一个类如下:

class Person {
    use Readonly;

    protected $name;
    //simply declaring this means "the 'name' property can be read by anyone"
    private $r_name;
}

这是我创建的特征:

trait Readonly {

    public function readonly_getProperty($prop){
        if (!property_exists($this,$prop)){
            //pretty close to the standard error if a protected property is accessed from a public scope
            // throw new \Error("Property '{$prop}' on class '".get_class($this)."' does not exist");
            trigger_error('Undefined property: '.get_class($this).'::\$'.$prop,E_USER_NOTICE);
        }
        
        $allow_read = property_exists($this, 'r_'.$prop );

        if ($allow_read){
            $actual = $this->$prop;
            return $actual;
        }
        
        throw new \Error("Cannot access non-public property '{$prop}' of class '".get_class($this)."'");
    }

    public function __get($prop){
        return $this->readonly_getProperty($prop);
    }
    
}

查看源代码并在我的GitLab上进行测试


我可能更喜欢使用docblock中的@readonly另一个版本,但我怀疑它会多消耗一些CPU周期。 - Reed

1
短答案是在PHP中无法创建只读对象成员变量。
实际上,大多数面向对象的语言都认为公开成员变量是不好的做法...(C#是个例外,它有属性构造)。
如果您想要一个类变量,请使用const关键字:
class MyClass {
    public const myVariable = 'x';
}

这个变量可以被访问:

echo MyClass::myVariable;

无论您创建多少不同类型的MyClass对象,此变量仅存在于一个版本中,在大多数面向对象的场景中,它几乎没有用处。如果您想要一个只读变量,可以针对每个对象使用不同的值,则应使用私有成员变量和访问器方法(也称为getter):
class MyClass {
    private $myVariable;
    public function getMyVariable() {
        return $this->myVariable;
    }
    public function __construct($myVar) {
        $this->myVariable = $myVar;
    }
}

变量在构造函数中被设置,通过没有setter方法变成只读。但是MyClass的每个实例可以拥有自己的myVariable值。
$a = new MyClass(1);
$b = new MyClass(2);

echo $a->getMyVariable(); // 1
echo $b->getMyVariable(); // 2

$a->setMyVariable(3); // causes an error - the method doesn't exist
$a->myVariable = 3; // also error - the variable is private

只读字段在DTO对象中并不会有任何问题。DTO仅用于为数据结构提供良好的文档化类型。另一方面,向数据结构添加大量逻辑可能会使其变得不太灵活。它只是一块数据,而不是具有明确定义行为的逻辑对象。 - Gherman

0

我知道这是一个老问题,但PASCAL的答案确实帮了我很多,我想再补充一点。

__get()不仅在不存在的属性上触发,还会在“无法访问”的属性上触发,例如受保护的属性。这使得创建只读属性变得非常容易!

class MyClass {
    protected $this;
    protected $that;
    protected $theOther;

    public function __get( $name ) {
        if ( isset( $this->$name ) ) {
            return $this->$name;
        } else {
            throw new Exception( "Call to nonexistent '$name' property of MyClass class" );
            return false;
        }
    }

    public function __set( $name ) {
        if ( isset( $this->$name ) ) {
            throw new Exception( "Tried to set nonexistent '$name' property of MyClass class" );
            return false;
        } else {
            throw new Exception( "Tried to set read-only '$name' property of MyClass class" );
            return false;
        }
    }
}

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