• 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
ActiveRecord for CodeIgniter: Rails-style model interactions

#41
[eluser]ralf57[/eluser]
[quote author="sophistry" date="1190687940"]@ralf57 - i have to agree with walesmd. i've been using CI Active Record for a year now and it's just great for most everything.

but, this class is an advancement I was ready to make - i am just lazy about writing lots of redundant code.
[/quote]

Thank you sophistry,
i'm evaluating this custom ActiveRecord implementation to solve a limitation of CI Active Record.
I'm going to explain:
the
Code:
$this->db->insert('mytable', $data);
method accepts an array
Code:
$data = array(
    'title' => 'My title' ,
    'name' => 'My Name' ,
    'date' => 'My date'
);
as the second parameter so it can produce
Code:
INSERT INTO mytable (title, name, date) VALUES ('My title', 'My name', 'My date')
but it doesn't take care of binding the table fields with the array keys so, if you pass the array in a different order it will insert the values in the wrong fields (for example "My title" into "name" field).
This was pushing me to find a solution in this custom class.
Can this issue solved with the built-in CI ActiveRecord class?

#42
[eluser]chrisj[/eluser]
[quote author="ralf57" date="1191413034"]but it doesn't take care of binding the table fields with the array keys so, if you pass the array in a different order it will insert the values in the wrong fields (for example "My title" into "name" field).
This was pushing me to find a solution in this custom class.
Can this issue solved with the built-in CI ActiveRecord class?[/quote]

I've been working on modifying ActiveRecord for this purpose as well. I can't say this is the best solution, but this is what I've been doing and seems to work well so far.

To the ActiveRecord class I've added these functions:
Code:
function populate_from_post() {
    foreach( $this->_columns as $column ) {
        $field = $column->Field;
        if($this->input->post($field) != null) {
            $this->$field = $this->input->post($field);    
        }        
    }
}

function validate() {
    $this->validation->set_fields( $this->_validation_fields );
    $this->validation->set_rules( $this->_validation_rules );
    return $this->validation->run();
}

Then in the constructor for my Model I create two variables that contain the validation info:
Code:
$this->_validation_fields = array( 'column' => 'Field Name', 'column2' => 'Another Field Name' ); // etc...
$this->_validation_rules = array( 'column' => 'trim|required', 'column2' => 'required|numeric' ); // etc...

So then I can do this in my controller:
Code:
function edit_post( $id )
{
  // grab the data for the record as it currently exists
  $post = $this->Post->find($id);
    
  // my custom isPost function, just basically checks the server vars...
  if( isPost() ) {
        // grabs any post vars that match column names
        // and overwrites values
        $post->populate_from_post();
        
        // now you can do any custom setting you want
        $post->guid = generate_custom_guid($id);
        $post->foo = 'bar';
        
        // validate all vars
         if( $post->validate()==true ) {
          $post->update();
            redirect('somehwere');
        }
    }
    
    // if not post or post didn't validate then show the view
    $data['post'] = $post;
    $this->load->view('admin/edit_post',$data);
}

And in the view I can just set the values of my form fields to $post->field. You can use the rest of the code igniter validation functions the same as you normally would.

The create action would work the same way but instead of
Code:
$post = $this->Post->find($id);

You'd do:
Code:
$post = new Post();

Hope this helps.

#43
[eluser]Piinaaja[/eluser]
[quote author="chrisj" date="1191395545"]Thanks BB & NM.

I found some major bugs with my _fetch_related function, however. Here's the new and improved version:
Code:
function _fetch_related($table, $inflected)
    {
       ...          
    }
[/quote]

Hi chrisj, thanks for adding improved relations fetching, it helps a lot!
I noticed that you might have a bug in the updated _fetch_related, the part where it searches for has_many or has_one, I think the line ending should be:

Code:
WHERE '. $this->_class_name .'_id = '.$this->id);
instead of:
Code:
WHERE '. $this->_class_name .'_id = id');

#44
[eluser]chrisj[/eluser]
Piinaaja, you're absolutely correct. I had it like that in my original version, and then I changed it for some reason.

#45
[eluser]wiredesignz[/eluser]
Quote:From the Wiki:

$this->person->deleteAll()

Empties the people table!

My understanding of encapsulation is that an object (here representing a table row), should not have any method which affects another object.

Please correct me if I'm mistook.

#46
[eluser]nmweb[/eluser]
I made another change to this great library. I was a bit annoyed with the queries executed everytime to get the columsn of the table. Therefore I implemented a cache system.

On load a config file is loaded which if empty will be filled with the serialized result of the SHOW COLUMNS query. I was hoping I could put a human-readable result in the file, it's possible but I would either have to do some weird stuff with __set_state() or change objects in the whole class to arrays. This would have led to a neater solution in the config-file.

