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

<?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();
}
}