where('id', $id) ->where($timestamp_start_name, '<=', $timestamp) ->where($timestamp_end_name, '>', $timestamp); self::enable_primary_key_check(); //Make sure the temporal stuff is activated $query->set_temporal_properties($timestamp, $timestamp_end_name, $timestamp_start_name); foreach ($relations as $relation) { $query->related($relation); } $query_result = $query->get_one(); // If the query did not return a result but null, then we cannot call // set_lazy_timestamp on it without throwing errors if ( $query_result !== null ) { $query_result->set_lazy_timestamp($timestamp); } return $query_result; } private function set_lazy_timestamp($timestamp) { $this->_lazy_timestamp = $timestamp; } /** * Overrides Model::get() to allow lazy loaded relations to be filtered * temporaly. * * @param string $property * @return mixed */ public function & get($property, array $conditions = array()) { // if a timestamp is set and that we have a temporal relation $rel = static::relations($property); if ($rel && is_subclass_of($rel->model_to, 'Orm\Model_Temporal')) { // find a specific revision or the newest if lazy timestamp is null $lazy_timestamp = $this->_lazy_timestamp ?: static::temporal_property('max_timestamp') - 1; //add the filtering and continue with the parent's behavour $class_name = $rel->model_to; $class_name::make_query_temporal($lazy_timestamp); $result =& parent::get($property, $conditions); $class_name::make_query_temporal(null); return $result; } return parent::get($property, $conditions); } /** * When a timestamp is set any query objects produced by this temporal model * will behave the same as find_revision() * * @param array $timestamp */ private static function make_query_temporal($timestamp) { $class = get_called_class(); static::$_lazy_filtered_classes[$class] = $timestamp; } /** * Overrides Model::query to provide a Temporal_Query * * @param array $options * @return Query_Temporal */ public static function query($options = array()) { $timestamp_start_name = static::temporal_property('start_column'); $timestamp_end_name = static::temporal_property('end_column'); $max_timestamp = static::temporal_property('max_timestamp'); $query = Query_Temporal::forge(get_called_class(), static::connection(), $options) ->set_temporal_properties($max_timestamp, $timestamp_end_name, $timestamp_start_name); //Check if we need to add filtering $class = get_called_class(); $timestamp = \Arr::get(static::$_lazy_filtered_classes, $class, null); if( ! is_null($timestamp)) { $query->where($timestamp_start_name, '<=', $timestamp) ->where($timestamp_end_name, '>', $timestamp); } elseif(static::get_primary_key_status() and ! static::get_primary_key_id_only_status()) { $query->where($timestamp_end_name, $max_timestamp); } return $query; } /** * Returns a list of revisions between the given times with the most recent * first. This does not load relations. * * @param int|string $id * @param timestamp $earliestTime * @param timestamp $latestTime */ public static function find_revisions_between($id, $earliestTime = null, $latestTime = null) { $timestamp_start_name = static::temporal_property('start_column'); $max_timestamp = static::temporal_property('max_timestamp'); if ($earliestTime == null) { $earliestTime = 0; } if($latestTime == null) { $latestTime = $max_timestamp; } static::disable_primary_key_check(); //Select all revisions within the given range. $query = static::query() ->where('id', $id) ->where($timestamp_start_name, '>=', $earliestTime) ->where($timestamp_start_name, '<=', $latestTime); static::enable_primary_key_check(); $revisions = $query->get(); return $revisions; } /** * Overrides the default find method to allow the latest revision to be found * by default. * * If any new options to find are added the switch statement will have to be * updated too. * * @param type $id * @param array $options * @return type */ public static function find($id = null, array $options = array()) { $timestamp_end_name = static::temporal_property('end_column'); $max_timestamp = static::temporal_property('max_timestamp'); switch ($id) { case 'all': case 'first': case 'last': break; default: $id = (array) $id; $count = 0; foreach(static::getNonTimestampPks() as $key) { $options['where'][] = array($key, $id[$count]); $count++; } break; } $options['where'][] = array($timestamp_end_name, $max_timestamp); static::enable_id_only_primary_key(); $result = parent::find($id, $options); static::disable_id_only_primary_key(); return $result; } /** * Returns an array of the primary keys that are not related to temporal * timestamp information. */ public static function getNonTimestampPks() { $timestamp_start_name = static::temporal_property('start_column'); $timestamp_end_name = static::temporal_property('end_column'); $pks = array(); foreach(parent::primary_key() as $key) { if ($key != $timestamp_start_name && $key != $timestamp_end_name) { $pks[] = $key; } } return $pks; } /** * Overrides the save method to allow temporal models to be * @param boolean $cascade * @param boolean $use_transaction * @param boolean $skip_temporal Skips temporal filtering on initial inserts. Should not be used! * @return boolean */ public function save($cascade = null, $use_transaction = false) { // Load temporal properties. $timestamp_start_name = static::temporal_property('start_column'); $timestamp_end_name = static::temporal_property('end_column'); $mysql_timestamp = static::temporal_property('mysql_timestamp'); $max_timestamp = static::temporal_property('max_timestamp'); $current_timestamp = $mysql_timestamp ? \Date::forge()->format('mysql') : \Date::forge()->get_timestamp(); // If this is new then just call the parent and let everything happen as normal if ($this->is_new()) { static::disable_primary_key_check(); $this->{$timestamp_start_name} = $current_timestamp; $this->{$timestamp_end_name} = $max_timestamp; static::enable_primary_key_check(); // Make sure save will populate the PK static::enable_id_only_primary_key(); $result = parent::save($cascade, $use_transaction); static::disable_id_only_primary_key(); return $result; } // If this is an update then set a new PK, save and then insert a new row else { // run the before save observers before checking the diff $this->observe('before_save'); // then disable it so it doesn't get executed by parent::save() $this->disable_event('before_save'); $diff = $this->get_diff(); if (count($diff[0]) > 0) { // Take a copy of this model $revision = clone $this; // Give that new model an end time of the current time after resetting back to the old data $revision->set($this->_original); self::disable_primary_key_check(); $revision->{$timestamp_end_name} = $current_timestamp; self::enable_primary_key_check(); // Make sure relations stay the same $revision->_original_relations = $this->_data_relations; // save that, now we have our archive self::enable_id_only_primary_key(); $revision_result = $revision->overwrite(false, $use_transaction); self::disable_id_only_primary_key(); if ( ! $revision_result) { // If the revision did not save then stop the process so the user can do something. return false; } // Now that the old data is saved update the current object so its end timestamp is now self::disable_primary_key_check(); $this->{$timestamp_start_name} = $current_timestamp; self::enable_primary_key_check(); $result = parent::save($cascade, $use_transaction); } else { // If nothing has changed call parent::save() to insure relations are saved too $result = parent::save($cascade, $use_transaction); } // make sure the before save event is enabled again $this->enable_event('before_save'); return $result; } } /** * ALlows an entry to be updated without having to insert a new row. * This will not record any changed data as a new revision. * * Takes the same options as Model::save() */ public function overwrite($cascade = null, $use_transaction = false) { return parent::save($cascade, $use_transaction); } /** * Restores the entity to this state. * * @return boolean */ public function restore() { $timestamp_end_name = static::temporal_property('end_column'); $max_timestamp = static::temporal_property('max_timestamp'); // check to see if there is a currently active row, if so then don't // restore anything. $activeRow = static::find('first', array( 'where' => array( array('id', $this->id), array($timestamp_end_name, $max_timestamp), ), )); if(is_null($activeRow)) { // No active row was found so we are ok to go and restore the this // revision $timestamp_start_name = static::temporal_property('start_column'); $mysql_timestamp = static::temporal_property('mysql_timestamp'); $max_timestamp = static::temporal_property('max_timestamp'); $current_timestamp = $mysql_timestamp ? \Date::forge()->format('mysql') : \Date::forge()->get_timestamp(); // Make sure this is saved as a new entry $this->_is_new = true; // Update timestamps static::disable_primary_key_check(); $this->{$timestamp_start_name} = $current_timestamp; $this->{$timestamp_end_name} = $max_timestamp; // Save $result = parent::save(); static::enable_primary_key_check(); return $result; } return false; } /** * Deletes all revisions of this entity permantly. */ public function purge() { // Get a clean query object so there's no temporal filtering $query = parent::query(); // Then select and delete return $query->where('id', $this->id) ->delete(); } /** * Overrides update to remove PK checking when performing an update. */ public function update() { static::disable_primary_key_check(); $result = parent::update(); static::enable_primary_key_check(); return $result; } /** * Allows correct PKs to be added when performing updates * * @param Query $query */ protected function add_primary_keys_to_where($query) { $primary_key = static::$_primary_key; foreach ($primary_key as $pk) { $query->where($pk, '=', $this->_original[$pk]); } } /** * Overrides the parent primary_key method to allow primaray key enforcement * to be turned off when updating a temporal model. */ public static function primary_key() { $id_only = static::get_primary_key_id_only_status(); $pk_status = static::get_primary_key_status(); if ($id_only) { return static::getNonTimestampPks(); } if ($pk_status && ! $id_only) { return static::$_primary_key; } return array(); } public function delete($cascade = null, $use_transaction = false) { // If we are using a transcation then make sure it's started if ($use_transaction) { $db = \Database_Connection::instance(static::connection(true)); $db->start_transaction(); } // Call the observers $this->observe('before_delete'); // Load temporal properties. $timestamp_end_name = static::temporal_property('end_column'); $mysql_timestamp = static::temporal_property('mysql_timestamp'); // Generate the correct timestamp and save it $current_timestamp = $mysql_timestamp ? \Date::forge()->format('mysql') : \Date::forge()->get_timestamp(); static::disable_primary_key_check(); $this->{$timestamp_end_name} = $current_timestamp; static::enable_primary_key_check(); // Loop through all relations and delete if we are cascading. $this->freeze(); foreach ($this->relations() as $rel_name => $rel) { // get the cascade delete status $relCascade = is_null($cascade) ? $rel->cascade_delete : (bool) $cascade; if ($relCascade) { if(get_class($rel) != 'Orm\ManyMany') { // Loop through and call delete on all the models foreach($rel->get($this) as $model) { $model->delete($cascade); } } } } $this->unfreeze(); parent::save(); $this->observe('after_delete'); // Make sure the transaction is committed if needed $use_transaction and $db->commit_transaction(); return $this; } /** * Disables PK checking */ private static function disable_primary_key_check() { $class = get_called_class(); self::$_pk_check_disabled[$class] = false; } /** * Enables PK checking */ private static function enable_primary_key_check() { $class = get_called_class(); self::$_pk_check_disabled[$class] = true; } /** * Returns true if the PK checking should be performed. Defaults to true */ private static function get_primary_key_status() { $class = get_called_class(); return \Arr::get(self::$_pk_check_disabled, $class, true); } /** * Returns true if the PK should only contain the ID. Defaults to false */ private static function get_primary_key_id_only_status() { $class = get_called_class(); return \Arr::get(self::$_pk_id_only, $class, false); } /** * Makes all PKs returned */ private static function disable_id_only_primary_key() { $class = get_called_class(); self::$_pk_id_only[$class] = false; } /** * Makes only id returned as PK */ private static function enable_id_only_primary_key() { $class = get_called_class(); self::$_pk_id_only[$class] = true; } }