Constructing an object for every single attribute would probably add quite a bit of overhead.
I was advocating abstraction/mediation of types that don’t translate easily between MySQL and PHP - strings and integers already translate well, so I don’t think those require special care. Abstracting a string as an object is not really PHP “style”, is it?
I was not thinking of abstracting strings and integers as objects. I think that would be overkill. I was thinking more of things like timestamps. Also it would help modularize your code. For instance, you could abstract a password attribute as an object, and contain the hash, reset, ect, methods and beforeSave logic in it.
I wonder if it could be somehow done ‘virtually’ though - so that a new object doesn’t get constructed every time. It really doesn’t need to. You have the values in their original form in the model object - all you need is a nice syntax for transparently converting the value, on-demand, so that there’s as little overhead as possible. If a single instance of the converter could handle the conversion for all instances, that would mean much less overhead…
As for the password component logic, you can already have that. For example:
<?php
class User {
...
private $_passwordManager;
public function getPassword()
{
if (!isset($this->_passwordManager))
$this->_passwordManager = new MyPasswordManager($this);
return $this->_passwordManager;
}
}
...
$user->password->set('hello');
...
if ($user->password->verify($_POST['password']))
{
....
}
The password manager will be loaded on-demand, constructed as-needed.
Lately I find that this approach works great for a number of things
A little OT: I also find this pattern very useful. It’s like a “cheap cache” for complex object properties that are not always used. Actually it’s so common in Yii and my code, that i feel like PHP could use another function type for easy declaration .
Something like this would be cool:
public cached function getPassword() {
return new MyPasswordManager($this);
}
PHP should translate that into:
private $__getPassword;
public function getPassword() {
if($this->__getPassword===null)
$this->__getPassword=newMyPasswordManager;
}
There could be another keyword to also specify the empty value (would default to null) to make it complete and also allow "null" as value:
public cached function getPassword() empty false{
return new MyPasswordManager($this);
}
private $__getPassword=false;
public function getPassword() {
if($this->__getPassword===false)
$this->__getPassword=newMyPasswordManager;
}
But i can hardly imagine that something this will ever go into PHP
Accepting that challenge, I came up with a little hack and syntactic sugar to that effect:
<?php
function cached()
{
$params = func_get_args();
$class = array_shift($params);
$caller = debug_backtrace();
$object = $caller[1]['object'];
$key = '__'.$caller[1]['function'].'_'.$caller[0]['line'];
if (isset($object->$key))
return $object->$key;
return $object->$key = call_user_func_array(
array(new ReflectionClass($class), 'newInstance'), $params
);
}
class Password {
public $md5;
public function __construct($password)
{
$this->md5 = md5($password);
}
}
class User
{
public function getPassword()
{
return cached('Password', 'abc123');
}
}
header('Content-type: text/plain');
$test = new User;
var_dump($test->getPassword(), $test);
The first argument to cached() is the class-name, and subsequent arguments will be applied to the constructor.
Note that I added the calling method’s line-number to the property name, since you might have more than one “return cached” statement in the same function. If you don’t care about that, you can simplify slightly:
$key = '__'.$caller[1]['function'];
PHP provides a lot of tricky little mechanisms these days - if you put them to work, you can can it to do pretty much anything you want
By the way, I don’t know if I like the term “cached” for the function-name… it’s not really a cache - more like a shortcut for lazy instantiation of a helper object.
Perhaps, "return lazy", hehe.
Or the ultra-short and more semantic version: return a(‘Password’);
By the way, I benchmarked this, and the overhead when compared with the same hard-coded pattern is somewhere around 400% and 500%
This may sound worse than it is - function call overhead is insignificant in most cases in the first place, so you may add 0.02 milliseconds of overhead per call, if you use this function instead of hard-coding the same pattern.
During a request that uses this pattern 100 times, you would incur an overhead of 0.2 milliseconds, so it’s still pretty insignificant and probably negligible unless you’re building some sort of performance-critical mega-operation
Nice solution. And “lazy” is definitely the right term. While i was sleeping i was thinking about something similar . In fact, if your benchmarks are right and since this is such a common requirement, should we think about adding this to CComponent?
The implementation is quite easy (6 new lines, as marked below plus a setter method):
class CComponent
{
private $_e;
private $_m;
private $_l; // NEW
public function __get($name)
{
$getter='get'.$name;
$lazyInit='lazy'.$name; // NEW
if(method_exists($this,$getter))
return $this->$getter();
else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
{
// duplicating getEventHandlers() here for performance
$name=strtolower($name);
if(!isset($this->_e[$name]))
$this->_e[$name]=new CList;
return $this->_e[$name];
}
else if(isset($this->_m[$name]))
return $this->_m[$name];
else if(isset($this->_l[$name])) // NEW
return $this->_l[$name]; // NEW
else if (method_exists($this,$lazyInit)) // NEW
return $this->_l[$name]=$this->$lazyInit(); // NEW
else if(is_array($this->_m))
{
foreach($this->_m as $object)
{
if($object->getEnabled() && (property_exists($object,$name) || $object->canGetProperty($name)))
return $object->$name;
}
}
throw new CException(Yii::t('yii','Property "{class}.{property}" is not defined.',
array('{class}'=>get_class($this), '{property}'=>$name)));
}
public function lazySet($name,$value)
{
$this->_l[$name]=$value;
}
To define such a lazy initiated property, we could use the lazy keyword, similar to get or set:
$x=new Something;
$password =$x->password; // this would trigger lazy init
On the other hand: Since CComponent is the basis for pretty much everything much care has to be taken that this component doesn’t get bloated. Maybe some more benchmarks could help here.
Adding another member variable to CComponent will add another member variable to (pretty much) every object ever instantiated in Yii.
Also, Behaviors in Yii are already managed and created using lazy instantiation - it’s probably overkill, adding another feature just for that?
If lazy instantiation is going to be available as a separate feature, I think Behaviors need to be refactored to use the same feature, so we don’t end up with two implementations, two standards,…
Yeah, maybe overkill. It was only an idea, but i’d prefer a solution in a clean OOP manner. So maybe i just have to get used to type this pattern again and again … .
Just came up with a much simpler way to implement the lazy construction pattern - check this out:
<?php
class Test
{
public function __get($name)
{
echo "invoking the magic accessor:\n\n";
return call_user_func(array($this, 'get'.ucfirst($name)));
}
public function getBigValue()
{
return $this->bigValue = array(
'big'=>'value',
'or'=>'some expensive object',
'...'
);
}
}
header('Content-type: text/plain');
$test = new Test;
var_dump($test->bigValue);
echo "\nmagic accessor not invoked the second time:\n\n";
var_dump($test->bigValue);
The first time the magic accessor is invoked, it simply assigns it’s return-value to a property with the same name - subsequent calls don’t even invoke the magic accessor then, so this is much more effiecient - and requires very little code, just the extra assignment in the return-statement.
The password example would look like this:
class User
{
public function getPassword()
{
return $this->password = new PasswordManager($this, 'supersecretpassword');
}
}
Hmm, does this work in E_STRICT? I doubt it, as you try to assign a non existent class variable. Even though it might work i would consider this approach rather dirty .
<?php
error_reporting(E_ALL | E_STRICT);
class Test
{
public $a;
public function hello()
{
echo 'hello';
}
}
header('Content-type: text/plain');
echo "Testing...\n\n";
$test = new Test;
$test->a = 'abc'; // declared property
$test->b = '123'; // undeclared property
$array['hello'] = 'world'; // implicit array creation
$test::hello(); // just to prove that E_STRICT is displayed
echo "\n\nEnd of Script";
Features like dynamic properties and implicit array creation are legacy PHP features - personally I agree with your way of thinking, they should cause an E_STRICT, but apparently they don’t…
Edit: updated test script to demonstrate that E_STRICT is reported.