├── .gitignore ├── bootstrap.php ├── classes ├── belongsto.php ├── hasmany.php ├── hasone.php ├── manymany.php ├── model.php ├── model │ ├── nestedset.php │ ├── soft.php │ └── temporal.php ├── observer.php ├── observer │ ├── createdat.php │ ├── self.php │ ├── slug.php │ ├── typing.php │ ├── updatedat.php │ └── validation.php ├── query.php ├── query │ ├── soft.php │ └── temporal.php └── relation.php ├── composer.json └── config └── orm.php /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bak 3 | Thumbs.db 4 | desktop.ini 5 | .DS_Store 6 | .buildpath 7 | .project 8 | .settings 9 | fuel/app/logs/*/*/* 10 | fuel/app/cache/*/* 11 | nbproject/ 12 | .idea -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | __DIR__.'/classes/model.php', 15 | 'Orm\\Query' => __DIR__.'/classes/query.php', 16 | 'Orm\\BelongsTo' => __DIR__.'/classes/belongsto.php', 17 | 'Orm\\HasMany' => __DIR__.'/classes/hasmany.php', 18 | 'Orm\\HasOne' => __DIR__.'/classes/hasone.php', 19 | 'Orm\\ManyMany' => __DIR__.'/classes/manymany.php', 20 | 'Orm\\Relation' => __DIR__.'/classes/relation.php', 21 | 22 | //Speclised models 23 | 'Orm\\Model_Soft' => __DIR__.'/classes/model/soft.php', 24 | 'Orm\\Query_Soft' => __DIR__.'/classes/query/soft.php', 25 | 'Orm\\Model_Temporal' => __DIR__.'/classes/model/temporal.php', 26 | 'Orm\\Query_Temporal' => __DIR__.'/classes/query/temporal.php', 27 | 'Orm\\Model_Nestedset' => __DIR__.'/classes/model/nestedset.php', 28 | 29 | // Observers 30 | 'Orm\\Observer' => __DIR__.'/classes/observer.php', 31 | 'Orm\\Observer_CreatedAt' => __DIR__.'/classes/observer/createdat.php', 32 | 'Orm\\Observer_Typing' => __DIR__.'/classes/observer/typing.php', 33 | 'Orm\\Observer_UpdatedAt' => __DIR__.'/classes/observer/updatedat.php', 34 | 'Orm\\Observer_Validation' => __DIR__.'/classes/observer/validation.php', 35 | 'Orm\\Observer_Self' => __DIR__.'/classes/observer/self.php', 36 | 'Orm\\Observer_Slug' => __DIR__.'/classes/observer/slug.php', 37 | 38 | // Exceptions 39 | 'Orm\\RecordNotFound' => __DIR__.'/classes/model.php', 40 | 'Orm\\FrozenObject' => __DIR__.'/classes/model.php', 41 | 'Orm\\InvalidContentType' => __DIR__.'/classes/observer/typing.php', 42 | 'Orm\\ValidationFailed' => __DIR__.'/classes/observer/validation.php', 43 | 'Orm\\RelationNotSoft' => __DIR__.'/classes/model/soft.php', 44 | )); 45 | 46 | // Ensure the orm's config is loaded 47 | \Config::load('orm', true); 48 | -------------------------------------------------------------------------------- /classes/belongsto.php: -------------------------------------------------------------------------------- 1 | name = $name; 31 | $this->model_from = $from; 32 | $this->model_to = array_key_exists('model_to', $config) 33 | ? $config['model_to'] : \Inflector::get_namespace($from).'Model_'.\Inflector::classify($name); 34 | $this->key_from = array_key_exists('key_from', $config) 35 | ? (array) $config['key_from'] : (array) \Inflector::foreign_key($this->model_to); 36 | $this->key_to = array_key_exists('key_to', $config) 37 | ? (array) $config['key_to'] : $this->key_to; 38 | $this->conditions = array_key_exists('conditions', $config) 39 | ? (array) $config['conditions'] : array(); 40 | 41 | // DEPRECATED SINCE 1.9 42 | $this->cascade_delete = array_key_exists('cascade_delete', $config) 43 | ? $config['cascade_delete'] : $this->cascade_delete; 44 | 45 | if (array_key_exists('constraint', $config) and in_array($config['constraint'], $this->valid_constraints)) 46 | { 47 | switch($config['constraint']) 48 | { 49 | case static::CONSTRAINT_RESTRICT: 50 | $this->cascade_check = true; 51 | break; 52 | case static::CONSTRAINT_CASCADE: 53 | $this->cascade_delete = true; 54 | break; 55 | case static::CONSTRAINT_SETDEFAULT: 56 | $this->cascade_delete = false; 57 | break; 58 | default: 59 | } 60 | } 61 | 62 | $this->cascade_save = array_key_exists('cascade_save', $config) 63 | ? $config['cascade_save'] : $this->cascade_save; 64 | 65 | if ( ! class_exists($this->model_to)) 66 | { 67 | throw new \FuelException('Related model not found by Belongs_To relation "'.$this->name.'": '.$this->model_to); 68 | } 69 | $this->model_to = get_real_class($this->model_to); 70 | } 71 | 72 | public function get(Model $from, array $conditions = array()) 73 | { 74 | $query = call_user_func(array($this->model_to, 'query')); 75 | reset($this->key_to); 76 | foreach ($this->key_from as $key) 77 | { 78 | // no point running a query when a key value is null 79 | if ($from->{$key} === null) 80 | { 81 | return null; 82 | } 83 | $query->where(current($this->key_to), $from->{$key}); 84 | next($this->key_to); 85 | } 86 | 87 | $conditions = \Arr::merge($this->conditions, $conditions); 88 | $query->_parse_where_array(\Arr::get($conditions, 'where', array())); 89 | 90 | return $query->get_one(); 91 | } 92 | 93 | public function join($alias_from, $rel_name, $alias_to_nr, $conditions = array()) 94 | { 95 | $alias_to = 't'.$alias_to_nr; 96 | $model = array( 97 | 'model' => $this->model_to, 98 | 'connection' => call_user_func(array($this->model_to, 'connection')), 99 | 'table' => array(call_user_func(array($this->model_to, 'table')), $alias_to), 100 | 'primary_key' => call_user_func(array($this->model_to, 'primary_key')), 101 | 'join_type' => \Arr::get($conditions, 'join_type') ?: \Arr::get($this->conditions, 'join_type', 'left'), 102 | 'join_on' => array(), 103 | 'columns' => $this->select($alias_to), 104 | 'rel_name' => strpos($rel_name, '.') ? substr($rel_name, strrpos($rel_name, '.') + 1) : $rel_name, 105 | 'relation' => $this, 106 | 'where' => \Arr::get($conditions, 'where', array()), 107 | 'order_by' => \Arr::get($conditions, 'order_by') ?: \Arr::get($this->conditions, 'order_by', array()), 108 | ); 109 | 110 | reset($this->key_to); 111 | foreach ($this->key_from as $key) 112 | { 113 | $model['join_on'][] = array($alias_from.'.'.$key, '=', $alias_to.'.'.current($this->key_to)); 114 | next($this->key_to); 115 | } 116 | foreach (array(\Arr::get($this->conditions, 'where', array()), \Arr::get($conditions, 'join_on', array())) as $c) 117 | { 118 | foreach ($c as $key => $condition) 119 | { 120 | ! is_array($condition) and $condition = array($key, '=', $condition); 121 | if ( ! $condition[0] instanceof \Fuel\Core\Database_Expression and strpos($condition[0], '.') === false) 122 | { 123 | $condition[0] = $alias_to.'.'.$condition[0]; 124 | } 125 | if (count($condition) == 2) // From Query::_where() 126 | { 127 | $condition = array($condition[0], '=', $condition[1]); 128 | } 129 | is_string($condition[2]) and $condition[2] = \Db::quote($condition[2], $model['connection']); 130 | 131 | $model['join_on'][] = $condition; 132 | } 133 | } 134 | 135 | return array($rel_name => $model); 136 | } 137 | 138 | public function save($model_from, $model_to, $original_model_id, $parent_saved, $cascade) 139 | { 140 | if ($parent_saved) 141 | { 142 | return; 143 | } 144 | 145 | if ( ! $model_to instanceof $this->model_to and $model_to !== null) 146 | { 147 | throw new \FuelException('Invalid Model instance added to relations in this model.'); 148 | } 149 | 150 | // Save if it's a yet unsaved object 151 | if ($model_to and $model_to->is_new()) 152 | { 153 | $model_to->save(false); 154 | } 155 | 156 | $current_model_id = $model_to ? $model_to->implode_pk($model_to) : null; 157 | 158 | // Check if there was another model assigned (this supersedes any change to the foreign key(s)) 159 | if ($current_model_id != $original_model_id) 160 | { 161 | // change the foreign keys in the model_from to point to the new relation 162 | reset($this->key_from); 163 | $model_from->unfreeze(); 164 | foreach ($this->key_to as $pk) 165 | { 166 | $model_from->{current($this->key_from)} = $model_to ? $model_to->{$pk} : null; 167 | next($this->key_from); 168 | } 169 | $model_from->freeze(); 170 | } 171 | 172 | // if not check the model_from's foreign_keys against the model_to's primary keys 173 | // because that is how the model stores them 174 | elseif ($current_model_id != null) 175 | { 176 | $foreign_keys = count($this->key_to) == 1 ? array($original_model_id) : explode('][', substr($original_model_id, 1, -1)); 177 | $changed = false; 178 | $new_rel_id = array(); 179 | reset($foreign_keys); 180 | $m = $this->model_to; 181 | foreach ($m::primary_key() as $pk) 182 | { 183 | if (is_null($model_to->{$pk})) 184 | { 185 | $changed = true; 186 | $new_rel_id = null; 187 | break; 188 | } 189 | elseif ($model_to->{$pk} != current($foreign_keys)) 190 | { 191 | $changed = true; 192 | } 193 | $new_rel_id[] = $model_to->{$pk}; 194 | next($foreign_keys); 195 | } 196 | 197 | // if any of the keys changed, reload the relationship - saving the object will save those keys 198 | if ($changed) 199 | { 200 | // Attempt to load the new related object 201 | if ( ! is_null($new_rel_id)) 202 | { 203 | $rel_obj = call_user_func(array($this->model_to, 'find'), $new_rel_id); 204 | if (empty($rel_obj)) 205 | { 206 | throw new \FuelException('New relation set on '.$this->model_from.' object wasn\'t found.'); 207 | } 208 | } 209 | else 210 | { 211 | $rel_obj = null; 212 | } 213 | 214 | // Add the new relation to the model_from 215 | $model_from->unfreeze(); 216 | $rel = $model_from->_relate(); 217 | $rel[$this->name] = $rel_obj; 218 | $model_from->_relate($rel); 219 | $model_from->freeze(); 220 | } 221 | } 222 | 223 | $cascade = is_null($cascade) ? $this->cascade_save : (bool) $cascade; 224 | if ($cascade and ! empty($model_to)) 225 | { 226 | $model_to->save(); 227 | } 228 | } 229 | 230 | public function delete($model_from, $parent_deleted, $cascade) 231 | { 232 | // fetch all related records 233 | $model_from->get($this->name); 234 | 235 | if ( ! $parent_deleted) 236 | { 237 | if ($this->cascade_check and ! empty($model_from->{$this->name})) 238 | { 239 | throw new \Orm\DeleteConstraintViolation($this->name); 240 | } 241 | 242 | return; 243 | } 244 | 245 | // break current relations, may be incomplete 246 | $model_from->unfreeze(); 247 | 248 | $rels = $model_from->_relate(); 249 | unset($rels[$this->name]); 250 | $model_from->_relate($rels); 251 | 252 | $model_from->freeze(); 253 | 254 | $cascade = is_null($cascade) ? $this->cascade_delete : (bool) $cascade; 255 | 256 | if ($cascade) 257 | { 258 | if ( ! empty($model_from->{$this->name})) 259 | { 260 | // yes, delete the reclated records 261 | $model_from->{$this->name}->delete(); 262 | } 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /classes/hasmany.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | $this->model_from = $from; 26 | $this->model_to = array_key_exists('model_to', $config) 27 | ? $config['model_to'] : \Inflector::get_namespace($from).'Model_'.\Inflector::classify($name); 28 | $this->key_from = array_key_exists('key_from', $config) 29 | ? (array) $config['key_from'] : $this->key_from; 30 | $this->key_to = array_key_exists('key_to', $config) 31 | ? (array) $config['key_to'] : (array) \Inflector::foreign_key($this->model_from); 32 | $this->conditions = array_key_exists('conditions', $config) 33 | ? (array) $config['conditions'] : array(); 34 | 35 | // DEPRECATED SINCE 1.9 36 | $this->cascade_delete = array_key_exists('cascade_delete', $config) 37 | ? $config['cascade_delete'] : $this->cascade_delete; 38 | 39 | if (array_key_exists('constraint', $config) and in_array($config['constraint'], $this->valid_constraints)) 40 | { 41 | switch($config['constraint']) 42 | { 43 | case static::CONSTRAINT_RESTRICT: 44 | $this->cascade_check = true; 45 | break; 46 | case static::CONSTRAINT_CASCADE: 47 | $this->cascade_delete = true; 48 | break; 49 | case static::CONSTRAINT_SETDEFAULT: 50 | $this->cascade_delete = false; 51 | break; 52 | default: 53 | } 54 | } 55 | 56 | $this->cascade_save = array_key_exists('cascade_save', $config) 57 | ? $config['cascade_save'] : $this->cascade_save; 58 | 59 | if ( ! class_exists($this->model_to)) 60 | { 61 | throw new \FuelException('Related model not found by Has_Many relation "'.$this->name.'": '.$this->model_to); 62 | } 63 | $this->model_to = get_real_class($this->model_to); 64 | } 65 | 66 | public function get(Model $from, array $conditions = array()) 67 | { 68 | $query = call_user_func(array($this->model_to, 'query')); 69 | reset($this->key_to); 70 | foreach ($this->key_from as $key) 71 | { 72 | // no point running a query when a key value is null 73 | if ($from->{$key} === null) 74 | { 75 | return array(); 76 | } 77 | $query->where(current($this->key_to), $from->{$key}); 78 | next($this->key_to); 79 | } 80 | 81 | $conditions = \Arr::merge($this->conditions, $conditions); 82 | $query->_parse_where_array(\Arr::get($conditions, 'where', array())); 83 | 84 | foreach (\Arr::get($conditions, 'order_by', array()) as $field => $direction) 85 | { 86 | if (is_numeric($field)) 87 | { 88 | $query->order_by($direction); 89 | } 90 | else 91 | { 92 | $query->order_by($field, $direction); 93 | } 94 | } 95 | 96 | return $query->get(); 97 | } 98 | 99 | public function join($alias_from, $rel_name, $alias_to_nr, $conditions = array()) 100 | { 101 | $alias_to = 't'.$alias_to_nr; 102 | $model = array( 103 | 'model' => $this->model_to, 104 | 'connection' => call_user_func(array($this->model_to, 'connection')), 105 | 'table' => array(call_user_func(array($this->model_to, 'table')), $alias_to), 106 | 'primary_key' => call_user_func(array($this->model_to, 'primary_key')), 107 | 'join_type' => \Arr::get($conditions, 'join_type') ?: \Arr::get($this->conditions, 'join_type', 'left'), 108 | 'join_on' => array(), 109 | 'columns' => $this->select($alias_to), 110 | 'rel_name' => strpos($rel_name, '.') ? substr($rel_name, strrpos($rel_name, '.') + 1) : $rel_name, 111 | 'relation' => $this, 112 | 'where' => \Arr::get($conditions, 'where', array()), 113 | 'order_by' => \Arr::get($conditions, 'order_by') ?: \Arr::get($this->conditions, 'order_by', array()), 114 | ); 115 | 116 | reset($this->key_to); 117 | foreach ($this->key_from as $key) 118 | { 119 | $model['join_on'][] = array($alias_from.'.'.$key, '=', $alias_to.'.'.current($this->key_to)); 120 | next($this->key_to); 121 | } 122 | foreach (array(\Arr::get($this->conditions, 'where', array()), \Arr::get($conditions, 'join_on', array())) as $c) 123 | { 124 | foreach ($c as $key => $condition) 125 | { 126 | ! is_array($condition) and $condition = array($key, '=', $condition); 127 | if ( ! $condition[0] instanceof \Fuel\Core\Database_Expression and strpos($condition[0], '.') === false) 128 | { 129 | $condition[0] = $alias_to.'.'.$condition[0]; 130 | } 131 | if (count($condition) == 2) // From Query::_where() 132 | { 133 | $condition = array($condition[0], '=', $condition[1]); 134 | } 135 | is_string($condition[2]) and $condition[2] = \Db::quote($condition[2], $model['connection']); 136 | 137 | $model['join_on'][] = $condition; 138 | } 139 | } 140 | 141 | return array($rel_name => $model); 142 | } 143 | 144 | public function save($model_from, $models_to, $original_model_ids, $parent_saved, $cascade) 145 | { 146 | if ( ! $parent_saved) 147 | { 148 | return; 149 | } 150 | 151 | if ( ! is_array($models_to) and ($models_to = is_null($models_to) ? array() : $models_to) !== array()) 152 | { 153 | throw new \FuelException('Assigned relationships must be an array or null, given relationship value for '. 154 | $this->name.' is invalid.'); 155 | } 156 | $original_model_ids === null and $original_model_ids = array(); 157 | 158 | foreach ($models_to as $key => $model_to) 159 | { 160 | if ( ! $model_to instanceof $this->model_to) 161 | { 162 | throw new \FuelException('Invalid Model instance added to relations in this model.'); 163 | } 164 | 165 | $current_model_id = ($model_to and ! $model_to->is_new()) ? $model_to->implode_pk($model_to) : null; 166 | 167 | // Check if the model was already assigned 168 | if (($model_to and $model_to->is_new()) or ! in_array($current_model_id, $original_model_ids)) 169 | { 170 | // assign this object to the new objects foreign keys 171 | reset($this->key_to); 172 | $frozen = $model_to->frozen(); // only unfreeze/refreeze when it was frozen 173 | $frozen and $model_to->unfreeze(); 174 | foreach ($this->key_from as $pk) 175 | { 176 | $model_to->{current($this->key_to)} = $model_from->{$pk}; 177 | next($this->key_to); 178 | } 179 | $model_to->is_new() and $model_to->save(false); 180 | $frozen and $model_to->freeze(); 181 | } 182 | // check if the model_to's foreign_keys match the model_from's primary keys 183 | else 184 | { 185 | // unset current model from from array 186 | unset($original_model_ids[array_search($current_model_id, $original_model_ids)]); 187 | 188 | // check if model_to still refers to this model_from 189 | $changed = false; 190 | reset($this->key_to); 191 | foreach ($this->key_from as $pk) 192 | { 193 | if ($model_to->{current($this->key_to)} != $model_from->{$pk}) 194 | { 195 | $changed = true; 196 | } 197 | next($this->key_to); 198 | } 199 | 200 | // if any of the keys changed, the relationship was broken - remove model_to from loaded objects 201 | if ($changed) 202 | { 203 | $model_from->unfreeze(); 204 | $rel = $model_from->_relate(); 205 | unset($rel[$this->name][$key]); 206 | $model_from->_relate($rel); 207 | $model_from->freeze(); 208 | 209 | // cascading this change won't work here, save just the object with cascading switched off 210 | $model_from->save(false); 211 | } 212 | } 213 | 214 | // Fix it if key isn't an imploded PK 215 | if ($key != ($current_model_id = $model_to->implode_pk($model_to))) 216 | { 217 | $model_from->unfreeze(); 218 | $rel = $model_from->_relate(); 219 | if ( ! empty($rel[$this->name][$key]) and $rel[$this->name][$key] === $model_to) 220 | { 221 | unset($rel[$this->name][$key]); 222 | } 223 | $rel[$this->name][$current_model_id] = $model_to; 224 | $model_from->_relate($rel); 225 | $model_from->freeze(); 226 | } 227 | } 228 | 229 | // if any original ids are left over in the array, they're no longer related - break them 230 | foreach ($original_model_ids as $original_model_id) 231 | { 232 | // if still loaded set this object's old relation's foreign keys to null 233 | if ($original_model_id and $obj = call_user_func(array($this->model_to, 'find'), 234 | count($this->key_to) == 1 ? array($original_model_id) : explode('][', substr($original_model_id, 1, -1)))) 235 | { 236 | $frozen = $obj->frozen(); // only unfreeze/refreeze when it was frozen 237 | $frozen and $obj->unfreeze(); 238 | foreach ($this->key_to as $fk) 239 | { 240 | $obj->{$fk} = null; 241 | } 242 | $frozen and $obj->freeze(); 243 | 244 | // cascading this change won't work here, save just the object with cascading switched off 245 | $obj->save(false); 246 | } 247 | } 248 | 249 | $cascade = is_null($cascade) ? $this->cascade_save : (bool) $cascade; 250 | if ($cascade and ! empty($models_to)) 251 | { 252 | foreach ($models_to as $m) 253 | { 254 | $m->save(); 255 | } 256 | } 257 | } 258 | 259 | public function delete($model_from, $parent_deleted, $cascade) 260 | { 261 | // fetch all related records 262 | $model_from->get($this->name); 263 | 264 | if ( ! $parent_deleted) 265 | { 266 | if ($this->cascade_check and ! empty($model_from->{$this->name})) 267 | { 268 | throw new \Orm\DeleteConstraintViolation($this->name); 269 | } 270 | 271 | return; 272 | } 273 | 274 | // break current relations, may be incomplete 275 | $model_from->unfreeze(); 276 | 277 | $rels = $model_from->_relate(); 278 | unset($rels[$this->name]); 279 | $model_from->_relate($rels); 280 | 281 | $model_from->freeze(); 282 | 283 | // check if we need to cascate the delete 284 | $cascade = is_null($cascade) ? $this->cascade_delete : (bool) $cascade; 285 | 286 | if ($cascade) 287 | { 288 | // delete the reclated records 289 | foreach ($model_from->{$this->name} as $m) 290 | { 291 | $m->delete(); 292 | } 293 | } 294 | else 295 | { 296 | // no, reset the foreign key and decouple 297 | foreach ($model_from->{$this->name} as $m) 298 | { 299 | if ( ! $m->frozen()) 300 | { 301 | foreach ($this->key_to as $fk) 302 | { 303 | $m->{$fk} = null; 304 | } 305 | $m->is_changed() and $m->save(); 306 | } 307 | } 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /classes/hasone.php: -------------------------------------------------------------------------------- 1 | name = $name; 27 | $this->model_from = $from; 28 | $this->model_to = array_key_exists('model_to', $config) 29 | ? $config['model_to'] : \Inflector::get_namespace($from).'Model_'.\Inflector::classify($name); 30 | $this->key_from = array_key_exists('key_from', $config) 31 | ? (array) $config['key_from'] : $this->key_from; 32 | $this->key_to = array_key_exists('key_to', $config) 33 | ? (array) $config['key_to'] : (array) \Inflector::foreign_key($this->model_from); 34 | $this->conditions = array_key_exists('conditions', $config) 35 | ? (array) $config['conditions'] : array(); 36 | 37 | // DEPRECATED SINCE 1.9 38 | $this->cascade_delete = array_key_exists('cascade_delete', $config) 39 | ? $config['cascade_delete'] : $this->cascade_delete; 40 | 41 | if (array_key_exists('constraint', $config) and in_array($config['constraint'], $this->valid_constraints)) 42 | { 43 | switch($config['constraint']) 44 | { 45 | case static::CONSTRAINT_RESTRICT: 46 | $this->cascade_check = true; 47 | break; 48 | case static::CONSTRAINT_CASCADE: 49 | $this->cascade_delete = true; 50 | break; 51 | case static::CONSTRAINT_SETDEFAULT: 52 | $this->cascade_delete = false; 53 | break; 54 | default: 55 | } 56 | } 57 | 58 | $this->cascade_save = array_key_exists('cascade_save', $config) 59 | ? $config['cascade_save'] : $this->cascade_save; 60 | 61 | if ( ! class_exists($this->model_to)) 62 | { 63 | throw new \FuelException('Related model not found by Has_One relation "'.$this->name.'": '.$this->model_to); 64 | } 65 | $this->model_to = get_real_class($this->model_to); 66 | } 67 | 68 | public function get(Model $from, array $conditions = array()) 69 | { 70 | $query = call_user_func(array($this->model_to, 'query')); 71 | reset($this->key_to); 72 | foreach ($this->key_from as $key) 73 | { 74 | // no point running a query when a key value is null 75 | if ($from->{$key} === null) 76 | { 77 | return null; 78 | } 79 | $query->where(current($this->key_to), $from->{$key}); 80 | next($this->key_to); 81 | } 82 | 83 | $conditions = \Arr::merge($this->conditions, $conditions); 84 | $query->_parse_where_array(\Arr::get($conditions, 'where', array())); 85 | 86 | return $query->get_one(); 87 | } 88 | 89 | public function join($alias_from, $rel_name, $alias_to_nr, $conditions = array()) 90 | { 91 | $alias_to = 't'.$alias_to_nr; 92 | $model = array( 93 | 'model' => $this->model_to, 94 | 'connection' => call_user_func(array($this->model_to, 'connection')), 95 | 'table' => array(call_user_func(array($this->model_to, 'table')), $alias_to), 96 | 'primary_key' => call_user_func(array($this->model_to, 'primary_key')), 97 | 'join_type' => \Arr::get($conditions, 'join_type') ?: \Arr::get($this->conditions, 'join_type', 'left'), 98 | 'join_on' => array(), 99 | 'columns' => $this->select($alias_to), 100 | 'rel_name' => strpos($rel_name, '.') ? substr($rel_name, strrpos($rel_name, '.') + 1) : $rel_name, 101 | 'relation' => $this, 102 | 'where' => \Arr::get($conditions, 'where', array()), 103 | 'order_by' => \Arr::get($conditions, 'order_by') ?: \Arr::get($this->conditions, 'order_by', array()), 104 | ); 105 | 106 | reset($this->key_to); 107 | foreach ($this->key_from as $key) 108 | { 109 | $model['join_on'][] = array($alias_from.'.'.$key, '=', $alias_to.'.'.current($this->key_to)); 110 | next($this->key_to); 111 | } 112 | foreach (array(\Arr::get($this->conditions, 'where', array()), \Arr::get($conditions, 'join_on', array())) as $c) 113 | { 114 | foreach ($c as $key => $condition) 115 | { 116 | ! is_array($condition) and $condition = array($key, '=', $condition); 117 | if ( ! $condition[0] instanceof \Fuel\Core\Database_Expression and strpos($condition[0], '.') === false) 118 | { 119 | $condition[0] = $alias_to.'.'.$condition[0]; 120 | } 121 | if (count($condition) == 2) // From Query::_where() 122 | { 123 | $condition = array($condition[0], '=', $condition[1]); 124 | } 125 | is_string($condition[2]) and $condition[2] = \Db::quote($condition[2], $model['connection']); 126 | 127 | $model['join_on'][] = $condition; 128 | } 129 | } 130 | 131 | return array($rel_name => $model); 132 | } 133 | 134 | public function save($model_from, $model_to, $original_model_id, $parent_saved, $cascade) 135 | { 136 | if ( ! $parent_saved) 137 | { 138 | return; 139 | } 140 | 141 | if ( ! $model_to instanceof $this->model_to and $model_to !== null) 142 | { 143 | throw new \FuelException('Invalid Model instance added to relations in this model.'); 144 | } 145 | 146 | $current_model_id = ($model_to and ! $model_to->is_new()) ? $model_to->implode_pk($model_to) : null; 147 | // Check if there was another model assigned (this supersedes any change to the foreign key(s)) 148 | if (($model_to and $model_to->is_new()) or $current_model_id != $original_model_id) 149 | { 150 | // assign this object to the new objects foreign keys 151 | if ( ! empty($model_to)) 152 | { 153 | reset($this->key_to); 154 | $frozen = $model_to->frozen(); // only unfreeze/refreeze when it was frozen 155 | $frozen and $model_to->unfreeze(); 156 | foreach ($this->key_from as $pk) 157 | { 158 | $model_to->{current($this->key_to)} = $model_from->{$pk}; 159 | next($this->key_to); 160 | } 161 | $frozen and $model_to->freeze(); 162 | } 163 | 164 | // if still loaded set this object's old relation's foreign keys to null 165 | if ($original_model_id and $obj = call_user_func(array($this->model_to, 'find'), 166 | count($this->key_to) == 1 ? array($original_model_id) : explode('][', substr($original_model_id, 1, -1)))) 167 | { 168 | // check whether the object still refers to this model_from 169 | $changed = false; 170 | reset($this->key_to); 171 | foreach ($this->key_from as $pk) 172 | { 173 | if ($obj->{current($this->key_to)} != $model_from->{$pk}) 174 | { 175 | $changed = true; 176 | } 177 | next($this->key_to); 178 | } 179 | 180 | // when it still refers to this object, reset the foreign key(s) 181 | if ( ! $changed) 182 | { 183 | $frozen = $obj->frozen(); // only unfreeze/refreeze when it was frozen 184 | $frozen and $obj->unfreeze(); 185 | foreach ($this->key_to as $fk) 186 | { 187 | $obj->{$fk} = null; 188 | } 189 | $frozen and $obj->freeze(); 190 | 191 | // cascading this change won't work here, save just the object with cascading switched off 192 | $obj->save(false); 193 | } 194 | } 195 | } 196 | // if not empty check the model_to's foreign_keys, when empty nothing changed 197 | elseif ( ! empty($model_to)) 198 | { 199 | // check if model_to still refers to this model_from 200 | $changed = false; 201 | reset($this->key_to); 202 | foreach ($this->key_from as $pk) 203 | { 204 | if ($model_to->{current($this->key_to)} != $model_from->{$pk}) 205 | { 206 | $changed = true; 207 | } 208 | next($this->key_to); 209 | } 210 | 211 | // if any of the keys changed, the relationship was broken - remove model_to from loaded objects 212 | if ($changed) 213 | { 214 | // Remove the model_to from the relationships of model_from 215 | $model_from->unfreeze(); 216 | $rel = $model_from->_relate(); 217 | $rel[$this->name] = null; 218 | $model_from->_relate($rel); 219 | $model_from->freeze(); 220 | } 221 | } 222 | 223 | $cascade = is_null($cascade) ? $this->cascade_save : (bool) $cascade; 224 | if ($cascade and ! empty($model_to)) 225 | { 226 | $model_to->save(); 227 | } 228 | } 229 | 230 | public function delete($model_from, $parent_deleted, $cascade) 231 | { 232 | // fetch all related records 233 | $model_from->get($this->name); 234 | 235 | if ( ! $parent_deleted) 236 | { 237 | if ($this->cascade_check and ! empty($model_from->{$this->name})) 238 | { 239 | throw new \Orm\DeleteConstraintViolation($this->name); 240 | } 241 | 242 | return; 243 | } 244 | 245 | // break current relations, may be incomplete 246 | $model_from->unfreeze(); 247 | 248 | $rels = $model_from->_relate(); 249 | unset($rels[$this->name]); 250 | $model_from->_relate($rels); 251 | 252 | $model_from->freeze(); 253 | 254 | if ( ! empty($model_from->{$this->name})) 255 | { 256 | $cascade = is_null($cascade) ? $this->cascade_delete : (bool) $cascade; 257 | 258 | if ($cascade) 259 | { 260 | // yes, delete the reclated records 261 | { 262 | $model_from->{$this->name}->delete(); 263 | } 264 | } 265 | elseif ( ! $model_from->{$this->name}->frozen()) 266 | { 267 | // no, reset the foreign key and decouple 268 | foreach ($this->key_to as $fk) 269 | { 270 | $model_from->{$this->name}->{$fk} = null; 271 | } 272 | $model_from->{$this->name}->is_changed() and $model_from->{$this->name}->save(); 273 | } 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /classes/manymany.php: -------------------------------------------------------------------------------- 1 | name = $name; 49 | $this->model_from = $from; 50 | $this->model_to = array_key_exists('model_to', $config) 51 | ? $config['model_to'] : \Inflector::get_namespace($from).'Model_'.\Inflector::classify($name); 52 | $this->key_from = array_key_exists('key_from', $config) 53 | ? (array) $config['key_from'] : $this->key_from; 54 | $this->key_to = array_key_exists('key_to', $config) 55 | ? (array) $config['key_to'] : $this->key_to; 56 | $this->conditions = array_key_exists('conditions', $config) 57 | ? (array) $config['conditions'] : array(); 58 | 59 | if ( ! empty($config['table_through'])) 60 | { 61 | $this->table_through = $config['table_through']; 62 | } 63 | else 64 | { 65 | $table_name = array($this->model_from, $this->model_to); 66 | natcasesort($table_name); 67 | $table_name = array_merge($table_name); 68 | $this->table_through = \Inflector::tableize($table_name[0]).'_'.\Inflector::tableize($table_name[1]); 69 | } 70 | $this->key_through_from = ! empty($config['key_through_from']) 71 | ? (array) $config['key_through_from'] : (array) \Inflector::foreign_key($this->model_from); 72 | $this->key_through_to = ! empty($config['key_through_to']) 73 | ? (array) $config['key_through_to'] : (array) \Inflector::foreign_key($this->model_to); 74 | 75 | // DEPRECATED SINCE 1.9 76 | $this->cascade_delete = array_key_exists('cascade_delete', $config) 77 | ? $config['cascade_delete'] : $this->cascade_delete; 78 | 79 | if (array_key_exists('constraint', $config) and in_array($config['constraint'], $this->valid_constraints)) 80 | { 81 | switch($config['constraint']) 82 | { 83 | case static::CONSTRAINT_RESTRICT: 84 | $this->cascade_check = true; 85 | break; 86 | case static::CONSTRAINT_CASCADE: 87 | $this->cascade_delete = true; 88 | break; 89 | case static::CONSTRAINT_SETDEFAULT: 90 | $this->cascade_delete = false; 91 | break; 92 | default: 93 | } 94 | } 95 | 96 | $this->cascade_save = array_key_exists('cascade_save', $config) 97 | ? $config['cascade_save'] : $this->cascade_save; 98 | 99 | if ( ! class_exists($this->model_to)) 100 | { 101 | throw new \FuelException('Related model not found by Many_Many relation "'.$this->name.'": '.$this->model_to); 102 | } 103 | $this->model_to = get_real_class($this->model_to); 104 | } 105 | 106 | public function get(Model $from, array $conditions = array()) 107 | { 108 | // Create the query on the model_through 109 | $query = call_user_func(array($this->model_to, 'query')); 110 | 111 | // set the model_from's keys as where conditions for the model_through 112 | $join = array( 113 | 'table' => array($this->table_through, 't0_through'), 114 | 'join_type' => null, 115 | 'join_on' => array(), 116 | 'columns' => $this->select_through('t0_through'), 117 | ); 118 | 119 | reset($this->key_from); 120 | foreach ($this->key_through_from as $key) 121 | { 122 | if ($from->{current($this->key_from)} === null) 123 | { 124 | return array(); 125 | } 126 | $query->where('t0_through.'.$key, $from->{current($this->key_from)}); 127 | next($this->key_from); 128 | } 129 | 130 | reset($this->key_to); 131 | foreach ($this->key_through_to as $key) 132 | { 133 | $join['join_on'][] = array('t0_through.'.$key, '=', 't0.'.current($this->key_to)); 134 | next($this->key_to); 135 | } 136 | 137 | $conditions = \Arr::merge($this->conditions, $conditions); 138 | $query->_parse_where_array(\Arr::get($conditions, 'where', array())); 139 | 140 | foreach (\Arr::get($conditions, 'order_by', array()) as $field => $direction) 141 | { 142 | if (strpos($field, '.') !== false) 143 | { 144 | $parts = explode('.', $field); 145 | if ($parts[0] == $join['table'][0]) 146 | { 147 | $parts[0] = $join['table'][1]; 148 | $field = implode('.', $parts); 149 | } 150 | } 151 | 152 | if (is_numeric($field)) 153 | { 154 | $query->order_by($direction); 155 | } 156 | else 157 | { 158 | $query->order_by($field, $direction); 159 | } 160 | } 161 | 162 | $query->_join($join); 163 | 164 | return $query->get(); 165 | } 166 | 167 | public function select_through($table) 168 | { 169 | foreach ($this->key_through_to as $to) 170 | { 171 | $properties[] = $table.'.'.$to; 172 | } 173 | foreach ($this->key_through_from as $from) 174 | { 175 | $properties[] = $table.'.'.$from; 176 | } 177 | 178 | return $properties; 179 | } 180 | 181 | public function join($alias_from, $rel_name, $alias_to_nr, $conditions = array()) 182 | { 183 | $alias_to = 't'.$alias_to_nr; 184 | 185 | $alias_through = array($this->table_through, $alias_to.'_through'); 186 | $alias_to_table = array(call_user_func(array($this->model_to, 'table')), $alias_to); 187 | 188 | $models = array( 189 | $rel_name.'_through' => array( 190 | 'model' => null, 191 | 'connection' => call_user_func(array($this->model_to, 'connection')), 192 | 'table' => $alias_through, 193 | 'primary_key' => null, 194 | 'join_type' => \Arr::get($conditions, 'join_type') ?: \Arr::get($this->conditions, 'join_type', 'left'), 195 | 'join_on' => array(), 196 | 'columns' => $this->select_through($alias_to.'_through'), 197 | 'rel_name' => $this->model_through, 198 | 'relation' => $this, 199 | ), 200 | $rel_name => array( 201 | 'model' => $this->model_to, 202 | 'connection' => call_user_func(array($this->model_to, 'connection')), 203 | 'table' => $alias_to_table, 204 | 'primary_key' => call_user_func(array($this->model_to, 'primary_key')), 205 | 'join_type' => \Arr::get($conditions, 'join_type') ?: \Arr::get($this->conditions, 'join_type', 'left'), 206 | 'join_on' => array(), 207 | 'columns' => $this->select($alias_to), 208 | 'rel_name' => strpos($rel_name, '.') ? substr($rel_name, strrpos($rel_name, '.') + 1) : $rel_name, 209 | 'relation' => $this, 210 | 'where' => \Arr::get($conditions, 'where', array()), 211 | ), 212 | ); 213 | 214 | reset($this->key_from); 215 | foreach ($this->key_through_from as $key) 216 | { 217 | $models[$rel_name.'_through']['join_on'][] = array($alias_from.'.'.current($this->key_from), '=', $alias_to.'_through.'.$key); 218 | next($this->key_from); 219 | } 220 | 221 | reset($this->key_to); 222 | foreach ($this->key_through_to as $key) 223 | { 224 | $models[$rel_name]['join_on'][] = array($alias_to.'_through.'.$key, '=', $alias_to.'.'.current($this->key_to)); 225 | next($this->key_to); 226 | } 227 | 228 | foreach (array(\Arr::get($this->conditions, 'where', array()), \Arr::get($conditions, 'join_on', array())) as $c) 229 | { 230 | foreach ($c as $key => $condition) 231 | { 232 | ! is_array($condition) and $condition = array($key, '=', $condition); 233 | if ( ! $condition[0] instanceof \Fuel\Core\Database_Expression and strpos($condition[0], '.') === false) 234 | { 235 | $condition[0] = $alias_to.'.'.$condition[0]; 236 | } 237 | if (count($condition) == 2) // From Query::_where() 238 | { 239 | $condition = array($condition[0], '=', $condition[1]); 240 | } 241 | is_string($condition[2]) and $condition[2] = \Db::quote($condition[2], $models[$rel_name]['connection']); 242 | 243 | $models[$rel_name]['join_on'][] = $condition; 244 | } 245 | } 246 | 247 | $order_by = \Arr::get($conditions, 'order_by') ?: \Arr::get($this->conditions, 'order_by', array()); 248 | foreach ($order_by as $key => $direction) 249 | { 250 | if ( ! $key instanceof \Fuel\Core\Database_Expression and strpos($key, '.') === false) 251 | { 252 | $key = $alias_to.'.'.$key; 253 | } 254 | else 255 | { 256 | $key = str_replace(array($alias_through[0], $alias_to_table[0]), array($alias_through[1], $alias_to_table[1]), $key); 257 | } 258 | $models[$rel_name]['order_by'][$key] = $direction; 259 | } 260 | 261 | return $models; 262 | } 263 | 264 | public function save($model_from, $models_to, $original_model_ids, $parent_saved, $cascade) 265 | { 266 | if ( ! $parent_saved) 267 | { 268 | return; 269 | } 270 | 271 | if ( ! is_array($models_to) and ($models_to = is_null($models_to) ? array() : $models_to) !== array()) 272 | { 273 | throw new \FuelException('Assigned relationships must be an array or null, given relationship value for '. 274 | $this->name.' is invalid.'); 275 | } 276 | $original_model_ids === null and $original_model_ids = array(); 277 | $del_rels = $original_model_ids; 278 | 279 | foreach ($models_to as $key => $model_to) 280 | { 281 | if ( ! $model_to instanceof $this->model_to) 282 | { 283 | throw new \FuelException('Invalid Model instance added to relations in this model.'); 284 | } 285 | 286 | // Save if it's a yet unsaved object 287 | if ($model_to->is_new()) 288 | { 289 | $model_to->save(false); 290 | } 291 | 292 | $current_model_id = $model_to ? $model_to->implode_pk($model_to) : null; 293 | 294 | // Check if the model was already assigned, if not INSERT relationships: 295 | if ( ! in_array($current_model_id, $original_model_ids)) 296 | { 297 | $ids = array(); 298 | reset($this->key_from); 299 | foreach ($this->key_through_from as $pk) 300 | { 301 | $ids[$pk] = $model_from->{current($this->key_from)}; 302 | next($this->key_from); 303 | } 304 | 305 | reset($this->key_to); 306 | foreach ($this->key_through_to as $pk) 307 | { 308 | $ids[$pk] = $model_to->{current($this->key_to)}; 309 | next($this->key_to); 310 | } 311 | 312 | \DB::insert($this->table_through)->set($ids)->execute(call_user_func(array($model_from, 'connection'), true)); 313 | $original_model_ids[] = $current_model_id; // prevents inserting it a second time 314 | } 315 | else 316 | { 317 | // unset current model from from array of new relations 318 | unset($del_rels[array_search($current_model_id, $original_model_ids)]); 319 | } 320 | 321 | // ensure correct pk assignment 322 | if ($key != $current_model_id) 323 | { 324 | $model_from->unfreeze(); 325 | $rel = $model_from->_relate(); 326 | if ( ! empty($rel[$this->name][$key]) and $rel[$this->name][$key] === $model_to) 327 | { 328 | unset($rel[$this->name][$key]); 329 | } 330 | $rel[$this->name][$current_model_id] = $model_to; 331 | $model_from->_relate($rel); 332 | $model_from->freeze(); 333 | } 334 | } 335 | 336 | // If any ids are left in $del_rels they are no longer assigned, DELETE the relationships: 337 | foreach ($del_rels as $original_model_id) 338 | { 339 | $query = \DB::delete($this->table_through); 340 | 341 | reset($this->key_from); 342 | foreach ($this->key_through_from as $key) 343 | { 344 | $query->where($key, '=', $model_from->{current($this->key_from)}); 345 | next($this->key_from); 346 | } 347 | 348 | $to_keys = count($this->key_to) == 1 ? array($original_model_id) : explode('][', substr($original_model_id, 1, -1)); 349 | reset($to_keys); 350 | foreach ($this->key_through_to as $key) 351 | { 352 | $query->where($key, '=', current($to_keys)); 353 | next($to_keys); 354 | } 355 | 356 | $query->execute(call_user_func(array($model_from, 'connection'), true)); 357 | } 358 | 359 | $cascade = is_null($cascade) ? $this->cascade_save : (bool) $cascade; 360 | if ($cascade and ! empty($models_to)) 361 | { 362 | foreach ($models_to as $m) 363 | { 364 | $m->save(); 365 | } 366 | } 367 | } 368 | 369 | public function delete($model_from, $parent_deleted, $cascade) 370 | { 371 | // fetch all related records 372 | $model_from->get($this->name); 373 | 374 | if ( ! $parent_deleted) 375 | { 376 | if ($this->cascade_check and ! empty($model_from->{$this->name})) 377 | { 378 | throw new \Orm\DeleteConstraintViolation($this->name); 379 | } 380 | 381 | return; 382 | } 383 | 384 | // break current relations, may be incomplete 385 | $model_from->unfreeze(); 386 | 387 | $rels = $model_from->_relate(); 388 | unset($rels[$this->name]); 389 | $model_from->_relate($rels); 390 | 391 | $model_from->freeze(); 392 | 393 | // Delete all relationship entries for the model_from 394 | $this->delete_related($model_from); 395 | 396 | $cascade = is_null($cascade) ? $this->cascade_delete : (bool) $cascade; 397 | 398 | if ($cascade) 399 | { 400 | // yes, delete the reclated records 401 | foreach ($model_from->{$this->name} as $m) 402 | { 403 | $m->delete(); 404 | } 405 | } 406 | } 407 | 408 | public function delete_related($model_from) 409 | { 410 | // Delete all relationship entries for the model_from 411 | $query = \DB::delete($this->table_through); 412 | reset($this->key_from); 413 | foreach ($this->key_through_from as $key) 414 | { 415 | $query->where($key, '=', $model_from->{current($this->key_from)}); 416 | next($this->key_from); 417 | } 418 | $query->execute(call_user_func(array($model_from, 'connection'), true)); 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /classes/model/soft.php: -------------------------------------------------------------------------------- 1 | 0 ? array_shift($temp_args) : 'all'; 145 | $options = count($temp_args) > 0 ? array_shift($temp_args) : array(); 146 | 147 | return static::deleted($find_type, $options); 148 | } 149 | 150 | return parent::__callStatic($method, $args); 151 | } 152 | 153 | protected function delete_self() 154 | { 155 | // If soft deleting has been disabled then just call the parent's delete 156 | if ($this->_disable_soft_delete) 157 | { 158 | return parent::delete_self(); 159 | } 160 | 161 | $deleted_column = static::soft_delete_property('deleted_field', static::$_default_field_name); 162 | $mysql_timestamp = static::soft_delete_property('mysql_timestamp', static::$_default_mysql_timestamp); 163 | 164 | // Generate the correct timestamp and save it 165 | $this->{$deleted_column} = $mysql_timestamp ? \Date::forge()->format('mysql') : \Date::forge()->get_timestamp(); 166 | $result = $this->save(false); 167 | 168 | return $result; 169 | } 170 | 171 | /** 172 | * Permanently deletes records using the parent Model delete function 173 | * 174 | * @param $cascade boolean 175 | * @param $use_transaction boolean 176 | * 177 | * @return boolean 178 | */ 179 | public function purge($cascade = null, $use_transaction = false) 180 | { 181 | $this->observe('before_purge'); 182 | 183 | $this->_disable_soft_delete = true; 184 | $result = parent::delete($cascade, $use_transaction); 185 | $this->_disable_soft_delete = false; 186 | 187 | $this->observe('after_purge'); 188 | 189 | return $result; 190 | } 191 | 192 | /** 193 | * Returns true unless the related model is not soft or temporal 194 | */ 195 | protected function should_cascade_delete($rel) 196 | { 197 | // Because temporal includes soft delete functionality it can be deleted too 198 | if ( ! is_subclass_of($rel->model_to, 'Orm\Model_Soft') && ! is_subclass_of($rel->model_to, 'Orm\Model_Temporal')) 199 | { 200 | // Throw if other is not soft 201 | throw new RelationNotSoft('Both sides of the relation must be subclasses of Model_Soft or Model_Temporal if cascade delete is true. '.$rel->model_to.' was found instead.'); 202 | } 203 | 204 | return true; 205 | } 206 | 207 | /** 208 | * Allows a soft deleted entry to be restored. 209 | */ 210 | public function restore($cascade_restore = null) 211 | { 212 | $deleted_column = static::soft_delete_property('deleted_field', static::$_default_field_name); 213 | $this->{$deleted_column} = null; 214 | 215 | //Loop through all relations and restore if we are cascading. 216 | $this->freeze(); 217 | foreach ($this->relations() as $rel_name => $rel) 218 | { 219 | //get the cascade delete status 220 | $rel_cascade = is_null($cascade_restore) ? $rel->cascade_delete : (bool) $cascade_restore; 221 | 222 | //Make sure that the other model is soft delete too 223 | if ($rel_cascade) 224 | { 225 | if (! is_subclass_of($rel->model_to, 'Orm\Model_Soft')) 226 | { 227 | //Throw if other is not soft 228 | throw new RelationNotSoft('Both sides of the relation must be subclasses of Model_Soft if cascade delete is true'); 229 | } 230 | 231 | if (get_class($rel) != 'Orm\ManyMany') 232 | { 233 | $model_to = $rel->model_to; 234 | $model_to::disable_filter(); 235 | 236 | //Loop through and call restore on all the models 237 | $models = $rel->get($this); 238 | 239 | // for hasmany/manymany relations 240 | if (is_array($models)) 241 | { 242 | foreach ($models as $model) 243 | { 244 | $model->restore($cascade_restore); 245 | } 246 | } 247 | // for hasone/belongsto relations 248 | else 249 | { 250 | $models->restore($cascade_restore); 251 | } 252 | 253 | $model_to::enable_filter(); 254 | } 255 | } 256 | } 257 | $this->unfreeze(); 258 | 259 | return $this->save(); 260 | } 261 | 262 | /** 263 | * Alias of restore() 264 | */ 265 | public function undelete() 266 | { 267 | return $this->restore(); 268 | } 269 | 270 | /** 271 | * Overrides the query method to allow soft delete items to be filtered out. 272 | */ 273 | public static function query($options = array()) 274 | { 275 | $query = Query_Soft::forge(get_called_class(), static::connection(), $options); 276 | 277 | if (static::get_filter_status()) 278 | { 279 | //Make sure we are filtering out soft deleted items 280 | $query->set_soft_filter(static::soft_delete_property('deleted_field', static::$_default_field_name)); 281 | } 282 | 283 | return $query; 284 | } 285 | 286 | /** 287 | * Alisas of find() but selects only deleted entries rather than non-deleted 288 | * ones. 289 | */ 290 | public static function deleted($id = null, array $options = array()) 291 | { 292 | //Make sure we are not filtering out soft deleted items 293 | $deleted_column = static::soft_delete_property('deleted_field', static::$_default_field_name); 294 | $options['where'][] = array($deleted_column, 'IS NOT', null); 295 | 296 | static::disable_filter(); 297 | $result = parent::find($id, $options); 298 | static::enable_filter(); 299 | 300 | return $result; 301 | } 302 | 303 | } 304 | -------------------------------------------------------------------------------- /classes/model/temporal.php: -------------------------------------------------------------------------------- 1 | where('id', $id) 153 | ->where($timestamp_start_name, '<=', $timestamp) 154 | ->where($timestamp_end_name, '>', $timestamp); 155 | self::enable_primary_key_check(); 156 | 157 | //Make sure the temporal stuff is activated 158 | $query->set_temporal_properties($timestamp, $timestamp_end_name, $timestamp_start_name); 159 | 160 | foreach ($relations as $relation) 161 | { 162 | $query->related($relation); 163 | } 164 | 165 | $query_result = $query->get_one(); 166 | 167 | // If the query did not return a result but null, then we cannot call 168 | // set_lazy_timestamp on it without throwing errors 169 | if ( $query_result !== null ) 170 | { 171 | $query_result->set_lazy_timestamp($timestamp); 172 | } 173 | return $query_result; 174 | } 175 | 176 | private function set_lazy_timestamp($timestamp) 177 | { 178 | $this->_lazy_timestamp = $timestamp; 179 | } 180 | 181 | /** 182 | * Overrides Model::get() to allow lazy loaded relations to be filtered 183 | * temporaly. 184 | * 185 | * @param string $property 186 | * @return mixed 187 | */ 188 | public function & get($property, array $conditions = array()) 189 | { 190 | // if a timestamp is set and that we have a temporal relation 191 | $rel = static::relations($property); 192 | if ($rel && is_subclass_of($rel->model_to, 'Orm\Model_Temporal')) 193 | { 194 | // find a specific revision or the newest if lazy timestamp is null 195 | $lazy_timestamp = $this->_lazy_timestamp ?: static::temporal_property('max_timestamp') - 1; 196 | //add the filtering and continue with the parent's behavour 197 | $class_name = $rel->model_to; 198 | 199 | $class_name::make_query_temporal($lazy_timestamp); 200 | $result =& parent::get($property, $conditions); 201 | $class_name::make_query_temporal(null); 202 | 203 | return $result; 204 | } 205 | 206 | return parent::get($property, $conditions); 207 | } 208 | 209 | /** 210 | * When a timestamp is set any query objects produced by this temporal model 211 | * will behave the same as find_revision() 212 | * 213 | * @param array $timestamp 214 | */ 215 | private static function make_query_temporal($timestamp) 216 | { 217 | $class = get_called_class(); 218 | static::$_lazy_filtered_classes[$class] = $timestamp; 219 | } 220 | 221 | /** 222 | * Overrides Model::query to provide a Temporal_Query 223 | * 224 | * @param array $options 225 | * @return Query_Temporal 226 | */ 227 | public static function query($options = array()) 228 | { 229 | $timestamp_start_name = static::temporal_property('start_column'); 230 | $timestamp_end_name = static::temporal_property('end_column'); 231 | $max_timestamp = static::temporal_property('max_timestamp'); 232 | 233 | $query = Query_Temporal::forge(get_called_class(), static::connection(), $options) 234 | ->set_temporal_properties($max_timestamp, $timestamp_end_name, $timestamp_start_name); 235 | 236 | //Check if we need to add filtering 237 | $class = get_called_class(); 238 | $timestamp = \Arr::get(static::$_lazy_filtered_classes, $class, null); 239 | 240 | if( ! is_null($timestamp)) 241 | { 242 | $query->where($timestamp_start_name, '<=', $timestamp) 243 | ->where($timestamp_end_name, '>', $timestamp); 244 | } 245 | elseif(static::get_primary_key_status() and ! static::get_primary_key_id_only_status()) 246 | { 247 | $query->where($timestamp_end_name, $max_timestamp); 248 | } 249 | 250 | return $query; 251 | } 252 | 253 | /** 254 | * Returns a list of revisions between the given times with the most recent 255 | * first. This does not load relations. 256 | * 257 | * @param int|string $id 258 | * @param timestamp $earliestTime 259 | * @param timestamp $latestTime 260 | */ 261 | public static function find_revisions_between($id, $earliestTime = null, $latestTime = null) 262 | { 263 | $timestamp_start_name = static::temporal_property('start_column'); 264 | $max_timestamp = static::temporal_property('max_timestamp'); 265 | 266 | if ($earliestTime == null) 267 | { 268 | $earliestTime = 0; 269 | } 270 | 271 | if($latestTime == null) 272 | { 273 | $latestTime = $max_timestamp; 274 | } 275 | 276 | static::disable_primary_key_check(); 277 | //Select all revisions within the given range. 278 | $query = static::query() 279 | ->where('id', $id) 280 | ->where($timestamp_start_name, '>=', $earliestTime) 281 | ->where($timestamp_start_name, '<=', $latestTime); 282 | static::enable_primary_key_check(); 283 | 284 | $revisions = $query->get(); 285 | return $revisions; 286 | } 287 | 288 | /** 289 | * Overrides the default find method to allow the latest revision to be found 290 | * by default. 291 | * 292 | * If any new options to find are added the switch statement will have to be 293 | * updated too. 294 | * 295 | * @param type $id 296 | * @param array $options 297 | * @return type 298 | */ 299 | public static function find($id = null, $options = null) 300 | { 301 | // options must be a nullable array 302 | if ( ! is_null($options) and ! is_array($options)) 303 | { 304 | throw new \FuelException(__FUNCTION__ . ': Argument #2 ($options) must be of type array, ' . gettype($options) . ' given'); 305 | } 306 | 307 | $timestamp_end_name = static::temporal_property('end_column'); 308 | $max_timestamp = static::temporal_property('max_timestamp'); 309 | 310 | switch ($id) 311 | { 312 | case 'all': 313 | case 'first': 314 | case 'last': 315 | break; 316 | default: 317 | is_null($options) and $options = array(); 318 | $id = (array) $id; 319 | $count = 0; 320 | foreach(static::getNonTimestampPks() as $key) 321 | { 322 | $options['where'][] = array($key, $id[$count]); 323 | 324 | $count++; 325 | } 326 | break; 327 | } 328 | 329 | $options['where'][] = array($timestamp_end_name, $max_timestamp); 330 | 331 | static::enable_id_only_primary_key(); 332 | $result = parent::find($id, $options); 333 | static::disable_id_only_primary_key(); 334 | 335 | return $result; 336 | } 337 | 338 | /** 339 | * Returns an array of the primary keys that are not related to temporal 340 | * timestamp information. 341 | */ 342 | public static function getNonTimestampPks() 343 | { 344 | $timestamp_start_name = static::temporal_property('start_column'); 345 | $timestamp_end_name = static::temporal_property('end_column'); 346 | 347 | $pks = array(); 348 | foreach(parent::primary_key() as $key) 349 | { 350 | if ($key != $timestamp_start_name && $key != $timestamp_end_name) 351 | { 352 | $pks[] = $key; 353 | } 354 | } 355 | 356 | return $pks; 357 | } 358 | 359 | /** 360 | * Overrides the save method to allow temporal models to be 361 | * @param boolean $cascade 362 | * @param boolean $use_transaction 363 | * @param boolean $skip_temporal Skips temporal filtering on initial inserts. Should not be used! 364 | * @return boolean 365 | */ 366 | public function save($cascade = null, $use_transaction = false) 367 | { 368 | // Load temporal properties. 369 | $timestamp_start_name = static::temporal_property('start_column'); 370 | $timestamp_end_name = static::temporal_property('end_column'); 371 | $mysql_timestamp = static::temporal_property('mysql_timestamp'); 372 | 373 | $max_timestamp = static::temporal_property('max_timestamp'); 374 | $current_timestamp = $mysql_timestamp ? 375 | \Date::forge()->format('mysql') : 376 | \Date::forge()->get_timestamp(); 377 | 378 | // If this is new then just call the parent and let everything happen as normal 379 | if ($this->is_new()) 380 | { 381 | static::disable_primary_key_check(); 382 | $this->{$timestamp_start_name} = $current_timestamp; 383 | $this->{$timestamp_end_name} = $max_timestamp; 384 | static::enable_primary_key_check(); 385 | 386 | // Make sure save will populate the PK 387 | static::enable_id_only_primary_key(); 388 | $result = parent::save($cascade, $use_transaction); 389 | static::disable_id_only_primary_key(); 390 | 391 | return $result; 392 | } 393 | // If this is an update then set a new PK, save and then insert a new row 394 | else 395 | { 396 | // run the before save observers before checking the diff 397 | $this->observe('before_save'); 398 | 399 | // then disable it so it doesn't get executed by parent::save() 400 | $this->disable_event('before_save'); 401 | 402 | $diff = $this->get_diff(); 403 | 404 | if (count($diff[0]) > 0) 405 | { 406 | // Take a copy of this model 407 | $revision = clone $this; 408 | 409 | // Give that new model an end time of the current time after resetting back to the old data 410 | $revision->set($this->_original); 411 | 412 | self::disable_primary_key_check(); 413 | $revision->{$timestamp_end_name} = $current_timestamp; 414 | self::enable_primary_key_check(); 415 | 416 | // Make sure relations stay the same 417 | $revision->_original_relations = $this->_data_relations; 418 | 419 | // save that, now we have our archive 420 | self::enable_id_only_primary_key(); 421 | $revision_result = $revision->overwrite(false, $use_transaction); 422 | self::disable_id_only_primary_key(); 423 | 424 | if ( ! $revision_result) 425 | { 426 | // If the revision did not save then stop the process so the user can do something. 427 | return false; 428 | } 429 | 430 | // Now that the old data is saved update the current object so its end timestamp is now 431 | self::disable_primary_key_check(); 432 | $this->{$timestamp_start_name} = $current_timestamp; 433 | self::enable_primary_key_check(); 434 | 435 | $result = parent::save($cascade, $use_transaction); 436 | } 437 | else 438 | { 439 | // If nothing has changed call parent::save() to insure relations are saved too 440 | $result = parent::save($cascade, $use_transaction); 441 | } 442 | 443 | // make sure the before save event is enabled again 444 | $this->enable_event('before_save'); 445 | 446 | return $result; 447 | } 448 | } 449 | 450 | /** 451 | * ALlows an entry to be updated without having to insert a new row. 452 | * This will not record any changed data as a new revision. 453 | * 454 | * Takes the same options as Model::save() 455 | */ 456 | public function overwrite($cascade = null, $use_transaction = false) 457 | { 458 | return parent::save($cascade, $use_transaction); 459 | } 460 | 461 | /** 462 | * Restores the entity to this state. 463 | * 464 | * @return boolean 465 | */ 466 | public function restore() 467 | { 468 | $timestamp_end_name = static::temporal_property('end_column'); 469 | $max_timestamp = static::temporal_property('max_timestamp'); 470 | 471 | // check to see if there is a currently active row, if so then don't 472 | // restore anything. 473 | $activeRow = static::find('first', array( 474 | 'where' => array( 475 | array('id', $this->id), 476 | array($timestamp_end_name, $max_timestamp), 477 | ), 478 | )); 479 | 480 | if(is_null($activeRow)) 481 | { 482 | // No active row was found so we are ok to go and restore the this 483 | // revision 484 | $timestamp_start_name = static::temporal_property('start_column'); 485 | $mysql_timestamp = static::temporal_property('mysql_timestamp'); 486 | 487 | $max_timestamp = static::temporal_property('max_timestamp'); 488 | $current_timestamp = $mysql_timestamp ? 489 | \Date::forge()->format('mysql') : 490 | \Date::forge()->get_timestamp(); 491 | 492 | // Make sure this is saved as a new entry 493 | $this->_is_new = true; 494 | 495 | // Update timestamps 496 | static::disable_primary_key_check(); 497 | $this->{$timestamp_start_name} = $current_timestamp; 498 | $this->{$timestamp_end_name} = $max_timestamp; 499 | 500 | // Save 501 | $result = parent::save(); 502 | static::enable_primary_key_check(); 503 | 504 | return $result; 505 | } 506 | 507 | return false; 508 | } 509 | 510 | /** 511 | * Deletes all revisions of this entity permantly. 512 | */ 513 | public function purge() 514 | { 515 | // Get a clean query object so there's no temporal filtering 516 | $query = parent::query(); 517 | // Then select and delete 518 | return $query->where('id', $this->id) 519 | ->delete(); 520 | } 521 | 522 | /** 523 | * Overrides update to remove PK checking when performing an update. 524 | */ 525 | public function update() 526 | { 527 | static::disable_primary_key_check(); 528 | $result = parent::update(); 529 | static::enable_primary_key_check(); 530 | 531 | return $result; 532 | } 533 | 534 | /** 535 | * Allows correct PKs to be added when performing updates 536 | * 537 | * @param Query $query 538 | */ 539 | protected function add_primary_keys_to_where($query) 540 | { 541 | $primary_key = static::$_primary_key; 542 | 543 | foreach ($primary_key as $pk) 544 | { 545 | $query->where($pk, '=', $this->_original[$pk]); 546 | } 547 | } 548 | 549 | /** 550 | * Overrides the parent primary_key method to allow primaray key enforcement 551 | * to be turned off when updating a temporal model. 552 | */ 553 | public static function primary_key() 554 | { 555 | $id_only = static::get_primary_key_id_only_status(); 556 | $pk_status = static::get_primary_key_status(); 557 | 558 | if ($id_only) 559 | { 560 | return static::getNonTimestampPks(); 561 | } 562 | 563 | if ($pk_status && ! $id_only) 564 | { 565 | return static::$_primary_key; 566 | } 567 | 568 | return array(); 569 | } 570 | 571 | public function delete($cascade = null, $use_transaction = false) 572 | { 573 | // If we are using a transcation then make sure it's started 574 | if ($use_transaction) 575 | { 576 | $db = \Database_Connection::instance(static::connection(true)); 577 | $db->start_transaction(); 578 | } 579 | 580 | // Call the observers 581 | $this->observe('before_delete'); 582 | 583 | // Load temporal properties. 584 | $timestamp_end_name = static::temporal_property('end_column'); 585 | $mysql_timestamp = static::temporal_property('mysql_timestamp'); 586 | 587 | // Generate the correct timestamp and save it 588 | $current_timestamp = $mysql_timestamp ? 589 | \Date::forge()->format('mysql') : 590 | \Date::forge()->get_timestamp(); 591 | 592 | static::disable_primary_key_check(); 593 | $this->{$timestamp_end_name} = $current_timestamp; 594 | static::enable_primary_key_check(); 595 | 596 | // Loop through all relations and delete if we are cascading. 597 | $this->freeze(); 598 | foreach ($this->relations() as $rel_name => $rel) 599 | { 600 | // get the cascade delete status 601 | $relCascade = is_null($cascade) ? $rel->cascade_delete : (bool) $cascade; 602 | 603 | if ($relCascade) 604 | { 605 | if(get_class($rel) != 'Orm\ManyMany') 606 | { 607 | // Loop through and call delete on all the models 608 | foreach($rel->get($this) as $model) 609 | { 610 | $model->delete($cascade); 611 | } 612 | } 613 | } 614 | } 615 | $this->unfreeze(); 616 | 617 | parent::save(); 618 | 619 | $this->observe('after_delete'); 620 | 621 | // Make sure the transaction is committed if needed 622 | $use_transaction and $db->commit_transaction(); 623 | 624 | return $this; 625 | } 626 | 627 | /** 628 | * Disables PK checking 629 | */ 630 | private static function disable_primary_key_check() 631 | { 632 | $class = get_called_class(); 633 | self::$_pk_check_disabled[$class] = false; 634 | } 635 | 636 | /** 637 | * Enables PK checking 638 | */ 639 | private static function enable_primary_key_check() 640 | { 641 | $class = get_called_class(); 642 | self::$_pk_check_disabled[$class] = true; 643 | } 644 | 645 | /** 646 | * Returns true if the PK checking should be performed. Defaults to true 647 | */ 648 | private static function get_primary_key_status() 649 | { 650 | $class = get_called_class(); 651 | return \Arr::get(self::$_pk_check_disabled, $class, true); 652 | } 653 | 654 | /** 655 | * Returns true if the PK should only contain the ID. Defaults to false 656 | */ 657 | private static function get_primary_key_id_only_status() 658 | { 659 | $class = get_called_class(); 660 | return \Arr::get(self::$_pk_id_only, $class, false); 661 | } 662 | 663 | /** 664 | * Makes all PKs returned 665 | */ 666 | private static function disable_id_only_primary_key() 667 | { 668 | $class = get_called_class(); 669 | self::$_pk_id_only[$class] = false; 670 | } 671 | 672 | /** 673 | * Makes only id returned as PK 674 | */ 675 | private static function enable_id_only_primary_key() 676 | { 677 | $class = get_called_class(); 678 | self::$_pk_id_only[$class] = true; 679 | } 680 | 681 | } 682 | -------------------------------------------------------------------------------- /classes/observer.php: -------------------------------------------------------------------------------- 1 | {$event}($instance); 37 | } 38 | } 39 | 40 | /** 41 | * Create an instance of this observer 42 | * 43 | * @param string name of the model class 44 | */ 45 | public static function instance($model_class) 46 | { 47 | $observer = get_called_class(); 48 | if (empty(static::$_instances[$observer][$model_class])) 49 | { 50 | static::$_instances[$observer][$model_class] = new static($model_class); 51 | } 52 | 53 | return static::$_instances[$observer][$model_class]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /classes/observer/createdat.php: -------------------------------------------------------------------------------- 1 | _mysql_timestamp = isset($props['mysql_timestamp']) ? $props['mysql_timestamp'] : static::$mysql_timestamp; 56 | $this->_property = isset($props['property']) ? $props['property'] : static::$property; 57 | $this->_overwrite = isset($props['overwrite']) ? $props['overwrite'] : true; 58 | } 59 | 60 | /** 61 | * Set the CreatedAt property to the current time. 62 | * 63 | * @param Model Model object subject of this observer method 64 | */ 65 | public function before_insert(Model $obj) 66 | { 67 | if ($this->_overwrite or empty($obj->{$this->_property})) 68 | { 69 | $obj->{$this->_property} = $this->_mysql_timestamp ? \Date::time()->format('mysql') : \Date::time()->get_timestamp(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /classes/observer/self.php: -------------------------------------------------------------------------------- 1 | _source = isset($props['source']) ? $props['source'] : static::$source; 80 | $this->_property = isset($props['property']) ? $props['property'] : static::$property; 81 | $this->_separator = isset($props['separator']) ? $props['separator'] : static::$separator; 82 | $this->_unique = isset($props['unique']) ? (bool) $props['unique'] : static::$unique; 83 | $this->_overwrite = isset($props['overwrite']) ? (bool) $props['overwrite'] : static::$overwrite; 84 | } 85 | 86 | /** 87 | * Creates a slug (unique by default) and adds it to the object 88 | * 89 | * @param Model Model object subject of this observer method 90 | */ 91 | public function before_insert(Model $obj) 92 | { 93 | // slug should be overwritten if it is enabled to be or there is no manually assigned value 94 | $overwrite = $this->_overwrite === true || empty($obj->{$this->_property}); 95 | $slug = $obj->{$this->_property}; 96 | 97 | // is this a soft model? 98 | if ($obj instanceof Model_Soft) 99 | { 100 | $class = get_class($obj); 101 | 102 | $class::disable_filter(); 103 | } 104 | 105 | // query to check for existence of this slug 106 | $query = $obj->query(); 107 | 108 | // only determine the slug if it should be overwritten 109 | // fill the query with appropriate where condition 110 | if ($overwrite === true) 111 | { 112 | $properties = (array) $this->_source; 113 | $source = ''; 114 | foreach ($properties as $property) 115 | { 116 | $source .= $this->_separator.$obj->{$property}; 117 | } 118 | $slug = \Inflector::friendly_title(substr($source, 1), $this->_separator, true); 119 | 120 | $query->where($this->_property, 'like', $slug.'%'); 121 | } 122 | else 123 | { 124 | $query->where($this->_property, $slug); 125 | } 126 | 127 | if($this->_unique === true) 128 | { 129 | // query to check for existence of this slug 130 | $query = $obj->query()->where($this->_property, 'like', $slug.'%'); 131 | 132 | // is this a temporal model? 133 | if ($obj instanceof Model_Temporal) 134 | { 135 | // add a filter to only check current revisions excluding the current object 136 | $class = get_class($obj); 137 | $query->where($class::temporal_property('end_column'), '=', $class::temporal_property('max_timestamp')); 138 | foreach($class::getNonTimestampPks() as $key) 139 | { 140 | $query->where($key, '!=', $obj->{$key}); 141 | } 142 | } 143 | 144 | // do we have records with this slug? 145 | $same = $query->get(); 146 | 147 | // is this a soft model? 148 | if ($obj instanceof Model_Soft) 149 | { 150 | $class::enable_filter(); 151 | } 152 | 153 | // make sure our slug is unique 154 | if ( ! empty($same)) 155 | { 156 | if ($overwrite === false) 157 | { 158 | throw new \FuelException('Slug ' . $slug . ' already exists.'); 159 | } 160 | 161 | $max = -1; 162 | 163 | foreach ($same as $record) 164 | { 165 | if (preg_match('/^'.$slug.'(?:-([0-9]+))?$/', $record->{$this->_property}, $matches)) 166 | { 167 | $index = isset($matches[1]) ? (int) $matches[1] : 0; 168 | $max < $index and $max = $index; 169 | } 170 | } 171 | 172 | $max < 0 or $slug .= $this->_separator.($max + 1); 173 | } 174 | } 175 | 176 | $obj->{$this->_property} = $slug; 177 | } 178 | 179 | /** 180 | * Creates a new slug (unique by default) and update the object 181 | * 182 | * @param Model Model object subject of this observer method 183 | */ 184 | public function before_update(Model $obj) 185 | { 186 | // determine the slug 187 | $properties = (array) $this->_source; 188 | $source = ''; 189 | foreach ($properties as $property) 190 | { 191 | $source .= $this->_separator.$obj->{$property}; 192 | } 193 | $slug = \Inflector::friendly_title(substr($source, 1), $this->_separator, true); 194 | 195 | // update it if it's different from the current one 196 | // and is not manually assigned 197 | if ($obj->{$this->_property} !== $slug) 198 | { 199 | $overwrite = $this->_overwrite; 200 | 201 | if ($overwrite === false and ! $obj->is_changed($this->_property)) 202 | { 203 | $this->_overwrite = true; 204 | } 205 | 206 | $this->before_insert($obj); 207 | 208 | $this->_overwrite = $overwrite; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /classes/observer/typing.php: -------------------------------------------------------------------------------- 1 | 'before', 32 | 'after_save' => 'after', 33 | 'after_load' => 'after', 34 | ); 35 | 36 | /** 37 | * @var array db type mappings 38 | */ 39 | public static $type_mappings = array( 40 | 'tinyint' => 'int', 41 | 'smallint' => 'int', 42 | 'mediumint' => 'int', 43 | 'bigint' => 'int', 44 | 'integer' => 'int', 45 | 'double' => 'float', 46 | 'decimal' => 'float', 47 | 'tinytext' => 'text', 48 | 'mediumtext' => 'text', 49 | 'longtext' => 'text', 50 | 'boolean' => 'bool', 51 | 'time_unix' => 'time', 52 | 'time_mysql' => 'time', 53 | 'timestamp' => 'time', 54 | 'datetime' => 'time', 55 | 'date' => 'time', 56 | ); 57 | 58 | /** 59 | * @var array db data types with the method(s) to use, optionally pre- or post-database 60 | */ 61 | public static $type_methods = array( 62 | 'varchar' => array( 63 | 'before' => 'Orm\\Observer_Typing::type_string', 64 | ), 65 | 'int' => array( 66 | 'before' => 'Orm\\Observer_Typing::type_integer', 67 | 'after' => 'Orm\\Observer_Typing::type_integer', 68 | ), 69 | 'float' => array( 70 | 'before' => 'Orm\\Observer_Typing::type_float_before', 71 | 'after' => 'Orm\\Observer_Typing::type_float_after', 72 | ), 73 | 'text' => array( 74 | 'before' => 'Orm\\Observer_Typing::type_string', 75 | ), 76 | 'set' => array( 77 | 'before' => 'Orm\\Observer_Typing::type_set_before', 78 | 'after' => 'Orm\\Observer_Typing::type_set_after', 79 | ), 80 | 'enum' => array( 81 | 'before' => 'Orm\\Observer_Typing::type_set_before', 82 | ), 83 | 'bool' => array( 84 | 'before' => 'Orm\\Observer_Typing::type_bool_to_int', 85 | 'after' => 'Orm\\Observer_Typing::type_bool_from_int', 86 | ), 87 | 'serialize' => array( 88 | 'before' => 'Orm\\Observer_Typing::type_serialize', 89 | 'after' => 'Orm\\Observer_Typing::type_unserialize', 90 | ), 91 | 'encrypt' => array( 92 | 'before' => 'Orm\\Observer_Typing::type_encrypt', 93 | 'after' => 'Orm\\Observer_Typing::type_decrypt', 94 | ), 95 | 'json' => array( 96 | 'before' => 'Orm\\Observer_Typing::type_json_encode', 97 | 'after' => 'Orm\\Observer_Typing::type_json_decode', 98 | ), 99 | 'time' => array( 100 | 'before' => 'Orm\\Observer_Typing::type_time_encode', 101 | 'after' => 'Orm\\Observer_Typing::type_time_decode', 102 | ), 103 | ); 104 | 105 | /** 106 | * @var array regexes for db types with the method(s) to use, optionally pre- or post-database 107 | */ 108 | public static $regex_methods = array( 109 | '/^decimal:([0-9])/uiD' => array( 110 | 'before' => 'Orm\\Observer_Typing::type_decimal_before', 111 | 'after' => 'Orm\\Observer_Typing::type_decimal_after', 112 | ), 113 | ); 114 | 115 | /** 116 | */ 117 | public static $use_locale = true; 118 | 119 | /** 120 | * Make sure the orm config is loaded 121 | */ 122 | public static function _init() 123 | { 124 | \Config::load('orm', true); 125 | 126 | static::$use_locale = \Config::get('orm.use_locale', static::$use_locale); 127 | } 128 | 129 | /** 130 | * Get notified of an event 131 | * 132 | * @param Model $instance 133 | * @param string $event 134 | */ 135 | public static function orm_notify(Model $instance, $event) 136 | { 137 | // if we don't serve this event, bail out immediately 138 | if (array_key_exists($event, static::$events)) 139 | { 140 | // get the event type of the event that triggered us 141 | $event_type = static::$events[$event]; 142 | 143 | // fetch the model's properties 144 | $properties = $instance->properties(); 145 | 146 | // and check if we need to do any datatype conversions 147 | foreach ($properties as $p => $settings) 148 | { 149 | // the property is part of the primary key, skip it 150 | if (in_array($p, $instance->primary_key())) 151 | { 152 | continue; 153 | } 154 | 155 | $instance->{$p} = static::typecast($p, $instance->{$p}, $settings, $event_type); 156 | } 157 | } 158 | } 159 | 160 | /** 161 | * Typecast a single column value based on the model properties for that column 162 | * 163 | * @param string $column name of the column 164 | * @param string $value value 165 | * @param string $settings column settings from the model 166 | * 167 | * @throws InvalidContentType 168 | * 169 | * @return mixed 170 | */ 171 | public static function typecast($column, $value, $settings, $event_type = 'before') 172 | { 173 | // only on before_save, check if null is allowed 174 | if ($value === null) 175 | { 176 | // only on before_save 177 | if ($event_type == 'before') 178 | { 179 | if (array_key_exists('null', $settings) and $settings['null'] === false) 180 | { 181 | // if a default is defined, use that instead 182 | if (array_key_exists('default', $settings)) 183 | { 184 | $value = $settings['default']; 185 | } 186 | else 187 | { 188 | throw new InvalidContentType('The property "'.$column.'" cannot be NULL.'); 189 | } 190 | } 191 | } 192 | } 193 | 194 | // still null? then let the DB deal with it 195 | if ($value === null) 196 | { 197 | return $value; 198 | } 199 | 200 | // no datatype given 201 | if (empty($settings['data_type'])) 202 | { 203 | return $value; 204 | } 205 | 206 | // get the data type for this column 207 | $data_type = $settings['data_type']; 208 | 209 | // is this a base data type? 210 | if ( ! isset(static::$type_methods[$data_type])) 211 | { 212 | // no, can we map it to one? 213 | if (isset(static::$type_mappings[$data_type])) 214 | { 215 | // yes, so swap it for a base data type 216 | $data_type = static::$type_mappings[$data_type]; 217 | } 218 | else 219 | { 220 | // can't be mapped, check the regexes 221 | foreach (static::$regex_methods as $match => $methods) 222 | { 223 | // fetch the method 224 | $method = ! empty($methods[$event_type]) ? $methods[$event_type] : false; 225 | 226 | if ($method) 227 | { 228 | if (preg_match_all($match, $data_type, $matches) > 0) 229 | { 230 | $value = call_user_func($method, $value, $settings, $matches); 231 | } 232 | } 233 | } 234 | return $value; 235 | } 236 | } 237 | 238 | // fetch the method 239 | $method = ! empty(static::$type_methods[$data_type][$event_type]) ? static::$type_methods[$data_type][$event_type] : false; 240 | 241 | // if one was found, call it 242 | if ($method) 243 | { 244 | $value = call_user_func($method, $value, $settings); 245 | } 246 | 247 | return $value; 248 | } 249 | 250 | /** 251 | * Casts to string when necessary and checks if within max length 252 | * 253 | * @param mixed value to typecast 254 | * @param array any options to be passed 255 | * 256 | * @throws InvalidContentType 257 | * 258 | * @return string 259 | */ 260 | public static function type_string($var, array $settings) 261 | { 262 | if (is_array($var) or (is_object($var) and ! method_exists($var, '__toString'))) 263 | { 264 | throw new InvalidContentType('Array or object could not be converted to varchar.'); 265 | } 266 | 267 | $var = strval($var); 268 | 269 | if (array_key_exists('character_maximum_length', $settings)) 270 | { 271 | $length = intval($settings['character_maximum_length']); 272 | if ($length > 0 and strlen($var) > $length) 273 | { 274 | $var = substr($var, 0, $length); 275 | } 276 | } 277 | 278 | return $var; 279 | } 280 | 281 | /** 282 | * Casts to int when necessary and checks if within max values 283 | * 284 | * @param mixed value to typecast 285 | * @param array any options to be passed 286 | * 287 | * @throws InvalidContentType 288 | * 289 | * @return int 290 | */ 291 | public static function type_integer($var, array $settings) 292 | { 293 | if (is_array($var) or is_object($var)) 294 | { 295 | throw new InvalidContentType('Array or object could not be converted to integer.'); 296 | } 297 | 298 | if ((array_key_exists('min', $settings) and $var < intval($settings['min'])) 299 | or (array_key_exists('max', $settings) and $var > intval($settings['max']))) 300 | { 301 | throw new InvalidContentType('Integer value outside of range: '.$var); 302 | } 303 | 304 | return intval($var); 305 | } 306 | 307 | /** 308 | * Casts float to string when necessary 309 | * 310 | * @param mixed value to typecast 311 | * 312 | * @throws InvalidContentType 313 | * 314 | * @return float 315 | */ 316 | public static function type_float_before($var, $settings = null) 317 | { 318 | if (is_array($var) or is_object($var)) 319 | { 320 | throw new InvalidContentType('Array or object could not be converted to float.'); 321 | } 322 | 323 | // do we need to do locale conversion? 324 | if (is_string($var) and static::$use_locale) 325 | { 326 | $locale_info = localeconv(); 327 | $var = str_replace($locale_info["thousands_sep"], "", $var); 328 | $var = str_replace($locale_info["decimal_point"], ".", $var); 329 | } 330 | 331 | // was a specific float format specified? 332 | if (isset($settings['db_decimals'])) 333 | { 334 | return sprintf('%.'.$settings['db_decimals'].'F', round((float) $var, $settings['db_decimals'])); 335 | } 336 | if (isset($settings['data_type']) and strpos($settings['data_type'], 'decimal:') === 0) 337 | { 338 | $decimal = explode(':', $settings['data_type']); 339 | return sprintf('%.'.$decimal[1].'F', round((float) $var, $decimal[1])); 340 | } 341 | 342 | return $var; 343 | } 344 | 345 | /** 346 | * Casts to float when necessary 347 | * 348 | * @param mixed value to typecast 349 | * 350 | * @throws InvalidContentType 351 | * 352 | * @return float 353 | */ 354 | public static function type_float_after($var) 355 | { 356 | if (is_array($var) or is_object($var)) 357 | { 358 | throw new InvalidContentType('Array or object could not be converted to float.'); 359 | } 360 | 361 | return floatval($var); 362 | } 363 | 364 | /** 365 | * Decimal pre-treater, converts a decimal representation to a float 366 | * 367 | * @param mixed value to typecast 368 | * 369 | * @throws InvalidContentType 370 | * 371 | * @return float 372 | */ 373 | public static function type_decimal_before($var, $settings = null) 374 | { 375 | if (is_array($var) or is_object($var)) 376 | { 377 | throw new InvalidContentType('Array or object could not be converted to decimal.'); 378 | } 379 | 380 | return static::type_float_before($var, $settings); 381 | } 382 | 383 | /** 384 | * Decimal post-treater, converts any number to a decimal representation 385 | * 386 | * @param mixed value to typecast 387 | * 388 | * @throws InvalidContentType 389 | * 390 | * @return float 391 | */ 392 | public static function type_decimal_after($var, array $settings, array $matches) 393 | { 394 | if (is_array($var) or is_object($var)) 395 | { 396 | throw new InvalidContentType('Array or object could not be converted to decimal.'); 397 | } 398 | 399 | if ( ! is_numeric($var)) 400 | { 401 | throw new InvalidContentType('Value '.$var.' is not numeric and can not be converted to decimal.'); 402 | } 403 | 404 | $dec = empty($matches[1][0]) ? 2 : $matches[1][0]; 405 | 406 | // do we need to do locale aware conversion? 407 | if (static::$use_locale) 408 | { 409 | return sprintf("%.".$dec."f", round(static::type_float_after($var), $dec)); 410 | } 411 | 412 | return sprintf("%.".$dec."F", round(static::type_float_after($var), $dec)); 413 | } 414 | 415 | /** 416 | * Value pre-treater, deals with array values, and handles the enum type 417 | * 418 | * @param mixed value 419 | * @param array any options to be passed 420 | * 421 | * @throws InvalidContentType 422 | * 423 | * @return string 424 | */ 425 | public static function type_set_before($var, array $settings) 426 | { 427 | $var = is_array($var) ? implode(',', $var) : strval($var); 428 | $values = array_filter(explode(',', trim($var))); 429 | 430 | if ($settings['data_type'] == 'enum' and count($values) > 1) 431 | { 432 | throw new InvalidContentType('Enum cannot have more than 1 value.'); 433 | } 434 | 435 | foreach ($values as $val) 436 | { 437 | if ( ! in_array($val, $settings['options'])) 438 | { 439 | throw new InvalidContentType('Invalid value given for '.ucfirst($settings['data_type']). 440 | ', value "'.$var.'" not in available options: "'.implode(', ', $settings['options']).'".'); 441 | } 442 | } 443 | 444 | return $var; 445 | } 446 | 447 | /** 448 | * Value post-treater, converts a comma-delimited string into an array 449 | * 450 | * @param mixed value 451 | * 452 | * @return array 453 | */ 454 | public static function type_set_after($var) 455 | { 456 | return explode(',', $var); 457 | } 458 | 459 | /** 460 | * Converts boolean input to 1 or 0 for the DB 461 | * 462 | * @param bool value 463 | * 464 | * @return int 465 | */ 466 | public static function type_bool_to_int($var) 467 | { 468 | return $var ? 1 : 0; 469 | } 470 | 471 | /** 472 | * Converts DB bool values to PHP bool value 473 | * 474 | * @param bool value 475 | * 476 | * @return int 477 | */ 478 | public static function type_bool_from_int($var) 479 | { 480 | return $var == '1' ? true : false; 481 | } 482 | 483 | /** 484 | * Returns the serialized input 485 | * 486 | * @param mixed value 487 | * @param array any options to be passed 488 | * 489 | * @throws InvalidContentType 490 | * 491 | * @return string 492 | */ 493 | public static function type_serialize($var, array $settings) 494 | { 495 | $var = serialize($var); 496 | 497 | if (array_key_exists('character_maximum_length', $settings)) 498 | { 499 | $length = intval($settings['character_maximum_length']); 500 | if ($length > 0 and strlen($var) > $length) 501 | { 502 | throw new InvalidContentType('Value could not be serialized, result exceeds max string length for field.'); 503 | } 504 | } 505 | 506 | return $var; 507 | } 508 | 509 | /** 510 | * Unserializes the input 511 | * 512 | * @param string value 513 | * 514 | * @return mixed 515 | */ 516 | public static function type_unserialize($var) 517 | { 518 | return empty($var) ? array() : unserialize($var); 519 | } 520 | 521 | /** 522 | * Returns the encrypted input 523 | * 524 | * @param mixed value 525 | * @param array any options to be passed 526 | * 527 | * @throws InvalidContentType 528 | * 529 | * @return string 530 | */ 531 | public static function type_encrypt($var, array $settings) 532 | { 533 | // make the variable serialized, we need to be able to encrypt any variable type 534 | $var = static::type_serialize($var, $settings); 535 | 536 | // and encrypt it 537 | if (array_key_exists('encryption_key', $settings)) 538 | { 539 | $var = \Crypt::encode($var, $settings['encryption_key']); 540 | } 541 | else 542 | { 543 | $var = \Crypt::encode($var); 544 | } 545 | 546 | // do a length check if needed 547 | if (array_key_exists('character_maximum_length', $settings)) 548 | { 549 | $length = intval($settings['character_maximum_length']); 550 | if ($length > 0 and strlen($var) > $length) 551 | { 552 | throw new InvalidContentType('Value could not be encrypted, result exceeds max string length for field.'); 553 | } 554 | } 555 | 556 | return $var; 557 | } 558 | 559 | /** 560 | * decrypt the input 561 | * 562 | * @param string value 563 | * 564 | * @return mixed 565 | */ 566 | public static function type_decrypt($var) 567 | { 568 | // decrypt it 569 | if (array_key_exists('encryption_key', $settings)) 570 | { 571 | $var = \Crypt::decode($var, $settings['encryption_key']); 572 | } 573 | else 574 | { 575 | $var = \Crypt::decode($var); 576 | } 577 | 578 | return $var; 579 | } 580 | 581 | /** 582 | * JSON encodes the input 583 | * 584 | * @param mixed value 585 | * @param array any options to be passed 586 | * 587 | * @throws InvalidContentType 588 | * 589 | * @return string 590 | */ 591 | public static function type_json_encode($var, array $settings) 592 | { 593 | $var = json_encode($var); 594 | 595 | if (array_key_exists('character_maximum_length', $settings)) 596 | { 597 | $length = intval($settings['character_maximum_length']); 598 | if ($length > 0 and strlen($var) > $length) 599 | { 600 | throw new InvalidContentType('Value could not be JSON encoded, exceeds max string length for field.'); 601 | } 602 | } 603 | 604 | return $var; 605 | } 606 | 607 | /** 608 | * Decodes the JSON 609 | * 610 | * @param string value 611 | * 612 | * @return mixed 613 | */ 614 | public static function type_json_decode($var, $settings) 615 | { 616 | $assoc = false; 617 | if (array_key_exists('json_assoc', $settings)) 618 | { 619 | $assoc = (bool) $settings['json_assoc']; 620 | } 621 | return json_decode($var, $assoc); 622 | } 623 | 624 | /** 625 | * Takes a Date instance and transforms it into a DB timestamp 626 | * 627 | * @param \Fuel\Core\Date value 628 | * @param array any options to be passed 629 | * 630 | * @throws InvalidContentType 631 | * 632 | * @return int|string 633 | */ 634 | public static function type_time_encode(\Fuel\Core\Date $var, array $settings) 635 | { 636 | if ( ! $var instanceof \Fuel\Core\Date) 637 | { 638 | throw new InvalidContentType('Value must be an instance of the Date class.'); 639 | } 640 | 641 | // deal with datetime values 642 | elseif ($settings['data_type'] == 'datetime') 643 | { 644 | return $var->format('%Y-%m-%d %H:%M:%S'); 645 | } 646 | 647 | // deal with date values 648 | elseif ($settings['data_type'] == 'date') 649 | { 650 | return $var->format('%Y-%m-%d'); 651 | } 652 | 653 | // deal with time values 654 | elseif ($settings['data_type'] == 'time') 655 | { 656 | return $var->format('%H:%M:%S'); 657 | } 658 | 659 | // deal with config defined timestamps 660 | elseif ($settings['data_type'] == 'time_mysql') 661 | { 662 | return $var->format('mysql'); 663 | } 664 | 665 | // assume a timestamo is required 666 | return $var->get_timestamp(); 667 | } 668 | 669 | /** 670 | * Takes a DB timestamp and converts it into a Date object 671 | * 672 | * @param string value 673 | * @param array any options to be passed 674 | * 675 | * @return \Fuel\Core\Date 676 | */ 677 | public static function type_time_decode($var, array $settings) 678 | { 679 | // deal with a 'nulled' date, which according to some RDMBS is a valid enough to store? 680 | if ($var == '0000-00-00 00:00:00') 681 | { 682 | if (array_key_exists('null', $settings) and $settings['null'] === false) 683 | { 684 | throw new InvalidContentType('Value '.$var.' is not a valid date and can not be converted to a Date object.'); 685 | } 686 | return null; 687 | } 688 | 689 | // deal with datetime values 690 | elseif ($settings['data_type'] == 'datetime') 691 | { 692 | try 693 | { 694 | $var = \Date::create_from_string($var, '%Y-%m-%d %H:%M:%S'); 695 | } 696 | catch (\UnexpectedValueException $e) 697 | { 698 | throw new InvalidContentType('Value '.$var.' is not a valid datetime and can not be converted to a Date object.'); 699 | } 700 | } 701 | 702 | // deal with date values 703 | elseif ($settings['data_type'] == 'date') 704 | { 705 | try 706 | { 707 | $var = \Date::create_from_string($var, '%Y-%m-%d'); 708 | } 709 | catch (\UnexpectedValueException $e) 710 | { 711 | throw new InvalidContentType('Value '.$var.' is not a valid date and can not be converted to a Date object.'); 712 | } 713 | } 714 | 715 | // deal with time values 716 | elseif ($settings['data_type'] == 'time') 717 | { 718 | try 719 | { 720 | $var = \Date::create_from_string($var, '%H:%M:%S'); 721 | } 722 | catch (\UnexpectedValueException $e) 723 | { 724 | throw new InvalidContentType('Value '.$var.' is not a valid time and can not be converted to a Date object.'); 725 | } 726 | } 727 | 728 | // deal with a configured datetime value 729 | elseif ($settings['data_type'] == 'time_mysql') 730 | { 731 | try 732 | { 733 | $var = \Date::create_from_string($var, 'mysql'); 734 | } 735 | catch (\UnexpectedValueException $e) 736 | { 737 | throw new InvalidContentType('Value '.$var.' is not a valid mysql datetime and can not be converted to a Date object.'); 738 | } 739 | } 740 | 741 | // else assume it is a numeric timestamp 742 | else 743 | { 744 | $var = \Date::forge($var); 745 | } 746 | 747 | return $var; 748 | } 749 | } 750 | -------------------------------------------------------------------------------- /classes/observer/updatedat.php: -------------------------------------------------------------------------------- 1 | _mysql_timestamp = isset($props['mysql_timestamp']) ? $props['mysql_timestamp'] : static::$mysql_timestamp; 56 | $this->_property = isset($props['property']) ? $props['property'] : static::$property; 57 | $this->_relations = isset($props['relations']) ? $props['relations'] : array(); 58 | } 59 | 60 | /** 61 | * Set the UpdatedAt property to the current time. 62 | * 63 | * @param Model Model object subject of this observer method 64 | */ 65 | public function before_save(Model $obj) 66 | { 67 | $this->before_update($obj); 68 | } 69 | 70 | /** 71 | * Set the UpdatedAt property to the current time. 72 | * 73 | * @param Model Model object subject of this observer method 74 | */ 75 | public function before_update(Model $obj) 76 | { 77 | // If there are any relations loop through and check if any of them have been changed 78 | $relation_changed = false; 79 | foreach ( $this->_relations as $relation) 80 | { 81 | if ($this->relation_changed($obj, $relation)) 82 | { 83 | $relation_changed = true; 84 | break; 85 | } 86 | } 87 | 88 | $objClassName = get_class($obj); 89 | $objProperties = $objClassName::properties(); 90 | 91 | if ($obj->is_changed(array_keys($objProperties)) or $relation_changed) 92 | { 93 | $obj->{$this->_property} = $this->_mysql_timestamp ? \Date::time()->format('mysql') : \Date::time()->get_timestamp(); 94 | } 95 | } 96 | 97 | /** 98 | * Checks to see if any models in the given relation are changed. This function is lazy so will return true as soon 99 | * as it finds something that has changed. 100 | * 101 | * @param Model $obj 102 | * @param string $relation 103 | * 104 | * @return bool 105 | */ 106 | protected function relation_changed(Model $obj, $relation) 107 | { 108 | // Check that the relation exists 109 | if ($obj->relations($relation) === false) 110 | { 111 | throw new \InvalidArgumentException('Unknown relation '.$relation); 112 | } 113 | 114 | // If the relation is not loaded then ignore it. 115 | if ( ! $obj->is_fetched($relation)) 116 | { 117 | return false; 118 | } 119 | 120 | $relation_object = $obj->relations($relation); 121 | 122 | // Check if whe have a singular relation 123 | if ($relation_object->is_singular()) 124 | { 125 | // If so check that one model 126 | return $obj->{$relation}->is_changed(); 127 | } 128 | 129 | // Else we have an array of related objects so start checking them all 130 | foreach ($obj->{$relation} as $related_model) 131 | { 132 | if ($related_model->is_changed()) 133 | { 134 | return true; 135 | } 136 | } 137 | 138 | return false; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /classes/observer/validation.php: -------------------------------------------------------------------------------- 1 | fieldset = $fieldset; 50 | } 51 | 52 | /** 53 | * Gets the Fieldset from this exception 54 | * 55 | * @return Fieldset 56 | */ 57 | public function get_fieldset() 58 | { 59 | return $this->fieldset; 60 | } 61 | } 62 | 63 | /** 64 | * Observer class to validate the properties of the model before save. 65 | * 66 | * It is also used in Fieldset generation based on a model, to populate the fields 67 | * and field validation rules of the Fieldset. 68 | */ 69 | class Observer_Validation extends Observer 70 | { 71 | /** 72 | * Set a Model's properties as fields on a Fieldset, which will be created with the Model's 73 | * classname if none is provided. 74 | * 75 | * @param string 76 | * @param \Fieldset|null 77 | * @return \Fieldset 78 | */ 79 | public static function set_fields($obj, $fieldset = null) 80 | { 81 | static $_generated = array(); 82 | static $_tabular_rows = array(); 83 | 84 | $class = is_object($obj) ? get_class($obj) : $obj; 85 | if (is_null($fieldset)) 86 | { 87 | $fieldset = \Fieldset::instance($class); 88 | if ( ! $fieldset) 89 | { 90 | $fieldset = \Fieldset::forge($class); 91 | } 92 | } 93 | 94 | // is our parent fieldset a tabular form set? 95 | $tabular_form = is_object($fieldset->parent()) ? $fieldset->parent()->get_tabular_form() : false; 96 | 97 | // don't cache tabular form fieldsets 98 | if ( ! $tabular_form) 99 | { 100 | ! array_key_exists($class, $_generated) and $_generated[$class] = array(); 101 | if (in_array($fieldset, $_generated[$class], true)) 102 | { 103 | return $fieldset; 104 | } 105 | $_generated[$class][] = $fieldset; 106 | } 107 | 108 | $primary_keys = is_object($obj) ? $obj->primary_key() : $class::primary_key(); 109 | $primary_key = count($primary_keys) === 1 ? reset($primary_keys) : false; 110 | $properties = is_object($obj) ? $obj->properties() : $class::properties(); 111 | 112 | if ($tabular_form and $primary_key and ! is_object($obj)) 113 | { 114 | isset($_tabular_rows[$class]) or $_tabular_rows[$class] = 0; 115 | } 116 | 117 | foreach ($properties as $p => $settings) 118 | { 119 | if (\Arr::get($settings, 'skip', in_array($p, $primary_keys))) 120 | { 121 | continue; 122 | } 123 | 124 | if (isset($settings['form']['options'])) 125 | { 126 | foreach ($settings['form']['options'] as $key => $value) 127 | { 128 | is_array($value) or $settings['form']['options'][$key] = \Lang::get($value, array(), false) ?: $value; 129 | } 130 | } 131 | 132 | // field attributes can be passed in form key 133 | $attributes = isset($settings['form']) ? $settings['form'] : array(); 134 | // label is either set in property setting, as part of form attributes or defaults to fieldname 135 | $label = isset($settings['label']) ? $settings['label'] : (isset($attributes['label']) ? $attributes['label'] : $p); 136 | $label = \Lang::get($label, array(), false) ?: $label; 137 | 138 | // change the fieldname and label for tabular form fieldset children 139 | if ($tabular_form and $primary_key) 140 | { 141 | if (is_object($obj)) 142 | { 143 | $p = $tabular_form.'['.$obj->{$primary_key}.']['.$p.']'; 144 | } 145 | else 146 | { 147 | $p = $tabular_form.'_new['.$_tabular_rows[$class].']['.$p.']'; 148 | } 149 | $label = ''; 150 | } 151 | 152 | // create the field and add validation rules 153 | $field = $fieldset->add($p, $label, $attributes); 154 | if ( ! empty($settings['validation'])) 155 | { 156 | foreach ($settings['validation'] as $rule => $args) 157 | { 158 | if (is_int($rule) and is_string($args)) 159 | { 160 | $args = array($args); 161 | } 162 | else 163 | { 164 | array_unshift($args, $rule); 165 | } 166 | 167 | call_fuel_func_array(array($field, 'add_rule'), $args); 168 | } 169 | } 170 | } 171 | 172 | // increase the row counter for tabular row fieldsets 173 | if ($tabular_form and $primary_key and ! is_object($obj)) 174 | { 175 | $_tabular_rows[$class]++; 176 | } 177 | 178 | return $fieldset; 179 | } 180 | 181 | /** 182 | * Execute before saving the Model 183 | * 184 | * @param Model the model object to validate 185 | * 186 | * @throws ValidationFailed 187 | */ 188 | public function before_save(Model $obj) 189 | { 190 | $this->validate($obj); 191 | } 192 | 193 | /** 194 | * Execute before inserting the row in the database 195 | * 196 | * @param Model the model object to validate 197 | * 198 | * @throws ValidationFailed 199 | */ 200 | public function before_insert(Model $obj) 201 | { 202 | $this->validate($obj); 203 | } 204 | 205 | /** 206 | * Execute before updating the row in the database 207 | * 208 | * @param Model the model object to validate 209 | * 210 | * @throws ValidationFailed 211 | */ 212 | public function before_update(Model $obj) 213 | { 214 | $this->validate($obj); 215 | } 216 | 217 | /** 218 | * Validate the model 219 | * 220 | * @param Model the model object to validate 221 | * 222 | * @throws ValidationFailed 223 | */ 224 | public function validate(Model $obj) 225 | { 226 | $fieldset = static::set_fields($obj); 227 | $val = $fieldset->validation(); 228 | 229 | $is_new = $obj->is_new(); 230 | 231 | // only allow partial validation on updates, specify the fields for updates to allow null 232 | $allow_partial = $is_new ? false : array(); 233 | 234 | $input = array(); 235 | foreach (array_keys($obj->properties()) as $p) 236 | { 237 | if ( ! in_array($p, $obj->primary_key()) and ($is_new or $obj->is_changed($p))) 238 | { 239 | $input[$p] = $obj->{$p}; 240 | is_array($allow_partial) and $allow_partial[] = $p; 241 | } 242 | } 243 | 244 | if ( ! empty($input) and $val->run($input, $allow_partial, array($obj)) === false) 245 | { 246 | throw new ValidationFailed($val->show_errors(), 0, null, $fieldset); 247 | } 248 | else 249 | { 250 | foreach ($input as $k => $v) 251 | { 252 | $obj->{$k} = $val->validated($k); 253 | } 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /classes/query.php: -------------------------------------------------------------------------------- 1 | from_cache = (bool) static::$caching; 172 | } 173 | 174 | $this->model = $model; 175 | 176 | if (is_array($connection)) 177 | { 178 | list($this->connection, $this->write_connection) = $connection; 179 | } 180 | else 181 | { 182 | $this->connection = $connection; 183 | $this->write_connection = $connection; 184 | } 185 | 186 | foreach ($options as $opt => $val) 187 | { 188 | switch ($opt) 189 | { 190 | case 'select': 191 | $val = (array) $val; 192 | call_fuel_func_array(array($this, 'select'), $val); 193 | break; 194 | case 'related': 195 | $val = (array) $val; 196 | $this->related($val); 197 | break; 198 | case 'use_view': 199 | $this->use_view($val); 200 | break; 201 | case 'or_where': 202 | $this->and_where_open(); 203 | foreach ($val as $where) 204 | { 205 | call_fuel_func_array(array($this, '_where'), array($where, 'or_where')); 206 | } 207 | $this->and_where_close(); 208 | break; 209 | case 'where': 210 | $this->_parse_where_array($val); 211 | break; 212 | case 'order_by': 213 | $val = (array) $val; 214 | $this->order_by($val); 215 | break; 216 | case 'group_by': 217 | call_fuel_func_array(array($this, 'group_by'), $val); 218 | break; 219 | case 'limit': 220 | $this->limit($val); 221 | break; 222 | case 'offset': 223 | $this->offset($val); 224 | break; 225 | case 'rows_limit': 226 | $this->rows_limit($val); 227 | break; 228 | case 'rows_offset': 229 | $this->rows_offset($val); 230 | break; 231 | case 'from_cache': 232 | $this->from_cache($val); 233 | break; 234 | } 235 | } 236 | } 237 | 238 | /** 239 | * Does the work for where() and or_where() 240 | * 241 | * @param array $condition 242 | * @param string $type 243 | * 244 | * @throws \FuelException 245 | * 246 | * @return $this 247 | */ 248 | public function _where($condition, $type = 'and_where') 249 | { 250 | if (is_array(reset($condition)) or is_string(key($condition))) 251 | { 252 | foreach ($condition as $k_c => $v_c) 253 | { 254 | is_string($k_c) and $v_c = array($k_c, $v_c); 255 | $this->_where($v_c, $type); 256 | } 257 | return $this; 258 | } 259 | 260 | // prefix table alias when not yet prefixed and not a DB expression object 261 | if (strpos($condition[0], '.') === false and ! $condition[0] instanceof \Fuel\Core\Database_Expression) 262 | { 263 | $condition[0] = $this->alias.'.'.$condition[0]; 264 | } 265 | 266 | if (count($condition) == 2) 267 | { 268 | $this->where[] = array($type, array($condition[0], '=', $condition[1])); 269 | } 270 | elseif (count($condition) == 3 or $condition[0] instanceof \Fuel\Core\Database_Expression) 271 | { 272 | $this->where[] = array($type, $condition); 273 | } 274 | else 275 | { 276 | throw new \FuelException('Invalid param count for where condition.'); 277 | } 278 | 279 | return $this; 280 | } 281 | 282 | /** 283 | * Does the work for having() and or_having() 284 | * 285 | * @param array $condition 286 | * @param string $type 287 | * 288 | * @throws \FuelException 289 | * 290 | * @return $this 291 | */ 292 | public function _having($condition, $type = 'and_having') 293 | { 294 | if (is_array(reset($condition)) or is_string(key($condition))) 295 | { 296 | foreach ($condition as $k_c => $v_c) 297 | { 298 | is_string($k_c) and $v_c = array($k_c, $v_c); 299 | $this->_having($v_c, $type); 300 | } 301 | return $this; 302 | } 303 | 304 | // prefix table alias when not yet prefixed and not a DB expression object 305 | if (strpos($condition[0], '.') === false and ! $condition[0] instanceof \Fuel\Core\Database_Expression) 306 | { 307 | $condition[0] = $this->alias.'.'.$condition[0]; 308 | } 309 | 310 | if (count($condition) == 2) 311 | { 312 | $this->having[] = array($type, array($condition[0], '=', $condition[1])); 313 | } 314 | elseif (count($condition) == 3 or $condition[0] instanceof \Fuel\Core\Database_Expression) 315 | { 316 | $this->having[] = array($type, $condition); 317 | } 318 | else 319 | { 320 | throw new \FuelException('Invalid param count for having condition.'); 321 | } 322 | 323 | return $this; 324 | } 325 | 326 | /** 327 | * Parses an array of where conditions into the query 328 | * 329 | * @param array $val 330 | * @param string $base 331 | * @param bool $or 332 | */ 333 | public function _parse_where_array(array $val, $base = '', $or = false) 334 | { 335 | $or and $this->or_where_open(); 336 | foreach ($val as $k_w => $v_w) 337 | { 338 | if (is_array($v_w) and ! empty($v_w[0]) and (is_string($v_w[0]) or $v_w[0] instanceof \Database_Expression)) 339 | { 340 | ! $v_w[0] instanceof \Database_Expression and strpos($v_w[0], '.') === false and $v_w[0] = $base.$v_w[0]; 341 | call_fuel_func_array(array($this, ($k_w === 'or' ? 'or_' : '').'where'), $v_w); 342 | } 343 | elseif (is_int($k_w) or $k_w == 'or') 344 | { 345 | $k_w === 'or' ? $this->or_where_open() : $this->where_open(); 346 | $this->_parse_where_array($v_w, $base, $k_w === 'or'); 347 | $k_w === 'or' ? $this->or_where_close() : $this->where_close(); 348 | } 349 | else 350 | { 351 | ! $k_w instanceof \Database_Expression and strpos($k_w, '.') === false and $k_w = $base.$k_w; 352 | $this->where($k_w, $v_w); 353 | } 354 | } 355 | $or and $this->or_where_close(); 356 | } 357 | 358 | /** 359 | /* normalize the select fields passed 360 | * 361 | * @param array list of columns to select 362 | * @param int counter of the number of selected columnss 363 | */ 364 | protected function _normalize($fields, &$i) 365 | { 366 | $select = array(); 367 | 368 | // for BC reasons, deal with the odd array(DB::expr, 'name') syntax first 369 | if (($value = reset($fields)) instanceOf \Fuel\Core\Database_Expression and is_string($index = next($fields))) 370 | { 371 | $select[$this->alias.'_c'.$i++] = $fields; 372 | } 373 | 374 | // otherwise iterate 375 | else 376 | { 377 | foreach ($fields as $index => $value) 378 | { 379 | // an array of field definitions is passed 380 | if (is_array($value)) 381 | { 382 | // recurse and add them individually 383 | $select = array_merge($select, $this->_normalize($value, $i)); 384 | } 385 | 386 | // a "field -> include" value pair is passed 387 | elseif (is_bool($value)) 388 | { 389 | if ($value) 390 | { 391 | // if include is true, add the field 392 | $select[$this->alias.'_c'.$i++] = (strpos($index, '.') === false ? $this->alias.'.' : '').$index; 393 | } 394 | else 395 | { 396 | // if not, add it to the filter list 397 | if ( ! in_array($index, $this->select_filter)) 398 | { 399 | $this->select_filter[] = $index; 400 | } 401 | } 402 | } 403 | 404 | // attempted a "SELECT *"? 405 | elseif ($value === '*') 406 | { 407 | // recurse and add all model properties 408 | $select = array_merge($select, $this->_normalize(array_keys(call_user_func($this->model.'::properties')), $i)); 409 | } 410 | 411 | // DB::expr() passed? 412 | elseif ($value instanceOf \Fuel\Core\Database_Expression) 413 | { 414 | // no column name given for the result? 415 | if (is_numeric($index)) 416 | { 417 | $select[$this->alias.'_c'.$i++] = array($value); 418 | } 419 | 420 | // add the index as the column name 421 | else 422 | { 423 | $select[$this->alias.'_c'.$i++] = array($value, $index); 424 | } 425 | } 426 | 427 | // must be a regular field 428 | else 429 | { 430 | $select[$this->alias.'_c'.$i++] = (strpos($value, '.') === false ? $this->alias.'.' : '').$value; 431 | } 432 | } 433 | } 434 | 435 | return $select; 436 | } 437 | 438 | /** 439 | * Returns target table (or view, if specified). 440 | */ 441 | protected function _table() 442 | { 443 | return $this->view ? $this->view['view'] : call_user_func($this->model.'::table'); 444 | } 445 | 446 | /** 447 | * Sets the name of the connection to use for this query. Set to null to use the default DB connection 448 | * 449 | * @param string $name 450 | */ 451 | public function connection($name) 452 | { 453 | $this->connection = $name; 454 | return $this; 455 | } 456 | 457 | /** 458 | * Enables or disables the object cache for this query 459 | * 460 | * @param bool $cache Whether or not to use the object cache on this query 461 | * 462 | * @return Query 463 | */ 464 | public function from_cache($cache = true) 465 | { 466 | $this->from_cache = (bool) $cache; 467 | 468 | return $this; 469 | } 470 | 471 | /** 472 | * Select which properties are included, each as its own param. Or don't give input to retrieve 473 | * the current selection. 474 | * 475 | * @param bool $add_pks Whether or not to add the Primary Keys to the list of selected columns 476 | * @param string|array $fields Optionally. Which field/fields must be retrieved 477 | * 478 | * @throws \FuelException No properties found in model 479 | * 480 | * @return void|array 481 | */ 482 | public function select($add_pks = true) 483 | { 484 | $fields = func_get_args(); 485 | 486 | if (empty($fields) or is_bool($add_pks)) 487 | { 488 | if (empty($this->select)) 489 | { 490 | $fields = array_keys(call_user_func($this->model.'::properties')); 491 | 492 | if (empty($fields)) 493 | { 494 | throw new \FuelException('No properties found in model.'); 495 | } 496 | foreach ($fields as $field) 497 | { 498 | in_array($field, $this->select_filter) or $this->select($field); 499 | } 500 | 501 | if ($this->view) 502 | { 503 | foreach ($this->view['columns'] as $field) 504 | { 505 | $this->select($field); 506 | } 507 | } 508 | } 509 | 510 | // backup select before adding PKs 511 | $select = $this->select; 512 | 513 | // ensure all PKs are being selected 514 | if ($add_pks) 515 | { 516 | $pks = call_user_func($this->model.'::primary_key'); 517 | foreach($pks as $pk) 518 | { 519 | if ( ! in_array($this->alias.'.'.$pk, $this->select)) 520 | { 521 | $this->select($pk); 522 | } 523 | } 524 | } 525 | 526 | // convert selection array for DB class 527 | $out = array(); 528 | foreach($this->select as $k => $v) 529 | { 530 | if (is_array($v)) 531 | { 532 | if (count($v) === 1) 533 | { 534 | $out[] = array($v[0], $k); 535 | } 536 | else 537 | { 538 | $out[$v[1]] = array($v[0], $k); 539 | } 540 | } 541 | else 542 | { 543 | $out[] = array($v, $k); 544 | } 545 | } 546 | 547 | // set select back to before the PKs were added 548 | $this->select = $select; 549 | 550 | return $out; 551 | } 552 | 553 | // get the current select count 554 | $i = count($this->select); 555 | 556 | // parse the passed fields list 557 | $this->select = array_merge($this->select, $this->_normalize($fields, $i)); 558 | 559 | return $this; 560 | } 561 | 562 | /** 563 | * Set a view to use instead of the table 564 | * 565 | * @param string $view Name of view which you want to use 566 | * 567 | * @throws \OutOfBoundsException Cannot use undefined database view, must be defined with Model 568 | * 569 | * @return Query 570 | */ 571 | public function use_view($view) 572 | { 573 | $views = call_user_func(array($this->model, 'views')); 574 | if ( ! array_key_exists($view, $views)) 575 | { 576 | throw new \OutOfBoundsException('Cannot use undefined database view, must be defined with Model.'); 577 | } 578 | 579 | $this->view = $views[$view]; 580 | $this->view['_name'] = $view; 581 | return $this; 582 | } 583 | 584 | /** 585 | * Creates a "GROUP BY ..." filter. 586 | * 587 | * @param mixed $coulmns Column name or array($column, $alias) or object 588 | * @return $this 589 | */ 590 | public function group_by() 591 | { 592 | $columns = func_get_args(); 593 | 594 | $this->group_by = array_merge($this->group_by, $columns); 595 | 596 | return $this; 597 | } 598 | 599 | /** 600 | * Set the limit 601 | * 602 | * @param int $limit 603 | * 604 | * @return $this 605 | */ 606 | public function limit($limit) 607 | { 608 | $this->limit = intval($limit); 609 | 610 | return $this; 611 | } 612 | 613 | /** 614 | * Set the offset 615 | * 616 | * @param int $offset 617 | * 618 | * @return $this 619 | */ 620 | public function offset($offset) 621 | { 622 | $this->offset = intval($offset); 623 | 624 | return $this; 625 | } 626 | 627 | /** 628 | * Set the limit of rows requested 629 | * 630 | * @param int $limit 631 | * 632 | * @return $this 633 | */ 634 | public function rows_limit($limit) 635 | { 636 | $this->rows_limit = intval($limit); 637 | 638 | return $this; 639 | } 640 | 641 | /** 642 | * Set the offset of rows requested 643 | * 644 | * @param int $offset 645 | * 646 | * @return $this 647 | */ 648 | public function rows_offset($offset) 649 | { 650 | $this->rows_offset = intval($offset); 651 | 652 | return $this; 653 | } 654 | 655 | /** 656 | * Set where condition 657 | * 658 | * @param string Property 659 | * @param string Comparison type (can be omitted) 660 | * @param string Comparison value 661 | * 662 | * @return $this 663 | */ 664 | public function where() 665 | { 666 | $condition = func_get_args(); 667 | is_array(reset($condition)) and $condition = reset($condition); 668 | 669 | return $this->_where($condition); 670 | } 671 | 672 | /** 673 | * Set or_where condition 674 | * 675 | * @param string Property 676 | * @param string Comparison type (can be omitted) 677 | * @param string Comparison value 678 | * 679 | * @return $this 680 | */ 681 | public function or_where() 682 | { 683 | $condition = func_get_args(); 684 | is_array(reset($condition)) and $condition = reset($condition); 685 | 686 | return $this->_where($condition, 'or_where'); 687 | } 688 | 689 | /** 690 | * Open a nested and_where condition 691 | * 692 | * @return $this 693 | */ 694 | public function and_where_open() 695 | { 696 | $this->where[] = array('and_where_open', array()); 697 | 698 | return $this; 699 | } 700 | 701 | /** 702 | * Close a nested and_where condition 703 | * 704 | * @return $this 705 | */ 706 | public function and_where_close() 707 | { 708 | $this->where[] = array('and_where_close', array()); 709 | 710 | return $this; 711 | } 712 | 713 | /** 714 | * Alias to and_where_open() 715 | * 716 | * @return $this 717 | */ 718 | public function where_open() 719 | { 720 | $this->where[] = array('and_where_open', array()); 721 | 722 | return $this; 723 | } 724 | 725 | /** 726 | * Alias to and_where_close() 727 | * 728 | * @return $this 729 | */ 730 | public function where_close() 731 | { 732 | $this->where[] = array('and_where_close', array()); 733 | 734 | return $this; 735 | } 736 | 737 | /** 738 | * Open a nested or_where condition 739 | * 740 | * @return $this 741 | */ 742 | public function or_where_open() 743 | { 744 | $this->where[] = array('or_where_open', array()); 745 | 746 | return $this; 747 | } 748 | 749 | /** 750 | * Close a nested or_where condition 751 | * 752 | * @return $this 753 | */ 754 | public function or_where_close() 755 | { 756 | $this->where[] = array('or_where_close', array()); 757 | 758 | return $this; 759 | } 760 | 761 | /** 762 | * Set having condition 763 | * 764 | * @param string Property 765 | * @param string Comparison type (can be omitted) 766 | * @param string Comparison value 767 | * 768 | * @return $this 769 | */ 770 | public function having() 771 | { 772 | $condition = func_get_args(); 773 | is_array(reset($condition)) and $condition = reset($condition); 774 | 775 | return $this->_having($condition); 776 | } 777 | 778 | /** 779 | * Set or_having condition 780 | * 781 | * @param string Property 782 | * @param string Comparison type (can be omitted) 783 | * @param string Comparison value 784 | * 785 | * @return $this 786 | */ 787 | public function or_having() 788 | { 789 | $condition = func_get_args(); 790 | is_array(reset($condition)) and $condition = reset($condition); 791 | 792 | return $this->_having($condition, 'or_having'); 793 | } 794 | 795 | /** 796 | * Open a nested and_having condition 797 | * 798 | * @return $this 799 | */ 800 | public function and_having_open() 801 | { 802 | $this->having[] = array('and_having_open', array()); 803 | 804 | return $this; 805 | } 806 | 807 | /** 808 | * Close a nested and_where condition 809 | * 810 | * @return $this 811 | */ 812 | public function and_having_close() 813 | { 814 | $this->having[] = array('and_having_close', array()); 815 | 816 | return $this; 817 | } 818 | 819 | /** 820 | * Alias to and_having_open() 821 | * 822 | * @return $this 823 | */ 824 | public function having_open() 825 | { 826 | $this->having[] = array('and_having_open', array()); 827 | 828 | return $this; 829 | } 830 | 831 | /** 832 | * Alias to and_having_close() 833 | * 834 | * @return $this 835 | */ 836 | public function having_close() 837 | { 838 | $this->having[] = array('and_having_close', array()); 839 | 840 | return $this; 841 | } 842 | 843 | /** 844 | * Open a nested or_having condition 845 | * 846 | * @return $this 847 | */ 848 | public function or_having_open() 849 | { 850 | $this->having[] = array('or_having_open', array()); 851 | 852 | return $this; 853 | } 854 | 855 | /** 856 | * Close a nested or_where condition 857 | * 858 | * @return $this 859 | */ 860 | public function or_having_close() 861 | { 862 | $this->having[] = array('or_having_close', array()); 863 | 864 | return $this; 865 | } 866 | 867 | /** 868 | * Set the order_by 869 | * 870 | * @param string|array $property 871 | * @param string $direction 872 | * 873 | * @return $this 874 | */ 875 | public function order_by($property, $direction = 'ASC') 876 | { 877 | if (is_array($property)) 878 | { 879 | foreach ($property as $p => $d) 880 | { 881 | if (is_int($p)) 882 | { 883 | is_array($d) ? $this->order_by($d[0], $d[1]) : $this->order_by($d, $direction); 884 | } 885 | else 886 | { 887 | $this->order_by($p, $d); 888 | } 889 | } 890 | return $this; 891 | } 892 | 893 | // prefix table alias when not yet prefixed and not a DB expression object 894 | if ( ! $property instanceof \Fuel\Core\Database_Expression and strpos($property, '.') === false) 895 | { 896 | $property = $this->alias.'.'.$property; 897 | } 898 | 899 | $this->order_by[] = array($property, $direction); 900 | 901 | return $this; 902 | } 903 | 904 | /** 905 | * Set a relation to include 906 | * 907 | * @param string $relation 908 | * @param array $conditions Optionally 909 | * 910 | * @throws \UnexpectedValueException Relation was not found in the model 911 | * 912 | * @return $this 913 | */ 914 | public function related($relation, $conditions = array()) 915 | { 916 | if (is_array($relation)) 917 | { 918 | foreach ($relation as $k_r => $v_r) 919 | { 920 | is_array($v_r) ? $this->related($k_r, $v_r) : $this->related($v_r); 921 | } 922 | return $this; 923 | } 924 | 925 | if (strpos($relation, '.')) 926 | { 927 | $rels = explode('.', $relation); 928 | $model = $this->model; 929 | foreach ($rels as $r) 930 | { 931 | $rel = call_user_func(array($model, 'relations'), $r); 932 | if (empty($rel)) 933 | { 934 | throw new \UnexpectedValueException('Relation "'.$r.'" was not found in the model "'.$model.'".'); 935 | } 936 | $model = $rel->model_to; 937 | } 938 | } 939 | else 940 | { 941 | $rel = call_user_func(array($this->model, 'relations'), $relation); 942 | if (empty($rel)) 943 | { 944 | throw new \UnexpectedValueException('Relation "'.$relation.'" was not found in the model.'); 945 | } 946 | } 947 | 948 | $this->relations[$relation] = array($rel, $conditions); 949 | 950 | if ( ! empty($conditions['related'])) 951 | { 952 | $conditions['related'] = (array) $conditions['related']; 953 | foreach ($conditions['related'] as $k_r => $v_r) 954 | { 955 | is_array($v_r) ? $this->related($relation.'.'.$k_r, $v_r) : $this->related($relation.'.'.$v_r); 956 | } 957 | 958 | unset($conditions['related']); 959 | } 960 | 961 | // avoid UnexpectedValue exceptions due to incorrect order 962 | ksort($this->relations); 963 | 964 | return $this; 965 | } 966 | 967 | /** 968 | * Add a table to join, consider this a protect method only for Orm package usage 969 | * 970 | * @param array $join 971 | * 972 | * @return $this 973 | */ 974 | public function _join(array $join) 975 | { 976 | $this->joins[] = $join; 977 | 978 | return $this; 979 | } 980 | 981 | /** 982 | * Set any properties for insert or update 983 | * 984 | * @param string|array $property 985 | * @param mixed $value Optionally 986 | * 987 | * @return $this 988 | */ 989 | public function set($property, $value = null) 990 | { 991 | if (is_array($property)) 992 | { 993 | foreach ($property as $p => $v) 994 | { 995 | $this->set($p, $v); 996 | } 997 | return $this; 998 | } 999 | 1000 | $this->values[$property] = $value; 1001 | 1002 | return $this; 1003 | } 1004 | 1005 | /** 1006 | * Build a select, delete or update query 1007 | * 1008 | * @param \Fuel\Core\Database_Query_Builder_Where DB where() query object 1009 | * @param array $columns Optionally 1010 | * @param string $type Type of query to build (count/select/update/delete/insert) 1011 | * 1012 | * @throws \FuelException Models cannot be related between different database connections 1013 | * @throws \UnexpectedValueException Trying to get the relation of an unloaded relation 1014 | * 1015 | * @return array with keys query and relations 1016 | */ 1017 | public function build_query(\Fuel\Core\Database_Query_Builder_Where $query, $columns = array(), $type = 'select') 1018 | { 1019 | // Are we generating a read or a write query? 1020 | $read_query = ! in_array($type, array('insert', 'update', 'delete')); 1021 | 1022 | // Get the limit 1023 | if ( ! is_null($this->limit)) 1024 | { 1025 | $query->limit($this->limit); 1026 | } 1027 | 1028 | // Get the offset 1029 | if ( ! is_null($this->offset)) 1030 | { 1031 | $query->offset($this->offset); 1032 | } 1033 | 1034 | $where_conditions = call_user_func($this->model.'::condition', 'where'); 1035 | empty($where_conditions) or $this->_parse_where_array($where_conditions); 1036 | 1037 | $where_backup = $this->where; 1038 | if ( ! empty($this->where)) 1039 | { 1040 | $open_nests = 0; 1041 | $where_nested = array(); 1042 | $include_nested = true; 1043 | foreach ($this->where as $key => $w) 1044 | { 1045 | list($method, $conditional) = $w; 1046 | 1047 | if ($read_query and (empty($conditional) or $open_nests > 0)) 1048 | { 1049 | $include_nested and $where_nested[$key] = $w; 1050 | if ( ! empty($conditional) and strpos($conditional[0], $this->alias.'.') !== 0) 1051 | { 1052 | $include_nested = false; 1053 | } 1054 | strpos($method, '_open') and $open_nests++; 1055 | strpos($method, '_close') and $open_nests--; 1056 | continue; 1057 | } 1058 | 1059 | if (empty($conditional) 1060 | or strpos($conditional[0], $this->alias.'.') === 0 1061 | or ( ! $read_query and $conditional[0] instanceof \Fuel\Core\Database_Expression)) 1062 | { 1063 | if ( ! $read_query and ! empty($conditional) 1064 | and ! $conditional[0] instanceof \Fuel\Core\Database_Expression) 1065 | { 1066 | $conditional[0] = substr($conditional[0], strlen($this->alias.'.')); 1067 | } 1068 | call_fuel_func_array(array($query, $method), $conditional); 1069 | unset($this->where[$key]); 1070 | } 1071 | } 1072 | 1073 | if ($include_nested and ! empty($where_nested)) 1074 | { 1075 | foreach ($where_nested as $key => $w) 1076 | { 1077 | list($method, $conditional) = $w; 1078 | 1079 | if (empty($conditional) 1080 | or strpos($conditional[0], $this->alias.'.') === 0 1081 | or ( ! $read_query and $conditional[0] instanceof \Fuel\Core\Database_Expression)) 1082 | { 1083 | if ( ! $read_query and ! empty($conditional) 1084 | and ! $conditional[0] instanceof \Fuel\Core\Database_Expression) 1085 | { 1086 | $conditional[0] = substr($conditional[0], strlen($this->alias.'.')); 1087 | } 1088 | call_fuel_func_array(array($query, $method), $conditional); 1089 | unset($this->where[$key]); 1090 | } 1091 | } 1092 | } 1093 | } 1094 | 1095 | // If it's a write query, we're done 1096 | if ( ! $read_query) 1097 | { 1098 | return array('query' => $query, 'models' => array()); 1099 | } 1100 | 1101 | // Alias number counter 1102 | $i = 1; 1103 | 1104 | // Add defined relations 1105 | $models = array(); 1106 | 1107 | foreach ($this->relations as $name => $rel) 1108 | { 1109 | // when there's a dot it must be a nested relation 1110 | if ($pos = strrpos($name, '.')) 1111 | { 1112 | if (empty($models[substr($name, 0, $pos)]['table'][1])) 1113 | { 1114 | throw new \UnexpectedValueException('Trying to get the relation of an unloaded relation, make sure you load the parent relation before any of its children.'); 1115 | } 1116 | 1117 | $alias = $models[substr($name, 0, $pos)]['table'][1]; 1118 | } 1119 | else 1120 | { 1121 | $alias = $this->alias; 1122 | } 1123 | 1124 | $join = $rel[0]->join($alias, $name, $i++, $rel[1]); 1125 | $models = array_merge($models, $this->modify_join_result($join, $name)); 1126 | } 1127 | 1128 | // if no order_by was given, see if a default was defined in the model 1129 | empty($this->order_by) and $this->order_by(call_user_func($this->model.'::condition', 'order_by')); 1130 | 1131 | if ($this->use_subquery()) 1132 | { 1133 | // Count queries should not select on anything besides the count 1134 | if ($type != 'count') 1135 | { 1136 | // Get the columns for final select 1137 | foreach ($models as $m) 1138 | { 1139 | foreach ($m['columns'] as $c) 1140 | { 1141 | $columns[] = $c; 1142 | } 1143 | } 1144 | } 1145 | 1146 | // do we need to add order_by clauses on the subquery? 1147 | foreach ($this->order_by as $idx => $ob) 1148 | { 1149 | if ( ! $ob[0] instanceof \Fuel\Core\Database_Expression) 1150 | { 1151 | if (strpos($ob[0], $this->alias.'.') === 0) 1152 | { 1153 | // order by on the current model 1154 | $query->order_by($ob[0], $ob[1]); 1155 | } 1156 | } 1157 | } 1158 | 1159 | // make current query subquery of ultimate query 1160 | $new_query = \Database_Connection::instance($this->connection)->select(array_values($columns)); 1161 | $query = $new_query->from(array($query, $this->alias)); 1162 | } 1163 | else 1164 | { 1165 | // Count queries should not select on anything besides the count 1166 | if ($type != 'count') 1167 | { 1168 | // add additional selected columns 1169 | foreach ($models as $m) 1170 | { 1171 | foreach ($m['columns'] as $c) 1172 | { 1173 | $query->select($c); 1174 | } 1175 | } 1176 | } 1177 | } 1178 | 1179 | // join tables 1180 | foreach ($this->joins as $j) 1181 | { 1182 | $join_query = $query->join($j['table'], $j['join_type']); 1183 | foreach ($j['join_on'] as $on) 1184 | { 1185 | $join_query->on($on[0], $on[1], $on[2]); 1186 | } 1187 | } 1188 | foreach ($models as $m) 1189 | { 1190 | if ($m['connection'] != $this->connection) 1191 | { 1192 | throw new \FuelException('Models cannot be related between different database connections.'); 1193 | } 1194 | 1195 | $join_query = $query->join($m['table'], $m['join_type']); 1196 | foreach ($m['join_on'] as $on) 1197 | { 1198 | $join_query->on($on[0], $on[1], $on[2]); 1199 | } 1200 | } 1201 | 1202 | // Get the order, if none set see if we have an order_by condition set 1203 | $order_by = $order_by_backup = $this->order_by; 1204 | 1205 | // Add any additional order_by and where clauses from the relations 1206 | foreach ($models as $m_name => $m) 1207 | { 1208 | if ( ! empty($m['order_by'])) 1209 | { 1210 | foreach ((array) $m['order_by'] as $k_ob => $v_ob) 1211 | { 1212 | if (is_int($k_ob)) 1213 | { 1214 | $v_dir = is_array($v_ob) ? $v_ob[1] : 'ASC'; 1215 | $v_ob = is_array($v_ob) ? $v_ob[0] : $v_ob; 1216 | if ( ! $v_ob instanceof \Fuel\Core\Database_Expression and strpos($v_ob, '.') === false) 1217 | { 1218 | $v_ob = $m_name.'.'.$v_ob; 1219 | } 1220 | 1221 | $order_by[] = array($v_ob, $v_dir); 1222 | } 1223 | else 1224 | { 1225 | strpos($k_ob, '.') === false and $k_ob = $m_name.'.'.$k_ob; 1226 | $order_by[] = array($k_ob, $v_ob); 1227 | } 1228 | } 1229 | } 1230 | if ( ! empty($m['where'])) 1231 | { 1232 | $this->_parse_where_array($m['where'], $m_name.'.'); 1233 | } 1234 | } 1235 | 1236 | // Get the order 1237 | if ( ! empty($order_by)) 1238 | { 1239 | foreach ($order_by as $ob) 1240 | { 1241 | if ( ! $ob[0] instanceof \Fuel\Core\Database_Expression) 1242 | { 1243 | if (strpos($ob[0], $this->alias.'.') === 0) 1244 | { 1245 | // get the field name 1246 | $fn = substr($ob[0], strlen($this->alias.'.')); 1247 | 1248 | // if not a a model property? 1249 | if ( ! call_fuel_func_array(array($this->model, 'property'), array($fn))) 1250 | { 1251 | // and it's not an alias? 1252 | foreach ($this->select as $salias => $sdef) 1253 | { 1254 | // if the fieldname matches 1255 | if (is_array($sdef) and $sdef[1] == $fn) 1256 | { 1257 | // order on it's alias instead 1258 | $ob[0] = $salias; 1259 | break; 1260 | } 1261 | } 1262 | } 1263 | } 1264 | else 1265 | { 1266 | // try to rewrite conditions on the relations to their table alias 1267 | $dotpos = strrpos($ob[0], '.'); 1268 | $relation = substr($ob[0], 0, $dotpos); 1269 | if ($dotpos > 0 and array_key_exists($relation, $models)) 1270 | { 1271 | $ob[0] = $models[$relation]['table'][1].substr($ob[0], $dotpos); 1272 | } 1273 | } 1274 | } 1275 | $query->order_by($ob[0], $ob[1]); 1276 | } 1277 | } 1278 | 1279 | // Get the grouping 1280 | if ( ! empty($this->group_by)) 1281 | { 1282 | foreach ($this->group_by as $gb) 1283 | { 1284 | if ( ! $gb instanceof \Fuel\Core\Database_Expression) 1285 | { 1286 | if (strpos($gb, $this->alias.'.') === false) 1287 | { 1288 | // try to rewrite on the relations to their table alias 1289 | $dotpos = strrpos($gb, '.'); 1290 | $relation = substr($gb, 0, $dotpos); 1291 | if ($dotpos > 0) 1292 | { 1293 | if(array_key_exists($relation, $models)) 1294 | { 1295 | $gb = $models[$relation]['table'][1].substr($gb, $dotpos); 1296 | } 1297 | } 1298 | else 1299 | { 1300 | $gb = $this->alias.'.'.$gb; 1301 | } 1302 | } 1303 | } 1304 | $query->group_by($gb); 1305 | } 1306 | } 1307 | 1308 | // Add any having filters 1309 | if ( ! empty($this->having)) 1310 | { 1311 | foreach ($this->having as $h) 1312 | { 1313 | list($method, $conditional) = $h; 1314 | 1315 | // try to rewrite conditions on the relations to their table alias 1316 | if ( ! empty($conditional) and is_array($conditional)) 1317 | { 1318 | $dotpos = strrpos($conditional[0], '.'); 1319 | $relation = substr($conditional[0], 0, $dotpos); 1320 | if ($dotpos > 0 and array_key_exists($relation, $models)) 1321 | { 1322 | $conditional[0] = $models[$relation]['table'][1].substr($conditional[0], $dotpos); 1323 | } 1324 | } 1325 | 1326 | call_fuel_func_array(array($query, $method), $conditional); 1327 | } 1328 | } 1329 | 1330 | // put omitted where conditions back 1331 | if ( ! empty($this->where)) 1332 | { 1333 | foreach ($this->where as $w) 1334 | { 1335 | list($method, $conditional) = $w; 1336 | 1337 | // try to rewrite conditions on the relations to their table alias 1338 | if ( ! empty($conditional) and is_array($conditional)) 1339 | { 1340 | $dotpos = strrpos($conditional[0], '.'); 1341 | $relation = substr($conditional[0], 0, $dotpos); 1342 | if ($dotpos > 0 and array_key_exists($relation, $models)) 1343 | { 1344 | $conditional[0] = $models[$relation]['table'][1].substr($conditional[0], $dotpos); 1345 | } 1346 | } 1347 | 1348 | call_fuel_func_array(array($query, $method), $conditional); 1349 | } 1350 | } 1351 | 1352 | $this->where = $where_backup; 1353 | $this->order_by = $order_by_backup; 1354 | 1355 | // Set the row limit and offset, these are applied to the outer query when a subquery 1356 | // is used or overwrite limit/offset when it's a normal query 1357 | ! is_null($this->rows_limit) and $query->limit($this->rows_limit); 1358 | ! is_null($this->rows_offset) and $query->offset($this->rows_offset); 1359 | 1360 | return array('query' => $query, 'models' => $models); 1361 | } 1362 | 1363 | /** 1364 | * Allows subclasses to make changes to the join information before it is used 1365 | */ 1366 | protected function modify_join_result($join_result, $name) 1367 | { 1368 | return $join_result; 1369 | } 1370 | 1371 | /** 1372 | * Determines whether a subquery is needed, is the case if there was a limit/offset on a join 1373 | * 1374 | * @return bool 1375 | */ 1376 | public function use_subquery() 1377 | { 1378 | return ( ! empty($this->relations) and ( ! empty($this->limit) or ! empty($this->offset))); 1379 | } 1380 | 1381 | /** 1382 | * Build the query and return hydrated results 1383 | * 1384 | * @return array 1385 | */ 1386 | public function get() 1387 | { 1388 | // closure to convert field name to column name 1389 | $field_to_column = function($fields, $alias) { 1390 | // storage for the result 1391 | $result = array(); 1392 | 1393 | // process the columns 1394 | foreach ($fields as $key => $value) 1395 | { 1396 | // check for db expressions 1397 | if (is_array($value)) 1398 | { 1399 | if ($value[0] instanceOf \Fuel\Core\Database_Expression) 1400 | { 1401 | $result[$value[1]] = $key; 1402 | } 1403 | else 1404 | { 1405 | $result[$value[1]] = substr($value[0], strlen($alias)+1); 1406 | } 1407 | } 1408 | else 1409 | { 1410 | $result[$key] = substr($value, strlen($alias)+1); 1411 | } 1412 | } 1413 | 1414 | return $result; 1415 | }; 1416 | 1417 | // Get the columns in this query 1418 | $columns = $this->select(); 1419 | 1420 | // Start building the query 1421 | $select = $columns; 1422 | if ($this->use_subquery()) 1423 | { 1424 | $select = array(); 1425 | foreach ($columns as $c) 1426 | { 1427 | $select[] = $c[0]; 1428 | } 1429 | } 1430 | 1431 | $query = \Database_Connection::instance($this->connection)->select(array_values($select)); 1432 | 1433 | // Set from view/table 1434 | $query->from(array($this->_table(), $this->alias)); 1435 | 1436 | // Build the query further 1437 | $tmp = $this->build_query($query, $columns); 1438 | $query = $tmp['query']; 1439 | $models = $tmp['models']; 1440 | 1441 | // list of models expected in the resulting rows 1442 | $qmodels = array($this->alias => array( 1443 | 'model' => $this->model, 1444 | 'pk' => $this->model::primary_key(), 1445 | 'columns' => $field_to_column($columns, $this->alias), 1446 | 'relation' => null, 1447 | 'singular' => true, 1448 | )); 1449 | 1450 | // Make models hierarchical 1451 | foreach ($models as $name => $values) 1452 | { 1453 | // add the model to the list 1454 | if ($values['model']) 1455 | { 1456 | $qmodels[$values['table'][1]] = array( 1457 | 'model' => $values['model'], 1458 | 'pk' => $values['model']::primary_key(), 1459 | 'columns' => $field_to_column($values['columns'], $values['table'][1]), 1460 | 'relation' => $name, 1461 | 'singular' => $values['relation']->is_singular(), 1462 | ); 1463 | } 1464 | } 1465 | 1466 | // fetch the result 1467 | $rows = $query->execute($this->connection)->as_array(); 1468 | 1469 | // storage for the fimal result 1470 | $result = array(); 1471 | 1472 | // process the result 1473 | foreach ($rows as $id => $row) 1474 | { 1475 | $this->process_row($row, $qmodels, $result); 1476 | } 1477 | // free up some memory 1478 | unset($rows); 1479 | 1480 | // convert the result into objects 1481 | $objects = array(); 1482 | foreach ($result as $key => $record) 1483 | { 1484 | if (is_array($record)) 1485 | { 1486 | $objects[$key] = $this->model::forge($record, false, $this->view ? $this->view['_name'] : null, $this->from_cache); 1487 | } 1488 | else 1489 | { 1490 | $objects[$key] = $record; 1491 | } 1492 | 1493 | // free up some memory 1494 | unset($result[$key]); 1495 | } 1496 | 1497 | return $objects; 1498 | } 1499 | 1500 | /** 1501 | * Process the retrieved data, convert rows to a hierarchical data structure 1502 | - * 1503 | - * @param array $row Row from the database 1504 | - * @param array $models Relations to be expected 1505 | - * @param array &$result array to accumulate the processed results in 1506 | - */ 1507 | public function process_row($row, $models, &$result) 1508 | { 1509 | // relation pointers 1510 | $pointers = array(); 1511 | 1512 | // relation types 1513 | $reltypes = array(); 1514 | 1515 | // process the models in the result row 1516 | foreach ($models as $alias => $model) 1517 | { 1518 | // fetch the relation 1519 | $relation = $model['relation']; 1520 | 1521 | isset($reltypes[$relation]) or $reltypes[$relation] = $model['singular']; 1522 | 1523 | // storage for extracting current record 1524 | $record = array(); 1525 | 1526 | // get this models data from the row 1527 | foreach ($row as $column => $value) 1528 | { 1529 | // check if this coulumn belongs to this model 1530 | if (array_key_exists($column, $model['columns'])) 1531 | { 1532 | // get the true column name 1533 | $column = $model['columns'][$column]; 1534 | 1535 | // is it a (part of a) primary key? 1536 | if (in_array($column, $model['pk'])) 1537 | { 1538 | // typecast the pk value 1539 | $value = Observer_Typing::typecast($column, $value, call_user_func($model['model'].'::property', $column)); 1540 | } 1541 | 1542 | // store the value 1543 | $record[$column] = $value; 1544 | } 1545 | } 1546 | 1547 | // determine the PK for this record 1548 | $pk = $model['model']::implode_pk($record); 1549 | 1550 | // skip the rest if we don't have a pk (= no result) 1551 | if (is_null($pk)) 1552 | { 1553 | continue; 1554 | } 1555 | 1556 | // root record? 1557 | if (is_null($model['relation'])) 1558 | { 1559 | // store the record if not already present 1560 | isset($result[$pk]) or $result[$pk] = $record; 1561 | 1562 | // and add a pointer to it 1563 | $pointers[""] =& $result[$pk]; 1564 | } 1565 | 1566 | // related record 1567 | else 1568 | { 1569 | $parent = explode('.', $relation); 1570 | $current = array_pop($parent); 1571 | $parent = implode('.', $parent); 1572 | 1573 | if ( ! array_key_exists($parent, $pointers)) 1574 | { 1575 | throw new \FuelException("Record hydration exception: parent record \"$parent\" can not be located. This should not happen!"); 1576 | } 1577 | 1578 | if ($reltypes[$relation]) 1579 | { 1580 | // singular relation 1581 | if ( ! isset($pointers[$parent][$current])) 1582 | { 1583 | $pointers[$parent][$current] = $record; 1584 | } 1585 | $pointers[$relation] =& $pointers[$parent][$current]; 1586 | } 1587 | else 1588 | { 1589 | // non-singular relation 1590 | if ( ! isset($pointers[$parent][$current])) 1591 | { 1592 | $pointers[$parent][$current] = array($pk => $record); 1593 | } 1594 | elseif ( ! isset($pointers[$parent][$current][$pk])) 1595 | { 1596 | $pointers[$parent][$current][$pk] = $record; 1597 | } 1598 | $pointers[$relation] =& $pointers[$parent][$current][$pk]; 1599 | } 1600 | } 1601 | } 1602 | } 1603 | 1604 | /** 1605 | * Get the Query as it's been build up to this point and return it as an object 1606 | * 1607 | * @return Database_Query 1608 | */ 1609 | public function get_query() 1610 | { 1611 | // Get the columns 1612 | $columns = $this->select(false); 1613 | 1614 | // Start building the query 1615 | $select = $columns; 1616 | if ($this->use_subquery()) 1617 | { 1618 | $select = array(); 1619 | foreach ($columns as $c) 1620 | { 1621 | $select[] = $c[0]; 1622 | } 1623 | } 1624 | $query = \Database_Connection::instance($this->connection)->select(array_values($select)); 1625 | 1626 | // Set the defined connection on the query 1627 | $query->set_connection($this->connection); 1628 | 1629 | // Set from table 1630 | $query->from(array($this->_table(), $this->alias)); 1631 | 1632 | // Build the query further 1633 | $tmp = $this->build_query($query, $columns); 1634 | 1635 | return $tmp['query']; 1636 | } 1637 | 1638 | /** 1639 | * Build the query and return single object hydrated 1640 | * 1641 | * @return Model 1642 | */ 1643 | public function get_one() 1644 | { 1645 | // save the current limits 1646 | $limit = $this->limit; 1647 | $rows_limit = $this->rows_limit; 1648 | 1649 | if ($this->rows_limit !== null) 1650 | { 1651 | $this->limit = null; 1652 | $this->rows_limit = 1; 1653 | } 1654 | else 1655 | { 1656 | $this->limit = 1; 1657 | $this->rows_limit = null; 1658 | } 1659 | 1660 | // get the result using normal find 1661 | $result = $this->get(); 1662 | 1663 | // put back the old limits 1664 | $this->limit = $limit; 1665 | $this->rows_limit = $rows_limit; 1666 | 1667 | return $result ? reset($result) : null; 1668 | } 1669 | 1670 | /** 1671 | * Count the result of a query 1672 | * 1673 | * @param bool $column False for random selected column or specific column, only works for main model currently 1674 | * @param bool $distinct True if DISTINCT has to be aded to the query 1675 | * 1676 | * @return mixed number of rows OR false 1677 | */ 1678 | public function count($column = null, $distinct = true) 1679 | { 1680 | $select = $column ?: \Arr::get(call_user_func($this->model.'::primary_key'), 0); 1681 | $select = (strpos($select, '.') === false ? $this->alias.'.'.$select : $select); 1682 | 1683 | // Get the columns 1684 | $columns = \DB::expr('COUNT('.($distinct ? 'DISTINCT ' : ''). 1685 | \Database_Connection::instance($this->connection)->quote_identifier($select). 1686 | ') AS count_result'); 1687 | 1688 | // Remove the current select and 1689 | $query = \Database_Connection::instance($this->connection)->select(array($columns)); 1690 | 1691 | // Set from view or table 1692 | $query->from(array($this->_table(), $this->alias)); 1693 | 1694 | $tmp = $this->build_query($query, $columns, 'count'); 1695 | $query = $tmp['query']; 1696 | $count = $query->execute($this->connection)->get('count_result'); 1697 | 1698 | // Database_Result::get('count_result') returns a string | null 1699 | if ($count === null) 1700 | { 1701 | return false; 1702 | } 1703 | 1704 | return (int) $count; 1705 | } 1706 | 1707 | /** 1708 | * Get the maximum of a column for the current query 1709 | * 1710 | * @param string $column Column 1711 | * @return bool|int maximum value OR false 1712 | */ 1713 | public function max($column) 1714 | { 1715 | is_array($column) and $column = array_shift($column); 1716 | 1717 | // Get the columns 1718 | $columns = \DB::expr('MAX('. 1719 | \Database_Connection::instance($this->connection)->quote_identifier($this->alias.'.'.$column). 1720 | ') AS max_result'); 1721 | 1722 | // Remove the current select and 1723 | $query = \Database_Connection::instance($this->connection)->select(array($columns)); 1724 | 1725 | // Set from table 1726 | $query->from(array($this->_table(), $this->alias)); 1727 | 1728 | $tmp = $this->build_query($query, $columns, 'max'); 1729 | $query = $tmp['query']; 1730 | $max = $query->execute($this->connection)->get('max_result'); 1731 | 1732 | // Database_Result::get('max_result') returns a string | null 1733 | if ($max === null) 1734 | { 1735 | return false; 1736 | } 1737 | 1738 | return $max; 1739 | } 1740 | 1741 | /** 1742 | * Get the minimum of a column for the current query 1743 | * 1744 | * @param string $column Column which min value you want to get 1745 | * 1746 | * @return bool|int minimum value OR false 1747 | */ 1748 | public function min($column) 1749 | { 1750 | is_array($column) and $column = array_shift($column); 1751 | 1752 | // Get the columns 1753 | $columns = \DB::expr('MIN('. 1754 | \Database_Connection::instance($this->connection)->quote_identifier($this->alias.'.'.$column). 1755 | ') AS min_result'); 1756 | 1757 | // Remove the current select and 1758 | $query = \Database_Connection::instance($this->connection)->select(array($columns)); 1759 | 1760 | // Set from table 1761 | $query->from(array($this->_table(), $this->alias)); 1762 | 1763 | $tmp = $this->build_query($query, $columns, 'min'); 1764 | $query = $tmp['query']; 1765 | $min = $query->execute($this->connection)->get('min_result'); 1766 | 1767 | // Database_Result::get('min_result') returns a string | null 1768 | if ($min === null) 1769 | { 1770 | return false; 1771 | } 1772 | 1773 | return $min; 1774 | } 1775 | 1776 | /** 1777 | * Run INSERT with the current values 1778 | * 1779 | * @return bool|int Last inserted ID (if present) or false on failure 1780 | */ 1781 | public function insert() 1782 | { 1783 | $res = \Database_Connection::instance($this->connection) 1784 | ->insert(call_user_func($this->model.'::table'), array_keys($this->values)) 1785 | ->values(array_values($this->values)) 1786 | ->execute($this->write_connection); 1787 | 1788 | // Failed to save the new record 1789 | if ($res[1] === 0) 1790 | { 1791 | return false; 1792 | } 1793 | 1794 | return $res[0]; 1795 | } 1796 | 1797 | /** 1798 | * Run UPDATE with the current values 1799 | * 1800 | * @return bool success of update operation 1801 | */ 1802 | public function update() 1803 | { 1804 | // temporary disable relations 1805 | $tmp_relations = $this->relations; 1806 | $this->relations = array(); 1807 | 1808 | // Build query and execute update 1809 | $query = \Database_Connection::instance($this->connection) 1810 | ->update(call_user_func($this->model.'::table')); 1811 | 1812 | $tmp = $this->build_query($query, array(), 'update'); 1813 | $query = $tmp['query']; 1814 | $res = $query->set($this->values)->execute($this->write_connection); 1815 | 1816 | // put back any relations settings 1817 | $this->relations = $tmp_relations; 1818 | 1819 | // Update can affect 0 rows when input types are different but outcome stays the same 1820 | return $res >= 0; 1821 | } 1822 | 1823 | /** 1824 | * Run DELETE with the current values 1825 | * 1826 | * @return bool success of delete operation 1827 | */ 1828 | public function delete() 1829 | { 1830 | // temporary disable relations 1831 | $tmp_relations = $this->relations; 1832 | $this->relations = array(); 1833 | 1834 | // Build query and execute update 1835 | $query = \Database_Connection::instance($this->connection) 1836 | ->delete(call_user_func($this->model.'::table')); 1837 | 1838 | $tmp = $this->build_query($query, array(), 'delete'); 1839 | $query = $tmp['query']; 1840 | $res = $query->execute($this->write_connection); 1841 | 1842 | // put back any relations settings 1843 | $this->relations = $tmp_relations; 1844 | 1845 | return $res > 0; 1846 | } 1847 | } 1848 | -------------------------------------------------------------------------------- /classes/query/soft.php: -------------------------------------------------------------------------------- 1 | _col_name = $col_name; 38 | return $this; 39 | } 40 | 41 | /** 42 | * Make sure the soft-filter is added to get() calls 43 | */ 44 | public function get() 45 | { 46 | $this->add_soft_filter(); 47 | return parent::get(); 48 | } 49 | 50 | /** 51 | * Make sure the soft-filter is added to count() calls 52 | */ 53 | public function count($column = null, $distinct = true) 54 | { 55 | $this->add_soft_filter(); 56 | return parent::count($column, $distinct); 57 | } 58 | 59 | /** 60 | * Make sure the soft-filter is added to min() calls 61 | */ 62 | public function min($column) 63 | { 64 | $this->add_soft_filter(); 65 | return parent::min($column); 66 | } 67 | 68 | /** 69 | * Make sure the soft-filter is added to max() calls 70 | */ 71 | public function max($column) 72 | { 73 | $this->add_soft_filter(); 74 | return parent::max($column); 75 | } 76 | 77 | protected function modify_join_result($join_result, $name) 78 | { 79 | if ( ! is_null($this->_col_name) and is_subclass_of($join_result[$name]['model'], '\Orm\Model_Soft')) 80 | { 81 | $table = $join_result[$name]['table'][1]; 82 | $join_result[$name]['join_on'][] = array("$table.$this->_col_name", 'IS', \DB::expr('NULL')); 83 | } 84 | 85 | return parent::modify_join_result($join_result, $name); 86 | } 87 | 88 | /** 89 | * Add an additional where clause if needed to execute the soft-filter 90 | */ 91 | protected function add_soft_filter() 92 | { 93 | if ($this->_col_name !== null) 94 | { 95 | // Capture any filtering that has already been added 96 | $current_where = $this->where; 97 | 98 | // If there is no filtering then we don't need to add any special organization 99 | if ( ! empty($current_where)) 100 | { 101 | $this->where = array(); 102 | 103 | // Make sure the existing filtering is wrapped safely 104 | $this->and_where_open(); 105 | $this->where = array_merge($this->where, $current_where); 106 | $this->and_where_close(); 107 | } 108 | 109 | // Finally add the soft delete filtering 110 | $this->where($this->_col_name, null); 111 | } 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /classes/query/temporal.php: -------------------------------------------------------------------------------- 1 | timestamp = $stamp; 42 | $this->timestamp_end_col = $timestamp_end_col; 43 | $this->timestamp_start_col = $timestamp_start_col; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * Adds extra where conditions when temporal filtering is needed. 50 | * 51 | * @param array $join_result 52 | * @param string $name 53 | * @return array 54 | */ 55 | protected function modify_join_result($join_result, $name) 56 | { 57 | if ( ! is_null($this->timestamp) and is_subclass_of($join_result[$name]['model'], '\Orm\Model_Temporal')) 58 | { 59 | //Add the needed conditions to allow for temporal-ness 60 | $table = $join_result[$name]['table'][1]; 61 | $query_time = \DB::escape($this->timestamp); 62 | $join_result[$name]['join_on'][] = array("$table.$this->timestamp_start_col", '<=', $query_time); 63 | $join_result[$name]['join_on'][] = array("$table.$this->timestamp_end_col", '>=', $query_time); 64 | } 65 | 66 | return $join_result; 67 | } 68 | 69 | public function hydrate(&$row, $models, \stdClass $result, $model = null, $select = null, $primary_key = null) 70 | { 71 | if( is_subclass_of($model, '\Orm\Model_Temporal')) 72 | { 73 | $primary_key[] = $this->timestamp_start_col; 74 | $primary_key[] = $this->timestamp_end_col; 75 | } 76 | parent::hydrate($row, $models, $result, $model, $select, $primary_key); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /classes/relation.php: -------------------------------------------------------------------------------- 1 | property, the table alias is 111 | * given to be included with the property 112 | * 113 | * @param string 114 | * @return array 115 | */ 116 | public function select($table) 117 | { 118 | $props = call_user_func(array($this->model_to, 'properties')); 119 | $i = 0; 120 | $properties = array(); 121 | foreach ($props as $pk => $pv) 122 | { 123 | $properties[] = array($table.'.'.$pk, $table.'_c'.$i); 124 | $i++; 125 | } 126 | 127 | return $properties; 128 | } 129 | 130 | /** 131 | * Returns tables to join and fields to select with optional additional settings like order/where 132 | * 133 | * @param $alias_from 134 | * @param $rel_name 135 | * @param $alias_to 136 | * 137 | * @return array 138 | */ 139 | abstract public function join($alias_from, $rel_name, $alias_to); 140 | 141 | /** 142 | * Saves the current relationships and may cascade saving to model_to instances 143 | * 144 | * @param Model $model_from 145 | * @param Model $model_to 146 | * @param $original_model_id 147 | * @param $parent_saved 148 | * @param bool|null $cascade 149 | * @internal param \Orm\instance $Model of model_from 150 | * @internal param array|\Orm\Model $single or multiple model instances to save 151 | * @internal param \Orm\whether $bool the model_from has been saved already 152 | * @internal param bool|null $either uses default setting (null) or forces when true or prevents when false 153 | * 154 | * @return 155 | */ 156 | abstract public function save($model_from, $model_to, $original_model_id, $parent_saved, $cascade); 157 | 158 | /** 159 | * Takes the current relations and attempts to delete them when cascading is allowed or forced 160 | * 161 | * @param Model $model_from instance of model_from 162 | * @param bool $parent_deleted whether the model_from has been deleted already 163 | * @param null|bool $cascade either uses default setting (null) or forces when true or prevents when false 164 | */ 165 | abstract public function delete($model_from, $parent_deleted, $cascade); 166 | 167 | /** 168 | * Allow outside access to protected properties 169 | * 170 | * @param $property 171 | * @throws \FuelException Invalid relation property 172 | * @return 173 | */ 174 | public function __get($property) 175 | { 176 | if (strncmp($property, '_', 1) == 0 or ! property_exists($this, $property)) 177 | { 178 | throw new \FuelException('Invalid relation property: '.$property); 179 | } 180 | 181 | return $this->{$property}; 182 | } 183 | 184 | /** 185 | * Returns true if this relation is a singular relation. Eg, has_one not has_many 186 | * 187 | * @return bool 188 | */ 189 | public function is_singular() 190 | { 191 | return $this->singular; 192 | } 193 | 194 | /** 195 | * Returns the model class this relation points to 196 | * 197 | * @return bool 198 | */ 199 | public function model() 200 | { 201 | return $this->model_to; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuel/orm", 3 | "description" : "FuelPHP 1.x ORM Package", 4 | "type": "fuel-package", 5 | "homepage": "https://github.com/fuel/orm", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "FuelPHP Development Team", 10 | "email": "team@fuelphp.com" 11 | } 12 | ], 13 | "require": { 14 | "composer/installers": "~1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /config/orm.php: -------------------------------------------------------------------------------- 1 | true, 16 | 17 | // lazy load related objects 18 | 'relation_lazy_load' => false, 19 | 20 | // temporal model settings 21 | 'sql_max_timestamp_mysql' => '2038-01-18 22:14:08', 22 | 'sql_max_timestamp_unix' => 2147483647, 23 | ); 24 | --------------------------------------------------------------------------------