active-record
                                
                                 active-record copied to clipboard
                                
                                    active-record copied to clipboard
                            
                            
                            
                        Magic getter in BaseActiveRecord has readability and extensibility issues
The magic __get($name) override in BaseActiveRecord is confusing, and its combination of private _attributes and _related array access and strange use of $this->hasAttributes($name) renders it inextensible.
public function __get($name)
{
    if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
        return $this->_attributes[$name];
    } elseif ($this->hasAttribute($name)) {
        return null;
    } else {
        if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
            return $this->_related[$name];
        }
        $value = parent::__get($name);
        if ($value instanceof ActiveQueryInterface) {
            return $this->_related[$name] = $value->findFor($name, $this);
        } else {
            return $value;
        }
    }
}
This line..
... elseif ($this->hasAttributes($name)) {return null;}
.. might return the correct value for the framework, but it makes no sense in a getter. Also if the hasAttributes($name) function is extended to include extra attributes, returning null here is not expected.
I would suggest the following replacement, which as far as I can tell produces the same logic, but allows for much greater extensibility:
public function __get($name)
{
    if ($this->hasAttribute($name)) {
        return $this->getAttribute($name);
    }  else { 
        if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
            return $this->_related[$name];
        }
        $value = parent::__get($name);
        if ($value instanceof ActiveQueryInterface) {
            return $this->_related[$name] = $value->findFor($name, $this);
        } else {
            return $value;
        }
    }
}
if ($this->hasAttribute($name)) { return $this->getAttribute($name);
This is exactly the same as current yii code, but current code is optimized for speed, so what is the benefit of your change?
Thanks cebe, the difference is that in the original code:
... elseif ($this->hasAttributes($name)) {return null;}
..will always return null, even if $this->hasAttributes($name) returns something that the model can access through $this->getAttributes($name).
It works for the base definitions of $this->hasAttributes($name) and $this->getAttributes($name) in an ActiveRecord, but doesn't make sense as soon as these attribute methods are overridden.
I agree with this. Two things can be further improved:
- logic: the return value of null is weird after a positive hasAttribute inside a getter
- performance: hasAttribute() performs isset($this->_attributes[$name]) for the second time
Here's my variant of it with comments. A bit more performant and maybe a bit more readable:
public function __get($name)
{
    // if there's a value for such attribute, return it
    if (isset($this->_attributes[$name])) {
        return $this->_attributes[$name];
    }
    // if there's no value or value is null and attribute exists, return null
    if (in_array($name, $this->attributes(), true)) {
        return null;
    }
    // if there's such relation, return it
    // isset here is for performance, array_key_exists is here because there
    // could be valid nulls
    if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
        return $this->_related[$name];
    }
    $value = parent::__get($name);
    if ($value instanceof ActiveQueryInterface) {
        return $this->_related[$name] = $value->findFor($name, $this);
    }
    return $value;
}
Overall, it's not a critical thing to hold release. Moving to next milestone.
Related with #18
Done #318