ExpressionEngine CMS
Open, Free, Amazing

Thread

This is an archived forum and the content is probably no longer relevant, but is provided here for posterity.

The active forums are here.

DataMapper 1.6.0

September 05, 2008 12:32pm

Subscribe [115]
  • #181 / Oct 15, 2008 11:24pm

    OverZealous

    1030 posts

    @Boyz26 - You are very welcome!

    should that work then for photos that have no album? or only exists in a profile?.. will many relationships on an item like a photo create performance issues?

    This should work fine.  I’m doing almost the exact same thing with notes.  My notes can be attached to multiple items, but are assigned to only one at a time (unlike how you have multiple items sharing a photo).

    Thist is pretty simple to model, you just need separate tables for each relationship.

    Also, for certain items, that say has one file, i can specify that in the has_one array, but i’m getting lost when trying to store a label for that (or config data), when producing a form for instance.. because the photos are generic but i want to be able to give it a specific purpose or name, depending on the item it’s attached to. I’m attempting to put that information in the model validation array, but i think i’m just adding too much to the problem.

    Well, I don’t know how to store a label based on which item is selected, but you can easily determine which type of owner a photo has.  Sadly, it requires one DB check for each type, but I’m currently just doing a lookup on each association, and then checking to see if it found anything — ie: if(empty($this->user->id)).  If it’s empty, you move on to the next type.  Ugly, but functional.

    If you are going the other way ($user->photo), you can check the related field from within your photo object.  Put together, it might look like this:

    public static $type_labels = array(
        'user' => 'User Photo',
        'group' => 'Group Photo',
        'userprofile' => 'User Photo'
    );
    
    function get_owner_type() {
        $type = NULL;
        if(isset($this->related) && isset($this->related['model']) {
            $type = $this->related['model'];
        } else {
            // check each model type
            foreach(self::$type_labels $k => $t) {
                $this->{$k}->get();
                if( ! empty($this->{$k}->id) ) {
                    $type = $k;
                    break;
                }
            }
        }
        return $type;
    }
    
    function get_type_label($type => NULL) {
        if(is_null($type)) {
            $type = $this->get_owner_type();
        }
        if(array_key_exists($type, self::$type_labels)) {
            return self::$type_labels[$type];
        } else {
            return 'ERROR';
        }
    }

    Hopefully that all makes sense.  I just typed it up here in the browser, so I don’t know how many errors it contains 😛

    SMALL UPDATE:  I tweaked my example to use a static array, instead of a class member array.  This prevents the array from being loaded into memory for each photo.  If using PHP4, you have to remove the public I believe.

  • #182 / Oct 16, 2008 2:41am

    ntheorist

    84 posts

    @OverZealous - cool.. thx for clarifying on the performance issues

    I haven’t really looked too closely at DM’s internal code, but i think i will take a crack at it… although i believe now the route i want to go involves creating interfaces for each kind of datamap i want to use. Each with a set of common properties, as well as its own unique functions based on its role.

    $content->get_label();
    $content->get_type();
    $content->get_add_form();
    // or like your function - haven't tested yet
    $content->get_related_labels();
    
    // additionally, for files
    $file->set_config();
    $file->upload_and_save();
    $file->lock();
    
    //etc

    i mean you understand i’m attempting to create a cms in which you can dynamically define what an object consists of and how it relates to other objects.. and have an interface within them, allowing those objects to be categorized, searchable, listable, with meta data about their relationships stored and accessible. I dunno yet if i want to start creating abstract classes or go thru extends.. this is my first true app so i’m learning as i go. Php5 does seem the way to go however, as it’s object model seems to exceed php4 quite a ways, and hosting is almost never a question nowadays.

    for instance, for the datatypes i am using (sorta like data archtypes), (string, date, currency, text, photo..) i’m thinking i need to make a class for each, with a common interface but varying methods, rules, etc. These then would be used directly by a datamap class to format its data & relationships for any purpose. So something like

    foreach($this->fields as $field) {
         $field->value = $this->{$field};
         $field->list_format();
    }
    
    // a field of type 'email' with the value '[email protected]' would produce
    <a href="mailto:[email protected]">[email protected]</a>
    
    // a field of type 'currency' with the value '1452.25' would produce
    $1,452.25 USD

    A kinda off topic problem i’m facing here however, is how to store multiple objects within an object for iteration and method chaining (arrays and serialization seem out of the question) - any advice here?

    anyway i will try to dive more into DM and try to use its strengths to maximum effect, it seems like one of those things where a little extra work goes a long way. thx for pointers and expertise, too!

    CC

  • #183 / Oct 16, 2008 2:57am

    OverZealous

    1030 posts

    It’s funny you mention the metadata.  I’m working on a similar concept.  Mine, however, allows fields to define what type of data they are - I’m using the existing validation array for this.

    I’m not going to bother trying to explain everything just yet, but to give you an idea how I’m handling the rendering:

    I wanted to be able to output <?= $object->field safely from within my views.  I didn’t want to have a whole bunch of escapes around every object.  Obviously, if I did it that way, I could never access the un-escaped value.

    What I did was create a special Html_viewer Library.  This class uses the __get($name) override to dynamically select the field from the original class, but run it through filters, convert it, or whatever needs to be done.

    To assign this class, I added this to my DataMapperExt:

    __get($name) {
        // lazily create the Html_viewer
        if($name == 'v') {
            if(!isset($this->v)) {
                $this->load->library('html_viewer');
                $this->v = new Html_viewer($this);
            }
            return $this->v;
        } else {
            // call DataMapper's __get
            return $parent::__get($name);
        }
    }

    What’s slick about this is that it doesn’t get loaded unless it is needed.  Now, to output the cleaned up element, I just call $object->v->field.

    There’s a ton more functionality I added, and I also created an Html_editor that works the same way.

    Hopefully this will give you some inspiration for how to access this dynamically.

    As for your chaining issue, I’m not sure what you are asking.

  • #184 / Oct 16, 2008 7:06pm

    ntheorist

    84 posts

    um, anyone know what happened to the recent versions of DM?

    if you go to the changelog or download page, it’s reverted to version 1.3.4 somehow..

    CC

  • #185 / Oct 16, 2008 7:28pm

    BaRzO

    105 posts

    Yesterday the stensi.com was not responding
    see attach you can get the latest version of DataMapper 1.4.5

  • #186 / Oct 17, 2008 3:44am

    Dready

    65 posts

    @OverZealous.com : your “v” thing really rocks ! It’s a really good idea for implementing features on top of a base class.

  • #187 / Oct 17, 2008 8:33am

    OverZealous

    1030 posts

    Thanks!  I’m working on integrating DoJo into the editing side.  That will allow views to simply call $object->e->name, and get a complete editing line, properly formatted, with id, name, and client-side validation.  It also allows $object->e->name->line($label), and prints the appropriate label.  What’s really cool is that line can be replaced by, say, html or plain for multiline editing, or just about any other form-input type.

    Once I get where a certain level appears to be functioning, I’ll see about posting the whole shebang up.

  • #188 / Oct 19, 2008 11:35pm

    ntheorist

    84 posts

    okay, so i’ve been attempting to hack datamapper’s code to fit it to what i’ve been trying to do, and it seems so far i’m having some success.

    While DM works great for organizing tables and data and providing a great object model for running queries, plus being a relatively tiny library (almost 1/5 the size of IgnitedRecord, which I’ve also been looking at), it doesn’t yet seem to handle the relations beyond loading the model of a related item. When a datamap’s related items are populated, it requires another entire query (double join) to load it, and if you’re listing hundreds of rows in a query, each with possibly several relations all requiring separate queries to load their data, and you again multiply that by perhaps many users running these queries at the same time.. it worried me about performance and scalability.

    So here’s what i’m working on to improve performance in terms of relations. Keep in mind too, this is a work in progress.. I’m making edits and adding functions/properties to datamapper to allow this to work, and i still have to address a few things, such as method chaining, etc.. Right now i’m focused on manipulating the active record for get() calls.

    When running a query with an item i know has related items, before calling get(), i call this function (which i’m just writing directly into datamapper to provide for all models)

    function join_related()
    {
        // Don't run a join_related directly on a related model (yet..)
        if ( ! empty($this->related))
        {
            return FALSE;
        }
        
        // Prevent Re-join if join has already been called
        if ( $this->tables_joined === TRUE)
        {
            return TRUE;
        }
        
        // Check if no select statements have been made yet, if not, the table
        
        if(empty($this->db->ar_select))
        {
            $this->db->select($this->table.'.*');
        }            
        
        // Join has_one models with their id and specified value(s)
        
        foreach($this->has_one as $model) 
        {
            $model_class = ucfirst($model);
            
            // Instantiate new model
            $ho_model = new $model_class();
            
            $ho_table = $ho_model->table;
            $ho_value = empty($ho_model->display_value) ? 'id' : $ho_model->display_value;
            $ho_relationship_table = $this->_get_relationship_table($this->prefix, $ho_table, $model);
            
            // Select values as aliased columns
            $this->db->select($ho_table.'.id AS '.$model.'_id');
            $this->db->select($ho_table.'.'.$ho_value.' AS '.$model.'_value');
            
            // Add aliased columns to datamap field list
            $this->fields[] = $model.'_id';
            $this->fields[] = $model.'_value';
            
            // Double join relation table
            $this->db->join($ho_relationship_table, $ho_relationship_table.'.'.$this->model.'_id = '.$this->table.'.id','left');
            $this->db->join($ho_table, $ho_table.'.id = '.$ho_relationship_table.'.'.$model.'_id','left');
        
        }
        
        // Join has_many models with their counts for this model
        
        foreach($this->has_many as $model)
        {
            $model_class = ucfirst($model);
            
            $hm_model = new $model_class();
            $hm_table = $hm_model->table;
            $hm_relationship_table = $this->_get_relationship_table($this->prefix, $hm_table, $model);
            
            $this->db->select('(SELECT count(*) FROM (`'.$hm_relationship_table.'`) WHERE `'.$this->model.'_id` = '.$this->table.'.id) AS '.$model.'_count');
            
            $this->fields[] = $model.'_count';
        }
        
        $this->tables_joined = TRUE;
    
        return TRUE;
    
    }

    The basic idea here is that if you have a ‘has_one’ model, meaning there’s one of those models for each of the current one you’re using, you are asking it to include the alias columns ‘model_id’ and ‘model_value’, in every row of the result. The ‘model_value’ field is determined in a model’s definition. For instance, a ‘User’ has_one ‘Userclass’ (Admin, Manager, Member, etc..), so when joining a userclass to a user, to set the data that populates in ‘userclass_value’ to the field ‘name’ of the userclass, its simply stated in the userclass model:

    class Userclass extends DataMapper {
    
        var $table = 'userclasses';
        var $controller = 'userclasses';
        var $model = 'userclass';
        
        var $has_many = array('user');
        
        var $display_value = 'name';
        
        var $validation = array(
            array(
                'field' => 'name',
                'label' => 'Class Name',
                'rules' => array('required', 'trim', 'unique', 'alpha', 'min_length' => 3)
            )
        );
            
    
        function Userclass()
        {
            parent::DataMapper();
        }
    }

    if no $display_value is defined, it defaults to id.

    thus, now i can call in my controller

    $user = new User();
    $user->join_related();
    $user->get();

    this produces only one query:

    SELECT users.*, userclasses.id AS userclass_id, userclasses.name AS userclass_value FROM (`users`) LEFT JOIN `userclasses_users` ON userclasses_users.user_id = users.id LEFT JOIN `userclasses` ON userclasses.id

    and i can continue in my controller:

    foreach($user->all as $u) {
        echo $u->username . ' is a ' . $u->userclass_value . ' which is userclass ID #'.$u->userclass_id.br();
    }

    This is also handy for generating forms where you may want to have a drop down to select a userclass, and you want to set it’s selected value to ‘userclass_id’. Also, it works nicely for creating links in the loop, ie:

    $link = anchor('userclasses/view/'.$u->userclass_id',$u->userclass_value);

    and yes, multiple has_one and has_many models can be all combined into a single query. I’ll probably have to implement table alias’s as well, to allow for multiple self-joins if needed

    more soon..

  • #189 / Oct 20, 2008 12:24am

    ntheorist

    84 posts

    As for ‘has_many’ fields, instead of joining one ID and one value column, it uses a subselect to return a count, with the field ‘model_count’ - (i’m soon implementing a subselect where clause function, so the count field will be based on specified conditions - or even multiple count fields can be used )

    $userclass = new Userclass(); // $has_many = array('user);
    $userclass->join_related();
    $userclass->get();
    
    // Produces SELECT userclasses.*, (SELECT count(*) FROM `users_userclasses` WHERE `userclass_id` = userclasses.id) AS user_count
    
    foreach($userclass->all as $uc)
    {
       echo $uc->name .' has '.$uc->user_count.' Users.';
    }

    btw, to acommodate being able to sort a query by one of the pseudo-colums ‘user_count’ or ‘userclass_value’, i changed the order_by function to

    function order_by($orderby, $direction = '')
    {
        // Check if this is a related object
        if ( ! empty($this->related)) 
        {
            $this->db->order_by($this->table . '.' . $orderby, $direction);
        }
        else if( count($this->has_many) > 0 || count($this->has_one) > 0 )
        {
            $relations = array_merge($this->has_many, $this->has_one);
            
            $model = substr($orderby, 0, (strlen($orderby)-6));
            
            if( ! in_array($model, $relations))
            {
                $this->db->order_by($this->table . '.' . $orderby, $direction);
            }
            else
            {
                $this->db->order_by($orderby, $direction);
            }
        }
        else            
        {
            $this->db->order_by($orderby, $direction);
        }
    
        // For method chaining
        return $this;
    }

    which will trigger to order by ‘userclass_value’ or ‘user_count’ if ‘userclass’ and ‘user were in fact models in has_one or has_many (conveniently ‘_value’ and ‘_count’ are 6 characters each), otherwise it will assume you are looking for a true column in the parent table and prepend the table name to prevent ambiguity. These are the only two kinds of pseudo columns you can order by so far, but again i’m working to develop this futher and would eventually like to be able to specify an array for display_value

    also, you may be wondering how to search the value of a related item:

    function where_related($model, $key, $value = NULL, $type = 'AND ', $escape = TRUE)
    {
        $relations = array_merge($this->has_one, $this->has_many);
        
        if(in_array($model, $relations))
        {
            $where_table = $this->{$model}->table;
            
            //$where_value = $key == 'value' ? ( ! empty($this->{$model}->display_value) ? $this->{$model}->display_value : 'id') : 'id';
            
            $this->db->_where($where_table.'.'.$key, $value, $type, $escape);
        }
    }

    now, i can search through ANY of the userclass tables’ columns

    $user = new User();
    $user->join_related();
    $user->where_related('userclass', 'access >','2');
    $user->order_by('userclass_name','asc');
    $user->get();

    vóila!.. well, it works now for searching thru has_one relations, and i’ll be adding the ability to search has_many soon, too. So you could write

    $userclass = new Userclass();
    $userclass->join_related();
    $userclass->where_count('user >','0');
    $userclass->get();
    
    // or even
    
    $userclass->where('name','admin')->where_count('user >','0')->get();

    i’m sure i’ll be making more modifications on DataMapper to allow for more intricate searches through related items. I was a bit hesitant at first to try subselects, but now it seems its preferable to have one query with a subselect or two, versus up to several hundred separate queries, each of which task the DB and also require loading a new Datamap with its resources, etc..

    I think i’m heading into the direction of changing the has_one and has_many arrays and their contents to allow for more dynamic relationship manipulation and explicit configuration, such as changing them to four arrays, each describing the object’s relationship to the others:

    $one_one    = array(); // Has and belongs to one object of the specified model
    $one_many   = array(); // Belongs to many objects of the specified model
    $many_one   = array(); // Many of these belong to one object
    $many_many  = array(); // Many of these belong to many other objects

    that would prove useful in deletes, updates & saves and such as well, but still have yet to get there.

    anyway.. more later. any comments or ideas or criticism is greatly appreciated.

    CC

  • #190 / Oct 20, 2008 6:57am

    Philipp Gérard

    46 posts

    Thank you very much stensi for this implementation of ActiveRecord, one less reason to learn Ruby 😉

    However, I am curious how using DatMapper instead of the built in ActiveRecord implementation in CI affects the overall speed of CI in production enviroments. Do you have any benchmarks on that?

    Thanks in advance,
    Philipp

  • #191 / Oct 21, 2008 4:20pm

    gusa

    47 posts

    Wow dude, you rock! This is really looking very usable to me now - after these few changes.

    Making the join tables use the model’s name is perfect!

    Greg

    totally agree with greg. thank you very much for listening to our suggestions!

    by the way, i change datamapper.php to allow the objects to select the database group.

    in datamapper.php add the following attribute:

    var $db_group = '';

    then, after the call to the parent constructor (parent::Model()), add these lines:

    if (!empty($this->db_group)) {
        $this->db = $this->load->database($this->db_group, true);
    }

    so, if you want your object to access to another database (rather than default), just declare an attribute with the database name:

    class Subject extends DataMapper {
        var $db_group = 'access_control_db';
        var $has_many = array("attribute" => "attributes");
    
        var $validation = array(
            'name' => array('required', 'trim', 'unique', 'min_length' => 3, 'max_length' => 20),
            'password' => array('required', 'trim', 'min_length' => 3, 'max_length' => 40, 'encrypt')
        );
        // ...
  • #192 / Oct 22, 2008 12:48am

    OverZealous

    1030 posts

    I’ve been working on what I believe is a critical addition to DataMapper: a where_related method.  It was actually really simple, although I have not yet thoroughly tested it.

    If you already have a DataMapper subclass, you can add the following code to it:

    /**
     * Get
     * 
     * Overridden to reset the _added_related array
     *
     */
    function get ($limit = NULL, $offset = NULL)
    {
        $ret = parent::get($limit, $offset);
        // clear the _added_related array
        $this->_added_related = array();
        return $ret;
    }
    
    
    // --------------------------------------------------------------------
    
    /**
     * Where Related
     *
     * Limits a query to a related object's field.
     *
     * @access    public
     * @param    object
     * @param    string
     * @param    string
     * @return    this
     */
    function where_related($object, $field = NULL, $value = NULL, $or = FALSE)
    {
        if (is_null($object))
        {
            show_error('where_related requires a valid related Object');
        }
        if (is_null($object->id) && is_null($field))
        {
            show_error('where_related requires a field or object id');
        }
        
        if ($field == NULL)
        {
            $field = 'id';
            $value = $object->id;
        }
        $this->_add_related($object, $field, $value, $or);
        
        return $this;
    }
    
    // --------------------------------------------------------------------
    
    /**
     * Or Where Related
     *
     * Limits a query to a related object's field.
     *
     * @access    public
     * @param    object
     * @param    string
     * @param    string
     * @return    this
     */
    function or_where_related($object, $field = NULL, $value = NULL) {
        return $this->where_related($object, $field, $value, TRUE);
    }
    
    
    // --------------------------------------------------------------------
    
    // used to keep track of related items
    var $_added_related = array();
    
    /**
     * Related
     *
     * Finds all related records of this objects current record.
     *
     * @access    private
     * @param    string
     * @param    integer
     * @return    void
     */
    function _add_related($object, $field, $value, $or)
    {
    
        // Prepare model
        $model = ucfirst(strtolower($object->model));
        //$object = new $model;
    
        $this->model = strtolower($this->model);
    
        // Determine relationship table name
        $relationship_table = $this->_get_relationship_table($object->prefix, $object->table, $object->model);
    
        // Retrieve related records
        if (empty($this->db->ar_select))
        {
            $this->db->select($this->table . '.*');
        }
    
        // Check if self referencing
        if ($this->table == $object->table)
        {
            if ( ! in_array($model, $this->_added_related) )
            {
                // can only perform where on id's for self-referencing
                if($field != 'id') {
                    show_error('Cannot perform where_related queries on self-referencing tables unless field is id');
                }
                $this->db->join($relationship_table, $object->table . '.id = ' . $this->model . '_id', 'left');
                $this->db->where($relationship_table . '.' . $object->model . '_id = ' . $value);
                array_push($this->_added_related, $model);
            }
        }
        else
        {
            // only add the table if it wasn't already joined
            if ( ! in_array($model, $this->_added_related) )
            {
                $this->db->join($relationship_table, $this->table . '.id = ' . $this->model . '_id', 'left');
                $this->db->join($object->table, $object->table . '.id = ' . $object->model . '_id', 'left');
                array_push($this->_added_related, $model);
            }
            if($or)
            {
                $this->db->or_where($object->table . '.' . $field, $value);
            }
            else
            {
                $this->db->where($object->table . '.' . $field, $value);
            }
        }
    
        $this->model = ucfirst($this->model);
    }

    Usage:
    If you want to filter a query based on the values of a related object, you call where_related before calling the get.
    Example:

    // get all admins
    $u = new User();
    $access = new Access();
    $u->where_related($access, 'level', 'admin');
    $u->get();

    You can also use it to filter on a specific item easily:

    $u = new User();
    $access = new Access();
    $access->where('level', 'admin')->get();
    $u->where_related($access);
    $u->get();

    Finally, you can use standard the where format:

    $u = new User();
    $access = new Access();
    $u->where_related($access, 'level >=', 2);
    $u->get();

    Limitations
    There is only one real limitation.  A self-referencing table can ONLY query by ID.  I could not think of an easy way around this.

    You can have multiple where_related or or_where_related statements.

    Let me know if there are any errors, or if you find transcription errors.  I made some changes while typing this, so I might have made a typo.

  • #193 / Oct 22, 2008 2:10am

    Boyz26

    28 posts

    Not sure if it is just me, but if you create a table called Battles, and the model battle, it won’t work.

  • #194 / Oct 22, 2008 2:19am

    OverZealous

    1030 posts

    What do you mean?

    Your tables MUST be called ‘battles’ and your model ‘Battle’ - that’s DataMapper Spec.  The majority of the code above was copied from DataMapper’s _related() method, which is used to handle normal related lookups (it would be called with $u->access->get()).

    Basically, I’m simply adding a standard join to the normal query flow, along with a where statement to make the join useful.

    UPDATE: Hopefully I didn’t sound rude there.  I am actually curious if I messed something up.  I haven’t had any real problems with this yet, but if you do, I want to know.  I think stensi was looking to add this into the core DataMapper model.

  • #195 / Oct 22, 2008 2:41am

    OverZealous

    1030 posts

    A tip for those creating self-referencing tables:
    You have to be a little creative when dealing with multiple-relationship, self-referencing tables.  What I mean by that is:

    Say you have a generic Employee model.  Employee can have many Tasks.  You also can have a Manager model.  The Manager model has both many Tasks (inherited from Employee, or defined explicitly) and many Employees.  Employees have one Manager.

    The issue comes in with the Task <> Employee and Task <> Manager relationship.  By default, DataMapper will look at the correct table, since both Manager and Employee share the ‘employees’ table.  For both models, the relationship table is correctly identified as ‘employees_tasks’.  BUT, the id fields use the model‘s name.  So, for Employees, the expected id field is ‘employee_id’, and for managers, it’s ‘manager_id’.

    The solution is simple.  Create your join table like this (note, Postgre, so alter accordingly):

    CREATE TABLE employees_tasks (
        id serial NOT NULL,
        employee_id integer,
        manager_id integer,
        task_id integer NOT NULL
    );
    

    You just have to add in a relationship column for both types.  The default for the unused column should be null, so you end up only storing the correct data.

    The biggest drawback is a possible error if you accidentally access a Manager through the Employee model, or vice-versa, and try to save a task.  Then the task will be saved incorrectly.  So be careful.  When Stensi gets back, maybe he can look into this, and see if there is a smarter way to handle this situation.

    Happy Code Igniting.

.(JavaScript must be enabled to view this email address)

ExpressionEngine News!

#eecms, #events, #releases