__get() and __set() with 1.5.4

#1
[eluser]Unknown[/eluser]
Hi, hoping someone can point me to a workaround here. I've cut everything down to a minimal example. First, the model:

Code:
<?php

class DummyModel extends Model
{
    private $data = array( 'Int' => 0, 'String' => '');

    public function DummyModel()
    {
        parent::Model();
    }

    public function __set( $name, $value )
    {
        $f = "valid{$name}";
        if ( $this->$f( $value ) )
        {
            $this->data[$name] = $value;
            return true;
        }
        throw new DummyModelException( "Invalid {$name}" );
    }

    private function validInt( $Int )
    {
        if ( is_numeric( $Int ) ) return true;
        throw new DummyModelException( "Invalid Int" );
    }

    private function validString( $String )
    {
        if ( strlen($String) < 255 ) return true;
        throw new DummyModelException( "Invalid String" );
    }
}

class DummyModelException extends Exception {}

?&gt;

Then, the controller:

Code:
&lt;?php
class Dummy extends Controller
{
    function Dummy()
    {
        parent::Controller();
        $this->load->model('DummyModel');
    }
}
?&gt;

And the error:

Code:
Fatal error: Call to undefined method DummyModel::valid_ci_scaffolding() in
C:\web\system\application\models\dummymodel.php on line 15

I know what's happening - __set() is grabbing a call to set_ci_scaffolding() for itself, munging it into "valid_ci_scaffolding" (line 14), and attempting to call the non-existent method (line 15).

The causal error is the grabbing of the set_ci_scaffolding() call by the __set() magic method, and I don't have a clue how to suppress/work around that without stirring CI's entrails with a stick. Thanks in advance for any insight.

(BTW, I know that's not sensible validation, they're just placeholders).

#2
[eluser]sophistry[/eluser]
two ideas... OTOMH

1) would get_class() help protect the __set from calling methods it's not supposed to?

2) can you turn off scaffolding?

#3
[eluser]Jim OHalloran[/eluser]
I had a similar problem in my models where I wanted o throw an exception whenever a attempt was mde to assign to an invalid variable... In the end I used a structure like this to resolve issues with the CI assignments...

Code:
function __set($var, $value) {
        switch ($var) {
            case 'var1':
                // do stuff here...
                break;
            case 'var2':
                // do stuff here...
                break;
            default:
                // CI properties like _ci_scaffolding come through here too, so we need to accept those
                // and not throw an exception..
                $this->$var = $value;
                break;
        }
    }

Hope that helps.

Jim.

#4
[eluser]PsyCow[/eluser]
First, you should check if the "valid{$name}" method exists in $this with :
Code:
public function __set( $name, $value )
    {
        $f = "valid{$name}";
        if (methods_exists($this, $f) and $this->$f( $value ) )
        {
            $this->data[$name] = $value;
            return true;
        }
        throw new DummyModelException( "Invalid {$name}" );
    }

In CI the coding standards seems to have a '_' before the private variables so you should add an other test to check if $name begin with such characters.

Code:
public function __set( $name, $value )
    {
        $f = "valid{$name}";
        if (methods_exists($this, $f) and $this->$f( $value ) )
        {
            $this->data[$name] = $value;
            return true;
        }
        if ($name[0] == '_') // CI private value case.
            $this->$name = $value;
        throw new DummyModelException( "Invalid {$name}" );
    }

#5
[eluser]Unknown[/eluser]
Thanks to everyone who replied; I understand the problem now (my description above is incorrect). Jim's answer works if you're dealing with a fixed set of class variables, and psycow's correct about the underscore (but there are a bunch more assignments to be accounted for).

Sophistry, I'm staring at the code and I don't see how to apply get_class() to this situation; I'd be grateful if you could spell it our for me?

As a synthesis of everyone's suggestions I came up with this (dropping the valid{$name}() call as it was confusing the issue):

Code:
public function __set( $name, $value )
    {
        if ( $name[0] == '_' || $name == 'config' || $name == 'input' || $name == 'benchmark' || $name == 'uri' || $name == 'output' || $name == 'lang' || $name == 'load' )
        {
            $this->$name = $value;
        }
        else
        {
            $this->data[$name] = $value;
        }
        return true;
    }

But of course that's too brittle to be used in anger.

I think that dynamically creating class "variables" at runtime using __set() and a private array just won't work reliably when you're extending a base class. A PHP problem, not a CI problem.