It is vital that you name your class exactly the same as your file.
Code:
/**
     * discover_table_columns
     *
     * Called on instantiation of a model to capture the field names
     * of the related table. By convention, the model name is singular
     * and the table name is plural.
     *
     * Sep 24: Reduced column lookup to one query, as suggested by gunter
     * Oct 10: Added cache by nmweb
     * @access    public
     * @return    object
     */    
    function discover_table_columns()
    {

        $this->config->load(get_parent_class($this).'_Cache',FALSE,TRUE);
        
        if($this->config->item($this->_table . '_table_columns_cache'))
        {
            
            return unserialize($this->config->item($this->_table . '_table_columns_cache'));
        }
        else
        {
            if ($this->config->item($this->_table . '_table_columns'))
            {
                return $this->config->item($this->_table . '_table_columns');
            }
            else
            {
                $columns = $this->db->query('SHOW COLUMNS FROM ' . $this->_table)->result();
                $this->config->set_item($this->_table . '_table_columns', $columns);
                
                $this->_cache_table_columns($this->_table . '_table_columns', $columns);
    
                return $columns;
            }
        }
    }
     /**
     * cache_table_columns
     *
     * Called when a table's coluns havent been cached, it looks for a a
     * configfile in the config directory and appends the cacheable data.
     * It serializes the object and saves the file.
     *
     *
     * @access    private
     * @return    boolean
     */      
    function _cache_table_columns($table,$columns)
    {

        $filename=APPPATH.'config/'.get_parent_class($this).'_Cache'.EXT;
        if($fp = @fopen($filename, "a+")){
            
            flock($fp, LOCK_EX);
            fwrite($fp, '<?php  $config[\''.$table.'_cache\'] = \'' . serialize($columns) . '\'; ?>'."\n");
            flock($fp, LOCK_UN);
            fclose($fp);
            return TRUE;
        }
        else
        {
            log_message('debug', 'ActiveRecord Cache file could not be opened: '.$filename);
            return FALSE;
        }
        
    }
The writing of the php can be made more neatly and I might do that in the near future. Also a flag to disable caching or delete the cache might be added in case you have tables with dynamic columns. Deleting the cache right now just means deleting the config file. Make sure the right permissions are set for writing the file.

I also came up with an idea to implement the ->update(); and ->save() functions in a destructor so there is an autosave Smile If anyone would like to write this, please and thank you Smile In my blog post you can find the entire ActiveRecord class with the some updates from this thread as well, namely the support for has_many and has_one and belongs_to relations.

#47
[eluser]KJSDFHASD78FASDF78SA[/eluser]
I wonder why there are so many eval(...) statements in this library? Is it to tackle bugs in some version of PHP5 or something?

For instance:
Code:
eval('return $this->' . $method . ';');
and
Code:
return $this->$method;
or even
Code:
eval('$return = new ' . $this->_class_name . '();');
and
Code:
$return = new $this->_class_name();
work the same, while eval version is somewhat slower and less readable?

Am I missing something?

#48
[eluser]chrisj[/eluser]
[quote author="wiredesignz" date="1191854883"]
$this->person->deleteAll()
Empties the people table!
My understanding of encapsulation is that an object (here representing a table row), should not have any method which affects another object.
Please correct me if I'm mistook.[/quote]

Yeah, this function scares the hell out of me. I've commented it out of my version. It seems to me if you have a table that you want to empty often (like a session table or something) you'd want to implement this in the child class, and it'd be just as easy as:

Code:
$this->db->query('TRUNCATE TABLE '.$this->_table);

[quote author="chromice" date="1193310405"]I wonder why there are so many eval(...) statements in this library? Is it to tackle bugs in some version of PHP5 or something? Am I missing something?[/quote]

I've been asking myself the same question. I've converted most of the eval commands in my copy like you describe here, and it seems to run just fine on PHP5. I thought maybe it was for PHP4 compatibility. I'm not really a php expert so I'm hesitant to justify it myself.

#49
[eluser]KJSDFHASD78FASDF78SA[/eluser]
OK, here's my two cents. I've attached my version of this nice library with all my changes:

-- I ditched discover_table_columns() method and all caching logic. Why? First of all, CI has a nice $this->db->list_fields('table_name') method which does the same in a cross-database-implementation way. Secondly, there is a $query->list_fields() method which returns an array of field names from your query object without making any additional queries. All in all, performance-wise you save one file read and lose one "SHOW COLUMNS FROM..." query for each table you .save() or .update() (but not .create() or .find_*()). Some hacky benchmarking's shown no difference, but the library got cleaner and there is no more "OMG, I changed my DB structure, but the old one stuck in the cache, and I forgot about that!"
-- Removed all eval's. PHP, my love, you can do without those!
-- Added join_related() method which JOINs all tables this one belongs_to. For instance, let us imaging we have a people and groups tables. Each person belongs to a group. People table has three columns 'id','group_id' and 'name', while groups table has 'id' and 'name'. You need to make a list of all people in the system and their group name .
You person model will look like this:
Code:
class Person extends ActiveRecord
{
    function __construct ()
    {
        parent::ActiveRecord();
        $this->_class_name = strtolower(get_class($this));
        $this->_table = 'people';
        $this->_belongs_to = array('groups');
    }
}
Then somewhere in you controller:
Code:
$this->load->model('person');
$this->join_related();
$data['people'] = $this->person->find(ALL);
And, finally, your view:
Code:
<ul>
&lt;?php foreach($people as $person): ?&gt;
  <li>&lt;?= $person->name ?&gt; (&lt;?= $person->group_name ?&gt;)</li>
&lt;?php endforeach; ?&gt;
<ul>
-- Fixed a couple of bugs here and there.

I created a wiki page where you can download the code.

#50
[eluser]KJSDFHASD78FASDF78SA[/eluser]
Hello again. I have extended the original library a bit further. Now joining several related rows is a piece of cake, as well as counting related records and even concatenating fields from multiple related rows in one go (useful, if you want to get as much information with one query).


Digg   Delicious   Reddit   Facebook   Twitter   StumbleUpon  


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