A modest collection of PHP libraries used at SparkFun.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1326 lines
38 KiB

  1. <?php
  2. use \SparkLib\DB;
  3. use \SparkLib\Blode\Event;
  4. /**
  5. * It's hard to believe this isn't a PHP builtin.
  6. *
  7. * @param $source array to pull a subset of key/value pairs out of
  8. * @param $keys list of keys to pull out
  9. * @return $result array
  10. */
  11. function array_subset_of_the_array_based_on_a_list_of_keys (array $source, array $keys)
  12. {
  13. $result = array();
  14. foreach ($keys as &$key)
  15. $result[$key] = $source[$key];
  16. return $result;
  17. }
  18. /**
  19. * SparkRecord - A base class for model objects stored in SQL
  20. *
  21. * See also:
  22. * burn.php - generate records
  23. * configuration/records.php - configuration for sparkgen.php
  24. * lib/classes/SparkFinder.php - get an iterator for a collection of dinosaurs
  25. * lib/classes/SparkFriendFinder.php - get an iterator for a collection of dinosaurs
  26. * lib/classes/dinosaurs/*Saurus.php - interface
  27. * lib/classes/dinosaurs/records/*Record.php - generated
  28. */
  29. abstract class SparkRecord {
  30. // This will be our data store:
  31. protected $_record = array();
  32. protected $_extraSelects = array();
  33. protected $_joined = array();
  34. protected $_tableId;
  35. // This will store record values we've changed using __set()
  36. // e.g., $somerecord->property = 'somevalue'
  37. protected $_changes = array();
  38. // This will store the original values of changed fields.
  39. protected $_originalValues = array();
  40. // Has the record changed?
  41. protected $_changed = false;
  42. // Has this record been loaded from the db?
  43. protected $_loaded = false;
  44. // Do we think we can plausibly delete the row represented by this record?
  45. protected $_canDelete = false;
  46. // Should we use the cache?
  47. protected $_useCache = false;
  48. // Should we save all modifications to this dino to MongoDB?
  49. protected $_logModifications = false;
  50. // are there any fields we don't care about for modification logging?
  51. protected $_ignoreModifications = array();
  52. // Store has_one and belongs_to results here.
  53. protected $_dinostore = array();
  54. protected static $_relationships;
  55. protected $_field_cache = array();
  56. // A place to stash data which can be used by update/delete/insert hooks -
  57. // stuff like an associated user id, change note, etc.
  58. protected $_modificationInfo = array();
  59. /**
  60. * Build a record, by one of
  61. * 1. Loading defaults for the class.
  62. * 2. Loading a single id from the database.
  63. * 3. Populating it from a row of values passed in - generally from a Spark(Friend)Finder.
  64. *
  65. * postLoad() will be called here. There's no preLoad() because
  66. * That sounds fairly dangerous
  67. *
  68. * @param mixed $loadFrom may be either an integer id or an array of key/value pairs, such as
  69. * returned by PDO's fetch().
  70. *
  71. * @param boolean $useCache should we check the cache for a copy of the record, and store one
  72. * there if it doesn't exist?
  73. */
  74. public function __construct ($loadFrom = null, $useCache = false)
  75. {
  76. $this->_record = static::$_defaults;
  77. // Caching? Avoid the whole question if caching is turned off site-wide:
  78. if (constant('\MEMCACHED_ENABLED') === true)
  79. $this->_useCache = $useCache;
  80. if (isset($loadFrom)) {
  81. if (is_numeric($loadFrom) && ($loadFrom >= 0)) {
  82. $this->loadFromId($loadFrom);
  83. } elseif (is_array($loadFrom)) {
  84. $this->loadFromRow($loadFrom);
  85. } else {
  86. throw new UnexpectedValueException('expected result row array or integer id 0 or above - got ' . $loadFrom);
  87. }
  88. }
  89. $this->postLoad();
  90. }
  91. /**
  92. * Post-load hook. Override this if you need to take an action after data
  93. * has been retrieved from the db. Be really careful.
  94. */
  95. protected function postLoad () { }
  96. /**
  97. * Set the primary key for this record.
  98. *
  99. * *this is a hack*
  100. *
  101. * @param int $value
  102. * @access public
  103. * @return void
  104. */
  105. public function setPrimaryKey ($value)
  106. {
  107. $this->_originalValues[static::$_tableKey] = $this->_record[static::$_tableKey];
  108. $this->_record[static::$_tableKey] = $value;
  109. $this->_changes[static::$_tableKey] = $value;
  110. $this->_changed = true;
  111. }
  112. protected function hasMany ($value)
  113. {
  114. return isset(static::$_relationships[$value])
  115. && static::$_relationships[$value]['type'] == 'has_many';
  116. }
  117. protected function hasOne ($value)
  118. {
  119. return isset(static::$_relationships[$value])
  120. && static::$_relationships[$value]['type'] == 'has_one';
  121. }
  122. protected function belongsTo ($value)
  123. {
  124. return isset(static::$_relationships[$value])
  125. && static::$_relationships[$value]['type'] == 'belongs_to';
  126. }
  127. /**
  128. * Setter magic - set a corresponding element in our record.
  129. *
  130. * if an appropriate dinosaur is passed into a valid has_one, its
  131. * the corresponding key will be set and the object loaded
  132. *
  133. * for now there's no support for setting a has_many
  134. */
  135. public function __set($property, $value)
  136. {
  137. if ($property === static::$_tableKey)
  138. throw new SparkRecordException('you may not manually set the primary key.');
  139. // Are we just setting a scalar value?
  140. if ($this->isValidField($property)) {
  141. $this->typeValidate($property, $value);
  142. if ($this->_record[$property] !== $value) {
  143. $this->_originalValues[$property] = $this->_record[$property];
  144. $this->_record[$property] = $value;
  145. $this->_changes[$property] = $value;
  146. $this->_changed = true;
  147. }
  148. return $value;
  149. }
  150. // Or are we setting a relationship? Value must be a dino.
  151. // belongs_to? Set our key to theirs.
  152. if ($this->belongsTo($property)) {
  153. $key = static::$_relationships[$property]['fk'];
  154. $this->$key = $value->getId();
  155. $this->_dinostore[$property] = $value;
  156. return $value;
  157. }
  158. // has_one? Set their key to ours.
  159. if ($this->hasOne($property)) {
  160. // Disabling this for now. It makes the update sequence confusing because
  161. // you would have to call it on the target dino. It has never worked anyway.
  162. throw new SparkRecordException('no setting has_one for now.');
  163. /*
  164. $key = static::$_relationships[$property]['pk'];
  165. $value->$key = $this->getId();
  166. $this->_dinostore[$property] = $value;
  167. return true;
  168. */
  169. }
  170. throw new SparkRecordException("setting $property not allowed in " . get_class($this));
  171. }
  172. /**
  173. * Assign values from a source array (or object).
  174. *
  175. * Expects an object with public properties or an array. Skips
  176. * items not set on the source or invalid for this SparkRecord.
  177. *
  178. * @param $source of key/value pairs
  179. * @param $fields array of fields to extract from $source and set
  180. * @return $this
  181. */
  182. public function setFrom ($source, $fields = null)
  183. {
  184. if (! isset($fields))
  185. $fields = $this->getFields();
  186. if (is_array($source))
  187. $source = (object)$source;
  188. foreach ($fields as &$field) {
  189. if ($field === static::$_tableKey)
  190. continue;
  191. if (isset($source->$field) && $this->isValidField($field))
  192. $this->__set($field, $source->$field);
  193. }
  194. return $this;
  195. }
  196. /**
  197. * Getter magic
  198. *
  199. * Get a scalar field, or a relationship.
  200. *
  201. * @param string property name
  202. * @return scalar|SparkRecord|SparkFinder value for that property
  203. */
  204. public function __get ($property)
  205. {
  206. if ($this->isValidField($property))
  207. return $this->_record[$property];
  208. if (isset($this->_dinostore[$property]))
  209. return $this->_dinostore[$property];
  210. if (isset(static::$_relationships[$property])) {
  211. $class = static::$_relationships[$property]['class'];
  212. $key = static::$_relationships[$property]['fk'];
  213. } else {
  214. throw new SparkRecordException("getting $property not allowed in " . get_class($this));
  215. }
  216. /* TODO: Ok, so I kind of want to make a belongsTo relationship
  217. return a false if not set, like the hasOne relationship
  218. sort of incidentally does, rather than throwing an
  219. Exception because the key isn't set or refers to something
  220. that doesn't exist.
  221. I know that in a sense this maybe _should_ be an exception -
  222. we're sort of enforcing database constraints in code that
  223. way - but I suspect that in practical terms a lot of code
  224. would be simpler to write and less buggy if we could just
  225. take it as given that $address->Country would be false if
  226. not present, for example.
  227. If we decide to go this route, I'll do an
  228. ack-grep --php -C 'try {'|less
  229. ...and clean up anywhere that expects an exception here.
  230. I don't think there are _that_ many.
  231. -- bpb 5/13/2012 */
  232. if ($this->belongsTo($property)) {
  233. // This right here is what will blow up if we don't have a value for
  234. // the relationship:
  235. $this->_dinostore[$property] = new $class($this->_record[$key]);
  236. return $this->_dinostore[$property];
  237. }
  238. // TODO: this only works if key names are identical
  239. if ($this->hasOne($property)) {
  240. $this->_dinostore[$property] = $class::finder()->where($key)->eq($this->getId())->getOne();
  241. return $this->_dinostore[$property];
  242. }
  243. if ($this->hasMany($property)) {
  244. return new SparkFriendFinder($class, $key, $this->_tableId);
  245. }
  246. }
  247. /**
  248. * Check isset() against a given property of the record.
  249. *
  250. * This is actually trickier than it seems at first blush, and
  251. * should probably be expanded to account for relationships, but
  252. * it's hard to say exactly where the right level of abstraction is.
  253. */
  254. public function __isset ($property)
  255. {
  256. if ($this->isValidField($property))
  257. return isset($this->_record[$property]);
  258. // this covers some cases where a value may be cached
  259. if (isset(static::$_relationships[$property]))
  260. return isset($this->_dinostore[$property]);
  261. return false;
  262. }
  263. /**
  264. * Access any extra fields (by name) from the result row that created
  265. * this dinosaur.
  266. */
  267. public function extraSelect ($key)
  268. {
  269. return $this->_extraSelects[$key];
  270. }
  271. /**
  272. * Access all extra selects as an array.
  273. */
  274. public function extraSelects ()
  275. {
  276. return $this->_extraSelects;
  277. }
  278. /**
  279. * See if we have a given extra select value.
  280. */
  281. public function hasExtraSelect ($key)
  282. {
  283. return array_key_exists($key, $this->_extraSelects);
  284. }
  285. /**
  286. * Check the type of a field.
  287. *
  288. * TODO: Should this be static?
  289. */
  290. public function typeOf ($field)
  291. {
  292. return static::getType($field);;
  293. }
  294. /**
  295. * Return a value theoretically juggled into the correct PHP type.
  296. *
  297. * Post-PDO TODO: I think PDO may actually do this for us if we want
  298. * it. Right now, that would probably break a lot of assumptions.
  299. */
  300. public function getTyped ($property)
  301. {
  302. $types = static::getTypes();
  303. if (! $this->isValidField($property))
  304. throw new SparkRecordException($property . ' is not a member of ' . get_called_class());
  305. switch($types[$property]) {
  306. case 'int': return (int) $this->_record[$property]; break;
  307. case 'float': return (float) $this->_record[$property]; break;
  308. case 'bool': return (boolean) $this->_record[$property]; break;
  309. default: return $this->_record[$property]; break;
  310. }
  311. }
  312. /**
  313. * Return the record, with each individual element passed through getTyped().
  314. */
  315. public function getTypedRecord ()
  316. {
  317. $rec = array();
  318. if (func_num_args()) {
  319. $keys = func_get_args();
  320. $rec = array_subset_of_the_array_based_on_a_list_of_keys($this->_record, $keys);
  321. } else {
  322. $rec = $this->_record;
  323. }
  324. foreach ($rec as $key => $val) {
  325. $rec[$key] = $this->getTyped($key);
  326. }
  327. return $rec;
  328. }
  329. /**
  330. * Return a standalone finder for a given has_many relationship.
  331. */
  332. public function hatch ($property)
  333. {
  334. if (! $this->hasMany($property)) {
  335. throw new SparkRecordException("finding $property not allowed in " . get_class($this));
  336. }
  337. $class = static::$_relationships[$property]['class'];
  338. return new SparkFriendFinder($class, static::$_tableKey, $this->_tableId);
  339. }
  340. /**
  341. * Jurassic Park, motherfuckers.
  342. */
  343. public function __clone ()
  344. {
  345. // Unset the id in case we want to insert this as a new record
  346. $this->_tableId = null;
  347. $this->_record[static::$_tableKey] = null;
  348. }
  349. /**
  350. * I have to admit that without the clones, it would have not been a victory.
  351. */
  352. public function cloneInto ($clone = null, $ignored_saurs = [])
  353. {
  354. if (!isset($clone))
  355. $clone = clone $this;
  356. $clone->insert();
  357. foreach(static::$_relationships as $saur=>$relationship) {
  358. if ($relationship['type'] == 'has_many' && !in_array($saur, $ignored_saurs)) {
  359. $records = $this->$saur->find();
  360. while($record = $records->getNext())
  361. {
  362. $cloned = clone $record;
  363. $cloned->$relationship['fk'] = $clone->getID();
  364. $record->cloneInto($cloned, $ignored_saurs);
  365. }
  366. }
  367. }
  368. return $clone;
  369. }
  370. /**
  371. * @return integer id of current record
  372. */
  373. public function getId () { return $this->_tableId; }
  374. /**
  375. * @return integer id of current record
  376. */
  377. public function id () { return $this->_tableId; }
  378. /**
  379. * @return string name of table modeled by this class
  380. */
  381. public function getTableName () { return static::$_tableName; }
  382. /**
  383. * @return string name of primary key
  384. */
  385. public function getTableKey () { return static::$_tableKey; }
  386. /**
  387. * This will return a list of the fields this class knows about. A subtle
  388. * trap: This doesn't take relationships into account at all; it only models
  389. * the core table.
  390. *
  391. * @return array list of fields
  392. */
  393. public function getFields () { return array_keys($this->_record); }
  394. /**
  395. * Static getters for some basic info. These are poorly named.
  396. */
  397. public static function getBaseName () { return static::$_baseName; }
  398. public static function getDefaultTableName () { return static::$_tableName; }
  399. public static function getDefaultTableKey () { return static::$_tableKey; }
  400. public static function getDefaults () { return static::$_defaults; }
  401. public static function getTypes () { return static::$_types; }
  402. public static function getType ($field)
  403. {
  404. return static::$_types[ $field ];
  405. }
  406. // TODO: This is used by SparkFriendFinder to see if rels are valid,
  407. // but it doesn't take has one and has many into account by
  408. // default. Possibly those should just get moved into
  409. // this array... Anyhow, causes that bug where you have to
  410. // specify primary and foreign keys explicitly in records.php
  411. // for a rel to get picked up.
  412. public static function getRelationships () { return static::$_relationships; }
  413. /**
  414. * Are we allowed to set/get the following key?
  415. */
  416. public function isValidField ($field)
  417. {
  418. if(empty($this->_field_cache))
  419. foreach($this->_record as $k => &$v)
  420. $this->_field_cache[$k] = true;
  421. return isset($this->_field_cache[$field]);
  422. }
  423. /**
  424. * Has a given field changed via set magic?
  425. *
  426. * @param $field string name of field
  427. */
  428. public function isChangedField ($field)
  429. {
  430. return isset($this->_changes[$field]);
  431. }
  432. /**
  433. * How many fields have changed via set magic?
  434. */
  435. public function countChanges ()
  436. {
  437. return count($this->_changes);
  438. }
  439. /**
  440. * Has anything changed via set magic?
  441. */
  442. public function isChanged () { return $this->_changed; }
  443. /**
  444. * Return the original value from the db/blank record.
  445. */
  446. public function originalValue ($field) {
  447. if (array_key_exists($field, $this->_originalValues))
  448. return $this->_originalValues[$field];
  449. else
  450. return $this->_record[$field];
  451. }
  452. /**
  453. * Has our record been loaded from the db?
  454. */
  455. public function isLoaded () { return $this->_loaded; }
  456. /**
  457. * Can this record be deleted?
  458. */
  459. public function canDelete () { return $this->_canDelete; }
  460. /**
  461. * Return a string dump of the record array.
  462. */
  463. public function dumpRecord (array $fields = array(), $format = 'verbose')
  464. {
  465. if (count($fields))
  466. $record = array_subset_of_the_array_based_on_a_list_of_keys($this->_record, $fields);
  467. else
  468. $record = $this->_record;
  469. if ('oneline' === $format) {
  470. $output = '';
  471. foreach ($record as $field => &$value) {
  472. $output .= "[$field]\t$value\t";
  473. }
  474. return $output;
  475. } else {
  476. return print_r($record, 1);
  477. }
  478. }
  479. /**
  480. * Return the record array.
  481. *
  482. * If given an array of keys, will limit the returned result to those keys.
  483. *
  484. * If given multiple strings, will limit the returned results to those keys.
  485. */
  486. public function getRecord ()
  487. {
  488. if (func_num_args()) {
  489. $keys = func_get_args();
  490. if (is_array($keys[0])) {
  491. $keys = $keys[0];
  492. }
  493. return array_subset_of_the_array_based_on_a_list_of_keys($this->_record, $keys);
  494. } else {
  495. return $this->_record;
  496. }
  497. }
  498. /**
  499. * Return just the values of the current record array.
  500. *
  501. * If given an array of keys, will limit the returned result to those keys.
  502. *
  503. * If given multiple strings, will limit the returned results to those keys.
  504. */
  505. public function getValues ()
  506. {
  507. if (func_num_args()) {
  508. $keys = func_get_args();
  509. if (is_array($keys[0])) {
  510. $keys = $keys[0];
  511. }
  512. return array_values($this->getRecord($keys));
  513. }
  514. return array_values($this->_record);
  515. }
  516. /**
  517. * Return the changes we've made.
  518. */
  519. public function changes() { return $this->_changes; }
  520. /**
  521. * Return the values before those changes;
  522. */
  523. public function originalValues() {
  524. return array_merge(
  525. $this->_record,
  526. $this->_originalValues
  527. );
  528. }
  529. /**
  530. * Return a string dump of the changes we've made.
  531. */
  532. public function dumpChanges () { return print_r($this->_changes, 1); }
  533. /**
  534. * reload the dinosaur from the DB, wiping any current changes
  535. *
  536. * TODO: Should postLoad() be called here?
  537. */
  538. public function refresh ()
  539. {
  540. if (! $this->isLoaded())
  541. throw new SparkRecordException('Cannot refresh an unloaded dinosaur.');
  542. $this->loadFromId($this->getId());
  543. $this->markUnchanged();
  544. $this->_dinostore = array();
  545. }
  546. /**
  547. * Mark this record as unchanged, after a refresh() or an update().
  548. *
  549. * There may be conceptual problems here.
  550. */
  551. protected function markUnchanged ()
  552. {
  553. $this->_changes = array();
  554. $this->_changed = false;
  555. $this->_originalValues = array();
  556. }
  557. /**
  558. * Find an array for this record in memcache, if one exists.
  559. *
  560. * @return array
  561. */
  562. protected function getCache ($id)
  563. {
  564. $key = $this->cacheKey($id);
  565. return unserialize(SparkCache::getInstance()->get($key));
  566. }
  567. /**
  568. * Store this record in memcache.
  569. */
  570. protected function setCache ()
  571. {
  572. $key = $this->cacheKey();
  573. return SparkCache::getInstance()->set($key, serialize($this->_record));
  574. }
  575. /**
  576. * Invalidate the memcache entry for this record.
  577. */
  578. public function invalidateCache ()
  579. {
  580. if (! MEMCACHED_ENABLED === true)
  581. return;
  582. return SparkCache::getInstance()->delete($this->cacheKey());
  583. }
  584. /**
  585. * Make a cache key based on the class of the current dinosaur plus an
  586. * id - by default the one loaded from the DB. Will die screaming if
  587. * the id is not available, which is probably a good indication someone
  588. * tried to update() a record before inserting it.
  589. *
  590. * @param $id integer optional id
  591. * @return string key
  592. */
  593. public function cacheKey ($id = null)
  594. {
  595. if (! isset($id))
  596. $id = $this->getId();
  597. $current_dinosaur = get_class($this) . $id;
  598. if (! $id) {
  599. throw new SparkRecordException(
  600. 'No id when making cache key for '
  601. . $current_dinosaur
  602. . ' - has record been inserted?'
  603. );
  604. }
  605. return $current_dinosaur;
  606. }
  607. /**
  608. * Load data based on an id.
  609. */
  610. protected function loadFromId ($id)
  611. {
  612. // Handle caching
  613. $values = $this->_useCache
  614. ? $this->getCache($id)
  615. : null;
  616. // Cache values after loading?
  617. $savecache = false;
  618. if (! $values) {
  619. $savecache = true;
  620. $dbh = DB::getInstance();
  621. $tn = static::$_tableName;
  622. $tk = static::$_tableKey;
  623. $query = "SELECT * FROM \"{$tn}\" WHERE \"{$tk}\" = :id;";
  624. try {
  625. $sth = $dbh->prepare($query);
  626. $sth->execute(['id' => $id]);
  627. if (true === constant('\DB_LOGQUERIES')) {
  628. \SparkLib\Fail::log($sth->queryString, 'info');
  629. }
  630. } catch (Exception $e) {
  631. throw new SparkRecordException("Query failed: {$query}: " . $e->getMessage());
  632. }
  633. $count = $sth->rowCount();
  634. if ($count != 1)
  635. throw new SparkRecordException("{$count} record(s) found using {$query} - id should be unique, id given: {$id}");
  636. $values = $sth->fetch(\PDO::FETCH_ASSOC);
  637. }
  638. $this->loadFromRow($values);
  639. if ($this->_useCache && $savecache)
  640. $this->setCache();
  641. }
  642. /**
  643. * Store a row that's already been loaded from the db, potentially
  644. * including related dinosaurs from a JOIN.
  645. *
  646. * @param array result row
  647. */
  648. protected function loadFromRow (array &$result_row)
  649. {
  650. $joined = array();
  651. foreach ($result_row as $field => &$value) {
  652. if ($this->isValidField($field)) {
  653. $this->_record[$field] = $value;
  654. } elseif (strstr($field, '__')) {
  655. // the __ delimiter is expected to mean that we did a join in SFF
  656. list($dino_basename, $column) = explode('__', $field);
  657. $joined[$dino_basename][$column] = $value;
  658. } else {
  659. $this->_extraSelects[$field] = $value;
  660. }
  661. }
  662. // make friends:
  663. foreach ($joined as $dino_basename => &$values) {
  664. $dino_class = $dino_basename . 'Saurus';
  665. $this->_dinostore[$dino_basename] = new $dino_class($values); // rawr
  666. }
  667. $this->_loaded = true;
  668. $this->_canDelete = true;
  669. $this->_tableId = $this->_record[static::$_tableKey];
  670. }
  671. /**
  672. * Store any changes to a record in the db.
  673. *
  674. * Context-aware method that update()s or insert()s based on what needs
  675. * to happen. Useful if you don't know if the record was just created or
  676. * was around before you started.
  677. */
  678. public function wbang()
  679. {
  680. if ($this->isLoaded())
  681. return $this->update();
  682. else
  683. return $this->insert();
  684. }
  685. /**
  686. * Set some metadata to be accessed by pre- and post- change hooks on
  687. * child classes. (preUpdate, postUpdate, etc.)
  688. */
  689. public function modificationInfo ($info = null)
  690. {
  691. if (is_array($info))
  692. $this->_modificationInfo = $info;
  693. return $this->_modificationInfo;
  694. }
  695. /**
  696. * Used by logModifications to convert strings to UTF8
  697. */
  698. protected function to_utf8()
  699. {
  700. return function ($value) {
  701. if(! is_string($value)) {
  702. return $value;
  703. }
  704. return utf8_encode($value);
  705. };
  706. }
  707. /**
  708. * Saves the changes to the current record to MongoDB.
  709. * Called by insert/update/delete when the _logModifications boolean
  710. * is set to true in the dinosaur, or if modificationInfo is set before
  711. * update.
  712. */
  713. protected function logModifications($action, array $changes)
  714. {
  715. $mongo = ModificationsDBI::getInstance();
  716. $table = static::$_tableName;
  717. foreach ($this->_ignoreModifications as $ignore_field) {
  718. if (isset($changes[$ignore_field])) {
  719. unset($changes[$ignore_field]);
  720. }
  721. }
  722. // if there's nothing to log, avoid logging an empty record
  723. if (! count($changes)) {
  724. return;
  725. }
  726. $modification = $this->modificationInfo();
  727. $modification['id'] = (int)$this->getId();
  728. $modification['action'] = $action;
  729. $modification['date'] = new MongoDate();
  730. $modification['changed'] = $changes;
  731. $mongo->$table->insert($modification);
  732. }
  733. /**
  734. * pre- and post-update hooks - override these if you need them
  735. */
  736. protected function preUpdate() { }
  737. protected function postUpdate() { }
  738. /**
  739. * Store any changes to a record in the db.
  740. *
  741. * Calls preUpdate() and postUpdate(), which can be overridden as-need
  742. * in the child class. Also invalidates any cached version of the record
  743. * (even if it doesn't wind up doing anything to the db - this could
  744. * probably use some thought).
  745. *
  746. * Returns true if we updated something, false if we didn't.
  747. * Throws a SparkRecordException if the update fails.
  748. */
  749. public function update ()
  750. {
  751. $this->invalidateCache();
  752. $this->preUpdate();
  753. // Bail out if we don't have anything to update:
  754. if (! $this->isChanged())
  755. return false;
  756. $COMMA = ', ';
  757. $SPACE = ' ';
  758. $dbh = DB::getInstance();
  759. $numcols = count($this->_changes);
  760. $cols = '';
  761. $i = 0;
  762. $update_data = [];
  763. foreach ($this->_changes as $col => $val)
  764. {
  765. $i++;
  766. $end = ($i < $numcols ? $COMMA : $SPACE);
  767. $cols .= '"' . $col . '" = ';
  768. $val = $this->typeJuggle($col, $val);
  769. if ($val instanceof DB\Literal) {
  770. $val_str = $val->literal();
  771. } else {
  772. $val_str = ':' . $col;
  773. // tack on to array for later execution of prep'd statement:
  774. $update_data[$col] = $val;
  775. }
  776. $cols .= $val_str . $end;
  777. }
  778. $q = 'UPDATE "' . static::$_tableName . '" SET ' . $cols . ' WHERE '
  779. . static::$_tableKey . ' = :_tablekey';
  780. $update_data['_tablekey'] = $this->_tableId;
  781. try {
  782. $sth = $dbh->prepare($q);
  783. foreach ($update_data as $col => $val) {
  784. $sth->bindValue(":$col", $val);
  785. }
  786. $sth->execute($update_data);
  787. } catch (Exception $e) {
  788. throw new SparkRecordException('Update failed: ' . $e->getMessage());
  789. }
  790. if ($this->_logModifications || count($this->modificationInfo()) > 0) {
  791. $this->logModifications('UPDATE', $this->_changes);
  792. }
  793. $this->logUpdate();
  794. $this->postUpdate();
  795. // Reset the changes array and the _changed flag so that future updates
  796. // using the same dinosaur don't rewrite the same fields:
  797. $this->markUnchanged();
  798. return true;
  799. }
  800. /**
  801. * Pre- and post-insertion hooks - override these if you need them.
  802. *
  803. * Changes are still available for examination within postInsert(),
  804. * but will be reset immediately afterwards.
  805. */
  806. protected function preInsert () { }
  807. protected function postInsert () { }
  808. /**
  809. * Insert a new record in the db.
  810. *
  811. * Calls preInsert() and postInsert(), if they are defined in the child
  812. * class.
  813. */
  814. public function insert ()
  815. {
  816. $this->preInsert();
  817. $dbh = DB::getInstance();
  818. // build the list of things we actually care about inserting - those differing
  819. // from defaults, specifically:
  820. $insert_values = [];
  821. foreach ($this->_record as $insert_key => $insert_val) {
  822. if ($insert_val !== static::$_defaults[ $insert_key ]) {
  823. $insert_values[ $insert_key ] = $insert_val;
  824. }
  825. }
  826. if (! isset($insert_values[ static::$_tableKey ]))
  827. $insert_values[ static::$_tableKey ] = DB::DefaultValue();
  828. $numcols = count($insert_values);
  829. $q = 'INSERT INTO "' . static::$_tableName . '"';
  830. $cols = ' (';
  831. $vals = ' VALUES (';
  832. $insert_data = [];
  833. $i = 0;
  834. foreach ($insert_values as $col => $val)
  835. {
  836. $i++;
  837. $end = $i < $numcols ? ", " : ") ";
  838. // quote column names in case we run into a reserved word
  839. $cols .= '"' . $col . '"' . $end;
  840. $val = $this->typeJuggle($col, $val);
  841. if ($val instanceof DB\Literal) {
  842. $vals .= $val->literal();
  843. } else {
  844. $vals .= ':' . $col;
  845. // tack on to array for later execution of prep'd statement:
  846. $insert_data[$col] = $val;
  847. }
  848. $vals .= $end;
  849. }
  850. $q .= $cols . $vals;
  851. if ('pgsql' === constant('\DB_SERVER_TYPE')) {
  852. $q .= ' RETURNING *' ;
  853. }
  854. try {
  855. $sth = $dbh->prepare($q);
  856. foreach ($insert_data as $col => $val) {
  857. $sth->bindValue(":$col", $val);
  858. }
  859. $sth->execute();
  860. if ('pgsql' === constant('\DB_SERVER_TYPE')) {
  861. $insert_result = $sth->fetch(\PDO::FETCH_ASSOC);
  862. $this->loadFromRow($insert_result);
  863. } else {
  864. $this->_tableId = $dbh->lastInsertId();
  865. }
  866. } catch (Exception $e) {
  867. $failure_data_string = '';
  868. foreach ($insert_values as $failed_insert_column => $failed_insert_value) {
  869. if ($failed_insert_value instanceof DB\Literal) {
  870. $failed_insert_value = $failed_insert_value->literal();
  871. }
  872. $failure_data_string .= "$failed_insert_column: $failed_insert_value; ";
  873. }
  874. throw new SparkRecordException(
  875. 'Insert failed: ' . $e->getMessage() . "\n[Query] " . $q . "\n[Values] $failure_data_string\n"
  876. );
  877. }
  878. // We have an id now, so we could do a delete operation.
  879. $this->_canDelete = true;
  880. // try and keep this in synch
  881. $this->_record[static::$_tableKey] = $this->_tableId;
  882. if($this->_logModifications || count($this->modificationInfo()) > 0) {
  883. $this->logModifications('INSERT', $this->_record);
  884. }
  885. $this->logInsert();
  886. $this->postInsert();
  887. // Reset the changes array and the _changed flag so that
  888. // any update() calls using the same dinosaur don't rewrite
  889. // those fields, and so that postUpdate() can behave sanely
  890. // in that case.
  891. $this->markUnchanged();
  892. return $this->_tableId;
  893. }
  894. /**
  895. * Normalize values to a scheme expected by insert() and update().
  896. */
  897. protected function typeJuggle ($col, $val)
  898. {
  899. // promote actual PHP nulls to SQL nulls
  900. if (is_null($val))
  901. $val = new DB\Null;
  902. if ($val instanceof DB\Literal)
  903. return $val;
  904. // if we just have a PHP native type here, we may need to do
  905. // some juggling:
  906. switch ($this->typeOf($col)) {
  907. case 'bool':
  908. if ($val) {
  909. $val = DB::True();
  910. } else {
  911. $val = DB::False();
  912. }
  913. break;
  914. case 'int':
  915. break;
  916. }
  917. return $val;
  918. }
  919. /**
  920. * Blow up if someone is trying to use a bogus value.
  921. */
  922. protected function typeValidate ($col, $val)
  923. {
  924. if (is_null($val) || ($val instanceof DB\Null)) {
  925. if (! static::$_nullable[$col]) {
  926. throw new SparkRecordException(
  927. "$col is not a nullable column on " . get_class($this) . "'s table, " . static::$_tableName
  928. );
  929. }
  930. }
  931. }
  932. /**
  933. * Pre- and post-deletion hooks - override these if you need them.
  934. */
  935. protected function preDelete() { }
  936. protected function postDelete() { }
  937. /**
  938. * Delete a record from the db. You probably shouldn't be doing this.
  939. *
  940. * Calls preDelete() and postDelete(), if they are defined in the child.
  941. *
  942. * TODO: Should this call markUnchanged()?
  943. */
  944. public function delete ()
  945. {
  946. $this->invalidateCache();
  947. if (! $this->canDelete())
  948. throw new SparkRecordException("can't delete this record - sure you inserted or loaded it from the db?");
  949. $this->preDelete();
  950. $dbh = DB::getInstance();
  951. $tk = static::$_tableKey;
  952. $tn = static::$_tableName;
  953. try {
  954. $sth = $dbh->prepare(
  955. "DELETE FROM \"{$tn}\" WHERE \"{$tk}\" = :id"
  956. );
  957. $sth->execute(['id' => $this->_tableId]);
  958. } catch (Exception $e) {
  959. throw new SparkRecordException('Delete failed: ' . $e->getMessage());
  960. }
  961. if($this->_logModifications || count($this->modificationInfo()) > 0) {
  962. $this->logModifications('DELETE', $this->_record);
  963. }
  964. $this->logDelete();
  965. $this->postDelete();
  966. return true;
  967. }
  968. /*
  969. * These are about telling the universal log that something has happened...
  970. *
  971. * TODO - should the notification framework stuff move in here?
  972. *
  973. * The way I see it, this gets called from every insert(), update(),
  974. * or delete(). It should talk to some kind of data store that allows
  975. * for collections of arbitrary key-value pairs, and usefully record
  976. *
  977. * a. the dinosaur in question
  978. * b. time of action
  979. * c. actor (This Part is Tricky and an Open Question)
  980. * d. the delta in data
  981. *
  982. * This would be a foundation for a really robust general-purpose audit
  983. * log, as well as for a new alert system - or more generally a framework
  984. * for letting users define actions to take on given events, or collections
  985. * of events to monitor.
  986. *
  987. * There are a bunch of NoSQL systems that might work well here. Probably
  988. * it should be the sort of thing that we can also invoke from other places
  989. * to log less granular events. We probably need a class much like \SparkLib\Fail
  990. * to drive it.
  991. *
  992. * For right now, messing with the following functions...
  993. */
  994. /**
  995. * Log an update.
  996. */
  997. protected function logUpdate ()
  998. {
  999. $this->logWrite('update',$this->_changes);
  1000. }
  1001. /**
  1002. * Log the insertion of a new record.
  1003. */
  1004. protected function logInsert ()
  1005. {
  1006. $this->logWrite('insert', $this->_record);
  1007. }
  1008. /**
  1009. * Log a deletion.
  1010. */
  1011. protected function logDelete ()
  1012. {
  1013. $this->logWrite('delete');
  1014. }
  1015. /**
  1016. * Log some write operation to the database.
  1017. */
  1018. protected function logWrite ($action, $payload = array())
  1019. {
  1020. Event::info(array(
  1021. 'event' => 'sparkrecord.write',
  1022. 'type' => get_class($this),
  1023. 'id' => $this->getId(),
  1024. 'action' => $action,
  1025. 'payload' => $payload
  1026. ));
  1027. if(defined('\FLAG_NOTIFY') && constant('\FLAG_NOTIFY')) {
  1028. $dino_name = get_class($this);
  1029. if( \Spark\Notify::validDino($dino_name) ) {
  1030. $payload = array(
  1031. 'id' => $this->getId(),
  1032. 'record' => $this->getRecord(),
  1033. 'changes' => $this->changes(),
  1034. 'originalValues' => $this->originalValues()
  1035. );
  1036. \Spark\Notify::emit($dino_name, $action, $payload);
  1037. }
  1038. }
  1039. }
  1040. /**
  1041. * Provide a readable description of a Dinosaur.
  1042. *
  1043. * This is a silly hack and should not be used other than maybe in your handy
  1044. * REPL. (See sparksh.)
  1045. */
  1046. public static function describe ()
  1047. {
  1048. $text = 'Dinosaur: ' . get_called_class() . "\n\n";
  1049. // fields
  1050. $fieldcount = count(static::getDefaults());
  1051. $text .= "Fields ($fieldcount):\n\t";
  1052. $pkey = static::getDefaultTableKey();
  1053. foreach (static::getDefaults() as $field => $value) {
  1054. $pri = '';
  1055. if ($pkey == $field)
  1056. $pri = " (PRIMARY)";
  1057. if (strlen($value))
  1058. $value = " <$value>";
  1059. $fields[] = "$field{$value}{$pri}";
  1060. }
  1061. $text .= wordwrap(implode($fields, ', '), 70, "\n\t");
  1062. $text .= "\n";
  1063. // methods
  1064. $all_methods = get_class_methods(get_called_class());
  1065. $default_methods = get_class_methods('SparkRecord');
  1066. $interesting_methods = array_diff($all_methods, $default_methods);
  1067. $text .= "\nCustom methods:\n\t"
  1068. . wordwrap(implode($interesting_methods, ', '), 70, "\n\t")
  1069. . "\n\n";
  1070. // relationships
  1071. if (static::getRelationships()) {
  1072. $text .= "\nRelationships:\n";
  1073. foreach (static::getRelationships() as $rel => $keys) {
  1074. $text .= "\t$rel: $keys[0] -> $keys[1]\n";
  1075. }
  1076. }
  1077. return $text;
  1078. }
  1079. /**
  1080. * Get a SparkFriendFinder for this class.
  1081. */
  1082. public static function finder ()
  1083. {
  1084. return new SparkFriendFinder(get_called_class());
  1085. }
  1086. /**
  1087. * Get a SparkFriendFinder for this class where $field matches $value,
  1088. * with find() already called
  1089. *
  1090. * @param $field string
  1091. * @param $value string
  1092. */
  1093. public static function findBy ($field, $value)
  1094. {
  1095. return static::finder()->where($field)->eq($value)->find();
  1096. }
  1097. /**
  1098. * Return the first record for this class where $field matches $value
  1099. *
  1100. * @param $field string
  1101. * @param $value string
  1102. */
  1103. public static function getBy ($field, $value)
  1104. {
  1105. return static::findBy($field, $value)->getOne();
  1106. }
  1107. /**
  1108. * Return the first record for this class where the primary key matches $value
  1109. *
  1110. * @param $value string
  1111. */
  1112. public static function getById ($value)
  1113. {
  1114. return static::getBy(static::$_tableKey, $value);
  1115. }
  1116. /**
  1117. * Return the first record for this class where the primary key matches $value,
  1118. * with a FOR UPDATE clause appended to the select query.
  1119. *
  1120. * @param $value string
  1121. */
  1122. public static function getByIdForUpdate ($value)
  1123. {
  1124. return static::finder()->where(static::$_tableKey)->eq($value)->forUpdate()->getOne();
  1125. }
  1126. }