CodeIgniter Forums
DataMapper 1.6.0 - Printable Version

+- CodeIgniter Forums (https://forum.codeigniter.com)
+-- Forum: Archived Discussions (https://forum.codeigniter.com/forumdisplay.php?fid=20)
+--- Forum: Archived Libraries & Helpers (https://forum.codeigniter.com/forumdisplay.php?fid=22)
+--- Thread: DataMapper 1.6.0 (/showthread.php?tid=11358)



DataMapper 1.6.0 - El Forum - 10-12-2008

[eluser]OverZealous[/eluser]
PS: Don't forget to include (if you so choose) get_once(), get_by_id() and I also user is_new() a lot. I also thought a clone() and copy() function would be useful, all are listed below:
Code:
/**
* Get Once
*
* Gets this object if it hasn't already been gotten.
* Only used in relationships.
*
* @access public
* @return $this
*/
function get_once()
{
    if( $this->is_new() && !empty($this->related))
    {
        $this->get();
    }
    return $this;
}

// --------------------------------------------------------------------

/**
* Is New
*
* Returns TRUE if this model has not been saved (has no id).
*
* @access public
* @return TRUE if this model has not been saved
*/
function is_new()
{
    return empty($this->id);
}

// --------------------------------------------------------------------

/**
* Get By Id
*
* Gets an object by id.
*
* @access public
* @param int id of object
* @return $this
*/
function get_by_id($id)
{
    return $this->where('id', intval($id))->get();
}

// --------------------------------------------------------------------

/**
* Get Clone
*
* Creates a clone of this object, and returns it
* Clones are exact duplicates, including the id.
*
* @access public
* @return clone, or NULL if id has not been set
*/
function get_clone()
{
    $clone = NULL;
    // try to get this object, if it is in a relationship
    $this->get_once();
    if( ! empty($this->id)) {
        $m = ucfirst($this->model);
        $clone = new $m();
        $clone->get_by_id($this->id);
    }
    return $clone;
}

// --------------------------------------------------------------------

/**
* Get Copy
*
* Creates a copy of this object, and returns it
* Copies are new objects, that have not yet been stored in the database.
*
* @access public
* @param  save if TRUE, saves the new value in the database immediately.
* @return clone, or NULL if id has not been set
*/
function get_copy($save = FALSE)
{
    $m = ucfirst($this->model);
    $copy = new $m();
    // try to get this object, if it is in a relationship
    $this->get_once();
    foreach($this->fields as $field)
    {
        $copy->{$field} = $this->{$field};
    }
    if($save)
    {
        $copy->save();
    }
    return $copy;
}



DataMapper 1.6.0 - El Forum - 10-12-2008

[eluser]ntheorist[/eluser]
hrmm.. i'm interested to see how the join_table thing might go.

Right now I'm deliberating about how to go through with combining files with content items in a cms thing i'm working on. I've created a document datamap, photo datamap, video etc, and an uploader class to deal with the $_FILES array (validation only checks the post array) - it seems to me like a good way to handle files as they have standard data, and i simply added a 'conf' array model validation to hold say allowed_types or max_size

Code:
array(
      'field' => 'user_photo',
      'type'    => 'photo',
      'label' => 'User Photo',
      'conf'    => array('required'=>'1','allowed_types'=>'jpg|gif|png','max_size'=>500),
      'rules' => array()
)

Many different things could have a photo for example, a user profile, a content item, a photo album (all other datamaps)..

And to track all of this it seems I either either have to create a table for every possible relationship between photos and everything else, and then declare every relationship in my photo model (then do the same with videos, documents and audio) also, this would call up a get on every has_one item, i believe.

Code:
$has_one = array('user','group','album', etc..)


..or, i record the id in the datamap that it belongs to, as a field, and save and insert the photo/document separately. The problem there is it seems a step back from 5th form to 4th, and also, more to the point, when I'm listing out many users with their photos i have to submit queries on every id that's been recorded, which seems pretty hard on the DB, thus a join would be optimal.

I'm sort of in a time crunch, so i'm going to give it a shot. I realize that the naming is important, as the joining fields could overwrite others, but perhaps if the fields were specified as a combination of the field name and joining objects' fields. Like you say, using list_fields everytime would be consuming, but for standard items (like photos) they can be explicit i think, and then using the 'AS' keyword to return referencable names

something like

Code:
// $name = The field name holding the id

function join_photo($name)
{
      // Downsizing the actual array count here
      $photofields = array('path','filename','width','height','size');
      $table = 'photos';

      foreach($photofields as $property)
      {
            $select = $table.'.'.$property.' AS '.$name.'_'.$property.', ';
      }

      $select = rtrim($select,', ');
      $this->db->select($select);
      $this->db->join($table, $table.'.id = '.$this->table.'.'.$name);
}
Then (with the types specified as above), you can retrieve the properties :
Code:
$user = new User();

foreach($user->validation as $field)
{
    $name = $field['field'];
    if($field['type'] == 'photo' && ! empty($user->$name))
    {
      $user->join_photo($user->{$name},$name));
    }
}

$user->get();

echo $user->user_photo // photo ID
echo $user->user_photo_path // path
echo $user->user_photo_filename // etc..

// or something like

function list_photo_properties($datamap, $fieldname)
{
     $photofields = array('path','filename','width','height','size');
     foreach($photofields as $property)
     {
          $photo_field_name = $fieldname . '_' . $property;
          echo $datamap->{$photo_field_name};
     }
}

list_photo_properties($user, 'user_photo');

i dunno, if that's the best approach or inspires any ideas but i'm going to try it out.

CC


DataMapper 1.6.0 - El Forum - 10-12-2008

[eluser]stensi[/eluser]
Version 1.4.3 has been released!

View the Change Log to see what's changed.

In short, I changed save() to only insert populated fields when doing INSERT queries (NULL or empty string values wont be included as they will default to those values in the Database). Also, I changed _assign_libraries() to assign only the required libraries. Lastly, I fixed validate() so fields that are not required are only validated if they contain a value.

This will be the last release for a while (unless of course a big issue is found), so I can have time to compile that list I've been talking about. I only decided to put this release up because I felt that validate() fix was an important one.

@commandercool: Let me know how that approach goes for you. I'm yet to sit down and write out my thoughts on how I'd approach this issue.


DataMapper 1.6.0 - El Forum - 10-13-2008

[eluser]OverZealous[/eluser]
Absolutely awesome. Everything works perfectly under PostGreSQL now.

The one bug that needs to be fixed is, now that it's only sending the changed data over, the ActiveRecord class is throwing an error when no data gets passed. Which is to be expected, but sometimes users click EDIT -> change nothing -> SAVE ;-).

The solution to this one is to basically not perform the save if $data[] ends up empty. I think this would work:
Code:
// line 426
if ( count($data) == 0 )
{
    /* transaction, save, transaction complete */
}
// continue with $this->validated = FALSE (or put it inside?)

It isn't needed on the insert - in fact the error message might be necessary to help find bugs.

I figure it's better to be able to call save() and have nothing happen if nothing changes than having to check everywhere in the code a save occurs.


DataMapper 1.6.0 - El Forum - 10-13-2008

[eluser]stensi[/eluser]
lol, and here I was thinking I wouldn't need to do another release for a while! Good find Wink

I've added a check for if $data is empty before the update. I'll leave it out from the insert because, as you said, it will help a developer to find faults in their design, since it shouldn't be possible for the user to insert a blank record (at least one field should always be mandatory on any inserts/updates, or else, what's the point of it?).


___________________

Version 1.4.4 has been released!

View the Change Log to see what's changed.

In short, doing a save() on an existing object without any changes, will see no UPDATE query performed (as it's not needed).


DataMapper 1.6.0 - El Forum - 10-13-2008

[eluser]Maxximus[/eluser]
I'm trying to do some stuff with DM, but I struggle a bit with some parts. Hope someone can help me on the way a bit.

The problem can be solved by using a foreach(), but I hate doing things in PHP that can be done in SQL easily, and getting in too much data to be processed in PHP.

Okay, here's the thing: I want to search a list of companies in a country, in a province of that country, of a certain type.

For that I have defined the following models:
Code:
//Country:
    public $table = "countries";
    
    public $has_many = array('company', 'province');

//Province
    public $table = "provinces";
    
    public $has_many = array('company');

    public $has_one = array('country');

//Company
    public $table = "companies";
    
    public $has_many = array('facility', 'type');

    public $has_one = array('country', 'province', 'companydetail');

//Type
    public $table = "types";
    
    public $has_many = array('company');

So far, so good. Also defined all the corresponding join tables.

Getting data without the type part is easy with DM:
Code:
$name = "Japan";
    $province = "Goki";
    
    // Get Country
    $c = new Country();
    $c->where('country', $name)->get();

    // Get the Goki province
    $c->province->where('province', $province)->get();

    $c->province->company->get();
        
    // Show all companies from this province
    foreach ($c->province->company->all as $company)
    {
        echo $company->name .' - '. $company->phone . '<br />';
    }
Plain and simple. By the way, this produces the following query:
Code:
SELECT m_companies.* FROM (`m_companies`)
LEFT JOIN `j_companies_provinces` ON m_companies.id = company_id
LEFT JOIN `m_provinces` ON m_provinces.id = province_id WHERE m_provinces.id = 13
A bit redundant perhaps, this would work just as good:
Code:
SELECT m_companies.* FROM (`m_companies`)
LEFT JOIN `j_companies_provinces` ON m_companies.id = j_companies_provinces.company_id
WHERE j_companies_provinces.province_id = 13
But probably harder to generate for DM.

Okay, now for the problem, A thing I can't figure out yet is how to get a join with the type relation done, without using a foreach(). Something like:
Code:
$c->province->company->type->where('type', 'COMPANY_TYPE_10')->get();
will not work, and gives a non working SQL statement:
Code:
SELECT m_companies.* FROM (`m_companies`) LEFT JOIN `j_companies_provinces` ON m_companies.id = company_id LEFT JOIN `m_provinces` ON m_provinces.id = province_id WHERE m_types.type = 'COMPANY_TYPE_10' AND m_provinces.id = 13
Okay, with a foreach I could crawl through all the records, but thats just wasting CPU cycles. Ideally the generated query would look like this:
Code:
SELECT m_companies.* FROM (`m_companies`)
LEFT JOIN `j_companies_provinces` ON m_companies.id = j_companies_provinces.company_id
LEFT JOIN j_companies_types ON m_companies.id = j_companies_types.company_id
WHERE j_companies_provinces.province_id = 13 AND j_companies_types.type_id = 10
Probably I'm just missing the clue here, but really think it must be possible somehow?


DataMapper 1.6.0 - El Forum - 10-13-2008

[eluser]BaRzO[/eluser]
Hi all,
i have a problem saving data & relations together
models
Code:
//Route:
    $table = "routes";
    $has_one = array('page');

//Page
    $table = "pages";
    $has_many = array('route', 'module');

i have one form to save all data and relations how can i do this what is the best way
thanks for your help...


DataMapper 1.6.0 - El Forum - 10-13-2008

[eluser]OverZealous[/eluser]
@BaRzO
I'm not sure what you are asking specifically, but DataMapper is used mostly to handle retrieving and saving the data, not loading the data from the form.

To save data, you simply set the fields on the object, and call ->save(). Repeat with each object.

To save a relationship, call obj1->save(obj2).

Your controller should handle figuring out what the data on the HTML form is. Then you need to either create new objects, or load existing ones.

So, for example, say you want to save an existing user with multiple phone numbers. Assume that each Phone has one User, and each User has multiple Phones.
Your form might look like this:
Code:
&lt;form action="/user/" method="post"&gt;
&lt;input type="hidden" name="id" value="25"/&gt;
Name: &lt;input type="text" name="name" value="Bob Smiley"/&gt;&lt;br/>
Home Phone: &lt;input type="text" name="home_phone" value="123-456-7890"/&gt;&lt;br/>
Mobile Phone: &lt;input type="text" name="mobile_phone" value="123-456-7890"/&gt;&lt;br/>
Work Phone: &lt;input type="text" name="work_phone" value="123-456-7890"/&gt;&lt;br/>
&lt;input type="submit" value="Save"/&gt;
&lt;/form&gt;

Then your inside the User->index() controller method:
Code:
$u = new User();
// load in existing user
$u->where('id', intval($this->input->post['id']));
$u->get();
// save changes
$u->name = $this->input->post('name');
$u->save();

// erase any existing phone numbers
$u->phone->get()->delete_all();

$types = array('home', 'mobile', 'work');
foreach($types as $type) {
    $p = new Phone();
    $p->type = $type;
    $p->number = $this->input->post($type.'_phone');
    $p->save();
    $p->save($u); // also, $u->save($p) would have worked, if you prefer.
}

Obviously, there's no error checking here (you need to test the ->save() functions), and much of it could be improved upon with better code.

Hopefully you see how DataMapper takes care of the DB code. With a little creative PHP, you can easily walk through an unknown number of items and store them as needed.


DataMapper 1.6.0 - El Forum - 10-13-2008

[eluser]ntheorist[/eluser]
hrm.. so i'm having a bit of success with my join queries, although i think i may be making it more work than necessary.. of course i'm building on TOP of datamapper as an extend, i haven't tried messing with it's guts.

The problem, of course, is that when displaying a list of say, users, i want the userclass (user has one userclass) to be listed as well, without forcing the db to run a query on each user, also, a user or another item could have other 'has_one' items, which further multiplies the number of queries.

Also, the problem is a bit different when creating a form. (keep in mind i'm attempting to do this all dynamically, so that any datamap will print an appropriate form, list etc)

so instead of has_one = array('userclass'), i just record the id in a db field called 'userclass'

then in it's model:

Code:
array(
    'field' => 'userclass',
    'type'    => 'key',
    'label' => 'User Class',
    'conf'  => array('class'=>'userclass','table'=>'userclasses','joins'=>array('name','access'),'list_field'=>'name','hr_after'),
    'rules' => array('required')
),

i'm using the 'type' variable so that the dynamic list/form generator i'm working on knows what to do with it. The config array is mostly self-explanatory. (this is also where i store file upload config for 'file' types) the 'joins' will join those fields when listing users as userclass_name, userclass_access, respectively (when the id isn't empty or equal to 0), and the 'list_field' is the field used to create a list dropdown, with id => 'list_field' as the source.

so i added this function to my datamapperobject (extend file)

Code:
function join_keys()
{
     foreach($this->validation as $field)
     {
          $type = $field['type'];
          $name = $field['field'];
            
          if($type == 'key')
          {
               if($this->joined === FALSE)
               {
                    $this->select($this->table.'.*');
                    $this->joined = TRUE;
               }
                
               $conf = $field['conf'];
                
               $class = $conf['class'];
               $table = $conf['table'];
               $joins = $conf['joins'];
                
               if(valid_array($joins))
               {
                    $select = '';
                    foreach($joins as $join)
                    {
                         $select .= $this->prefix.$table.'.'.$join.' AS '.$name.'_'.$join . ', ';
                    }
                    $select = rtrim($select, ', ');
                    $this->db->select($select);
                    $this->db->join($this->prefix.$table, $this->prefix.$table.'.id = '.$this->table.'.'.$name);
               }
          }
     }
}

hope there's not too much confusion there. i have it test for 'joined', because writing a select overwrites the default table.* statement - and it only needs that once.

also the valid_array function is one i added to the array helper.

I have yet to test it out with several keys in the table, but it seems to work like a charm for any 'has_one' item. It get's called before the get(), the only thing is i have to specify the table in the where clause.

Code:
$user = new User();
$user->join_keys();
$user->where($user->table.'.id',1)->get();
produces
Code:
SELECT ncore_users.*, ncore_userclasses.name AS userclass_name, ncore_userclasses.access AS userclass_access FROM (`ncore_users`) JOIN `ncore_userclasses` ON ncore_userclasses.id = ncore_users.userclass WHERE ncore_users.id = '1'

then i simply access

Code:
echo $user->userclass_name;

obviously it doesn't work for 'many' items, because those would require multiple joins to the same table, but that is where the datamapper should come in handy. I'm thinking of recording those as counts in the parent datamap, for easy count keeping. anyway, i'm gonna keep plugging away at it because so far this method works for singly related items. I do feel though as if its more 'clunky' because it's reducing the normal form of the database, but oh well. i just know there's an easier way down the line though, so we'll see where this goes.

CC


DataMapper 1.6.0 - El Forum - 10-13-2008

[eluser]OverZealous[/eluser]
I think I found a new bug related to the changed_existing checks recently added.

If a field does not have validation rules, it will never get marked as changed, and therefore never saved.

This is pretty severe, because I have a non-validated column for storing the number of login attempts, and it never gets updated.

I don't know an easy solution to this one, unless the _changed() is called on every single column whether or not it is validated.

Optionally, each database field could be dynamically added to the validated[] array if it isn't already in there, with no validation rules.

Any thoughts on this one?

UPDATE:
This works, added to the constructor:
Code:
// Temporary fix for validation/changed
    foreach($this->fields as $field) {
        $found = FALSE;
        foreach($this->validation as $v) {
            if($v['field'] == $field) {
                $found = TRUE;
                break;
            }
        }
        if( ! $found ) {
            $v = array('field' => $field, 'label' => '', 'rules' => array());
            array_push($this->validation, $v);
        }
    }

It's not pretty, but it will get the job done. I'd rather have been able to loop through the validations, and then add those fields that didn't already exist, but that seemed like even uglier code. This, at least, doesn't create even more arrays.

Alternatively, if you need a quick fix, you could add a validation for every field.