SitePoint Sponsor |
|
User Tag List
Results 1 to 25 of 35
-
May 8, 2009, 00:54 #1
- Join Date
- Dec 2003
- Location
- Poland
- Posts
- 930
- Mentioned
- 7 Post(s)
- Tagged
- 0 Thread(s)
Can I intercept setting of any public property $obj->var = 'value'?
I would like to accomplish the following - whenever a public property of an object is set, preferably only outside the class, I want a given function to be called that could for example change the value before assignment or log the fact of assigning somewhere else:
Code:$obj = new MyClass; $obj->var = 'value';
Code:$obj->var = 'value';
Code:$obj->var('value');
-
May 8, 2009, 01:38 #2
- Join Date
- Jul 2006
- Location
- Augusta, Georgia, United States
- Posts
- 4,194
- Mentioned
- 17 Post(s)
- Tagged
- 5 Thread(s)
Set the property indirectly. Properties should only be directly accessible within the class anyway.
PHP Code:class Foo {
protected $var;
public function setVar($var) {
$this->var = $this->_handleVar($var);
}
protected function _handleVar($var) {
// log or change value before assignment
return $var.' changed';
}
}
-
May 8, 2009, 01:44 #3
- Join Date
- Jul 2006
- Location
- Augusta, Georgia, United States
- Posts
- 4,194
- Mentioned
- 17 Post(s)
- Tagged
- 5 Thread(s)
You could do something similar with magical methods, but I really think it would be over complicating things.
PHP Code:class Image {
protected $width;
protected $height;
public function __set($name,$value) {
switch($name) {
case 'width':
$this->width = $this->_handleWidth($value);
break;
case 'height':
$this->height = $this->_handleHeight($value);
break;
}
}
protected function _handleWidth($width) {
// log or change value before assignment
return ($width-100);
}
protected function _handleHeight($height) {
// log or change value before assignment
return ($height+100);
}
}
-
May 8, 2009, 01:55 #4
- Join Date
- Dec 2003
- Location
- Poland
- Posts
- 930
- Mentioned
- 7 Post(s)
- Tagged
- 0 Thread(s)
Thanks, that is an idea I have come across but the point is I want to leave the properties public. I am looking for a solution similar to SPL ArrayAccess which can map $obj['a'] = 5; to $obj->a = 5;
Only in this case I want $obj->a = 5; to be mapped to $obj->a(5);
-
May 8, 2009, 03:17 #5
- Join Date
- Apr 2008
- Location
- North-East, UK.
- Posts
- 6,111
- Mentioned
- 3 Post(s)
- Tagged
- 0 Thread(s)
Wouldn't this approach be a little obscure? I'm not saying it isn't possible, but IMHO, if you need to add logic when setting a property, you should use a setter.
There's a lot to be said for API clarity, why would you need to provide this pseudo magical function when a simple setter would suffice? Is there a specific need you're trying to address, to me, it sounds like you're approaching a problem from the wrong angle.
Given my propensity for forgetfulness ,I'm pretty sure I'd be scratching my head 2 years down the line (when using a class that had this magic feature) wondering why the value I set, isn't the value being set.
My 2c.@AnthonySterling: I'm a PHP developer, a consultant for oopnorth.com and the organiser of @phpne, a PHP User Group covering the North-East of England.
-
May 8, 2009, 03:21 #6
-
May 8, 2009, 03:29 #7
- Join Date
- Apr 2008
- Location
- North-East, UK.
- Posts
- 6,111
- Mentioned
- 3 Post(s)
- Tagged
- 0 Thread(s)
Sure, you can use __set and __get but, to me, this sounds like the OP has a more specific issue which we could address, just throwing some exploratory questions out there.
You could use the following, but should you, that's my point of view here.PHP Code:<?php
class Foo
{
private $bar;
public function __set($key, $value)
{
if(property_exists($this, $key))
{
$this->$key = (Integer)$value + 10;
}
}
public function __get($key)
{
if(property_exists($this, $key))
{
return $this->$key;
}
}
}
$oFoo = new Foo();
$oFoo->bar = 1;
echo $oFoo->bar; #11
?>@AnthonySterling: I'm a PHP developer, a consultant for oopnorth.com and the organiser of @phpne, a PHP User Group covering the North-East of England.
-
May 8, 2009, 04:14 #8
- Join Date
- Dec 2003
- Location
- Poland
- Posts
- 930
- Mentioned
- 7 Post(s)
- Tagged
- 0 Thread(s)
Yes, you are absolutely right, what I'm trying to accomplish is against the rules of clean API in favour of gaining more speed (I know, speed is not the right goal for a good OOP programmer
).
So let me explain: I have created a very simple active record system that is designed to be fast while providing the basic conveniences I find most important. Something like Propel light with only the functions I need. One of the 'features' is that all properties corresponding to a db row are public and no getters or setters are required (although they are available if I needed them for some reason):
Code:$user = UserPeer::getByPK(777); $username = $user->username; $user->is_online = 1; $user->save();
Of course, as you probably suspect I don't want to give up direct access to properties, I want them to be fast and don't want to apply any magic for this. But saving to db is usually not so frequent on a live site so I don't mind using a setter for that. I hope it's clear now.
-
May 8, 2009, 04:58 #9
-
May 8, 2009, 05:05 #10
- Join Date
- Dec 2003
- Location
- Poland
- Posts
- 930
- Mentioned
- 7 Post(s)
- Tagged
- 0 Thread(s)
Thanks, but I don't this this would work because __set() is invoked only on inaccessible properties while in my case all the properties are public. So
Code:$obj->a = 'value';
Code:$obj->__set();
Code:public $a;
Last edited by Lemon Juice; May 8, 2009 at 05:37. Reason: mistake in first line of code
-
May 8, 2009, 05:15 #11
- Join Date
- Apr 2008
- Location
- North-East, UK.
- Posts
- 6,111
- Mentioned
- 3 Post(s)
- Tagged
- 0 Thread(s)
But essentially it is public, just by-proxy.
@AnthonySterling: I'm a PHP developer, a consultant for oopnorth.com and the organiser of @phpne, a PHP User Group covering the North-East of England.
-
May 8, 2009, 05:20 #12
- Join Date
- Sep 2006
- Location
- Nottingham, UK
- Posts
- 3,133
- Mentioned
- 1 Post(s)
- Tagged
- 0 Thread(s)
Well it wouldn't anyway, because $obj->a() would be looking for a function, not a property
You could store all properties in a private array instead then:
PHP Code:class Whatever {
private changedFields = array();
private data = array();
public function __set ($key, $value) {
$this->changedFields[] = $key;
$this->data[$key] = $value;
}
public function __get ($key) {
if (isset($this->data[$key])) {
return $this->data[$key];
} else {
//trigger error? Return false or null? up to you
}//if
}
}
-
May 8, 2009, 05:29 #13
- Join Date
- Sep 2006
- Location
- Nottingham, UK
- Posts
- 3,133
- Mentioned
- 1 Post(s)
- Tagged
- 0 Thread(s)
And to be honest, I don't think this is that bad a use of __set and __get - the data isn't being modified, it's just being used to help set a flag elsewhere so that updates are more efficient. I see nothing wrong with that.
I would say though that storing the data twice and comparing them to decide what has changed would be better - With the above system, I could 'change' the data back to what it was in the first place, and it would be flagged as a change, when the data is in fact the same.
-
May 8, 2009, 05:54 #14
- Join Date
- Dec 2003
- Location
- Poland
- Posts
- 930
- Mentioned
- 7 Post(s)
- Tagged
- 0 Thread(s)
Sorry, my mistake, of course I meant was that
Code:$obj->a = 'value';
I agree this use of __set is quite nice, however I doubt it will give any efficiency increase - on the one hand updates will not send redundant data but on the other magic __set and __get are so slow in php that overall the performance could be worse than sending all unnecessary values back to the db. Perhaps this could help only in case of large text or blob fields. Standard setters will be much faster.
Anyway, I will never use __get for accessing properties in active record because this would be quite a serious slow down. __get is about 5 times slower than a standard getter and accessing properties is used very often.
I would say though that storing the data twice and comparing them to decide what has changed would be better - With the above system, I could 'change' the data back to what it was in the first place, and it would be flagged as a change, when the data is in fact the same.
-
May 8, 2009, 06:08 #15
- Join Date
- Sep 2006
- Location
- Nottingham, UK
- Posts
- 3,133
- Mentioned
- 1 Post(s)
- Tagged
- 0 Thread(s)
I wouldn't worry about efficiency - sending smaller queries to the database server is always a good idea, as it is often the database that is the bottleneck. If you are coding for efficiency (as in, php execution), you are probably doing it wrong. __set and __get aren't slow enough for it to be a concern, not by a long way. Imagine if you come across a database table with large amounts of data in each field - then it will definitely become a problem sending redundant data.
Only in extreme cases should you need to code for efficiency - this kind of stuff isn't a problem.
It wouldn't be the SELECT query that is less efficient - you just run the query once and then copy the data. It really isn't a problem, speed wise.
-
May 8, 2009, 07:39 #16
- Join Date
- Jul 2006
- Location
- Augusta, Georgia, United States
- Posts
- 4,194
- Mentioned
- 17 Post(s)
- Tagged
- 5 Thread(s)
My active record merely stores every change in a multidimensional array.
PHP Code:array(
'name'=>array('current name','previous name')
)
The active record also has a hasChanged() method. It merely checks if a dynamic property exists and has changed.
PHP Code:if($user->hasChanged('pwd')===true) {
}
PHP Code:$user->getProperty('title')
-
May 8, 2009, 07:51 #17
- Join Date
- Jul 2006
- Location
- Augusta, Georgia, United States
- Posts
- 4,194
- Mentioned
- 17 Post(s)
- Tagged
- 5 Thread(s)
If you would like you play around with the below class and interface and learn from it. This is the data container for my active record.
PHP Code:interface IActiveRecordDataEntity {
public function getProperty($pName);
public function setProperty($pName,$pValue);
public function hasProperty($pName);
public function hasChanged($pName);
public function addRecord($pPropertyName,IActiveRecordDataEntity $pRecord,$pArrayByDefault=false);
public function getRecord($pPropertyName,$pPrimaryKey,$pField);
public function removeProperty($pPropertyName);
public function getData();
public function cast();
}
PHP Code:class ActiveRecordDataEntity implements IActiveRecordDataEntity {
private $_data;
public function __construct() {
$this->_data = array();
}
public function setProperty($pName,$pValue) {
if($this->hasProperty($pName)===true) {
array_unshift($this->_data[$pName],$pValue);
} else {
$this->_data[$pName] = array($pValue);
}
}
public function getProperty($pName) {
if($this->hasProperty($pName)===true) {
return $this->_data[$pName][0];
}
}
public function hasProperty($pName) {
return array_key_exists($pName,$this->_data);
}
public function getRecord($pPropertyName,$pPrimaryKey,$pField) {
if(!array_key_exists($pPropertyName,$this->_data)) return false;
if(!($this->_data[$pPropertyName][0] instanceof ActiveRecord) && $this->_data[$pPropertyName][0] instanceof arrayaccess) {
foreach($this->_data[$pPropertyName][0] as $record) {
if($record->$pField == $pPrimaryKey) return $record;
}
} else {
if($this->_data[$pPropertyName][0]->$pField == $pPrimaryKey) {
return $this->_data[$pPropertyName][0];
}
}
return false;
}
public function addRecord($pPropertyName,IActiveRecordDataEntity $pRecord,$pArrayByDefault=false) {
if(array_key_exists($pPropertyName,$this->_data)===true) {
if($this->_data[$pPropertyName][0] instanceof arrayaccess) {
$this->_data[$pPropertyName][0][] = $pRecord;
} else {
//$this->_data[$pPropertyName][0] = array($this->_data[$pPropertyName][0]);
$this->_data[$pPropertyName][0] = new ActiveRecordCollection($this->_data[$pPropertyName][0]);
}
} else {
// for a hasMany relationship with only one item. Otehrwise if something
// only has one item in its result set but has a hasMany relationship
// a array would not exists which seems wrong.
if($pArrayByDefault === true) {
$this->_data[$pPropertyName] = array(new ActiveRecordCollection($pRecord));
} else {
$this->_data[$pPropertyName] = array($pRecord);
}
}
}
public function removeProperty($pPropertyName) {
if($this->hasProperty($pPropertyName)===true) {
unset($this->_data[$pPropertyName]);
return true;
} else {
return false;
}
}
public function getData() {
return $this;
}
public function hasChanged($pName) {
if($this->hasProperty($pName)===true) {
return count($this->_data[$pName])>1?true:false;
}
}
public function cast() {
foreach($this->_data as $key=>$data) {
$value = $this->_data[$key][0];
$this->_data[$key] = array($value);
}
}
}
PHP Code:$data = new ActiveRecordDataEntity();
$data->setProperty('name','oddz');
echo $data->getProperty('name'); // 'oddz'
-
May 8, 2009, 08:07 #18
- Join Date
- Jul 2006
- Location
- Augusta, Georgia, United States
- Posts
- 4,194
- Mentioned
- 17 Post(s)
- Tagged
- 5 Thread(s)
In a nutshell then ActiveRecord also implements that interface so any instance of IActiveRecordDataEntity can be used interchangably.
PHP Code:abstract class ActiveRecord implements IActiveRecordDataEntity {
private $_data;
public function __construct() {
$this->_data = new ActiveRecordDataEntity();
}
public function __set($pName,$pValue) {
$this->setProperty($pName,$pValue);
}
public function __get($pName) {
return $this->getProperty($pName);
}
public function setProperty($pName,$pValue) {
$this->_data->setProperty($pName,$pValue);
}
public function getProperty($pName) {
if($this->_data->hasProperty($pName)===true) {
return $this->_data->getProperty($pName);
} else {
$config = ActiveRecordModelConfig::getModelConfig(get_class($this));
foreach($config->getHasMany() as $model) {
if(strcmp($pName,$model)==0) {
$this->setProperty($pName,new ActiveRecordCollection());
return $this->_data->getProperty($pName);
}
}
}
}
public function hasProperty($pName) {
return $this->_data->hasProperty($pName);
}
public function removeProperty($pName) {
return $this->_data->removeProperty($pName);
}
public function getRecord($pPropertyName,$pPrimaryKey,$pField) {
return $this->_data->getRecord($pPropertyName,$pPrimaryKey,$pField);
}
public function addRecord($pPropertyName,IActiveRecordDataEntity $pRecord,$pArrayByDefault=false) {
$this->_data->addRecord($pPropertyName,$pRecord,$pArrayByDefault);
}
public function getData() {
return $this->_data;
}
public function cast() {
$this->_data->cast();
}
public function hasChanged($pName) {
return $this->_data->hasChanged($pName);
}
}
?>
-
May 8, 2009, 08:54 #19
@Lemon Juice, Stop worrying about performance. using methods over direct public properties is not going to make a dent in performance. If you are so concerned about performance, then STOP doing MIRCO-OPTIMIZATIONS! Profile your code and find the real slow parts. 1 or 2 ms difference is not something to be concerned about.
Read: Common Optimization Mistakes Slides
You have to realize using public properties goes against the problem you are trying to solve! Just use accessor methods, seriously there is no reason not too.
-
May 11, 2009, 18:10 #20
- Join Date
- Dec 2003
- Location
- Poland
- Posts
- 930
- Mentioned
- 7 Post(s)
- Tagged
- 0 Thread(s)
I checked that and indeed performance-wise this system is not bad at all. I compared the time it takes to clone an object holding a quite large database record to sending an UPDATE query with the same data - cloning object was extremely fast while the UPDATE quite slow and becoming proportionately slower as the query grew in size. Even comparing the old data to new was much faster than sending large updates. So finally I implemented the system you describe with comparing old to new data and it seems to work quite well. However, I didn't switch to getters and setters
Interesting read, thanks. It's not that I don't agree with these ideas, I just don't think I really need getters and setters for active record so much. I have coded a few sites using both Propel and my own system with public properties and I find the need for getters and setters very rare - if I don't find much use for them then I can choose the method that will execute faster.
With Propel I could feel that the performance was a bit sluggish when I needed to do lots of db access - generally the server managed OK but I had to think about optimizing code in a few places just because of the heavy ORM layer. And I didn't really find the need to abstract every database field with a getter and setter. However, I still use getters and setters in the active row objects in situations where I need to manipulate the data in some way, for example $obj->getPageLink() while there is no 'link' field in the table and the link is created from other fields like an ID or name - I find this very useful. I know you might say that I may not know if I wanted to manipulate data sometime in the future and having a getter would make the implementation of such a manipulation easy once the application is made but those cases are very rare when I need to make this kind of switch.
I don't strive to abstract from db like a full-featured ORM tries to do, I just want an easy and flexible way of accessing db and having objects with data that I can extend with methods is very convenient for me. So I have kept what I wanted while having a system that executes fast and I can get most data from db with almost no overhead, only marginally slower than pure mysql(i) functions. Maybe I think too much about optimization now but later when I start coding I don't have to worry about it as much as with slower implementations.
-
May 11, 2009, 18:28 #21
The current problem you are trying to solve, you cannot use public properties. It is really simple tho:
PHP Code:class Testing {
protected $propa, $propb, $propc;
public function __set ( $prop, $value )
{
if ( property_exists( $this, $prop ) )
$this->{ $prop } = $value;
return $this;
}
public function __get ( $prop )
{
if ( property_exists( $this, $prop ) )
return $this->{ $prop };
}
}
-
May 11, 2009, 18:56 #22
- Join Date
- Dec 2003
- Location
- Poland
- Posts
- 930
- Mentioned
- 7 Post(s)
- Tagged
- 0 Thread(s)
Can you explain what problem you are referring to? My main problem was to optimize db updates when doing save() in an efficient way and I have already done this with Stormrider's idea, although I initially thought I would find a different solution. So what problem were you trying to solve with the above code?
-
May 11, 2009, 19:10 #23
This one posed at that very top of this topic.
...whenever a public property of an object is set...I want a given function to be called...
-
May 12, 2009, 01:36 #24
- Join Date
- Dec 2003
- Location
- Poland
- Posts
- 930
- Mentioned
- 7 Post(s)
- Tagged
- 0 Thread(s)
Ok, I see. I was asking because SilverBulletUK posted something very similar so I thought you had something different in mind. The reason why I wrote "whenever a public property of an object is set" was that I wanted them (properties) to really stay public so that accessing them was as fast as possible - applying magic methods like this would slow down property access. I could use this solution with _get and _set in the future if I needed to but for now there is no need because I achieved my primary goal - optimizing updates on save() - by comparing old data to new.
-
May 12, 2009, 01:41 #25
- Join Date
- Jul 2006
- Location
- Augusta, Georgia, United States
- Posts
- 4,194
- Mentioned
- 17 Post(s)
- Tagged
- 5 Thread(s)
Hard coding the properties into the model as public members doesn't make for a very flexible system. Especially if you start to consider the need to overload a single ActiveRecord with other ActiveRecords or overload calculated columns. For instance, to create a aggregate calculation when finding data. A fundamental advantage of the active record is the flexibility of dynamic properties. The time it takes to run array_key_exists() on a array isn't worth removing the dynamic nature of the pattern. If you don't like __get and __set then use a method: $record->getProperty('title') | $record->setProperty('title','gh jkdh'). Either way by removing the dynamic nature your defeating the purpose of the pattern.
$category = new Category(20);
$category->products[] = new Product(array('name'=>'product'));
$category->save();Last edited by oddz; May 12, 2009 at 11:33.
Bookmarks