I was writing a class that uses __get() and __set() to store and retrieve array elements in a master array. I had a check to make some elements ungettable, basically to re-create private properties.
I noticed that it seemed that __get intercepts all calls to class properties. This sucks for me, because I wanted to have a variable private to the outside world ( unavailable via get ), but I was trying to access it by directly referencing the master array from within the class. Of course, the master array is not in the whitelist of gettable properties :(
Is there a way I can emulate public and private properties in a php class that uses __get() and __set()?
Example:
<?
abstract class abstraction {
private $arrSettables;
private $arrGettables;
private $arrPropertyValues;
private $arrProperties;
private $blnExists = FALSE;
public function __construct( $arrPropertyValues, $arrSettables, $arrGettables ) {
$this->arrProperties = array_keys($arrPropertyValues);
$this->arrPropertyValues = $arrPropertyValues;
$this->arrSettables = $arrSettables;
$this->arrGettables = $arrGettables;
}
public function __get( $var ) {
echo "__get()ing:\n";
if ( ! in_array($var, $this->arrGettables) ) {
throw new Exception("$var is not accessible.");
}
return $this->arrPropertyValues[$var];
}
public function __set( $val, $var ) {
echo "__set()ing:\n";
if ( ! in_array($this->arrSettables, $var) ) {
throw new Exception("$var is not settable.");
}
return $this->arrPropertyValues[$var];
}
} // end class declaration
class concrete extends abstraction {
public function __construct( $arrPropertyValues, $arrSettables, $arrGettables ) {
parent::__construct( $arrPropertyValues, $arrSettables, $arrGettables );
}
public function runTest() {
echo "Accessing array directly:\n";
$this->arrPropertyValues['color'] = "red";
echo "Color is {$this->arrPropertyValues['color']}.\n";
echo "Referencing property:\n";
echo "Color is {$this->color}.\n";
$this->color = "blue";
echo "Color is {$this->color}.\n";
$rand = "a" . mt_rand(0,10000000);
$this->$rand = "Here is a random value";
echo "'$rand' is {$this->$rand}.\n";
}
}
try {
$objBlock = & new concrete( array("color"=>"green"), array("color"), array("color") );
$objBlock->runTest();
} catch ( exception $e ) {
echo "Caught Exeption $e./n/n";
}
// no terminating delimiter
$ php test.php
Accessing array directly:
__get()ing:
Caught Exeption exception 'Exception' with message 'arrPropertyValues is not accessible.' in /var/www/test.php:23
Stack trace:
#0 /var/www/test.php(50): abstraction->__get('arrPropertyValu...')
#1 /var/www//test.php(68): concrete->runTest()
#2 {main}.
Is there a way I can emulate public and private properties in a php class that uses __get() and __set()?
Not directly (if you discount debug_backtrace).
But you can have a private method getPriv that does all the work your current __get does. Then __get would only wrap this private method and check accessibility.
function __get($name) {
if (in_array($name, $this->privateProperties))
throw new Exception("The property ". __CLASS__ . "::$name is private.");
return $this->getPriv($name);
}
Inside your class, you would call getPriv, thus bypassing __get.
Make abstraction::$arrPropertyValues protected or do what Artefacto wrote (if you need additional checks), except that abstraction::getPriv() should be protected.
Rather than manually enlisting private/protected properties, you could use PHPs cumbersome reflection methods:
function __get($name) {
$reflect = new ReflectionObject($this);
$publics = $reflect->getProperties(ReflectionProperty::IS_PUBLIC);
if (in_array($name, $publics)) {
return $this->{$name};
}
}
Related
I'm making a BaseModel class and want to use magic methods __set and __get instead of defining setters and getters for every single property.
I'm currently using variable variables because I couldn't find another way by googling it. Are variable variables considered bad practice or am I fretting over nothing?
abstract class BaseModel implements \ArrayAccess {
/**
* Don't allow these member variables to be written by __set
*
* #var array
*/
protected $noSet = array();
/**
* Don't allow these member variables to be retrieved by __get
*
* #var array
*/
protected $noGet = array();
public function offsetExists( $offset )
{
return property_exists($this, $offset);
}
public function offsetGet( $offset )
{
return $this->__get($offset);
}
public function offsetSet( $offset , $value )
{
return $this->__set($offset, $value);
}
public function offsetUnset( $offset )
{
unset($this->$offset);
}
public function __get($member)
{
if( $member == 'noSet' || $member == 'noGet')
{
throw new \InvalidArgumentException ("Tried to access a forbidden property", 1);
}
if( ! property_exists($this, $member))
{
throw new \InvalidArgumentException ("Tried to access a non-existent property", 1);
}
if( in_array($member, $this->noGet))
{
throw new \InvalidArgumentException ("Tried to access a forbidden property", 1);
}
return $this->$member;
}
public function __set($member, $value)
{
if( $member == 'noSet' || $member == 'noGet')
{
throw new \DomainException ("Tried write to a non-writable property.", 1);
}
if( ! property_exists($this, $member))
{
throw new \InvalidArgumentException ("Tried to access a non-existent property", 1);
}
if( in_array($member, $this->noSet))
{
throw new \DomainException ("Tried write to a non-writable property.", 1);
}
return $this->$member = $value;
}
First, it seems you think that protected keyword makes a property unable to be set/get using magic methods. This is not the case. This just makes it to where you can't directly access/modify these properties from outside the scope of the class (i.e. you can't do something like $object->foo = 'bar')
Second, you seem to have misunderstanding of magic methods. What they in fact do is to enforce behavior when a user tries to directly access/modify a property. So in my example, if a user tries to do:
$object->foo = 'bar';
This is actually calls the __set() method and is equivalent to:
$object->__set('foo', 'bar');
So a typical class implementation using get/set magic methods might look like this:
class some_class {
protected $foo;
protected $foo2;
public $pub;
public function __construct() {
// maybe do something here
}
public function __get($prop) {
if(!property_exists($this, $prop) {
throw new Exception('Tried to get unknown property ' . $prop);
} else {
return $this->{$prop};
}
}
public function __set($prop, $value) {
if(!property_exists($this, $prop) {
throw new Exception('Tried to set unknown property ' . $prop);
} else {
$this->{$prop} = $value;
return true; // or whatever you want to return
}
}
}
Usage would be as follows:
$object = new some_class();
$object->foo = 'bar'; // sets 'bar'
echo $object->foo; // echo 'bar;
var_dump($object->foo2); // null
$object->pub = 'something'; // does not call __set() as this property is available from global scope
echo $object->pub; // echo 'something' does not call __get() as this property is available from global scope
$object->no_prop; // throws Exception from __get() as property does not exist
It would seem to be odd usage to try to actually call __get() or __set() from within the class.
Check out the PHP documentation on object overloading for more information:
http://www.php.net/manual/en/language.oop5.overloading.php#object.get
Variable variables are indeed the way to go.
With this code I'm trying to test if I can call certain functions
if (method_exists($this, $method))
$this->$method();
however now I want to be able to restrict the execution if the $method is protected, what would I need to do?
You'll want to use Reflection.
class Foo {
public function bar() { }
protected function baz() { }
private function qux() { }
}
$f = new Foo();
$f_reflect = new ReflectionObject($f);
foreach($f_reflect->getMethods() as $method) {
echo $method->name, ": ";
if($method->isPublic()) echo "Public\n";
if($method->isProtected()) echo "Protected\n";
if($method->isPrivate()) echo "Private\n";
}
Output:
bar: Public
baz: Protected
qux: Private
You can also instantiate the ReflectionMethod object by class and function name:
$bar_reflect = new ReflectionMethod('Foo', 'bar');
echo $bar_reflect->isPublic(); // 1
You should use ReflectionMethod. You can use isProtected and isPublic as well as getModifiers
http://www.php.net/manual/en/class.reflectionmethod.php
http://www.php.net/manual/en/reflectionmethod.getmodifiers.php
$rm = new ReflectionMethod($this, $method); //first argument can be string name of class or an instance of it. i had get_class here before but its unnecessary
$isPublic = $rm->isPublic();
$isProtected = $rm->isProtected();
$modifierInt = $rm->getModifiers();
$isPublic2 = $modifierInt & 256; $isProtected2 = $modifierInt & 512;
As for checking whether or not the method exists, you can do it as you do now with method_exists or just attempt to construct the ReflectionMethod and an exception will be thrown if it doesn't exist. ReflectionClass has a function getMethods to get you an array of all of a class's methods if you'd like to use that.
Disclaimer - I don't know PHP Reflection too well, and there might be a more direct way to do this with ReflectionClass or something else
I want to write a sort of "plugin/module" system for my code, and it would make it much easier if I could "add" stuff into a class after it's been defined.
For example, something like this:
class foo {
public function a() {
return 'b';
}
}
There's the class. Now I want to add another function/variable/const to it, after it's defined.
I realize that this is probably not possible, but I need confirmation.
No, you cannot add methods to an already defined class at runtime.
But you can create similar functionality using __call/__callStatic magic methods.
Class Extendable {
private $handlers = array();
public function registerHandler($handler) {
$this->handlers[] = $handler;
}
public function __call($method, $arguments) {
foreach ($this->handlers as $handler) {
if (method_exists($handler, $method)) {
return call_user_func_array(
array($handler, $method),
$arguments
);
}
}
}
}
Class myclass extends Extendable {
public function foo() {
echo 'foo';
}
}
CLass myclass2 {
public function bar() {
echo 'bar';
}
}
$myclass = new myclass();
$myclass->registerHandler(new myclass2());
$myclass->foo(); // prints 'foo'
echo "\n";
$myclass->bar(); // prints 'bar'
echo "\n";
This solution is quite limited but maybe it will work for you
To add/change how classes behave at runtime, you should use Decorators and/or Strategies. This is the prefered OO approach over resorting to any magic approaches or monkey patching.
A Decorator wraps an instance of a class and provides the same API as that instance. Any calls are delegated to the wrapped instance and results are modified where needed.
class Decorator
{
// …
public function __construct($decoratedInstance)
{
$this->_decoratedInstace = $decoratedInstance;
}
public function someMethod()
{
// call original method
$result = $this->_decoratedInstance->someMethod();
// decorate and return
return $result * 10;
}
// …
}
For Strategy, see my more complete example at Can I include code into a PHP class?
More details and example code can be found at
http://sourcemaking.com/design_patterns/decorator
http://sourcemaking.com/design_patterns/strategy
I have a few methods for you to try. :) Have fun coding.
Method for only one class:
class main_class {
private $_MODS = array(),...;
public ...;
public function __construct(...) {
...
global $MODS_ENABLED;
$this -> $_MODS = $MODS_ENABLED;
}
...
public function __get( $var ) {
foreach ( $this->_MODS as $mod )
if ( property_exists( $mod, $var ) )
return $mod -> $var;
}
public function __call( $method, $args ) {
foreach ( $this->_MODS as $mod )
if ( method_exists( $mod, $method ) )
return call_user_method_array( $method, $mod, $args );
}
}
Method for when you want to deal with more than one class:
class modMe {
private $_MODS = array();
public function __construct__() {
global $MODS_ENABLED;
$this -> $_MODS = $MODS_ENABLED;
}
public function __get( $var ) {
foreach ( $this->_MODS as $mod )
if ( property_exists( $mod, $var ) )
return $mod -> $var;
}
public function __call( $method, $args ) {
foreach ( $this->_MODS as $mod )
if ( method_exists( $mod, $method ) )
return call_user_method_array( $method, $mod, $args );
}
}
class mainClass extends modMe {
function __construct(...){
$this -> __construct__();
}
}
Now lets try to use them:
$MODS_ENABLED = array();
$MODS_ENABLED[] = new mod_mail();
$myObj = new main_class(...);
$myObj -> mail("me#me.me","you#you.you","subject","message/body","Extra:Headers;More:Headers");
# Hey look, my mail class was just added into my main_class for later use.
Note:
I am currently using the first method (I only have one class, the mods are exceptions) in my own CMS that I have made from scratch (http://sitegen.com.au), and it works great, my reason on needing this is because I have my main_class that is getting generated after I have required all mods in ./mods-enabled/* creating functions and changing how other functions work, I will also come back here another time with a solution for two mods to both change a function without one winning as it ran first. I have split my plugins in two, mods that run on every site, and plugins that have settings for a site and may not even be enabled.
Have fun programming.
You can extend the class
class foo {
public function a() {
return 'b';
}
}
class woo extends foo {
public function newStuff() {
$var = $this->a();
echo $var;
}
}
By extending foo from the woo class the functionality in foo is usable while you can also create new methods in woo. That's the easiest way to add new functionality to a class.
You can use magic functionality of PHP to provide actions on methods that are not defined at compile time.
It is actually possible. For instance:
<?php
class Test {
function set($set, $val) {
$this->$set = $val;
}
function get($get) {
return $this->$get;
}
}
$t = new Test();
$t->set('hello', 'world');
echo $t->get('hello');
exit;
?>
If it is not enough magic for you, you can use dynamic objects. The common idea is here: https://github.com/ptrofimov/jslikeobject
I have a class where I'm using __set. Because I don't want it to set just anything, I have an array of approved variables that it checks before it will actually set a class property.
However, on construct, I want the __construct method to set several class properties, some of which are not in the approved list. So when construct happens, and I do $this->var = $value, I of course get my exception that I'm not allowed to set that variable.
Can I get around this somehow?
Declare the class members:
class Blah
{
private $imAllowedToExist; // no exception thrown because __set() wont be called
}
Declaring the class members is your best bet. If that doesn't work, you could have a switch ($this->isInConstructor?) which determines whether to throw the error.
On the other hand, you could also use the __get method as well as the __set method and have both of them map to a wrapped library:
class Foo
{
private $library;
private $trustedValues;
public function __construct( array $values )
{
$this->trustedValues = array( 'foo', 'bar', 'baz' );
$this->library = new stdClass();
foreach( $values as $key=>$value )
{
$this->library->$key = $value;
}
}
public function __get( $key )
{
return $this->library->$key;
}
public function __set( $key, $value )
{
if( in_array( $key, $this->trustedValues ) )
{
$this->library->$key = $value;
}
else
{
throw new Exception( "I don't understand $key => $value." );
}
}
}
In using PHP's DOM classes (DOMNode, DOMEElement, etc) I have noticed that they possess truly readonly properties. For example, I can read the $nodeName property of a DOMNode, but I cannot write to it (if I do PHP throws a fatal error).
How can I create readonly properties of my own in PHP?
You can do it like this:
class Example {
private $__readOnly = 'hello world';
function __get($name) {
if($name === 'readOnly')
return $this->__readOnly;
user_error("Invalid property: " . __CLASS__ . "->$name");
}
function __set($name, $value) {
user_error("Can't set property: " . __CLASS__ . "->$name");
}
}
Only use this when you really need it - it is slower than normal property access. For PHP, it's best to adopt a policy of only using setter methods to change a property from the outside.
Since PHP 8.1 there are implemented native readonly properties
Documentation
You can initialize readonly property only once during the declaration of the property.
class Test {
public readonly string $prop;
public function __construct(string $prop) {
$this->prop = $prop;
}
}
--
class Test {
public function __construct(
public readonly string $prop,
) {}
}
Trying to modify the readonly propety will cause following error:
Error: Cannot modify readonly property Test::$prop
Update PHP 8.2
Since PHP 8.2 you are able to define as readonly a whole class.
readonly class Test {
public string $prop;
public function __construct(string $prop) {
$this->prop = $prop;
}
}
But private properties exposed only using __get() aren't visible to functions that enumerate an object's members - json_encode() for example.
I regularly pass PHP objects to Javascript using json_encode() as it seems to be a good way to pass complex structures with lots of data populated from a database. I have to use public properties in these objects so that this data is populated through to the Javascript that uses it, but this means that those properties have to be public (and therefore run the risk that another programmer not on the same wavelength (or probably myself after a bad night) might modify them directly). If I make them private and use __get() and __set(), then json_encode() doesn't see them.
Wouldn't it be nice to have a "readonly" accessibility keyword?
Here is a way to render all property of your class read_only from outside, inherited class have write access ;-).
class Test {
protected $foo;
protected $bar;
public function __construct($foo, $bar) {
$this->foo = $foo;
$this->bar = $bar;
}
/**
* All property accessible from outside but readonly
* if property does not exist return null
*
* #param string $name
*
* #return mixed|null
*/
public function __get ($name) {
return $this->$name ?? null;
}
/**
* __set trap, property not writeable
*
* #param string $name
* #param mixed $value
*
* #return mixed
*/
function __set ($name, $value) {
return $value;
}
}
tested in php7
I see you have already got your answer but for the ones who still are looking:
Just declare all "readonly" variables as private or protected and use the magic method __get() like this:
/**
* This is used to fetch readonly variables, you can not read the registry
* instance reference through here.
*
* #param string $var
* #return bool|string|array
*/
public function __get($var)
{
return ($var != "instance" && isset($this->$var)) ? $this->$var : false;
}
As you can see I have also protected the $this->instance variable as this method will allow users to read all declared variabled. To block several variables use an array with in_array().
For those looking for a way of exposing your private/protected properties for serialization, if you choose to use a getter method to make them readonly, here is a way of doing this (#Matt: for json as an example):
interface json_serialize {
public function json_encode( $asJson = true );
public function json_decode( $value );
}
class test implements json_serialize {
public $obj = null;
protected $num = 123;
protected $string = 'string';
protected $vars = array( 'array', 'array' );
// getter
public function __get( $name ) {
return( $this->$name );
}
// json_decode
public function json_encode( $asJson = true ) {
$result = array();
foreach( $this as $key => $value )
if( is_object( $value ) ) {
if( $value instanceof json_serialize )
$result[$key] = $value->json_encode( false );
else
trigger_error( 'Object not encoded: ' . get_class( $this ).'::'.$key, E_USER_WARNING );
} else
$result[$key] = $value;
return( $asJson ? json_encode( $result ) : $result );
}
// json_encode
public function json_decode( $value ) {
$json = json_decode( $value, true );
foreach( $json as $key => $value ) {
// recursively loop through each variable reset them
}
}
}
$test = new test();
$test->obj = new test();
echo $test->string;
echo $test->json_encode();
Class PropertyExample {
private $m_value;
public function Value() {
$args = func_get_args();
return $this->getSet($this->m_value, $args);
}
protected function _getSet(&$property, $args){
switch (sizeOf($args)){
case 0:
return $property;
case 1:
$property = $args[0];
break;
default:
$backtrace = debug_backtrace();
throw new Exception($backtrace[2]['function'] . ' accepts either 0 or 1 parameters');
}
}
}
This is how I deal with getting/setting my properties, if you want to make Value() readonly ... then you simply just have it do the following instead:
return $this->m_value;
Where as the function Value() right now would either get or set.