12

When trying to change it,throw an exception.

user198729
  • 61,774
  • 108
  • 250
  • 348
  • There is a [draft rfc](https://wiki.php.net/rfc/readonly_and_immutable_properties?s[]=readonly) currently (Jun 27, 2020) to propose adding `readonly` features to PHP 8.0: **"This is a early draft, currently looking for feedback."** The author's email is listed & I believe you can email them with suggestions. – Reed Jun 27 '20 at 18:54

7 Answers7

24

I suppose a solution, for class properties, would be to :

  • not define a property with the name that interests you
  • use the magic __get method to access that property, using the "fake" name
  • define the __set method so it throws an exception when trying to set that property.
  • See Overloading, for more informations on magic methods.

For variables, I don't think it's possible to have a read-only variable for which PHP will throw an exception when you're trying to write to it.


For instance, consider this little class :

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
        }
    }
}

Instanciating the class and trying to read the property :

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

Will get you the expected output :

test

While trying to write to the property :

$a->myVar = 10;

Will get you an Exception :

Exception: not allowed : myVar in /.../temp.php on line 19
Pascal MARTIN
  • 395,085
  • 80
  • 655
  • 663
  • Good answer!BTW,what's `__call` used for? – user198729 Feb 26 '10 at 18:25
  • `__call` is called when you're trying to call a method that doesn't exit in the class (like `__get` is called when you're trying to read a property that doesn't exist in the class) -- see http://fr2.php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.methods – Pascal MARTIN Feb 26 '10 at 18:27
  • @Pascal MARTIN ,thanks!I also know that you are an experienced user of symfony/doctrine,can you take a look at this post:http://stackoverflow.com/questions/2339800/how-can-i-fetch-the-entire-tree-in-a-single-query-with-doctrine ? – user198729 Feb 26 '10 at 18:29
  • You're welcome :-) ;;; Symfony ? hu, I have never really used Symfony -- I might be more of a ZF user ;;; Oh, that question is about Doctrine, I seen, and not Symfony ;;; and I've never used trees with Doctrine yet -- sorry... – Pascal MARTIN Feb 26 '10 at 18:32
  • Oh,that's fine.What about this one:http://stackoverflow.com/questions/2331723/how-does-the-local-field-for-relation-work-in-doctrine?I'm really having a hard time converting sql to YAML,especially the `local/foreign` settings in `relation` part.. – user198729 Feb 26 '10 at 18:37
  • Just checking by again, that's not a read only version. Add the method `public function modify() { $this->_data['myVar'] = 'bla'; }`. Or just extend `MyClass` and overload the `__set()`-method. It's just as "read only" as **a regular protected value is** - it all depends on the class methods' bodies. – chelmertz Apr 04 '11 at 17:33
15
class test {
   const CANT_CHANGE_ME = 1;
}

and you refer it as test::CANT_CHANGE_ME

chelmertz
  • 20,399
  • 5
  • 40
  • 46
  • How will you throw a customized exception? – user198729 Feb 26 '10 at 18:15
  • 5
    @user198729 Why would you *want* to throw a customized exception? – user229044 Feb 26 '10 at 18:25
  • And if I want two instances of `test` with two different values for `CANT_CHANGE_ME`? This is a class variable, not a member and it will exist only in one copy... – Erk Oct 25 '18 at 21:13
  • @Erk you could hide the value behind a method in an interface, and implement that interface in all classes that should have a different value. That goes both for a "set once" member that is probably requested by this question, or something that's defined before run-time. – chelmertz Oct 28 '18 at 18:58
1

Use a constant. Keyword const

froadie
  • 79,995
  • 75
  • 166
  • 235
1

The short answer is you can't create a read-only object member variable in PHP.

In fact, most object-oriented languages consider it poor form to expose member variables publicly anyway... (C# being the big, ugly exception with its property-constructs).

If you want a class variable, use the const keyword:

class MyClass {
    public const myVariable = 'x';
}

This variable can be accessed:

echo MyClass::myVariable;

This variable will exist in exactly one version regardless of how many different objects of type MyClass you create, and in most object-oriented scenarios it has little to no use.

If, however, you want a read-only variable that can have different values per object, you should use a private member variable and an accessor method (a k a getter):

class MyClass {
    private $myVariable;
    public function getMyVariable() {
        return $this->myVariable;
    }
    public function __construct($myVar) {
        $this->myVariable = $myVar;
    }
}

The variable is set in the constructor, and it's being made read-only by not having a setter. But each instance of MyClass can have its own value for 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
Erk
  • 1,159
  • 15
  • 9
  • Read-only fields wouldn't be poor at all in a DTO object. A DTO is used only to give well documented types to a data structure. On the other hand adding a lot of logic to a data structure may make it much less flexible. It's just a chunk of data, not a logical object with well defined behavior. – Gherman Apr 07 '21 at 15:52
1

I cooked up a version, too, using a trait.

Though in this case, the property can still be set by its declaring class.

Declare a class like:

class Person {
    use Readonly;

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

And this is the trait I made:

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);
    }
    
}

See the source code & test on my gitlab

Reed
  • 14,703
  • 8
  • 66
  • 110
  • I might prefer [my other version](https://stackoverflow.com/a/66587746/802469) that uses `@readonly` in the docblock, but I suspect its a few more cpu cycles. – Reed Mar 11 '21 at 18:21
1

I made another version that uses @readonly in the docblock instead of private $r_propname. This still doesn't stop the declaring class from setting the property, but will work for public readonly access.

Sample Class:

class Person {
    use Readonly;

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

    protected $phoneNumber;

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

The ReadOnly trait

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);
    }
    
}

See the source code & test on my gitlab

Reed
  • 14,703
  • 8
  • 66
  • 110
0

I know this is an old question, but PASCAL's answer really helped me and I wanted to add to it a bit.

__get() fires not only on nonexistent properties, but "inaccessible" ones as well, e.g. protected ones. This makes it easy to make read-only properties!

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;
        }
    }
}
Stephen R
  • 3,512
  • 1
  • 28
  • 45