#6
[eluser]sophistry[/eluser]
@robertx

my notion about get_class() was just to use it (and/or it's brother __CLASS__ ) to determine what your current scope is. in fact, it's just what you said above about extending a base class. using get_class() and/or __CLASS__ was my suggestion to get more info from PHP about where the code should be executing. that said, i haven't done it myself, so it is just theory.

i came upon a similiar problem in PHP4 trying to implement the recently released Active Record class.

#7
[eluser]Unknown[/eluser]
[quote author="RobertX" date="1191216136"]
As a synthesis of everyone's suggestions I came up with this (dropping the valid{$name}() call as it was confusing the issue):

Code:
public function __set( $name, $value )
    {
        if ( $name[0] == '_' || $name == 'config' || $name == 'input' || $name == 'benchmark' || $name == 'uri' || $name == 'output' || $name == 'lang' || $name == 'load' )
        {
            $this->$name = $value;
        }
        else
        {
            $this->data[$name] = $value;
        }
        return true;
    }

But of course that's too brittle to be used in anger.

I think that dynamically creating class "variables" at runtime using __set() and a private array just won't work reliably when you're extending a base class. A PHP problem, not a CI problem.[/quote]

Hey RobertX. I just got done fighting with this little issue as well. It was a pain to try and figure out. You're initial thought process wasn't too far off actually. You were right that CI is trying to call a bunch of internal assignments that are in fact getting passed to your __set method. It didn't necessarily have anything to do with you adding "valid" like you mentioned though.

I think you and I are actually trying to do similar things. And although your post was from last September, I'm assuming other individuals may be running across this issue so I figured I'd share my "workaround". Hopefully maybe there's a way to fix this in the CI core that would more easily allow the use of the magic __set method. Although, the workaround I came up with isn't too bad. The code that you have above isn't far from what I came up with, but you limit your code to only core CI libraries. If you end up adding additional libraries for your application, the above code will fail. If you're using the __set method to update a private array then I'm assuming you know what fields will be in that array. If you do, then you can easily say something like

Code:
public function __set( $name, $value )
    {
        if(in_array($name,$this->data))
            $this->data[$name] = $value
        else
            $this->$name = $value
        endif;
    }

and I believe that would fix your problem. Then...if the $name parameter exists as part of your private $data array then it would update the value, if not we'd assume is was an internal CI call and just let it do it's thing. Does this all make sense?

Here's how I could see this playing out. Just off the top of my head - not tested.

Code:
class MY_Model extends Model
{
    private $this->table;
    private $this->columns;
    private $this->data;

    public function __construct()
    {
        parent::Model
        $this->table    = strtolower(substr(get_class($this), 0, -5));
        $this->columns  = $this->db->list_fields($this->table);
    }

    public function __get($var)
    {
        return $this->data[$var];
    }

    public function __set($var, $val)
    {
        if(in_array($var,$this->columns)):
            $this->data[$var] = $val;
        else:
            $this->$var = $val;            
        endif;          
    }    
}

Obviously there would be more to that, but I think that's a good start.

Hope this helps someone, it definitely gave me fits for a couple of days. I'll try to take a look at the core CI code and see if there isn't a way to make this a little easier.

#8
[eluser]echadwickb[/eluser]
I know this thread is a little old, but wanted to add a twist to conrad.decker's solution.

I extended the model class and ran into this same issue. I did not want to add an empty key to $this->data every time I added a field to my table, so using array_key_exists in the __set() function was a no go.

From what I understand, the goal is preventing core CI stuff from rolling into $this->data. Here's my solution:

Code:
function __set($field, $value)
{
    $CI =& get_instance();
    if (array_key_exists($field, get_class_vars(get_class($CI)) + get_object_vars($CI)))
    {
        $this->{$field} = $value;    
    }
    else
    {
        $this->data[$field] = $value;
    }        
}

I still don't understand all that is going on with magic_methods and CI. This may slow things down a bit, but it at least filters out properties and methods CI is trying to assign to the model. We'll see how well it scales. Kind of crude, but it works. Be curious to know what CI's more experienced forum members think of this code.

#9
[eluser]sunny.by[/eluser]
Thanks


Digg   Delicious   Reddit   Facebook   Twitter   StumbleUpon  


  Theme © 2014 iAndrew  
Powered By MyBB, © 2002-2021 MyBB Group.