I think this is more of a fantasy then idea that would actually work (for speed reasons), but I can't help but get these thoughts out.
What if attributes in AR were actually classes that you could define?
Imagine a CAttribute parent class. It would represent an attribute in AR. It would have the magic method __toString() defined so that you could still do the following as you can now:
<?php
class password extends CAttribute
{
/**
* The value of this attribute can be accessed internally via $this->value
*/
//events
protected function beforeSave() {...}
protected function afterSave() {...}
protected function beforeValidate() {...}
//more events similar to that in AR
// -- New Events not in AR
/**
* -- onSet()
* Called when the attribute value is changed using CModel::setAttributes or when
* changed directly.
* $newValue is the new value for this attribute
* The old attribute is still stored in $this->value
*
* This example use: When the password attribute is changed, encrypt it
*/
protected function onSet($newValue) {
parent::onSet($newValue); //parent implantation sets the new value
$this->encrypt();
}
/**
* Explicitly encrypts attributes.
* Can be called with $AR->password->encrypt
*/
public function encrypt() {
$this->value = md5($this->value);
}
}
Read the comments. Just a fantasy, or a good idea?
Also in the attribute class you could perhaps define validation rules, default values, etc. The possibilities are endless.
class Musician(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
instrument = models.CharField(max_length=100)
class Album(models.Model):
artist = models.ForeignKey(Musician)
name = models.CharField(max_length=100)
release_date = models.DateField()
num_stars = models.IntegerField()
In php you probably have to jump through some hoops to get this working fashionably. The __set() of the AR class calling the setValue() of the field etc. The syntax can't be like Python, in effect there is a statement in the class property assignment in the above example, try doing that in php. Yii solved that already though with the class methods returning arrays.
It works really neat though when you have classes like Email, Password, DateTime, etc. In Django especially in combination with the form generation stuff it is a charm to work with. Combined with behaviours it could be a powerful combination.
Some good points there. The only solution I can think of for setting attributes would be to use this syntax:
$AR->attributeName->value = 'value';
But then value would not actually be a real variable at CAttribute, so that it goes through __set(), and it can run the onSet event and return CAttribute::_value (so we rename CAttribute::value to CAttribute::_value from my example)
EDIT: or __set() could also differentiate if the attribute is a class. So regular old $AR->attributeName='value' would work too EDIT2: dalip actually pointed this out
To declare these attributes, you could map them in CModel::attributes(), or it could auto-load them like so:
I think the key part is how to declare attributes as objects.
Defining an attributes() or attributeTypes() method is probably the best way to go. The method will declare the types of attributes. The type can be either class type or basic types like string, integer, etc. They are also useful for code generation tools to generate suitable HTML forms.
The __get() and __set() methods then need to be modified so that when getting or setting an object-typed attribute, we actually call the corresponding methods of the object. Here, we may incur some performance hit.
The next question is how the CAttribute class should look like.
<?php
class CAttribute extends CComponent
{
/**
* The value of this attribute can be accessed internally via $this->value
*/
private $_value = null;
//events
//not sure if all of these are needed
public function onBeforeSave();
public function onAfterSave();
public function onBeforeDelete();
public function onAfterDelete();
public function onAfterConstruct();
public function onAfterFind();
public function onBeforeValidate();
public function onAfterValidate();
/**
* -- onSet event
* Called when the attribute value is changed using CModel::setAttributes or when
* changed directly.
* $newValue is the new value for this attribute
* The old attribute is still stored in $this->value
* Returns whether to continue setting value
* Called by setValue()
*/
public function onSet($newValue) {
return true;
}
/**
* -- onGet event
* Called when the attribute value is fetched (in a controller or something, but when saving onSave() called instead below)
*/
public function onGet() {
}
public function setValue($newValue) {
if ($this->onSet($newValue))
$this->_value = $newValue;
}
public function getValue() {
$this->onGet();
return $this->_value;
}
/**
* -- onSave event
* Returns the value to save
*/
public function onSave() {
return $this->getValue();
}
/**
* More event ideas: onIsset, onUnset
*/
/**
* @return array validation rules for this attribute.
*/
public function rules()
{
return array();
}
/**
* @return array list of scenarios that this attribute is considered safe for,
* or true for all scenarios
*/
public function safe()
{
return true;
}
}
Another idea idea is to have CAttribute::DbExpression, which when set to true, the value is treated like a mysql expression.
Here is the nicest example I could think of:
<?php
class password extends CAttribute
{
//whether the password is encrypted
protected $isEncrypted = false;
public function afterConstruct() {
if (!$this->Owner->isNewRecord) {
//password already hashed (because it came from the DB)
$this->isEncrypted = true;
}
}
public function onSet($value) {
$this->isEncrypted = false;
return $value;
}
/**
* @return value to be put in DB
*/
public function onSave() {
return $this->getEncrypted();
}
public function getEncrypted() {
if ($this->isEncrypted)
return $this->getValue();
else
return $this->encrypt($this->value);
}
protected function encrypt($value) {
return md5($value);
}
}
//Controller
//New record example
$AR = new User;
$AR->password = 'dog';
echo $AR->password; //dog
echo $AR->password->encrypted; //123ku6hs5df3u489sfsd (eg hashed version of 'dog')
$AR->save(); //saves hashed password in DB
echo $AR->password; //dog
echo $AR->password->encrypted; //123ku6hs5df3u489sfsd (eg hashed version of 'dog')
// -- old record example:
$AR = User::find(...);
echo $AR->password; ///123ku6hs5df3u489sfsd (can't obtain unencrypted version)
echo $AR->password->encrypted; //123ku6hs5df3u489sfsd
$AR->password = 'dog';
echo $AR->password; //dog
echo $AR->password->encrypted; //123ku6hs5df3u489sfsd
$AR->save(); //saves hashed password in DB
Changed my mind. All attribute classes should probably all be contained in a single directory, and then you would have to 'install' attributes to a model, something like this:
<?php
public function attributes() {
array(
'attributeName' => 'attributeClassName',
'created, modified' => 'autoDatetimeAttribute', //example
);
}
This way attributes are reusable, and you may define a single attribute class to be used in multiple models.
There could also be a 'cached' type. If you cache the parsing of markdown or anything in a separate field in your database you could have one attribute handling both the cached and uncached version.
class CachedField extends CAttribute
{
public function getCached(){}
public function getUncached(){}
}
One virtual field would then act instead of having two real ones.
A string class is also neat syntax wise ($object->trim()->uppercase(), beats uppercase(trim($string)).
Regarding implementation, I belief right now the database schema is cached somewhere. This change would make the database schema available in the model and it might be necessary to have a BaseModel and a Model class so the yiic tool can always generate the BaseModel without overwriting the editable Model class.