EllisLab text mark
Advanced Search
1 of 70
1
   
DataMapper 1.6.0
Posted: 05 September 2008 12:32 PM
Avatar
Joined: 2008-06-01
112 posts

UPDATE

10 December 2008 - Version 1.6.0 Released (Current version)
6 December 2008 - Version 1.5.4 Released
5 December 2008 - Version 1.5.3 Released
4 December 2008 - Version 1.5.2 Released
4 December 2008 - Version 1.5.1 Released
3 December 2008 - Version 1.5.0 Released
15 October 2008 - Version 1.4.5 Released
13 October 2008 - Version 1.4.4 Released
13 October 2008 - Version 1.4.3 Released
12 October 2008 - Version 1.4.2 Released
11 October 2008 - Version 1.4.1 Released
11 October 2008 - Version 1.4 Released
8 October 2008 - Version 1.3.4 Released
3 October 2008 - Version 1.3.3 Released
2 October 2008 - Version 1.3.2 Released
2 October 2008 - Version 1.3.1 Released
29 September 2008 - Version 1.3.0 Released
24 September 2008 - Version 1.2.1 Released
17 September 2008 - Version 1.2 Released
7 September 2008 - Version 1.0 Released

View the Change Log to see what’s changed.

____________________________________

My DataMapper is finished!

Ever since Michael Wales posted a sneak peak of the DataMapper ORM class he was working on, I was inspired to start making my own!

The concept of having your Database tables as objects, and being able to manipulate them and their relationships so easily was too appealing for me to wait. So, sorry if I’m stealing some of your thunder Michael red face


DataMapper

DataMapper is an Object Relational Mapper written in PHP for CodeIgniter. It is designed to map your Database tables into easy to work with objects, fully aware of the relationships between each other.

Features
  * Everything is an object!
  * Easy to setup, easy to use.
  * Custom Validation on object properties.
  * Lazy Loading (related objects are only loaded upon access).
  * Relations and their integrity are automatically managed for you.
  * One to One, One to Many, and Many to Many relations fully supported.

I went to a lot of trouble designing this and have even gone so far as to fully document it with a dedicated User Guide!

DataMapper Source Files
DataMapper User Guide

Enjoy grin

 Signature 

