TIP: Use Markdown or, <pre> for multi line code blocks / <code> for inline code.
These forums are read-only and for archival purposes only!
Please join our new forums at discourse.kohanaframework.org
Please welcome Aurora
  • Dear Kohana,

    Please welcome Aurora: https://github.com/enov/Aurora

    Aurora is a Kohana 3.2 module for manual ORM mappings. It allows your models to be POPOs and without any database persistence logic inside. All persistence logic are placed in separate Aurora files.

    Also, Aurora encodes and decodes JSON by taking care of getters (get_) and setters (set_) and treats them as standard properties in the resulting JSON. This is done without any magic.

    Moreover, it can exposes a REST-like interface, primarily intented to be used in conjunction with Backbone.js. In order to acheive this, it relies on the krestful module from @samsoir.

    Aurora is still in its alpha. I was hoping to announce it once it reaches beta, but I think I need some rest (no pun intended) by now, and some insights. I am open to your ideas and criticism.

    Yours, Samuel

  • Sounds interesting. Can you prepare some sample app to download and check this in action?

  • @thejw23, thank you for your interest.

    Good idea. Allow some time as I need to catch up with other things.

  • Hello,

    I would like to introduce another alpha for Aurora.

    Major changes are:

    • Drop krestful dependency, rely on Kohana_Controller_REST
    • Remove static from Auroras that needs to be created for Models
    • Special treatment for DateTime while json_encoding/decoding
    • Aurora::factory to instantiate and decorate your Auroras with Aurora_Core API

    For a demo app, please refer to Aurora-Calendar: https://github.com/enov/Aurora-Calendar

    Yours, Samuel

  • Great work!

  • Thank you, @thejw23

  • Dear Kohana,

    I am happy to announce another development release of Aurora!

    Major changes:

    • Auto-loader: now, optionally, you can place Aurora and Collection classes (preferably) under the related Model class definition. This helps you to manage less files when working on a larger project. Aurora auto-loader uses the standard Kohana auto-loader underneath and is enabled by default.

    • Hooks: a set of 10 hooks on the Core API for fine-grained control when loading, saving and deleting models. Just implement related Interface_Aurora_Hook_* in your Auroras.

    • JSON serialization/deserialization: Removed serialization Views. Now, if you want customized serialization, instead of overriding Views just implement related JSON interfaces in your Auroras. For example, if your Model has only vanilla properties just implement Interface_Aurora_JSON_Serialize and return your model, and save the module from inspecting your Model class.

    • Faster Collections: Generally speaking, this is like swimming against the tide, against the PHP language and culture. After too many attempts, I reverted to the original implementation, but with a few tweeks to make it faster.

    • Multiple Row processing per Model: Now you can define your Aurora qview (or QueryView: a database view joining multiple tables) to return multiple rows (with a join with one-to-many relationship) for a single Model. Aurora will test the row's id, get the Model, process the same Model again. You have the chance to retreive your Model's scalar properties, Model properties as well as Collection properties in a single database query!

    • Transactions: Built-in transaction support, that can be optionally disabled per Aurora instance. Save and delete transactions will roll back in case of an exception when dealing hooks in a sophisticated Model/Aurora setup or when one instance of Model in a Collection fails.

    • Profiler Marks: Built-in profiler marks for you to benchmark your application, detailed for each method of the Core API, all under the single "aurora" category!

    I will call this a beta release. Hopefully, I will not refactor the main API until it is released.

    Best Regards, Samuel

  • Thanks! :) When we can expect final/stable release?

  • Hello @thejw23!

    Regarding the release date, I am not sure.

    Still have to write some unit tests and make documentation better with examples. We also need a compatible Auth driver.

    Once it is a candidate for release, upgrade to Kohana 3.3 and if all is well, release both.

    Too much things to do... It shows that to open source something is not an easy task!

    Thank you again for your interest! :)

  • I'm interested, since it's an interesting library :) So more or less, now you will focus on fixes and pushing v1.0 out, keeping API changes for release 1.1?

  • Yeap... that's what I am intending to do :)

  • Hello,

    I am releasing Aurora 0.6-beta with some polish and bug fixing.

    No new features have been added.

    Also, there is a test application that lives in a separate repository: https://github.com/enov/Aurora-QA

    Currently, the tests covers around 50% of the code.

    Happy coding :)

  • Looks very promising - going to take a look. I particularly like ideas of collections. Can you explain more about what you mean with comments under "faster collections" on Jun 7.

  • Hi @yehosef,

    While loading the collections, internally, Aurora uses the to_array method to get hold of the internal array of the collection. This speeds up the loading tremendously on PHP 5.3, while keeping the add and offsetSet implementations of the Collection in its simplest.

    https://github.com/enov/Aurora/blob/master/classes/aurora/aurora/core.php#L278

    Also, again while loading, models are added to the internal array of the collection using the ID as key. The benefit of this is that when we want to get by ID, we can look first at the key before we foreach over the array.

    https://github.com/enov/Aurora/blob/master/classes/aurora/aurora/core.php#L286

    https://github.com/enov/Aurora/blob/master/classes/aurora/collection.php#L57

  • Hello,

    Aurora is now 0.7-beta:

    • No new features have been added.
    • Again, some minor polish, bug fixing, notably a rewrite of Aurora auto-loader
    • Aurora-Calendar demo app is merged into Aurora-QA, so that I maintain one repo for demo and testing and you download once for demo and for testing :)
    • Currently, the tests covers around 75% of the code.

    Thank you.

  • @enov

    Thanks for the explanation and progress. looking great!

  • Great work, I guess you're now really close to 1.0 Stable - excellent :)

  • Thank you both :)

  • Hello,

    I just bumped the version to 0.8-beta.

    • Drop Aurora_Backbone, the Backbone.js code generation class, and its respective views: it seems this should go to a separate module. There is @zeebee's kohana-generator https://github.com/zeebinz/kohana-generator, which is an excellent code generation module. I think it's a good idea to have something based on that, after the stable release.

    • Various bug fixes and clean up. PHPUnit test code coverage 95%.

    It remains little things to be done: revisit customized JSON serialization/deserialization, add some tests for that. If everything good, push a candidate for a release and sync the documentation for all the changes done.

    Regards

  • Thanks and looking forward to 1.0 :)

  • Hello Kohana!

    I am pleased to announce Aurora version 1.0, for Kohana 3.2, codenamed a-better-mousetrap.

    My son and I love Tom and Jerry. I will dedicate this release to Tom the cat, and the genius behind Tom and Jerry shows, in general.

    Someone has stitched images from a show here. I am not affiliated to that website, but it's a nice read:

    A better mousetrap

    A better mousetrap

    A better mousetrap

    I hope you enjoy this release. Please report bugs to the Github repository. You can find the docs when you enable the official user guide.

    This would PROBABLY work in Kohana 3.3, if you enable auto_load_lowercase.

    I enjoy reading criticism and/or thumbs ups, so you are welcome to post your comments.

    Thank you @thejw23 and @yehosef for the early encouragement.

    Best Regards, Samuel

  • Thanks :) great work!

  • This looks fantastic. I love the images and I appreciate the work.

    I'm going to look into using it with Kohana 3.3 and see where I get. As you say in the documentation ("Aurora is NOT an Auto-Modeler. Which means that probably it's not an ORM, after all..."), I don't believe it to be ORM either due to the fact that the instances don't have any persistence control or w/e.

    I've been using my own data mapper / entity combination, and I'm hoping to try and put this into my project now, with a layer on top of it to handle caching.

  • Hello Jack Ellis,

    Thank you for your interest in the library.

    Please use the master branch for 3.3. Note that it's beta, as I am still upgrading. Things in the routing of Kohana have changed. I was wrong when I thought that the code for 3.2 will just work on 3.3.

    Well, I chose to say "manual ORM", and I thought it will increase the visibility of the library - lol. I will clear confusion, if any, once I am done with 3.3.

    Above all, kindly share your experience. If you come up with a good caching layer, you might want to share that too :)

  • I've been playing around with it this evening and the documentation is a tad broken at points (I believe).

    Also, I'm trying to understand why the config setting 'fetch_table_names' is set as true by default? When it's enabled, I get the primary key as: ['user.id'] rather than ['id'].

    Edit: I was wrong about one point I put in. Removed.

  • Hi Jack Ellis,

    why the config setting 'fetch_table_names' is set as true by default

    Well that's one of the strength of Aurora, it's its ability to load deeply nested models with a single database call. Take for example this Aurora:

    class Aurora_Article implements Interface_Aurora_Database
    {
    
        public function qview() {
            return DB::select()->from('articles')
                ->join('categories')->on('cat_id', '=', 'categories.id')
                ->join('brands')->on('brand_id', '=', 'brands.id')
                ->join('material_knits', 'left')->on('knit_id', '=', 'material_knits.id')
                ->join('material_fabrics', 'left')->on('fabric_id', '=', 'material_fabrics.id');
        }
    
        public function db_persist($model) {
            /* @var $model Model_Article */
            return array(
                'id' => $model->get_id(),
                'label' => $model->get_label(),
                'name' => $model->get_name(),
                'description' => $model->get_description(),
                'designation' => $model->get_designation(),
                'cat_id' => $model->get_category()->get_id(),
                'brand_id' => $model->get_brand()->get_id(),
                'fabric_id' => $model->get_fabric()->get_id(),
                'knit_id' => $model->get_knit()->get_id(),
                'weight' => $model->get_weight(),
            );
        }
    
        public function db_retrieve($model, array $row) {
            /* @var $model Model_Article */
            $tbl = Au::db()->table($this);
            $model->set_id($row[$tbl . '.id']);
            $model->set_name($row[$tbl . '.name']);
            $model->set_label($row[$tbl . '.label']);
            $model->set_description($row[$tbl . '.description']);
            $model->set_designation($row[$tbl . '.designation']);
            $model->set_weight($row[$tbl . '.weight']);
            // Delegate retrieving the row to Aurora_Brand
            $model->set_brand(
              Au::factory('Brand')
                ->db_retrieve(Model::factory('Brand'), $row)
            );
            // Delegate retrieving the row to Aurora_Category
            $model->set_category(
              Au::factory('Category')
                ->db_retrieve(Model::factory('Category'), $row)
            );
            // Delegate retrieving the row to Aurora_Material_Fabric
            $model->set_fabric(
              $row[$tbl . '.fabric_id'] ?
                Au::factory('Material_Fabric')
                  ->db_retrieve(Model::factory('Material_Fabric'), $row) :
                NULL
            );
            // Delegate retrieving the row to Aurora_Material_Knit
            $model->set_knit(
              $row[$tbl . '.knit_id'] ?
                Au::factory('Material_Knit')
                  ->db_retrieve(Model::factory('Material_Knit'), $row) :
                NULL
            );
        }
    
    }
    

    In this example, Aurora_Article::qview() is setup to read from multiple joined tables. If fetch_table_names was not set to TRUE, there would have been a collision with column names of the different tables. But now, we are able to delegate the retrieving of those models to their respective Auroras, like Aurora_Brand below:

    class Aurora_Brand implements Interface_Aurora_Database
    {
    
        public function db_persist($model) {
            return array(
                'id' => $model->get_id(),
                'label' => $model->get_label(),
            );
        }
        public function db_retrieve($model, array $row) {
            $tbl_dot = Au::db()->table($this) . '.';
            $model->set_id($row[$tbl_dot . 'id']);
            $model->set_label($row[$tbl_dot . 'label']);
            return $model;
        }
    
    }
    
    

    However, you are probably right. Setting fetch_table_names to TRUE by default might be confusing.

    In all cases, documentation has still some milestones to go. I appreciate advises and would be grateful of some help. Thanks.

  • Awesome reply, thanks. My only issue with the fetch_table_names was that it was searching my database table for users.id (as a key) rather than just id, and thus causing an error :(

    I'm still playing with it. I've got a fork and I'm going to play with it today too, I absolutely love the after_load() functionality, that's really great, and it's solved a problem that my own version couldn't solve.

  • My only issue with the fetch_table_names was that it was searching my database table for users.id (as a key) rather than just id, and thus causing an error :(

    If you're using the official Auth/ORM modules for user management, it would be a good idea to use different database configs. In your Aurora files, you can specify the config property like this:

    class Aurora_Article implements Interface_Aurora_Database
    {
        public $config = 'pdo';
    }
    

    This way, you can still benefit from fetch_table_names, without affecting your other models that depend on ORM.

  • Gotcha. And with the qview function, is that the DEFAULT query for the object returned from Aurora::factory() for both models and collection loading? Or does the model always go straight for the table and peform a: where($pkey, '=', $id_given_to_load_function) ? I could find this out by looking at the code more but it's much better to hear it from the horses mouth ;)

    So for example, I'd have this on the Aurora_Feed_Item (implementing Interface_Aurora_Database):

    public function qview() 
    {
        return DB::select()->from('feed_items');
    }
    

    And that would be my base query? If I then had 2 functions, such as find_by_email and find_by_username (for example):

    public function find_by_email($email)
    {
        $this->qview = $this->qview ? : $this->qview();
        $this->qview->where('email', '=', $email)
    }
    
    public function find_by_username($username)
    {
        $this->qview = $this->qview ? : $this->qview();
        $this->qview->where('username', '=', $username);
    }
    

    And then I'd call them like this:

    $load_user_by_username = Aurora::factory('feed_item')->find_by_email([email protected]')->load();
    $load_user_by_email = Aurora::factory('feed_item')->find_by_username('JackEllis')->load();
    

    If that's correct, I'd also like to know if there's a way of doing the load() within the find_by_email() function? I'm unsure if there is, but you'll know :)

  • I hate to double post, but I thought you might be interested / have some comments on my 'cache layer'. A heavy work in progress and far from completed:

    <?php defined('SYSPATH') or die('No direct script access.');
    
    /**
     * The Data class is responsible for returning instances of Data_ classes (if they exist) or returning itself if they don't.
     * All Data classes will extend this
     */
    class Data {
    
        private static $_instances = array();
    
        public static $_external_cache_instance = NULL;
    
        /**
         * Return a new instance or an existing one
         */
        public static function instance($type = NULL)
        {
            if ($type === NULL)
            {
                throw new Exception('Please specify the Data\'s type when calling Data::instance()');
            }
    
            if ( ! isset(self::$_instances[$type]))
            {
                $class_name = 'Data_'.ucfirst($type);
    
                if ( ! class_exists($class_name))
                {
                    // Set the class name as this if the Data_ class doesn't exist
                    $class_name = get_class();
                }
    
                // Create an instance of the Data class (and pass the type through)
                $instance = new $class_name($type);
    
                self::$_instances[$type] = $instance;
            }
    
            return self::$_instances[$type];
        }
    
        /**
         * @var  array  An array containing retrieved records since the object has been instantiated. Indexed by PK.
         */
        protected $_local_cache = array();
    
        /**
         * @var  string  Data  The cache key prefix for caching
         */
        protected $_external_cache_key = '[Data]::';
    
        /**
         * @var  boolean  false  Reset the data? (AKA, don't use any cache except for $_cache)
         */
        protected $_use_external_cache = TRUE;
    
    
        public function __construct($type = 'Data', $reset = FALSE)
        {
            // If the script requested a reset OR caching is off, reset all
            if ($reset === TRUE OR Kohana::$caching === FALSE)
            {
                $this->_use_external_cache = FALSE;
            }
    
            $this->_type = $type;
    
            // Change the external cache key based on the supplied type
            // eg. instance('user') becomes cache key [Data_User]::
            $this->_external_cache_key = str_replace('Data', 'Data_'.$type, $this->_external_cache_key);
        }
    
        public function load($pk)
        {
            // Check instances cache
            if (isset($this->_local_cache[$pk]))
            {
                // Return a model that has already been generated on this request
                return $this->_local_cache[$pk];
            }
    
            // Check that we can use external cache
            if ($this->_use_external_cache)
            {
                // External cache key
                $external_cache_key = $this->_external_cache_key . $pk;
    
                // Check external cache
                if (($model = Cache::instance('file')->get($external_cache_key)) !== NULL)
                {
                    $this->_local_cache[$pk] = $model;
    
                    return unserialize($model);
                }
            }
    
            // Last resort, load with Aurora
            $model = Aurora::factory($this->_type)->load($pk);
    
            // Add model to local cache 
            $this->_local_cache[$pk] = $model;
    
            // If we are using external cache, store it
            if ($this->_use_external_cache)
            {
                Cache::instance('file')->set($external_cache_key, serialize($model));
            }
    
            return $model;
        }
    
    }
    

    Use scenario would be:

    $user_model = Data::instance('user')->load($user_pk)
    

    I chose to have a 'local' cache array because it's going to be faster than retrieving from the file cache each time.

    I wasn't sure what the custom json encoding / decoding was far on the Aurora objects, so I didn't use it and I opted for serializing instead.

    Edit: I modified my caching ways because I figured that load() was going to be one of very many potential methods (load_by_username(), load_by_email() etc.), so I changed it around to the following (sorry if I'm hijacking the thread a bit here):

    /**
         * Method to check to see if cache is stored locally or externally
         */
        private function _check_cache($method, $identifier)
        {
            // The cache key
            // e.g: _check_cache('load', 1) becomes ->load(1)
            $cache_key = '->'.$method.'('.$identifier.')';
    
            // Check instances cache
            if ($model = Arr::get($this->_local_cache, $cache_key, FALSE))
            {
                // Return a model that has already been generated on this request
                return $model;
            }
    
            // Check that we can use external cache
            if ($this->_use_external_cache)
            {
                // External cache key
                $external_cache_key = $this->_external_cache_key_prefix . $cache_key;
    
                // Check external cache
                if (($model = Cache::instance('file')->get($external_cache_key)) !== NULL)
                {
                    $this->_local_cache[$cache_key] = $model;
    
                    return unserialize($model);
                }
            }
    
            return FALSE;
        }
    
        /**
         * Method to store the cached model
         */
        private function _set_cache($method, $identifier, $model)
        {
            // The cache key
            // e.g: _check_cache('load', 1) becomes ->load(1)
            $cache_key = '->'.$method.'('.$identifier.')';
    
            // Add model to local cache
            $this->_local_cache[$cache_key] = $model;
    
            // If we are using external cache, store it
            if ($this->_use_external_cache)
            {
                Cache::instance('file')->set($this->_external_cache_key_prefix .$cache_key, serialize($model));
            }
    
            return $model;
        }
    
        /**
         * Load model by primary key
         */
        public function load($pk)
        {
            // Check to see if we have it in cache
            if ($model = $this->_check_cache('load', $pk))
            {
                return $model;
            }
    
            // Retrieve model
            $model = Aurora::factory($this->_type)->load($pk);
    
            // Add model to cache (and return it)
            return $this->_set_cache('load', $pk, $model);
        }
    

    I was hoping I'd be able to do a $this->check_cache() call at the start of a function (without the if and return), but I'm unsure if that's possible. Let me know what you think.

    EDIT: I'm not sure on the expected functionality of the core's load() function but when I performed a query, if there was a single result, it returned a collection rather than a model... Is there some way that you can request it to return as a model? I came up with this hack: http://i.imgur.com/l8uk1rG.png - But it's flawed, because if someone is expecting a collection to be returned, they will be annoyed. I may be missing something but I couldn't find anything. But at the same time, it almost looks like you're attempting to count the database results in these lines:

    if (empty($mode)) {
        $mode = ($count == 1) ? 'model' : 'collection';
    }
    

    Unless I'm way off track and the only time that load() intends to return a model is when a pkey is specified? I just thought that if a user intended to load a single result (for example, with find_by_username()), they could have the option to specify that they want it as a model? Perhaps by overriding something within the Aurora extended class?

    As a quick fix, I modified my Collection_User to offer an easy way to get the only result (expected):

    <?php defined('SYSPATH') or die('No direct script access.');
    
    class Collection_User extends Aurora_Collection {
    
        public function single_result()
        {
            return current($this->_collection);
        }
    
    }
    
  • Hi Jack Ellis, thank you for your time raising issues:


    And with the qview function, is that the DEFAULT query for the object returned from Aurora::factory() for both models and collection loading?

    Yes. Please note that you do not always need to specify a qview()



    ... If I then had 2 functions, such as find_by_email and find_by_username (for example) ...

    Yes. find_by_email and find_by_username functions you provided are good, and you should be calling them the way you specified. However, in those specific uses, you don't need the functions, as Aurora has built in method to map directly to columns:

    $col = Au::load('Feed_Item', array('email' => [email protected]')); // will return loaded Collection_Feed_Item
    
    $col = Au::load('Feed_Item', array('username' => 'JackEllis')); // will return loaded Collection_Feed_Item
    



    Is there some way that you can request it to return as a model?

    Yes. Just load the model.

    $model = Model::factory('Feed_Item');
    Au::load($model, array('email' => [email protected]')); // load a model from first row returned
    
    // In the future, I am considering something like this:
    $model = Au::load('Model_Feed_Item', array('email' => [email protected]')); // won't work now
    



    the only time that load() intends to return a model is when a pkey is specified?

    See here. If $params is scalar, it would be a model.



    if a user intended to load a single result (for example, with find_by_username())

    Hmm... if username and emails columns in feed_items table have unique index, then it would be better to implement Interface_Aurora_Before_Load which allows you to hack around the $params of load():

    class Aurora_Feed_Item implements Interface_Aurora_Hook_Before_Load, ...
    {
        public function before_load(&$params) {
            if (Valid::email($params)) // if valid email, load by email
                $params = array('email' => $params);
            else if (Valid::username($params)) // if valid username, load by username
                $params = array('username' => $params);
        }
    }
    

    Then in your controller:

    $model = Au::load('Feed_Item', 'JackEllis'); // model loaded by username, no need for array('username' => 'JackEllis')
    // or
    $model = Au::load('Feed_Item', [email protected]'); // model loaded by email, no need for array('email' => [email protected]')
    

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

In this Discussion