|
|
- <?php
- use \SparkLib\DB;
- use \SparkLib\Blode\Event;
-
- /**
- * It's hard to believe this isn't a PHP builtin.
- *
- * @param $source array to pull a subset of key/value pairs out of
- * @param $keys list of keys to pull out
- * @return $result array
- */
- function array_subset_of_the_array_based_on_a_list_of_keys (array $source, array $keys)
- {
- $result = array();
- foreach ($keys as &$key)
- $result[$key] = $source[$key];
- return $result;
- }
-
- /**
- * SparkRecord - A base class for model objects stored in SQL
- *
- * See also:
- * burn.php - generate records
- * configuration/records.php - configuration for sparkgen.php
- * lib/classes/SparkFinder.php - get an iterator for a collection of dinosaurs
- * lib/classes/SparkFriendFinder.php - get an iterator for a collection of dinosaurs
- * lib/classes/dinosaurs/*Saurus.php - interface
- * lib/classes/dinosaurs/records/*Record.php - generated
- */
- abstract class SparkRecord {
-
- // This will be our data store:
- protected $_record = array();
- protected $_extraSelects = array();
- protected $_joined = array();
-
- protected $_tableId;
-
- // This will store record values we've changed using __set()
- // e.g., $somerecord->property = 'somevalue'
- protected $_changes = array();
-
- // This will store the original values of changed fields.
- protected $_originalValues = array();
-
- // Has the record changed?
- protected $_changed = false;
-
- // Has this record been loaded from the db?
- protected $_loaded = false;
-
- // Do we think we can plausibly delete the row represented by this record?
- protected $_canDelete = false;
-
- // Should we use the cache?
- protected $_useCache = false;
-
- // Should we save all modifications to this dino to MongoDB?
- protected $_logModifications = false;
-
- // are there any fields we don't care about for modification logging?
- protected $_ignoreModifications = array();
-
- // Store has_one and belongs_to results here.
- protected $_dinostore = array();
-
- protected static $_relationships;
-
- protected $_field_cache = array();
-
- // A place to stash data which can be used by update/delete/insert hooks -
- // stuff like an associated user id, change note, etc.
- protected $_modificationInfo = array();
-
- /**
- * Build a record, by one of
- * 1. Loading defaults for the class.
- * 2. Loading a single id from the database.
- * 3. Populating it from a row of values passed in - generally from a Spark(Friend)Finder.
- *
- * postLoad() will be called here. There's no preLoad() because
- * That sounds fairly dangerous
- *
- * @param mixed $loadFrom may be either an integer id or an array of key/value pairs, such as
- * returned by PDO's fetch().
- *
- * @param boolean $useCache should we check the cache for a copy of the record, and store one
- * there if it doesn't exist?
- */
- public function __construct ($loadFrom = null, $useCache = false)
- {
- $this->_record = static::$_defaults;
-
- // Caching? Avoid the whole question if caching is turned off site-wide:
- if (constant('\MEMCACHED_ENABLED') === true)
- $this->_useCache = $useCache;
-
- if (isset($loadFrom)) {
- if (is_numeric($loadFrom) && ($loadFrom >= 0)) {
- $this->loadFromId($loadFrom);
- } elseif (is_array($loadFrom)) {
- $this->loadFromRow($loadFrom);
- } else {
- throw new UnexpectedValueException('expected result row array or integer id 0 or above - got ' . $loadFrom);
- }
- }
-
- $this->postLoad();
- }
-
- /**
- * Post-load hook. Override this if you need to take an action after data
- * has been retrieved from the db. Be really careful.
- */
- protected function postLoad () { }
-
- /**
- * Set the primary key for this record.
- *
- * *this is a hack*
- *
- * @param int $value
- * @access public
- * @return void
- */
- public function setPrimaryKey ($value)
- {
- $this->_originalValues[static::$_tableKey] = $this->_record[static::$_tableKey];
- $this->_record[static::$_tableKey] = $value;
- $this->_changes[static::$_tableKey] = $value;
- $this->_changed = true;
- }
-
- protected function hasMany ($value)
- {
- return isset(static::$_relationships[$value])
- && static::$_relationships[$value]['type'] == 'has_many';
- }
-
- protected function hasOne ($value)
- {
- return isset(static::$_relationships[$value])
- && static::$_relationships[$value]['type'] == 'has_one';
- }
-
- protected function belongsTo ($value)
- {
- return isset(static::$_relationships[$value])
- && static::$_relationships[$value]['type'] == 'belongs_to';
- }
- /**
- * Setter magic - set a corresponding element in our record.
- *
- * if an appropriate dinosaur is passed into a valid has_one, its
- * the corresponding key will be set and the object loaded
- *
- * for now there's no support for setting a has_many
- */
- public function __set($property, $value)
- {
- if ($property === static::$_tableKey)
- throw new SparkRecordException('you may not manually set the primary key.');
-
- // Are we just setting a scalar value?
- if ($this->isValidField($property)) {
-
- $this->typeValidate($property, $value);
-
- if ($this->_record[$property] !== $value) {
- $this->_originalValues[$property] = $this->_record[$property];
- $this->_record[$property] = $value;
- $this->_changes[$property] = $value;
- $this->_changed = true;
- }
- return $value;
- }
-
- // Or are we setting a relationship? Value must be a dino.
-
- // belongs_to? Set our key to theirs.
- if ($this->belongsTo($property)) {
- $key = static::$_relationships[$property]['fk'];
- $this->$key = $value->getId();
- $this->_dinostore[$property] = $value;
- return $value;
- }
-
- // has_one? Set their key to ours.
- if ($this->hasOne($property)) {
- // Disabling this for now. It makes the update sequence confusing because
- // you would have to call it on the target dino. It has never worked anyway.
- throw new SparkRecordException('no setting has_one for now.');
- /*
- $key = static::$_relationships[$property]['pk'];
- $value->$key = $this->getId();
- $this->_dinostore[$property] = $value;
- return true;
- */
- }
-
- throw new SparkRecordException("setting $property not allowed in " . get_class($this));
- }
-
- /**
- * Assign values from a source array (or object).
- *
- * Expects an object with public properties or an array. Skips
- * items not set on the source or invalid for this SparkRecord.
- *
- * @param $source of key/value pairs
- * @param $fields array of fields to extract from $source and set
- * @return $this
- */
- public function setFrom ($source, $fields = null)
- {
- if (! isset($fields))
- $fields = $this->getFields();
-
- if (is_array($source))
- $source = (object)$source;
-
- foreach ($fields as &$field) {
- if ($field === static::$_tableKey)
- continue;
-
- if (isset($source->$field) && $this->isValidField($field))
- $this->__set($field, $source->$field);
- }
-
- return $this;
- }
-
- /**
- * Getter magic
- *
- * Get a scalar field, or a relationship.
- *
- * @param string property name
- * @return scalar|SparkRecord|SparkFinder value for that property
- */
- public function __get ($property)
- {
- if ($this->isValidField($property))
- return $this->_record[$property];
-
- if (isset($this->_dinostore[$property]))
- return $this->_dinostore[$property];
-
- if (isset(static::$_relationships[$property])) {
- $class = static::$_relationships[$property]['class'];
- $key = static::$_relationships[$property]['fk'];
- } else {
- throw new SparkRecordException("getting $property not allowed in " . get_class($this));
- }
-
- /* TODO: Ok, so I kind of want to make a belongsTo relationship
- return a false if not set, like the hasOne relationship
- sort of incidentally does, rather than throwing an
- Exception because the key isn't set or refers to something
- that doesn't exist.
-
- I know that in a sense this maybe _should_ be an exception -
- we're sort of enforcing database constraints in code that
- way - but I suspect that in practical terms a lot of code
- would be simpler to write and less buggy if we could just
- take it as given that $address->Country would be false if
- not present, for example.
-
- If we decide to go this route, I'll do an
-
- ack-grep --php -C 'try {'|less
-
- ...and clean up anywhere that expects an exception here.
- I don't think there are _that_ many.
-
- -- bpb 5/13/2012 */
-
- if ($this->belongsTo($property)) {
- // This right here is what will blow up if we don't have a value for
- // the relationship:
- $this->_dinostore[$property] = new $class($this->_record[$key]);
- return $this->_dinostore[$property];
- }
-
- // TODO: this only works if key names are identical
- if ($this->hasOne($property)) {
- $this->_dinostore[$property] = $class::finder()->where($key)->eq($this->getId())->getOne();
- return $this->_dinostore[$property];
- }
-
- if ($this->hasMany($property)) {
- return new SparkFriendFinder($class, $key, $this->_tableId);
- }
- }
-
- /**
- * Check isset() against a given property of the record.
- *
- * This is actually trickier than it seems at first blush, and
- * should probably be expanded to account for relationships, but
- * it's hard to say exactly where the right level of abstraction is.
- */
- public function __isset ($property)
- {
- if ($this->isValidField($property))
- return isset($this->_record[$property]);
-
- // this covers some cases where a value may be cached
- if (isset(static::$_relationships[$property]))
- return isset($this->_dinostore[$property]);
-
- return false;
- }
-
- /**
- * Access any extra fields (by name) from the result row that created
- * this dinosaur.
- */
- public function extraSelect ($key)
- {
- return $this->_extraSelects[$key];
- }
-
- /**
- * Access all extra selects as an array.
- */
- public function extraSelects ()
- {
- return $this->_extraSelects;
- }
-
- /**
- * See if we have a given extra select value.
- */
- public function hasExtraSelect ($key)
- {
- return array_key_exists($key, $this->_extraSelects);
- }
-
- /**
- * Check the type of a field.
- *
- * TODO: Should this be static?
- */
- public function typeOf ($field)
- {
- return static::getType($field);;
- }
-
- /**
- * Return a value theoretically juggled into the correct PHP type.
- *
- * Post-PDO TODO: I think PDO may actually do this for us if we want
- * it. Right now, that would probably break a lot of assumptions.
- */
- public function getTyped ($property)
- {
- $types = static::getTypes();
-
- if (! $this->isValidField($property))
- throw new SparkRecordException($property . ' is not a member of ' . get_called_class());
-
- switch($types[$property]) {
- case 'int': return (int) $this->_record[$property]; break;
- case 'float': return (float) $this->_record[$property]; break;
- case 'bool': return (boolean) $this->_record[$property]; break;
- default: return $this->_record[$property]; break;
- }
- }
-
- /**
- * Return the record, with each individual element passed through getTyped().
- */
- public function getTypedRecord ()
- {
- $rec = array();
-
- if (func_num_args()) {
- $keys = func_get_args();
- $rec = array_subset_of_the_array_based_on_a_list_of_keys($this->_record, $keys);
- } else {
- $rec = $this->_record;
- }
-
- foreach ($rec as $key => $val) {
- $rec[$key] = $this->getTyped($key);
- }
-
- return $rec;
- }
-
- /**
- * Return a standalone finder for a given has_many relationship.
- */
- public function hatch ($property)
- {
- if (! $this->hasMany($property)) {
- throw new SparkRecordException("finding $property not allowed in " . get_class($this));
- }
- $class = static::$_relationships[$property]['class'];
- return new SparkFriendFinder($class, static::$_tableKey, $this->_tableId);
- }
-
- /**
- * Jurassic Park, motherfuckers.
- */
- public function __clone ()
- {
- // Unset the id in case we want to insert this as a new record
- $this->_tableId = null;
- $this->_record[static::$_tableKey] = null;
- }
-
- /**
- * I have to admit that without the clones, it would have not been a victory.
- */
- public function cloneInto ($clone = null, $ignored_saurs = [])
- {
- if (!isset($clone))
- $clone = clone $this;
-
- $clone->insert();
-
- foreach(static::$_relationships as $saur=>$relationship) {
- if ($relationship['type'] == 'has_many' && !in_array($saur, $ignored_saurs)) {
- $records = $this->$saur->find();
- while($record = $records->getNext())
- {
- $cloned = clone $record;
- $cloned->$relationship['fk'] = $clone->getID();
- $record->cloneInto($cloned, $ignored_saurs);
- }
- }
- }
-
- return $clone;
- }
-
- /**
- * @return integer id of current record
- */
- public function getId () { return $this->_tableId; }
-
- /**
- * @return integer id of current record
- */
- public function id () { return $this->_tableId; }
-
- /**
- * @return string name of table modeled by this class
- */
- public function getTableName () { return static::$_tableName; }
-
- /**
- * @return string name of primary key
- */
- public function getTableKey () { return static::$_tableKey; }
-
- /**
- * This will return a list of the fields this class knows about. A subtle
- * trap: This doesn't take relationships into account at all; it only models
- * the core table.
- *
- * @return array list of fields
- */
- public function getFields () { return array_keys($this->_record); }
-
- /**
- * Static getters for some basic info. These are poorly named.
- */
- public static function getBaseName () { return static::$_baseName; }
- public static function getDefaultTableName () { return static::$_tableName; }
- public static function getDefaultTableKey () { return static::$_tableKey; }
- public static function getDefaults () { return static::$_defaults; }
- public static function getTypes () { return static::$_types; }
-
- public static function getType ($field)
- {
- return static::$_types[ $field ];
- }
-
- // TODO: This is used by SparkFriendFinder to see if rels are valid,
- // but it doesn't take has one and has many into account by
- // default. Possibly those should just get moved into
- // this array... Anyhow, causes that bug where you have to
- // specify primary and foreign keys explicitly in records.php
- // for a rel to get picked up.
- public static function getRelationships () { return static::$_relationships; }
-
- /**
- * Are we allowed to set/get the following key?
- */
- public function isValidField ($field)
- {
- if(empty($this->_field_cache))
- foreach($this->_record as $k => &$v)
- $this->_field_cache[$k] = true;
-
- return isset($this->_field_cache[$field]);
- }
-
- /**
- * Has a given field changed via set magic?
- *
- * @param $field string name of field
- */
- public function isChangedField ($field)
- {
- return isset($this->_changes[$field]);
- }
-
- /**
- * How many fields have changed via set magic?
- */
- public function countChanges ()
- {
- return count($this->_changes);
- }
-
- /**
- * Has anything changed via set magic?
- */
- public function isChanged () { return $this->_changed; }
-
- /**
- * Return the original value from the db/blank record.
- */
- public function originalValue ($field) {
- if (array_key_exists($field, $this->_originalValues))
- return $this->_originalValues[$field];
- else
- return $this->_record[$field];
- }
-
- /**
- * Has our record been loaded from the db?
- */
- public function isLoaded () { return $this->_loaded; }
-
- /**
- * Can this record be deleted?
- */
- public function canDelete () { return $this->_canDelete; }
-
- /**
- * Return a string dump of the record array.
- */
- public function dumpRecord (array $fields = array(), $format = 'verbose')
- {
- if (count($fields))
- $record = array_subset_of_the_array_based_on_a_list_of_keys($this->_record, $fields);
- else
- $record = $this->_record;
-
- if ('oneline' === $format) {
- $output = '';
- foreach ($record as $field => &$value) {
- $output .= "[$field]\t$value\t";
- }
- return $output;
- } else {
- return print_r($record, 1);
- }
- }
-
- /**
- * Return the record array.
- *
- * If given an array of keys, will limit the returned result to those keys.
- *
- * If given multiple strings, will limit the returned results to those keys.
- */
- public function getRecord ()
- {
- if (func_num_args()) {
- $keys = func_get_args();
- if (is_array($keys[0])) {
- $keys = $keys[0];
- }
- return array_subset_of_the_array_based_on_a_list_of_keys($this->_record, $keys);
- } else {
- return $this->_record;
- }
- }
-
- /**
- * Return just the values of the current record array.
- *
- * If given an array of keys, will limit the returned result to those keys.
- *
- * If given multiple strings, will limit the returned results to those keys.
- */
- public function getValues ()
- {
- if (func_num_args()) {
- $keys = func_get_args();
- if (is_array($keys[0])) {
- $keys = $keys[0];
- }
- return array_values($this->getRecord($keys));
- }
-
- return array_values($this->_record);
- }
-
- /**
- * Return the changes we've made.
- */
- public function changes() { return $this->_changes; }
-
- /**
- * Return the values before those changes;
- */
- public function originalValues() {
- return array_merge(
- $this->_record,
- $this->_originalValues
- );
- }
-
- /**
- * Return a string dump of the changes we've made.
- */
- public function dumpChanges () { return print_r($this->_changes, 1); }
-
- /**
- * reload the dinosaur from the DB, wiping any current changes
- *
- * TODO: Should postLoad() be called here?
- */
- public function refresh ()
- {
- if (! $this->isLoaded())
- throw new SparkRecordException('Cannot refresh an unloaded dinosaur.');
-
- $this->loadFromId($this->getId());
- $this->markUnchanged();
- $this->_dinostore = array();
- }
-
- /**
- * Mark this record as unchanged, after a refresh() or an update().
- *
- * There may be conceptual problems here.
- */
- protected function markUnchanged ()
- {
- $this->_changes = array();
- $this->_changed = false;
- $this->_originalValues = array();
- }
-
- /**
- * Find an array for this record in memcache, if one exists.
- *
- * @return array
- */
- protected function getCache ($id)
- {
- $key = $this->cacheKey($id);
- return unserialize(SparkCache::getInstance()->get($key));
- }
-
- /**
- * Store this record in memcache.
- */
- protected function setCache ()
- {
- $key = $this->cacheKey();
- return SparkCache::getInstance()->set($key, serialize($this->_record));
- }
-
- /**
- * Invalidate the memcache entry for this record.
- */
- public function invalidateCache ()
- {
- if (! MEMCACHED_ENABLED === true)
- return;
- return SparkCache::getInstance()->delete($this->cacheKey());
- }
-
- /**
- * Make a cache key based on the class of the current dinosaur plus an
- * id - by default the one loaded from the DB. Will die screaming if
- * the id is not available, which is probably a good indication someone
- * tried to update() a record before inserting it.
- *
- * @param $id integer optional id
- * @return string key
- */
- public function cacheKey ($id = null)
- {
- if (! isset($id))
- $id = $this->getId();
-
- $current_dinosaur = get_class($this) . $id;
-
- if (! $id) {
- throw new SparkRecordException(
- 'No id when making cache key for '
- . $current_dinosaur
- . ' - has record been inserted?'
- );
- }
-
- return $current_dinosaur;
- }
-
- /**
- * Load data based on an id.
- */
- protected function loadFromId ($id)
- {
- // Handle caching
- $values = $this->_useCache
- ? $this->getCache($id)
- : null;
-
- // Cache values after loading?
- $savecache = false;
-
- if (! $values) {
- $savecache = true;
- $dbh = DB::getInstance();
-
- $tn = static::$_tableName;
- $tk = static::$_tableKey;
-
- $query = "SELECT * FROM \"{$tn}\" WHERE \"{$tk}\" = :id;";
- try {
- $sth = $dbh->prepare($query);
- $sth->execute(['id' => $id]);
- if (true === constant('\DB_LOGQUERIES')) {
- \SparkLib\Fail::log($sth->queryString, 'info');
- }
- } catch (Exception $e) {
- throw new SparkRecordException("Query failed: {$query}: " . $e->getMessage());
- }
-
- $count = $sth->rowCount();
- if ($count != 1)
- throw new SparkRecordException("{$count} record(s) found using {$query} - id should be unique, id given: {$id}");
-
- $values = $sth->fetch(\PDO::FETCH_ASSOC);
- }
-
- $this->loadFromRow($values);
-
- if ($this->_useCache && $savecache)
- $this->setCache();
- }
-
- /**
- * Store a row that's already been loaded from the db, potentially
- * including related dinosaurs from a JOIN.
- *
- * @param array result row
- */
- protected function loadFromRow (array &$result_row)
- {
- $joined = array();
- foreach ($result_row as $field => &$value) {
- if ($this->isValidField($field)) {
- $this->_record[$field] = $value;
- } elseif (strstr($field, '__')) {
- // the __ delimiter is expected to mean that we did a join in SFF
- list($dino_basename, $column) = explode('__', $field);
- $joined[$dino_basename][$column] = $value;
- } else {
- $this->_extraSelects[$field] = $value;
- }
- }
-
- // make friends:
- foreach ($joined as $dino_basename => &$values) {
- $dino_class = $dino_basename . 'Saurus';
- $this->_dinostore[$dino_basename] = new $dino_class($values); // rawr
- }
-
- $this->_loaded = true;
- $this->_canDelete = true;
- $this->_tableId = $this->_record[static::$_tableKey];
- }
-
- /**
- * Store any changes to a record in the db.
- *
- * Context-aware method that update()s or insert()s based on what needs
- * to happen. Useful if you don't know if the record was just created or
- * was around before you started.
- */
- public function wbang()
- {
- if ($this->isLoaded())
- return $this->update();
- else
- return $this->insert();
- }
-
- /**
- * Set some metadata to be accessed by pre- and post- change hooks on
- * child classes. (preUpdate, postUpdate, etc.)
- */
- public function modificationInfo ($info = null)
- {
- if (is_array($info))
- $this->_modificationInfo = $info;
- return $this->_modificationInfo;
- }
-
- /**
- * Used by logModifications to convert strings to UTF8
- */
- protected function to_utf8()
- {
- return function ($value) {
- if(! is_string($value)) {
- return $value;
- }
- return utf8_encode($value);
- };
- }
-
- /**
- * Saves the changes to the current record to MongoDB.
- * Called by insert/update/delete when the _logModifications boolean
- * is set to true in the dinosaur, or if modificationInfo is set before
- * update.
- */
- protected function logModifications($action, array $changes)
- {
- $mongo = ModificationsDBI::getInstance();
- $table = static::$_tableName;
-
- foreach ($this->_ignoreModifications as $ignore_field) {
- if (isset($changes[$ignore_field])) {
- unset($changes[$ignore_field]);
- }
- }
-
- // if there's nothing to log, avoid logging an empty record
- if (! count($changes)) {
- return;
- }
-
- $modification = $this->modificationInfo();
- $modification['id'] = (int)$this->getId();
- $modification['action'] = $action;
- $modification['date'] = new MongoDate();
- $modification['changed'] = $changes;
-
- $mongo->$table->insert($modification);
- }
-
- /**
- * pre- and post-update hooks - override these if you need them
- */
- protected function preUpdate() { }
- protected function postUpdate() { }
-
- /**
- * Store any changes to a record in the db.
- *
- * Calls preUpdate() and postUpdate(), which can be overridden as-need
- * in the child class. Also invalidates any cached version of the record
- * (even if it doesn't wind up doing anything to the db - this could
- * probably use some thought).
- *
- * Returns true if we updated something, false if we didn't.
- * Throws a SparkRecordException if the update fails.
- */
- public function update ()
- {
- $this->invalidateCache();
- $this->preUpdate();
-
- // Bail out if we don't have anything to update:
- if (! $this->isChanged())
- return false;
-
- $COMMA = ', ';
- $SPACE = ' ';
-
- $dbh = DB::getInstance();
-
- $numcols = count($this->_changes);
- $cols = '';
- $i = 0;
-
- $update_data = [];
-
- foreach ($this->_changes as $col => $val)
- {
- $i++;
- $end = ($i < $numcols ? $COMMA : $SPACE);
- $cols .= '"' . $col . '" = ';
-
- $val = $this->typeJuggle($col, $val);
-
- if ($val instanceof DB\Literal) {
- $val_str = $val->literal();
- } else {
- $val_str = ':' . $col;
- // tack on to array for later execution of prep'd statement:
- $update_data[$col] = $val;
- }
-
- $cols .= $val_str . $end;
- }
-
- $q = 'UPDATE "' . static::$_tableName . '" SET ' . $cols . ' WHERE '
- . static::$_tableKey . ' = :_tablekey';
-
- $update_data['_tablekey'] = $this->_tableId;
- try {
- $sth = $dbh->prepare($q);
- foreach ($update_data as $col => $val) {
- $sth->bindValue(":$col", $val);
- }
- $sth->execute($update_data);
- } catch (Exception $e) {
- throw new SparkRecordException('Update failed: ' . $e->getMessage());
- }
-
- if ($this->_logModifications || count($this->modificationInfo()) > 0) {
- $this->logModifications('UPDATE', $this->_changes);
- }
-
- $this->logUpdate();
- $this->postUpdate();
-
- // Reset the changes array and the _changed flag so that future updates
- // using the same dinosaur don't rewrite the same fields:
- $this->markUnchanged();
-
- return true;
- }
-
- /**
- * Pre- and post-insertion hooks - override these if you need them.
- *
- * Changes are still available for examination within postInsert(),
- * but will be reset immediately afterwards.
- */
- protected function preInsert () { }
- protected function postInsert () { }
-
- /**
- * Insert a new record in the db.
- *
- * Calls preInsert() and postInsert(), if they are defined in the child
- * class.
- */
- public function insert ()
- {
- $this->preInsert();
-
- $dbh = DB::getInstance();
-
- // build the list of things we actually care about inserting - those differing
- // from defaults, specifically:
- $insert_values = [];
- foreach ($this->_record as $insert_key => $insert_val) {
- if ($insert_val !== static::$_defaults[ $insert_key ]) {
- $insert_values[ $insert_key ] = $insert_val;
- }
- }
-
- if (! isset($insert_values[ static::$_tableKey ]))
- $insert_values[ static::$_tableKey ] = DB::DefaultValue();
-
- $numcols = count($insert_values);
-
- $q = 'INSERT INTO "' . static::$_tableName . '"';
- $cols = ' (';
- $vals = ' VALUES (';
-
- $insert_data = [];
-
- $i = 0;
- foreach ($insert_values as $col => $val)
- {
- $i++;
- $end = $i < $numcols ? ", " : ") ";
- // quote column names in case we run into a reserved word
- $cols .= '"' . $col . '"' . $end;
-
- $val = $this->typeJuggle($col, $val);
-
- if ($val instanceof DB\Literal) {
- $vals .= $val->literal();
- } else {
- $vals .= ':' . $col;
- // tack on to array for later execution of prep'd statement:
- $insert_data[$col] = $val;
- }
-
- $vals .= $end;
- }
-
- $q .= $cols . $vals;
-
- if ('pgsql' === constant('\DB_SERVER_TYPE')) {
- $q .= ' RETURNING *' ;
- }
-
- try {
- $sth = $dbh->prepare($q);
- foreach ($insert_data as $col => $val) {
- $sth->bindValue(":$col", $val);
- }
- $sth->execute();
-
- if ('pgsql' === constant('\DB_SERVER_TYPE')) {
- $insert_result = $sth->fetch(\PDO::FETCH_ASSOC);
- $this->loadFromRow($insert_result);
- } else {
- $this->_tableId = $dbh->lastInsertId();
- }
- } catch (Exception $e) {
- $failure_data_string = '';
- foreach ($insert_values as $failed_insert_column => $failed_insert_value) {
- if ($failed_insert_value instanceof DB\Literal) {
- $failed_insert_value = $failed_insert_value->literal();
- }
- $failure_data_string .= "$failed_insert_column: $failed_insert_value; ";
- }
- throw new SparkRecordException(
- 'Insert failed: ' . $e->getMessage() . "\n[Query] " . $q . "\n[Values] $failure_data_string\n"
- );
- }
-
- // We have an id now, so we could do a delete operation.
- $this->_canDelete = true;
-
- // try and keep this in synch
- $this->_record[static::$_tableKey] = $this->_tableId;
-
- if($this->_logModifications || count($this->modificationInfo()) > 0) {
- $this->logModifications('INSERT', $this->_record);
- }
-
- $this->logInsert();
- $this->postInsert();
-
- // Reset the changes array and the _changed flag so that
- // any update() calls using the same dinosaur don't rewrite
- // those fields, and so that postUpdate() can behave sanely
- // in that case.
- $this->markUnchanged();
-
- return $this->_tableId;
- }
-
- /**
- * Normalize values to a scheme expected by insert() and update().
- */
- protected function typeJuggle ($col, $val)
- {
- // promote actual PHP nulls to SQL nulls
- if (is_null($val))
- $val = new DB\Null;
-
- if ($val instanceof DB\Literal)
- return $val;
-
- // if we just have a PHP native type here, we may need to do
- // some juggling:
- switch ($this->typeOf($col)) {
- case 'bool':
- if ($val) {
- $val = DB::True();
- } else {
- $val = DB::False();
- }
- break;
- case 'int':
- break;
- }
-
- return $val;
- }
-
- /**
- * Blow up if someone is trying to use a bogus value.
- */
- protected function typeValidate ($col, $val)
- {
- if (is_null($val) || ($val instanceof DB\Null)) {
- if (! static::$_nullable[$col]) {
- throw new SparkRecordException(
- "$col is not a nullable column on " . get_class($this) . "'s table, " . static::$_tableName
- );
- }
- }
- }
-
- /**
- * Pre- and post-deletion hooks - override these if you need them.
- */
- protected function preDelete() { }
- protected function postDelete() { }
-
- /**
- * Delete a record from the db. You probably shouldn't be doing this.
- *
- * Calls preDelete() and postDelete(), if they are defined in the child.
- *
- * TODO: Should this call markUnchanged()?
- */
- public function delete ()
- {
- $this->invalidateCache();
- if (! $this->canDelete())
- throw new SparkRecordException("can't delete this record - sure you inserted or loaded it from the db?");
-
- $this->preDelete();
-
- $dbh = DB::getInstance();
-
- $tk = static::$_tableKey;
- $tn = static::$_tableName;
-
- try {
- $sth = $dbh->prepare(
- "DELETE FROM \"{$tn}\" WHERE \"{$tk}\" = :id"
- );
- $sth->execute(['id' => $this->_tableId]);
- } catch (Exception $e) {
- throw new SparkRecordException('Delete failed: ' . $e->getMessage());
- }
-
- if($this->_logModifications || count($this->modificationInfo()) > 0) {
- $this->logModifications('DELETE', $this->_record);
- }
-
- $this->logDelete();
- $this->postDelete();
-
- return true;
- }
-
- /*
- * These are about telling the universal log that something has happened...
- *
- * TODO - should the notification framework stuff move in here?
- *
- * The way I see it, this gets called from every insert(), update(),
- * or delete(). It should talk to some kind of data store that allows
- * for collections of arbitrary key-value pairs, and usefully record
- *
- * a. the dinosaur in question
- * b. time of action
- * c. actor (This Part is Tricky and an Open Question)
- * d. the delta in data
- *
- * This would be a foundation for a really robust general-purpose audit
- * log, as well as for a new alert system - or more generally a framework
- * for letting users define actions to take on given events, or collections
- * of events to monitor.
- *
- * There are a bunch of NoSQL systems that might work well here. Probably
- * it should be the sort of thing that we can also invoke from other places
- * to log less granular events. We probably need a class much like \SparkLib\Fail
- * to drive it.
- *
- * For right now, messing with the following functions...
- */
-
- /**
- * Log an update.
- */
- protected function logUpdate ()
- {
- $this->logWrite('update',$this->_changes);
- }
-
- /**
- * Log the insertion of a new record.
- */
- protected function logInsert ()
- {
- $this->logWrite('insert', $this->_record);
- }
-
- /**
- * Log a deletion.
- */
- protected function logDelete ()
- {
- $this->logWrite('delete');
- }
-
- /**
- * Log some write operation to the database.
- */
- protected function logWrite ($action, $payload = array())
- {
- Event::info(array(
- 'event' => 'sparkrecord.write',
- 'type' => get_class($this),
- 'id' => $this->getId(),
- 'action' => $action,
- 'payload' => $payload
- ));
-
- if(defined('\FLAG_NOTIFY') && constant('\FLAG_NOTIFY')) {
-
- $dino_name = get_class($this);
-
- if( \Spark\Notify::validDino($dino_name) ) {
-
- $payload = array(
- 'id' => $this->getId(),
- 'record' => $this->getRecord(),
- 'changes' => $this->changes(),
- 'originalValues' => $this->originalValues()
- );
-
- \Spark\Notify::emit($dino_name, $action, $payload);
-
- }
-
- }
- }
-
- /**
- * Provide a readable description of a Dinosaur.
- *
- * This is a silly hack and should not be used other than maybe in your handy
- * REPL. (See sparksh.)
- */
- public static function describe ()
- {
- $text = 'Dinosaur: ' . get_called_class() . "\n\n";
-
- // fields
- $fieldcount = count(static::getDefaults());
- $text .= "Fields ($fieldcount):\n\t";
- $pkey = static::getDefaultTableKey();
- foreach (static::getDefaults() as $field => $value) {
- $pri = '';
- if ($pkey == $field)
- $pri = " (PRIMARY)";
- if (strlen($value))
- $value = " <$value>";
- $fields[] = "$field{$value}{$pri}";
- }
- $text .= wordwrap(implode($fields, ', '), 70, "\n\t");
- $text .= "\n";
-
- // methods
- $all_methods = get_class_methods(get_called_class());
- $default_methods = get_class_methods('SparkRecord');
- $interesting_methods = array_diff($all_methods, $default_methods);
-
- $text .= "\nCustom methods:\n\t"
- . wordwrap(implode($interesting_methods, ', '), 70, "\n\t")
- . "\n\n";
-
- // relationships
- if (static::getRelationships()) {
- $text .= "\nRelationships:\n";
- foreach (static::getRelationships() as $rel => $keys) {
- $text .= "\t$rel: $keys[0] -> $keys[1]\n";
- }
- }
-
- return $text;
- }
-
- /**
- * Get a SparkFriendFinder for this class.
- */
- public static function finder ()
- {
- return new SparkFriendFinder(get_called_class());
- }
-
- /**
- * Get a SparkFriendFinder for this class where $field matches $value,
- * with find() already called
- *
- * @param $field string
- * @param $value string
- */
- public static function findBy ($field, $value)
- {
- return static::finder()->where($field)->eq($value)->find();
- }
-
- /**
- * Return the first record for this class where $field matches $value
- *
- * @param $field string
- * @param $value string
- */
- public static function getBy ($field, $value)
- {
- return static::findBy($field, $value)->getOne();
- }
-
- /**
- * Return the first record for this class where the primary key matches $value
- *
- * @param $value string
- */
- public static function getById ($value)
- {
- return static::getBy(static::$_tableKey, $value);
- }
-
- /**
- * Return the first record for this class where the primary key matches $value,
- * with a FOR UPDATE clause appended to the select query.
- *
- * @param $value string
- */
- public static function getByIdForUpdate ($value)
- {
- return static::finder()->where(static::$_tableKey)->eq($value)->forUpdate()->getOne();
- }
-
- }
|