[ DataMapper | DataMapper Source Files | DataMapper User Guide ] - [ History | History Source Files | History User Guide ]

 
Posted: 05 September 2008 01:14 PM   [ # 1 ]   [ Rating: 0 ]
Avatar
Joined: 2007-06-10
2937 posts

Wow man, you’ve done an awesome job there, documentation and all.

Excellent work. wink

 Signature 

URI Language Identifier | Modular Extensions - HMVC | View Object | Widget plugin | Access Control library

 
Posted: 05 September 2008 01:35 PM   [ # 2 ]   [ Rating: 0 ]
Avatar
Joined: 2008-02-24
614 posts

Well done!

Great library, and well documented. Keep up the good work wink

 Signature 

THE CODEIGNITER HANDBOOK - A new CI book for everybody!
—-
Freelance Web Developer - @jamierumbelow - http://jamieonsoftware.com

 
Posted: 06 September 2008 09:53 PM   [ # 3 ]   [ Rating: 0 ]
Avatar
Joined: 2008-06-01
112 posts

Thanks guys grin

In case people were wanting to see some examples in the thread instead, here’s a simple one.

First, we’ll setup a few DataMapper models:

User model

<?php

class User extends DataMapper {

    
var $has_many = array('book' => 'books');
    var 
$has_one = array('country' => 'countries');

    var 
$validation = array(
        array(
            
'field' => 'username',
            
'label' => 'Username',
            
'rules' => array('required''trim''unique''alpha_dash''min_length' => 3'max_length' => 20),
        ),
        array(
            
'field' => 'password',
            
'label' => 'Password',
            
'rules' => array('required''trim''unique''min_length' => 6'encrypt'),
        ),
        array(
            
'field' => 'confirm_password',
            
'label' => 'Confirm Password',
            
'rules' => array('encrypt''matches' => 'password'),
        ),
        array(
            
'field' => 'email',
            
'label' => 'Email Address',
            
'rules' => array('required''trim''valid_email')
        )
    );

    function 
User()
    
{
        parent
::DataMapper();
    
}

    
function login()
    
{
        
// Create a temporary user object
        
$u = new User();

        
// Get this users stored record via their username
        
$u->where('username'$this->username)->get();

        
// Give this user their stored salt
        
$this->salt $u->salt;

        
// Validate and get this user by their property values,
        // this will see the 'encrypt' validation run, encrypting the password with the salt
        
$this->validate()->get();

        
// If the username and encrypted password matched a record in the database,
        // this user object would be fully populated, complete with their ID.

        // If there was no matching record, this user would be completely cleared so their id would be empty.
        
if (empty($this->id))
        
{
            
// Login failed, so set a custom error message
            
$this->error_message('login''Username or password invalid');

            return 
FALSE;
        
}
        
else
        
{
            
// Login succeeded
            
return TRUE;
        
}
    }

    
// Validation prepping function to encrypt passwords
    // If you look at the $validation array, you will see the password field will use this function
    
function _encrypt($field)
    
{
        
// Don't encrypt an empty string
        
if (!empty($this->{$field}))
        
{
            
// Generate a random salt if empty
            
if (empty($this->salt))
            
{
                $this
->salt md5(uniqid(rand(), true));
            
}

            $this
->{$field} sha1($this->salt $this->{$field});
        
}
    }
}

/* End of file user.php */
/* Location: ./application/models/user.php */ 


Country model

<?php

class Country extends DataMapper {

    
var $table "countries";

    var 
$has_many = array("user" => "users");

    var 
$validation = array(
        array(
            
'field' => 'name',
            
'label' => 'Country',
            
'rules' => array('required''trim''unique''alpha_dash''min_length' => 1'max_length' => 50),
        )

    function 
Country()
    
{
        parent
::DataMapper();
    
}
}

/* End of file country.php */
/* Location: ./application/models/country.php */ 


Book model

<?php

class Book extends DataMapper {

    
var $has_many = array("user" => "users");

    var 
$validation = array(
        array(
            
'field' => 'title',
            
'label' => 'Title',
            
'rules' => array('required''trim''unique''alpha_dash''min_length' => 1'max_length' => 50),
        ),
        array(
            
'field' => 'description',
            
'label' => 'Description',
            
'rules' => array('required''trim''alpha_slash_dot''min_length' => 10'max_length' => 200),
        ),
        array(
            
'field' => 'year',
            
'label' => 'Year',
            
'rules' => array('required''trim''numeric''exact_length' => 4),
        )
    );

    function 
Book()
    
{
        parent
::DataMapper();
    
}
}

/* End of file book.php */
/* Location: ./application/models/book.php */ 

UPDATE

Updated to reflect version 1.3.

 Signature 

[ DataMapper | DataMapper Source Files | DataMapper User Guide ] - [ History | History Source Files | History User Guide ]

 
Posted: 06 September 2008 09:53 PM   [ # 4 ]   [ Rating: 0 ]
Avatar
Joined: 2008-06-01
112 posts

And now the good part where you get to use your tables as objects.

Example controller

<?php

class Users extends Controller {

    
function Users()
    
{
        parent
::Controller();
    
}

    
function index()
    
{
        
// Let's create a user
        
$u = new User();
        
$u->username 'Fred Smith';
        
$u->password 'apples';
        
$u->email 'fred@smith.com';

        
// And save them to the database (validation rules will run)
        
if ($u->save())
        
{
            
// User object now has an ID
            
echo 'ID: ' $u->id '<br />';
            echo 
'Username: ' $u->username '<br />';
            echo 
'Email: ' $u->email '<br />';

            
// Not that we'd normally show the password, but when we do, you'll see it has been automatically encrypted
            // since the User model is setup with an encrypt rule in the $validation array for the password field
            
echo 'Password: ' $u->password '<br />';
        
}
        
else
        
{
            
// If validation fails, we can show the error for each property
            
echo $u->error->username;
            echo 
$u->error->password;
            echo 
$u->error->email;

            
// or we can loop through the error's all list
            
foreach ($u->error->all as $error)
            
{
                
echo $error;
            
}

            
// or we can just show all errors in one string!
            
echo $u->error->string;

            
// Each individual error is automatically wrapped with an error_prefix and error_suffix, which you can change (default: <p>error message</p>)
        
}

        
// Let's now get the first 5 books from our database
        
$b = new Book();
        
$b->limit(5)->get();

        
// Let's look at the first book
        
echo 'ID: ' $b->id '<br />';
        echo 
'Name: ' $b->title '<br />';
        echo 
'Description: ' $b->description '<br />';
        echo 
'Year: ' $b->year '<br />';

        
// Now let's look through all of them
        
foreach ($b->all as $book)
        
{
            
echo 'ID: ' $book->id '<br />';
            echo 
'Name: ' $book->title '<br />';
            echo 
'Description: ' $book->description '<br />';
            echo 
'Year: ' $book->year '<br />';
            echo 
'<br />';
        
}

        
// Let's relate the user to these books
        
$u->save($b->all);

        
// Yes, it's as simple as that! You can add relations in several ways, even different types of relations at the same time

        // Get the Country with an ID of 10
        
$c = new Country();
        
$c->where('id'10)->get();

        
// Get all Books from the year 2000
        
$b = new Book();
        
$b->where('year'2000)->get();

        
// Relate the user to them
        
$u->save(array($c$b->all));

        
// Now let's access those relations from the user

        // First we'll get all related books
        
$u->book->get();

        
// You can just show the first related book
        
echo 'ID: ' $u->book->id '<br />';
        echo 
'Name: ' $u->book->title '<br />';
        echo 
'Description: ' $u->book->description '<br />';
        echo 
'Year: ' $u->book->year '<br />';

        
// Or if you're expecting more than one, which we are, loop through all the books!
        
foreach ($u->book->all as $book)
        
{
            
echo 'ID: ' $book->id '<br />';
            echo 
'Name: ' $book->title '<br />';
            echo 
'Description: ' $book->description '<br />';
            echo 
'Year: ' $book->year '<br />';
            echo 
'<br />';

            
// And there's no need to stop there,
            // we can see what other users are related to each book! (and you can chain the get() call if you don't want to do it on its own, before the loop)
            
foreach ($book->user->get()->all as $user)
            
{
                
// Show user if it's not the original user as we want to show him the other users
                
if ($user->id != $u->id)
                
{
                    
echo 'User ' $user->username ' also likes this book<br >';
                
}
            }
        }

        
// We know there was only one country so we'll access the first record rather than loop through $u->country->all

        // Get related country
        
$u->country->get();

        echo 
'User is from Country: ' $u->country->name '<br />';

        
// One of the great things about related records is that they're only loaded when you access them!

        // Lets say the user no longer likes the first book from his year 2000 list, removing that relation is as easy as adding one!

        // This will remove the users relation to the first record in the $b object (supplying $b->all would remove relations to all books in the books current all list)
        
$u->delete($b);

        
// You can delete multiple relations of different types in the same way you can save them

        // Now that we're done with the user, let's delete him
        
$u->delete();

        
// When you delete the user, you delete all his relations with other objects. DataMapper does all the tidying up for you :)
    
}

    
// Continued below... 

UPDATE

Updated to reflect version 1.3.

 Signature 

[ DataMapper | DataMapper Source Files | DataMapper User Guide ] - [ History | History Source Files | History User Guide ]

 
Posted: 06 September 2008 09:53 PM   [ # 5 ]   [ Rating: 0 ]
Avatar
Joined: 2008-06-01
112 posts
// Continued from above...

     
function register()
    
{
        
// Create user object
        
$u = new User();

        
// Put user supplied data into user object
        // (no need to validate the post variables in the controller,
        // if you've set your DataMapper models up with validation rules)
        
$u->username $this->input->post('username');
        
$u->password $this->input->post('password');
        
$u->confirm_password $this->input->post('confirm_password');
        
$u->email $this->input->post('email');

        
// Attempt to save the user into the database
        
if ($u->save())
        
{
            
echo '<p>You have successfully registered</p>';
        
}
        
else
        
{
            
// Show all error messages
            
echo '<p>' $u->error->string '</p>';
        
}
    }

    
function login()
    
{
        
// Create user object
        
$u = new User();

        
// Put user supplied data into user object
        // (no need to validate the post variables in the controller,
        // if you've set your DataMapper models up with validation rules)
        
$u->username $this->input->post('username');
        
$u->password $this->input->post('password');

        
// Attempt to log user in with the data they supplied, using the login function setup in the User model
        // You might want to have a quick look at that login function up the top of this page to see how it authenticates the user
        
if ($u->login())
        
{
            
echo '<p>Welcome ' $u->username '!</p>';
            echo 
'<p>You have successfully logged in so now we know that your email is ' $u->email '.</p>';
        
}
        
else
        
{
            
// Show the custom login error message
            
echo '<p>' $u->error->login '</p>';
        
}
    }
}

/* End of file login.php */
/* Location: ./application/controllers/login.php */ 

In case people are wondering when the DataMapper models are actually loaded, I’ll explain.

To set it up is simply a matter of adding the main DataMapper model file to your model autoload array:

$autoload['model'= array('datamapper'); 

That’s it.  All your models that extend DataMapper are automatically loaded by it when you create or access them, which is why there’s no need to fill up your Controller’s constructor with bunches of $this->load->model(‘Object’); lines.  DataMapper is smart enough to load them for you, as you ask for them.

UPDATE

Updated to reflect version 1.3.

 Signature 

[ DataMapper | DataMapper Source Files | DataMapper User Guide ] - [ History | History Source Files | History User Guide ]

 
Posted: 07 September 2008 05:14 AM   [ # 6 ]   [ Rating: 0 ]
Avatar
Joined: 2006-07-27
2617 posts

Wonderful stuff.

 Signature 

Check out the Template Library
Oh yeah, I tweet, too (regarding CodeIgniter on occassion).

 
Posted: 09 September 2008 07:13 AM   [ # 7 ]   [ Rating: 0 ]
Joined: 2006-04-03
4 posts

Great work stensi,

I am going to have a play with this now

The only thing that worries me is domain object knowing about the data mapper, in this case through inheritance. The domain object should not strictly know the data mapper and act as a layer that separates the in-memory objects from the database, which allows the transfer data between the two, while isolating them from each other.

For example,

http://www.martinfowler.com/eaaCatalog/dataMapper.html

I cant help but feel that the code examples above still feel like active record, as the data access logic is in the domain object and the object is open to carry both data and behaviour.

http://www.martinfowler.com/eaaCatalog/activeRecord.html

This is probably the just side effect of using inheritance between the domain object and data mapper, and not composition in order to keep things simple.

But anyhows, a mix between active record and data mapper is interesting.

 
Posted: 09 September 2008 08:15 PM   [ # 8 ]   [ Rating: 0 ]
Avatar
Joined: 2008-06-01
112 posts

Thanks andyr.  I hear what you’re saying about the separation and yes, the Domain Object knowing about the DataMapper is simply a side effect of the inheritance.  That’s where the straying from the pattern ends.

Strictly speaking I should have separated things to have a DomainObject class and the DataMapper class but for simplicity’s sake, and ease of use and implementation, I felt doing it the way I did resulted in the same functionality the DataMapper pattern provides, but with much less code than would otherwise have been required.

So yes, it still sticks to the DataMapper pattern, just a little loosely in that regard.

Hope you find it useful smile

 Signature 

[ DataMapper | DataMapper Source Files | DataMapper User Guide ] - [ History | History Source Files | History User Guide ]

 
Posted: 11 September 2008 10:39 AM   [ # 9 ]   [ Rating: 0 ]
Avatar
Joined: 2008-02-17
278 posts

From your user guide I understand find() basically accepts “where” parameter, what we would use as $this->db->where(). Is there a way to pass “limit” or “sort” parameters?

 
Posted: 11 September 2008 08:13 PM   [ # 10 ]   [ Rating: 0 ]
Avatar
Joined: 2008-06-01
112 posts

I’m actually working on adding in extra functionality like that now. I’m still deciding on the best way to implement it in terms of ease of use and consistency but once I’m done, you’ll be able to do all the various types of queries such as like, group by, order by, limit and offset.

Once I’ve completed and tested that ok, I’ll post the new version, which already has many improvements on the validation side of things.  I’m finding that part very useful for my sites right now.  Cuts down the code in my controllers a huge amount.

Anyway, any suggestions/feature requests are welcome!

 Signature 

[ DataMapper | DataMapper Source Files | DataMapper User Guide ] - [ History | History Source Files | History User Guide ]

 
Posted: 11 September 2008 08:21 PM   [ # 11 ]   [ Rating: 0 ]
Avatar
Joined: 2008-02-17
278 posts

Thanks, I’m using your DataMapper for a simple project (band->album->song type of thing). I like how it saves time and cuts down the code, but right now it’s 50/50 between DataMapper and regular code, although I didn’t switch to DataMapper validation yet.

Introducing features you mentioned would be a great improvement. Keep us posted.

 
Posted: 15 September 2008 02:39 AM   [ # 12 ]   [ Rating: 0 ]
Avatar
Joined: 2008-06-01
112 posts

At the moment, I’ve changed DataMapper’s find() method to accept another two optional parameters, those being limit and offset. This is useful for paging your results.  Example usages:

// Get all books from year 2000
$book = new Book(array('year' => 2000));

// Get all books from year 2000, limiting to 20 results
$book = new Book(array('year' => 2000), 20);

// Get all books from year 2000, limiting to 20 results,
// with an offset of 40 to view what would be the 3rd page of results
$book = new Book(array('year' => 2000), 2040); 

Alternatively, I’m setting it up so you can do it this way:

$book = new Book();
$b->year 2000;
$b->limit 20;
$b->offset 40;
$b->find(); 

Which gives the same result.  You’ll be able to setup a default limit in your DataMapper models, and offset will always default to 0 unless you specify it.  After each find() call it will reset to 0 again.

Note:This newer version hasn’t been uploaded yet as I’m hoping to finish off a few other additions first, including some cleaner handling of validation errors.

I’m having some problems coming up with a good naming convention for putting in the rest of the CodeIgniter DB functionality, such as ordering, grouping and like queries, into DataMapper.  Obviously it’s a little difficult to cram it all into the find() method and still keep it useable.  Here’s some samples of what I was thinking of doing:

// This will produce results ordered by the book name in ascending order, grouped by year.
$book = new Book();
$book->order = array('name''asc');
$book->group = array('year');
$book->find(array('year >' => 1990), 20); 

I’m not too happy with having it so find() has a whole mess of optional parameters, such as:

$object->find($condition$limit$offset$order$group); 

But if that’s what you’d prefer instead of, or in addition to the example above it, I can do both or either.

It’s likely that I’ll end up having to add some other methods such as search() or find_like() for like queries but I’m hoping to get suggestions on the naming and usability.

Then there’s select_max, select_min, select_avg etc that I was looking to add in.  I’ve pretty much decided on those though.  They’ll look something like:

// Get the history genre
$genre = new Genre(array('name' => 'history'));

// Get the oldest year of this genres books
$oldest_year $genre->book->min('year');

// Get the newest year of this genres books
$newest_year $genre->book->max('year');

// Get the average year of this genres books
$average_year $genre->book->avg('year');

// Get the total cost of buying all books from this genre
$total_cost $genre->book->sum('price'); 

So yes, any suggestions on the naming/usage for ordering, grouping, and like queries and anything else are very welcome! grin

UPDATE

The above is no longer valid as of version 1.2.

 Signature 

[ DataMapper | DataMapper Source Files | DataMapper User Guide ] - [ History | History Source Files | History User Guide ]

 
Posted: 16 September 2008 02:18 AM   [ # 13 ]   [ Rating: 0 ]
Joined: 2007-07-22
40 posts

Hey, stensi this is great library, may i suggest this:

$object->find((array $condition), (array $limit $offset), (array $order $group));

all condition parameters wrapped into array, also the limit and offset as well as the order and group.

thanks, keep this up. smile

 Signature 

FROST

 
Posted: 16 September 2008 03:10 AM   [ # 14 ]   [ Rating: 0 ]
Avatar
Joined: 2008-06-01
112 posts

Thanks for the suggestion zeratool.  I’ll give that approach a try although I’m currently messing around with doing it the following way:

$u = new User();

// Get the first 10 moderators via method chaining
$u->where('role''moderator')->order_by('username''asc')->limit(10)->get();

// or do the same but without method chaining
$u->where('role''moderator');
$u->order_by('username''asc')
$u->limit(10)
$u->get();

// Show results
foreach($u->all as $user)
{
    
echo $user->username '<br />';

    
// Just as an example, remove role from foo
    
if ($user->username == 'foo')
    
{
        $user
->role 'none';
        
$user->save();
    
}

It’s a bit more like Active Record this way, since it uses its methods, so people already using Active Record will find the above much more familiar.  It uses almost all of the existing methods available in Active Record except those few that are rendered obsolete by DataMapper (such as from() since DataMapper already knows the table name).

If I go with the above, it will mean the find() method is gone and replaced with get() or get_where() for the actual method that kicks off the data retrieval.

Anyway, this sort of approach, to me at least, looks to have more flexibility. All the rest of DataMapper remains the same since its handling of relationships, saving objects, validation and so on are the main parts that make it a DataMapper.

Would you prefer I continue with this approach?

UPDATE:
Actually, I think I will do it this way.  I’ve finished off implementing it and it’s actually made some of the other functions tidier and work a little better, such as validate and save.

I’ll do some more testing to make sure it’s good to release and then I’ll update the documentation to reflect the new version.

 Signature 

[ DataMapper | DataMapper Source Files | DataMapper User Guide ] - [ History | History Source Files | History User Guide ]

 
Posted: 16 September 2008 09:19 PM   [ # 15 ]   [ Rating: 0 ]
Avatar
Joined: 2008-06-01
112 posts

Version 1.2 has been released!

View the Change Log to see what’s changed.

In short, the find method has been removed and replaced with the get method, using functionality similar to retrieving data with CodeIgniter’s Active Record class.  My post above shows one of the many ways in which you can now populate your objects (including method chaining).  Validation has been improved too, most specifically the error messages.  Automated timestamping is also included.

Enjoy grin

 Signature 

[ DataMapper | DataMapper Source Files | DataMapper User Guide ] - [ History | History Source Files | History User Guide ]

 
1 of 70
1