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.

[In the Works] Datamapper ORM Class

August 27, 2008 9:50am

Subscribe [12]
  • #1 / Aug 27, 2008 9:50am

    Michael Wales's avatar

    Michael Wales

    2070 posts

    Many of you have noticed that I haven’t been posting much - needless to say I have been insanely busy. Only one more month in the desert then it is back home with the family.

    In the small amount of free time I still have I’ve been working on quite a few projects. My favorite of which is my Datamapper ORM Class although I’m pretty sure it doesn’t meet the Datamapper pattern completely.

    This is, as of now, not yet ready for release - but here’s a taste of what is to come.

    Here’s a model, let’s say - for a user:

    model/user.php

    <?php
    class User extends Datamapper {
    
        var $username;
        var $email;
        var $password;
        var $salt;
        
        var $validation = array(
            'username' => array('required', 'unique', 'min' => 3, 'max' => 20, 'trim'),
            'email' => array('required', 'unique', 'valid_email', 'trim'),
            'password' => array('required', 'min' => 3, 'max' => 20, 'constraint' => 120));
            
        function save_salt() {
            $this->load->helper('string');
            if (!isset($this->salt)) {
                $this->salt = random_string('alnum', 10);
            }
        }
        
        function save_password() {
            $this->load->helper('security');
            if (!isset($this->salt)) {
                $this->save_salt();
            }
            $this->password = dohash($this->salt . $this->password);
        }
    }

    One of the first things you’ll notice is the validation array. Yes, Validation is being moved 100% completely into the model. All of the CodeIgniter standard validation rules are in-place, in addition to a few new ones (unique can be seen above, which is self explanatory).

    You also should note the constructor as it’s passing an array to it’s parent class. Datamapper models are designed to really be used as objects, not just a class to make your organization a bit better. More on this in the Controller area below.

    Finally, you’ll see the method save_salt(). As of now only data modification upon save is implemented not sure if there is really a need for it anywhere else. Some might suggest upon “getting” the data but I dgress - isn’t the View in charge of making data pretty?

    A few more bits you might not pick up from reviewing the code within the model:

    - There is a class variable $salt but there are no validation rules to match! Datamapper understands this as meaning “this is a user property but it can’t be changed via a form - only in the code.” This allows us to use a handy little function later to just pull in all POST data to the model and believe it will be okay (no worrying about whether a malicious user has added an admin ENUM to your form hoping you just accept everything).

    - All models have an id, created_on, updated_on field that is automatically updated and managed by the Datamapper model.

    - Currently enabled in the model (because it is not explicitly disabled) is database analysis. The Datamapper automatically reviews your model, determines what the table structure should be, then checks to see if the table is created and matches the correct structure. In plain English: you write your model, Datamapper automatically creates the table for you. You need to add a new parameter to your model? Just write it in. No more hopping back and forth between code and phpMyAdmin. There’s still a lot of work to do on this front, primarily more intelligent column type identification and better parameters within the model. I’m trying to think of a graceful way to accomplish this - right now it’s just part of the validation array (see the constraint key within password, that tells Datamapper to make the field limit 120 characters).

    Controllers

    <?php
    class Users extends Controller {
    
        function Users() {
            parent::Controller();
            $this->output->enable_profiler(TRUE);
        }
        
        function index() {
            $this->datamapper->model('user');
            // Get the user whose username == walesmd and echo his email
            $u = new User(array('username'=>'walesmd'));
            echo $u->email;
            
            // Get all users who are admins
            $u = new User(array('type'=>'admin'));
            // We fully expect more than one user, so we foreach the all class variable
            foreach ($u->all as $users) {
                echo $u->username;
            }
    
            // Let's create a new user
            $u = new User();
            $u->username = 'dallard';
            $u->password = 'robots';
            $u->save();
    
            // Let's create another new user - this time after a form has been submitted
            // Form Input names are: signup[username], signup[password], and signup[email]
            $u = new User();
            $u->fromForm('signup');
            $u->save();
    
            // Let's update user #1's email address
            $u = new User(array('id'=>1));
            $u->email = 'null@codeigniter.com';
            $u->save();
        }
    }

    Lots of stuff here but it should be pretty easy to understand. When creating a new Datamapper object you can pass an array - this will be used within a CodeIgniter ActiveRecord get_where() query that will return all objects that match that query.

    If you are expecting more than one result to come back, go ahead and foreach through the Models all variable. Note: This will still work if you only return one object, Datamapper is designed around your expectations. If you expect more than one, loop it. If you expect only one - don’t.

    We created a new user, which was pretty simple.

  • #2 / Aug 27, 2008 9:56am

    Michael Wales's avatar

    Michael Wales

    2070 posts

    We also created a new user from our signup form. That form looks like:

    <form method="post" action="signup" name="signup" id="signup"><!-- Note: This doesn't matter -->
    Username:
    
    <input type="text" name="signup[username]" id="signup_username" maxlength="20" /></p>
    Password:
    
    <input type="password" name="signup[password]" id="signup_password" maxlength="20" /></p>
    Email Address:
    
    <input type="email" name="signup[email]" id="signup_email" maxlength="120" /></p>
    <input type="submit" name="signup[submit]" value="Signup" /></p>
    </form>

    The magic happens because of how we named all of our input fields (not id - just the name parameter). We can now use the fromForm() method to easily pull all these values down into our model, validate them against our rules, and save them to the database. Remember how we didn’t add any validation rules for fields like salt? That means no matter if a user changes your form to add in a salt field - it’s not going anywhere near your database. All, without any real work on your behalf.

    Finally we updated a user’s information. You’ll be glad to know that Datamapper doesn’t just issue a wide-open UPDATE statement to the database - that would be to simple. Datamapper is optimized to the gills and will only issue updates for fields that actually change.

  • #3 / Aug 27, 2008 10:26am

    zdknudsen's avatar

    zdknudsen

    305 posts

    This is awesome Michael! With enough work I think the database analysis feature will be very powerful indeed. But is it possible to disable it? I wouldn’t like unnecessary queries in a production environment.

  • #4 / Aug 27, 2008 10:31am

    Michael Wales's avatar

    Michael Wales

    2070 posts

    @Zacharies
    Yes - that is the definite intent of it. Enabled by default, for development but easily disabled for production environments.

  • #5 / Aug 27, 2008 10:36am

    wiredesignz's avatar

    wiredesignz

    2882 posts

    I like this, good work man, will be eager to get into it.

  • #6 / Aug 27, 2008 12:00pm

    Sam Dark's avatar

    Sam Dark

    242 posts

    Good one. Like it better than our Automodels.

  • #7 / Aug 27, 2008 12:31pm

    m4rw3r's avatar

    m4rw3r

    647 posts

    Makes me almost a bit jealous 😊

  • #8 / Aug 27, 2008 12:34pm

    Michael Wales's avatar

    Michael Wales

    2070 posts

    Heh - don’t m4r. To be completely honest, I’ve wasted the past 2-3 weeks writing my own framework only to scrap it because it wasn’t exactly how I wanted it (or I was just rewriting CI).

    This class will be as good as I can make my “optimal development environment” within PHP but I was on the verge of writing a language parser in PHP and designing my own language - so frustrating.

    There’s still a lot of work to be done on this but it’s definitely getting to the point that it is amazingly useful.

  • #9 / Aug 28, 2008 5:10am

    erik.brannstrom

    125 posts

    Regarding the use of more advanced queries. Will you implement methods that can, for example, compare values in one table against another, or will this require a seperate Model?

    Anyhow, this looks really cool! I’m amazed at the simple yet clever approach to database handling. Looking forward to trying it out!

  • #10 / Aug 28, 2008 12:52pm

    Michael Wales's avatar

    Michael Wales

    2070 posts

    Comparing values - what exactly do you mean?

    There will be relational features implement - your typical has_many and belong_to with an optional through table.

  • #11 / Aug 28, 2008 3:32pm

    erik.brannstrom

    125 posts

    Sorry, I was in a hurry 😊 I guess my question is if a class extending Datamapper can fetch other values than those in the specific table (in this case containing users), perhaps in a similair way to the save_salt and save_password function? So if I had a, say, load_movies function that would grab a users favorite movies from another table, that value would be passed along to the User object.

    Maybe I’m out on a limb here. I know what ORM stands for in a literal way, however all deeper meaning is beyond me 😊

  • #12 / Aug 28, 2008 4:03pm

    Greg Salt's avatar

    Greg Salt

    3988 posts

    Wow Mr Wales, I can’t wait to try this!

    Good work.

  • #13 / Aug 29, 2008 12:13am

    steelaz's avatar

    steelaz

    252 posts

    This looks like a real time saver. Any ETA on beta (alpha?)?

  • #14 / Aug 29, 2008 7:27am

    Michael Wales's avatar

    Michael Wales

    2070 posts

    Sorry, I was in a hurry I guess my question is if a class extending Datamapper can fetch other values than those in the specific table (in this case containing users), perhaps in a similair way to the save_salt and save_password function? So if I had a, say, load_movies function that would grab a users favorite movies from another table, that value would be passed along to the User object.

    Of course, you would be able to write this yourself but more than likely you would want to accomplish it in this manner:

    MySQL Tables:

    users
    -----
    id
    username
    email
    password
    created_on
    updated_on
    
    movies
    ------
    id
    title
    year_released
    created_on
    updated_on
    
    movies_users
    ------------
    movie_id
    user_id

    Users Model:

    class User extends Datamapper {
      var $username;
      var $email;
      var $password;
      var $salt;
    
      var $validation = array(
        'username' => array('required', 'unique', 'min' => 3, 'max' => 20, 'trim'),
        'email' => array('required', 'unique', 'valid_email'),
        'password' => array('required', 'min' => 3, 'max' => 20, 'constraint' => 120));
    
      var $has_many = array('movies', 'through' => 'movies_users');
      
      function User($search) {
        parent::Datamapper($search);
      }
    }

    Movies Model:

    class Movie extends Datamapper {
    
      var $title;
      var $year_released;
    
      var $validation = array(
        'title' => array('required', 'unique', 'max' => '120', 'trim'),
        'year_released' => array('required', 'integer', 'min' => 4, 'max' => 4));
    
      var $has_many = array('users', 'through' => 'movies_users');
    
      function Movie($search) {
        parent::Datamapper($search);
      }
    
    }


    Alright, now to the good stuff - our controller. The following is the code for a User’s profile, that lists all of the movies they have favorited:

    function favorites($user_id) {
      $u = new User(array('id'=>$user_id));
      foreach ($u->movies->all as $movie) {
        echo $movie->title . '
    ';
      }
    }

    How about viewing a movie and listing all of the user’s that have favorited that movie?

    function movie($id) {
      $m = new Movie(array('id' => $id));
      foreach ($m->users->all as $user) {
        echo $user->username . '
    ';
      }
    }

    Something a bit more difficult - let’s say a user has selected a movie to make as one of their favorites.

    function add_favorite($user_id, $movie_id) {
      // First we get our user's record
      $u = new User(array('id' => $user_id));
      // Now we add the movie and save the user
      $u->movie = $movie_id;
      $u->save();
    }

    Note: The syntax here is speculative as relations haven’t been finalized within the class itself yet. Syntax may change slightly but this is the general outlook and plan on how it will go down.

    You can always refer to Ruby’s Datamapper class for basic ideas of how this class will work - it will either be as simple or even more simple once it is complete: http://datamapper.org/

  • #15 / Aug 29, 2008 7:37am

    Michael Wales's avatar

    Michael Wales

    2070 posts

    This looks like a real time saver. Any ETA on beta (alpha?)?

    Not yet, I’ve put a good 5 or so hours into the class and I’d estimate it is at 30-40% complete. If I had to guess I’d say another 2 weeks or so? Depends on how much free time I get here.

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

ExpressionEngine News!

#eecms, #events, #releases