PageRenderTime 44ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/fuel/packages/orm/classes/model/temporal.php

https://bitbucket.org/trujka/codegrounds
PHP | 665 lines | 361 code | 104 blank | 200 comment | 28 complexity | 3ee4b97b6ac66d6e511d92761fd23036 MD5 | raw file
Possible License(s): MIT, BSD-3-Clause, LGPL-2.1
  1. <?php
  2. /**
  3. * Fuel
  4. *
  5. * Fuel is a fast, lightweight, community driven PHP5 framework.
  6. *
  7. * @package Fuel
  8. * @version 1.6
  9. * @author Fuel Development Team
  10. * @license MIT License
  11. * @copyright 2010 - 2013 Fuel Development Team
  12. * @link http://fuelphp.com
  13. */
  14. namespace Orm;
  15. /**
  16. * Allows revisions of database entries to be kept when updates are made.
  17. *
  18. * @package Orm
  19. * @author Fuel Development Team
  20. */
  21. class Model_Temporal extends Model
  22. {
  23. /**
  24. * Compound primary key that includes the start and end times is required
  25. */
  26. protected static $_primary_key = array('id', 'temporal_start', 'temporal_end');
  27. /**
  28. * Override to change default temporal paramaters
  29. */
  30. protected static $_temporal = array();
  31. /**
  32. * Contains cached temporal properties.
  33. */
  34. protected static $_temporal_cached = array();
  35. /**
  36. * Contains the status of the primary key disable flag for temporal models
  37. */
  38. protected static $_pk_check_disabled = array();
  39. /**
  40. * Contains the status for classes that defines if primaryKey() should return
  41. * just the ID.
  42. */
  43. protected static $_pk_id_only = array();
  44. /**
  45. * If the model has been loaded through find_revision then this will be set
  46. * to the timestamp used to find the revision.
  47. */
  48. protected $_lazy_timestamp = null;
  49. /**
  50. * Contains the filtering status for temporal queries
  51. */
  52. protected static $_lazy_filtered_classes = array();
  53. public static function _init()
  54. {
  55. \Config::load('orm', true);
  56. }
  57. /**
  58. * Gets the temporal properties.
  59. * Mostly stolen from the parent class properties() function
  60. *
  61. * @return array
  62. */
  63. public static function temporal_properties()
  64. {
  65. $class = get_called_class();
  66. // If already determined
  67. if (array_key_exists($class, static::$_temporal_cached))
  68. {
  69. return static::$_temporal_cached[$class];
  70. }
  71. $properties = array();
  72. // Try to grab the properties from the class...
  73. if (property_exists($class, '_temporal'))
  74. {
  75. //Load up the info
  76. $properties['start_column'] =
  77. \Arr::get(static::$_temporal, 'start_column', 'temporal_start');
  78. $properties['end_column'] =
  79. \Arr::get(static::$_temporal, 'end_column', 'temporal_end');
  80. $properties['mysql_timestamp'] =
  81. \Arr::get(static::$_temporal, 'mysql_timestamp', false);
  82. $properties['max_timestamp'] = ($properties['mysql_timestamp']) ?
  83. \Config::get('orm.sql_max_timestamp_mysql') :
  84. \Config::get('orm.sql_max_timestamp_unix');
  85. }
  86. // cache the properties for next usage
  87. static::$_temporal_cached[$class] = $properties;
  88. return static::$_temporal_cached[$class];
  89. }
  90. /**
  91. * Fetches temporal property description array, or specific data from
  92. * it.
  93. * Stolen from parent class.
  94. *
  95. * @param string property or property.key
  96. * @param mixed return value when key not present
  97. * @return mixed
  98. */
  99. public static function temporal_property($key, $default = null)
  100. {
  101. $class = get_called_class();
  102. // If already determined
  103. if (!array_key_exists($class, static::$_temporal_cached))
  104. {
  105. static::temporal_properties();
  106. }
  107. return \Arr::get(static::$_temporal_cached[$class], $key, $default);
  108. }
  109. /**
  110. * Finds a specific revision for the given ID. If a timestamp is specified
  111. * the revision returned will reflect the entity's state at that given time.
  112. * This will also load relations when requested.
  113. *
  114. * @param type $id
  115. * @param int $timestamp Null to get the latest revision (Same as find($id))
  116. * @param array $relations Names of the relations to load.
  117. * @return Subclass of Orm\Model_Temporal
  118. */
  119. public static function find_revision($id, $timestamp = null, $relations = array())
  120. {
  121. if ($timestamp == null)
  122. {
  123. return parent::find($id);
  124. }
  125. $timestamp_start_name = static::temporal_property('start_column');
  126. $timestamp_end_name = static::temporal_property('end_column');
  127. // Select the next latest revision after the requested one then use that
  128. // to get the revision before.
  129. self::disable_primary_key_check();
  130. $query = static::query()
  131. ->where('id', $id)
  132. ->where($timestamp_start_name, '<=', $timestamp)
  133. ->where($timestamp_end_name, '>', $timestamp);
  134. self::enable_primary_key_check();
  135. //Make sure the temporal stuff is activated
  136. $query->set_temporal_properties($timestamp, $timestamp_end_name, $timestamp_start_name);
  137. foreach ($relations as $relation)
  138. {
  139. $query->related($relation);
  140. }
  141. $query_result = $query->get_one();
  142. $query_result->set_lazy_timestamp($timestamp);
  143. return $query_result;
  144. }
  145. private function set_lazy_timestamp($timestamp)
  146. {
  147. $this->_lazy_timestamp = $timestamp;
  148. }
  149. /**
  150. * Overrides Model::get() to allow lazy loaded relations to be filtered
  151. * temporaly.
  152. *
  153. * @param string $property
  154. * @return mixed
  155. */
  156. public function & get($property)
  157. {
  158. // if a timestamp is set and that we have a temporal relation
  159. $rel = static::relations($property);
  160. if ($rel && is_subclass_of($rel->model_to, 'Orm\Model_Temporal'))
  161. {
  162. // find a specific revision or the newest if lazy timestamp is null
  163. $lazy_timestamp = $this->_lazy_timestamp ?: static::temporal_property('max_timestamp') - 1;
  164. //add the filtering and continue with the parent's behavour
  165. $class_name = $rel->model_to;
  166. $class_name::make_query_temporal($lazy_timestamp);
  167. $result =& parent::get($property);
  168. $class_name::make_query_temporal(null);
  169. return $result;
  170. }
  171. return parent::get($property);
  172. }
  173. /**
  174. * When a timestamp is set any query objects produced by this temporal model
  175. * will behave the same as find_revision()
  176. *
  177. * @param array $timestamp
  178. */
  179. private static function make_query_temporal($timestamp)
  180. {
  181. $class = get_called_class();
  182. static::$_lazy_filtered_classes[$class] = $timestamp;
  183. }
  184. /**
  185. * Overrides Model::query to provide a Temporal_Query
  186. *
  187. * @param array $options
  188. * @return Query_Temporal
  189. */
  190. public static function query($options = array())
  191. {
  192. $timestamp_start_name = static::temporal_property('start_column');
  193. $timestamp_end_name = static::temporal_property('end_column');
  194. $max_timestamp = static::temporal_property('max_timestamp');
  195. $query = Query_Temporal::forge(get_called_class(), static::connection(), $options)
  196. ->set_temporal_properties($max_timestamp, $timestamp_end_name, $timestamp_start_name);
  197. //Check if we need to add filtering
  198. $class = get_called_class();
  199. $timestamp = \Arr::get(static::$_lazy_filtered_classes, $class, null);
  200. if(! is_null($timestamp) )
  201. {
  202. $query->where($timestamp_start_name, '<=', $timestamp)
  203. ->where($timestamp_end_name, '>', $timestamp);
  204. }
  205. return $query;
  206. }
  207. /**
  208. * Returns a list of revisions between the given times with the most recent
  209. * first. This does not load relations.
  210. *
  211. * @param int|string $id
  212. * @param timestamp $earliestTime
  213. * @param timestamp $latestTime
  214. */
  215. public static function find_revisions_between($id, $earliestTime = null, $latestTime = null)
  216. {
  217. $timestamp_start_name = static::temporal_property('start_column');
  218. $max_timestamp = static::temporal_property('max_timestamp');
  219. if ($earliestTime == null)
  220. {
  221. $earliestTime = 0;
  222. }
  223. if($latestTime == null)
  224. {
  225. $latestTime = $max_timestamp;
  226. }
  227. static::disable_primary_key_check();
  228. //Select all revisions within the given range.
  229. $query = static::query()
  230. ->where('id', $id)
  231. ->where($timestamp_start_name, '>=', $earliestTime)
  232. ->where($timestamp_start_name, '<=', $latestTime);
  233. static::enable_primary_key_check();
  234. $revisions = $query->get();
  235. return $revisions;
  236. }
  237. /**
  238. * Overrides the default find method to allow the latest revision to be found
  239. * by default.
  240. *
  241. * If any new options to find are added the switch statement will have to be
  242. * updated too.
  243. *
  244. * @param type $id
  245. * @param array $options
  246. * @return type
  247. */
  248. public static function find($id = null, array $options = array())
  249. {
  250. $timestamp_end_name = static::temporal_property('end_column');
  251. $max_timestamp = static::temporal_property('max_timestamp');
  252. switch ($id)
  253. {
  254. case 'all':
  255. case 'first':
  256. case 'last':
  257. break;
  258. default:
  259. $id = (array) $id;
  260. $count = 0;
  261. foreach(static::getNonTimestampPks() as $key)
  262. {
  263. $options['where'][] = array($key, $id[$count]);
  264. $count++;
  265. }
  266. break;
  267. }
  268. $options['where'][] = array($timestamp_end_name, $max_timestamp);
  269. static::enable_id_only_primary_key();
  270. $result = parent::find($id, $options);
  271. static::disable_id_only_primary_key();
  272. return $result;
  273. }
  274. /**
  275. * Returns an array of the primary keys that are not related to temporal
  276. * timestamp information.
  277. */
  278. public static function getNonTimestampPks()
  279. {
  280. $timestamp_start_name = static::temporal_property('start_column');
  281. $timestamp_end_name = static::temporal_property('end_column');
  282. $pks = array();
  283. foreach(parent::primary_key() as $key)
  284. {
  285. if ($key != $timestamp_start_name && $key != $timestamp_end_name)
  286. {
  287. $pks[] = $key;
  288. }
  289. }
  290. return $pks;
  291. }
  292. /**
  293. * Overrides the save method to allow temporal models to be
  294. * @param boolean $cascade
  295. * @param boolean $use_transaction
  296. * @param boolean $skip_temporal Skips temporal filtering on initial inserts. Should not be used!
  297. * @return boolean
  298. */
  299. public function save($cascade = null, $use_transaction = false)
  300. {
  301. // Load temporal properties.
  302. $timestamp_start_name = static::temporal_property('start_column');
  303. $timestamp_end_name = static::temporal_property('end_column');
  304. $mysql_timestamp = static::temporal_property('mysql_timestamp');
  305. $max_timestamp = static::temporal_property('max_timestamp');
  306. $current_timestamp = $mysql_timestamp ?
  307. \Date::forge()->format('mysql') :
  308. \Date::forge()->get_timestamp();
  309. // If this is new then just call the parent and let everything happen as normal
  310. if ($this->is_new())
  311. {
  312. static::disable_primary_key_check();
  313. $this->{$timestamp_start_name} = $current_timestamp;
  314. $this->{$timestamp_end_name} = $max_timestamp;
  315. static::enable_primary_key_check();
  316. // Make sure save will populate the PK
  317. static::enable_id_only_primary_key();
  318. $result = parent::save($cascade, $use_transaction);
  319. static::disable_id_only_primary_key();
  320. return $result;
  321. }
  322. // If this is an update then set a new PK, save and then insert a new row
  323. else
  324. {
  325. $diff = $this->get_diff();
  326. if (count($diff[0]) > 0)
  327. {
  328. // Take a copy of this model
  329. $revision = clone $this;
  330. // Give that new model an end time of the current time after resetting back to the old data
  331. $revision->set($this->_original);
  332. self::disable_primary_key_check();
  333. $revision->{$timestamp_end_name} = $current_timestamp;
  334. self::enable_primary_key_check();
  335. // Make sure relations stay the same
  336. $revision->_original_relations = $this->_data_relations;
  337. // save that, now we have our archive
  338. self::enable_id_only_primary_key();
  339. $revision_result = $revision->overwrite(false, $use_transaction);
  340. self::disable_id_only_primary_key();
  341. if ( ! $revision_result)
  342. {
  343. // If the revision did not save then stop the process so the user can do something.
  344. return false;
  345. }
  346. // Now that the old data is saved update the current object so its end timestamp is now
  347. self::disable_primary_key_check();
  348. $this->{$timestamp_start_name} = $current_timestamp;
  349. self::enable_primary_key_check();
  350. $result = parent::save($cascade, $use_transaction);
  351. return $result;
  352. }
  353. else
  354. {
  355. // If nothing has changed call parent::save() to insure relations are saved too
  356. return parent::save($cascade, $use_transaction);
  357. }
  358. }
  359. }
  360. /**
  361. * ALlows an entry to be updated without having to insert a new row.
  362. * This will not record any changed data as a new revision.
  363. *
  364. * Takes the same options as Model::save()
  365. */
  366. public function overwrite($cascade = null, $use_transaction = false)
  367. {
  368. return parent::save($cascade, $use_transaction);
  369. }
  370. /**
  371. * Restores the entity to this state.
  372. *
  373. * @return boolean
  374. */
  375. public function restore()
  376. {
  377. $timestamp_end_name = static::temporal_property('end_column');
  378. $max_timestamp = static::temporal_property('max_timestamp');
  379. // check to see if there is a currently active row, if so then don't
  380. // restore anything.
  381. $activeRow = static::find('first', array(
  382. 'where' => array(
  383. array('id', $this->id),
  384. array($timestamp_end_name, $max_timestamp),
  385. ),
  386. ));
  387. if(is_null($activeRow))
  388. {
  389. // No active row was found so we are ok to go and restore the this
  390. // revision
  391. $timestamp_start_name = static::temporal_property('start_column');
  392. $mysql_timestamp = static::temporal_property('mysql_timestamp');
  393. $max_timestamp = static::temporal_property('max_timestamp');
  394. $current_timestamp = $mysql_timestamp ?
  395. \Date::forge()->format('mysql') :
  396. \Date::forge()->get_timestamp();
  397. // Make sure this is saved as a new entry
  398. $this->_is_new = true;
  399. // Update timestamps
  400. static::disable_primary_key_check();
  401. $this->{$timestamp_start_name} = $current_timestamp;
  402. $this->{$timestamp_end_name} = $max_timestamp;
  403. // Save
  404. $result = parent::save();
  405. static::enable_primary_key_check();
  406. return $result;
  407. }
  408. return false;
  409. }
  410. /**
  411. * Deletes all revisions of this entity permantly.
  412. */
  413. public function purge()
  414. {
  415. // Get a clean query object so there's no temporal filtering
  416. $query = parent::query();
  417. // Then select and delete
  418. return $query->where('id', $this->id)
  419. ->delete();
  420. }
  421. /**
  422. * Overrides update to remove PK checking when performing an update.
  423. */
  424. public function update()
  425. {
  426. static::disable_primary_key_check();
  427. $result = parent::update();
  428. static::enable_primary_key_check();
  429. return $result;
  430. }
  431. /**
  432. * Allows correct PKs to be added when performing updates
  433. *
  434. * @param Query $query
  435. */
  436. protected function add_primary_keys_to_where($query)
  437. {
  438. $timestamp_start_name = static::temporal_property('start_column');
  439. $timestamp_end_name = static::temporal_property('end_column');
  440. $primary_key = array(
  441. 'id',
  442. $timestamp_start_name,
  443. $timestamp_end_name,
  444. );
  445. foreach ($primary_key as $pk)
  446. {
  447. $query->where($pk, '=', $this->_original[$pk]);
  448. }
  449. }
  450. /**
  451. * Overrides the parent primary_key method to allow primaray key enforcement
  452. * to be turned off when updating a temporal model.
  453. */
  454. public static function primary_key()
  455. {
  456. $id_only = static::get_primary_key_id_only_status();
  457. $pk_status = static::get_primary_key_status();
  458. if ($id_only)
  459. {
  460. return static::getNonTimestampPks();
  461. }
  462. if ($pk_status && ! $id_only)
  463. {
  464. return static::$_primary_key;
  465. }
  466. return array();
  467. }
  468. public function delete($cascade = null, $use_transaction = false)
  469. {
  470. // If we are using a transcation then make sure it's started
  471. if ($use_transaction)
  472. {
  473. $db = \Database_Connection::instance(static::connection(true));
  474. $db->start_transaction();
  475. }
  476. // Call the observers
  477. $this->observe('before_delete');
  478. // Load temporal properties.
  479. $timestamp_end_name = static::temporal_property('end_column');
  480. $mysql_timestamp = static::temporal_property('mysql_timestamp');
  481. // Generate the correct timestamp and save it
  482. $current_timestamp = $mysql_timestamp ?
  483. \Date::forge()->format('mysql') :
  484. \Date::forge()->get_timestamp();
  485. static::disable_primary_key_check();
  486. $this->{$timestamp_end_name} = $current_timestamp;
  487. static::enable_primary_key_check();
  488. // Loop through all relations and delete if we are cascading.
  489. $this->freeze();
  490. foreach ($this->relations() as $rel_name => $rel)
  491. {
  492. // get the cascade delete status
  493. $relCascade = is_null($cascade) ? $rel->cascade_delete : (bool) $cascade;
  494. if ($relCascade)
  495. {
  496. if(get_class($rel) != 'Orm\ManyMany')
  497. {
  498. // Loop through and call delete on all the models
  499. foreach($rel->get($this) as $model)
  500. {
  501. $model->delete($cascade);
  502. }
  503. }
  504. }
  505. }
  506. $this->unfreeze();
  507. parent::save();
  508. $this->observe('after_delete');
  509. // Make sure the transaction is committed if needed
  510. $use_transaction and $db->commit_transaction();
  511. return $this;
  512. }
  513. /**
  514. * Disables PK checking
  515. */
  516. private static function disable_primary_key_check()
  517. {
  518. $class = get_called_class();
  519. self::$_pk_check_disabled[$class] = false;
  520. }
  521. /**
  522. * Enables PK checking
  523. */
  524. private static function enable_primary_key_check()
  525. {
  526. $class = get_called_class();
  527. self::$_pk_check_disabled[$class] = true;
  528. }
  529. /**
  530. * Returns true if the PK checking should be performed. Defaults to true
  531. */
  532. private static function get_primary_key_status()
  533. {
  534. $class = get_called_class();
  535. return \Arr::get(self::$_pk_check_disabled, $class, true);
  536. }
  537. /**
  538. * Returns true if the PK should only contain the ID. Defaults to false
  539. */
  540. private static function get_primary_key_id_only_status()
  541. {
  542. $class = get_called_class();
  543. return \Arr::get(self::$_pk_id_only, $class, false);
  544. }
  545. /**
  546. * Makes all PKs returned
  547. */
  548. private static function disable_id_only_primary_key()
  549. {
  550. $class = get_called_class();
  551. self::$_pk_id_only[$class] = false;
  552. }
  553. /**
  554. * Makes only id returned as PK
  555. */
  556. private static function enable_id_only_primary_key()
  557. {
  558. $class = get_called_class();
  559. self::$_pk_id_only[$class] = true;
  560. }
  561. }