/model/AbstractModelCollection.php
PHP | 925 lines | 538 code | 65 blank | 322 comment | 66 complexity | 630a9fdc81cd5fe9156475a47002ec5f MD5 | raw file
- <?php
- namespace QuestPC;
- /**
- * Provides container for AbstractModel descendant instances.
- *
- * New separate model instance factory via ->newSeparateModel() with
- * optional ->modelInitHook[] called before new separate model returned.
- *
- * Use ->addModel() to add separate model into collection with
- * optional ->modelAddHook[] called before model becomes part of collection.
- *
- * foreach() iteration of models in collection.
- *
- * SQL CRUD processing of models via ->delete() ->update().
- * Do not forget to use ->setPendingDelete() on models beforehand.
- *
- * Model loading can be speed up via ->loadPkTableRows(), which loads
- * only single row of primary table of each model, which is enough in some cases.
- * Complete read of all tables might be performed by ->loadAllByPkTable().
- *
- * Model tables INSERT UNIQUE KEY might contain multiple fields.
- * INSERT surrogate key (PK) must be integer only to simplify and
- * to make code run faster. However also because SphinxSearch
- * id (kind of PK) can only be integer single field.
- *
- * Supports optional SphinxSearch index creation / update.
- *
- */
- class AbstractModelCollection
- extends FeaturesObject
- implements \Countable, \Iterator {
- /**
- * How many models should be added into collection before flushing
- * (bufferized update) will occur.
- * Please note that currently bufferized updates should be initiated
- * manually via ->bufferizedUpdate() call.
- * @default: disable bufferized updates
- */
- protected static $flushThreshold = 0;
- /**
- * key: serialized array
- * model UNIQ (source data unique key w/o surrogate keys);
- * they are available in RAM before new records are inserted.
- * value: instanceof AbstractModel
- * AbstractModel descendants
- */
- protected $models = array();
- /**
- * Reverse lookup of model by surrogate PK.
- * Surrogate PK's are available only when model records are already in DB;
- * they are automatically generated by SQL server for new records.
- * We assume that surrogate key `primary_table.primary_key` is a single field,
- * which is true in most circumstances.
- */
- protected $modelPKs = array();
- /**
- * string: sphinx rtindex name
- * boolean: false - collection does not use sphinx index
- */
- protected static $rtIndexName = false;
- /**
- * array
- * key: string
- * UNIQ of model
- * value: mixed
- * null : no PK load attempt
- * false : no PK found in DB
- * int: PK value for model with specified UNIQ key
- */
- protected $pkCache = array();
- /**
- * @note:
- * When there's only one UNIQUE KEY field for ->pkTable,
- * count( $model->uniqFieldNames ) === 1.
- * Encoded UNIQ's are matching source UNIQ's (string) in such case.
- *
- * When there's multiple UNIQUE KEY fields for ->pkTable,
- * encoded UNIQ's are serialized strings of ('field' => value, ...) array.
- *
- * Source UNIQ's variable naming convention is $sourceUniq.
- * All of the rest uniq variable names must be encoded UNIQ's.
- */
- # which ->models[] UNIQs has to be updated into DB
- protected $updateModelUniqs = array();
- # UNIQs of which models should be deleted
- protected $deleteModelUniqs = array();
- protected $sphinxReplaceRows = array();
- protected $sphinxDeleteUniqs = array();
- /*** begin of iterator methods ***/
- public function count() {
- return count( $this->models );
- }
- function rewind() {
- reset( $this->models );
- }
- function current() {
- return current( $this->models );
- }
- function key() {
- return key( $this->models );
- }
- function next() {
- next( $this->models );
- }
- function valid() {
- return key( $this->models ) !== null;
- }
- /*** end of iterator methods ***/
- function clear() {
- $this->models = array();
- $this->modelPKs = array();
- $this->pkCache = array();
- }
- /*** model hooks ***/
- /**
- * Optionally supports "fixed" hooks via ['method' => $instance].
- */
- /**
- * Optionally called after new separate model created by ->newSeparateModel().
- * ->modelInitHook[] callback does not return value.
- */
- public $modelInitHook = array();
- /**
- * Optionally called before separate model added to collection.
- * ->modelAddHook[] callback should return false to skip
- * (do not add) model (after additional checks).
- */
- public $modelAddHook = array();
- /*** end of model hooks ***/
- /*** new separate model creation ***/
- protected $modelMixinClassNames = array();
- public function addModelMixinClass( $className ) {
- $this->modelMixinClassNames[AutoLoader::getFqnClassName( $className )] = true;
- }
- /**
- * Creates suitable model instance, which is separated
- * (not included to collection yet).
- */
- protected function createSeparateModel() {
- return new AbstractModel();
- }
- protected function applyModelMixins( AbstractModel $model ) {
- foreach ( array_keys( $this->modelMixinClassNames ) as $mixinClassName ) {
- $model->addMixin( new $mixinClassName() );
- }
- return $model;
- }
- protected function setFixedHook( $method, &$hook ) {
- if ( is_string( $method ) &&
- is_array( $hook ) &&
- count( $hook ) === 1 &&
- is_object( $hook[0] ) ) {
- # "fixed" method hook
- $hook[1] = $method;
- }
- }
- public function newSeparateModel() {
- $model = $this->applyModelMixins( $this->createSeparateModel() );
- foreach ( $this->modelInitHook as $method => $hook ) {
- $this->setFixedHook( $method, $hook );
- if ( !is_callable( $hook ) ) {
- SdvException::throwError( 'Hook is not callable', __METHOD__, $hook );
- }
- call_user_func( $hook, $model );
- }
- return $model;
- }
- /*** end of new separate model creation ***/
-
- /*** abstract PK retrieval ***/
- # DB table name where PK's are created by INSERT
- protected $pkTable;
- # DB table PK field name
- protected $pkFieldName;
- # list of DB tables used by model
- protected $modelTables;
- # array
- # value: UNIQUE KEY fields of ->pkTable for new rows,
- # where is no surrogate key yet.
- protected $uniqFieldNames;
- function __construct() {
- $model = $this->newSeparateModel();
- # todo: make late static binding
- $this->pkTable = $model->getPkTable();
- $this->pkFieldName = $model->getPkFieldName();
- $this->modelTables = $model->getTables();
- $this->uniqFieldNames = $model->getUniqFieldNames();
- }
- protected function setPkCache( $uniq, $pkVal ) {
- $this->pkCache[$uniq] = $pkVal;
- }
- protected function getPkFromCache( $uniq ) {
- if ( array_key_exists( $uniq, $this->pkCache ) ) {
- return $this->pkCache[$uniq];
- }
- $pkVal = $this->models[$uniq]->getPK();
- $this->pkCache[$uniq] = $pkVal;
- return $pkVal;
- }
- /**
- * Load collection from ->pkTable rows supplied.
- * @param $rows array
- */
- public function loadPkTableRows( array $rows ) {
- foreach ( $rows as $row ) {
- $model = $this->newSeparateModel();
- $model->loadPrimaryRow( $row );
- $this->addModel( $model );
- }
- }
- /**
- * Load collection from ->pkTable rows supplied.
- * @param $rows array
- */
- public function setPkTableRows( array $rows ) {
- foreach ( $rows as $row ) {
- $model = $this->newSeparateModel();
- $model->loadPrimaryRow( $row );
- $this->addModel( $model, true );
- }
- }
- /**
- * Removes all models which are pending to delete from collection _only_.
- * No db or related files removal.
- */
- public function reducePendingModels() {
- $deleteModelKeys = array();
- foreach ( $this->models as $uniq => $model ) {
- if ( $model->isPendingDelete() ) {
- $deleteModelKeys[$uniq] = $this->getPkFromCache( $uniq );
- }
- }
- foreach ( $deleteModelKeys as $uniq => $pkVal ) {
- # @note: Do not uncomment next line.
- # We are virtually reducing, NOT removing from storage.
- # $this->models[$uniq]->destroy();
- unset( $this->models[$uniq] );
- if ( $pkVal !== null && $pkVal !== false ) {
- unset( $this->modelPKs[$pkVal] );
- }
- }
- }
- /**
- * Converts source array UNIQ to internal string UNIQ.
- * @param $sourceUniq array
- * key: string
- * ->pkTable field name that is part of non-surrogate UNIQUE KEY
- * value: mixed
- * value of the field
- * @return string
- */
- protected function encodeUniq( array &$sourceUniq ) {
- if ( count( $this->uniqFieldNames ) === 1 ) {
- foreach ( $sourceUniq as $uniq ) {
- return $uniq;
- }
- } else {
- ksort( $sourceUniq );
- return serialize( $sourceUniq );
- }
- }
- public function addModel( AbstractModel $model, $replaceUniq = false ) {
- $sourceUniq = $model->getUniq();
- $uniq = $this->encodeUniq( $sourceUniq );
- if ( array_key_exists( $uniq, $this->models ) ) {
- if ( $replaceUniq ) {
- $pkVal = $this->getPkFromCache( $uniq );
- # @note: Do not uncomment next line.
- # We are virtually reducing, NOT removing from storage.
- # $this->models[$uniq]->destroy();
- unset( $this->models[$uniq] );
- if ( $pkVal !== null && $pkVal !== false ) {
- unset( $this->modelPKs[$pkVal] );
- }
- } else {
- SdvException::throwRecoverable( 'Non-unique model UNIQ', __METHOD__, $uniq );
- }
- }
- # @todo: Check whether it is good to call this hook again when $replaceUniq === true.
- foreach ( $this->modelAddHook as $method => $hook ) {
- $this->setFixedHook( $method, $hook );
- if ( !is_callable( $hook ) ) {
- SdvException::throwError( 'Hook is not callable', __METHOD__, $hook );
- }
- if ( call_user_func( $hook, $model ) === false ) {
- return;
- }
- }
- # model uniq lookup
- $this->models[$uniq] = $model;
- # table PK (surrogate key) lookup
- $pkVal = $this->getPkFromCache( $uniq );
- if ( $pkVal !== null && $pkVal !== false ) {
- $this->modelPKs[$pkVal] = $model;
- }
- }
- protected function hasPendingUpdate() {
- return count( $this->updateModelUniqs ) > 0;
- }
- protected function hasPendingDelete() {
- return count( $this->deleteModelUniqs ) > 0;
- }
- /**
- * Set ->models[] UNIQ values pending to delete and to update.
- * note: has side effect of calling $model->preload() for models
- * which are not already pending to delete.
- */
- protected function setPendingModelUniqs() {
- # Dbg\log(__METHOD__.':className',get_class( $this ));
- $this->deleteModelUniqs = array();
- $this->updateModelUniqs = array();
- # A subset of ->updateModelUniqs which have
- # ->getRtIndexRow() !== false.
- $this->sphinxReplaceRows = array();
- # A superset of ->deleteModelUniqs which also have
- # ->getRtIndexRow() === false.
- $this->sphinxDeleteUniqs = array();
- # collect model UNIQ to delete / update
- foreach ( $this->models as $uniq => $model ) {
- # Make sure ->afterLoadAll() was performed on models created from XML / in code.
- $model->afterLoadAll();
- if ( !$model->isFinallyPendingDelete() ) {
- # if the download of photo was unsuccessful AND
- # there was no previous photo of model, will
- # trigger $model->isPendingDelete() = true;
- $model->preload();
- }
- if ( $model->isPendingDelete() ) {
- # for DELETE
- $this->deleteModelUniqs[] = $uniq;
- } else {
- # for INSERT ON DUPLICATE KEY UPDATE
- $this->updateModelUniqs[] = $uniq;
- }
- if ( static::$rtIndexName !== false ) {
- if ( $model->isPendingDelete() ) {
- $this->sphinxDeleteUniqs[] = $uniq;
- } else {
- $row = $model->getRtIndexRow();
- if ( $row !== false ) {
- $this->sphinxReplaceRows[$uniq] = $row;
- } else {
- $this->sphinxDeleteUniqs[] = $uniq;
- }
- }
- }
- }
- # Dbg\log(__METHOD__.':deleteModelUniqs',$this->deleteModelUniqs);
- # Dbg\log(__METHOD__.':updateModelUniqs',$this->updateModelUniqs);
- }
- /**
- * @param $modelUniqs array
- * values are UNIQ of selected models
- * @return array
- * rows of ->pkTable matching to _existing_ $modelUniqs which should be processable by
- * ->getModelUniqByRow()
- */
- protected function modelUniqsQuery( array $modelUniqs ) {
- # Dbg\log(__METHOD__,$modelUniqs);
- $dbw = $GLOBALS['appContext']->dbw;
- # Retrieve $uniqFieldName(s) value(s) to easily match
- # surrogate PK to row in ->getModelUniqByRow().
- if ( count( $this->uniqFieldNames ) === 1 ) {
- $uniqFieldName = $this->uniqFieldNames[0];
- return $dbw->query(
- ":SELECT * FROM", "^{$this->pkTable}",
- "WHERE", "\${$uniqFieldName}",
- new DbwParams( array( $uniqFieldName => $modelUniqs ) )
- );
- } else {
- $query = array( ":SELECT * FROM", "^{$this->pkTable}",
- "WHERE", "*uniq2d",
- new DbwParams( array(
- 'uniq2d' => new Dbw2dFieldSet( array_map( 'unserialize', $modelUniqs ) )
- ) )
- );
- return call_user_func_array( array( $dbw, 'query' ), $query );
- }
- }
- /**
- * Loads primary rows of models by their uniqs (assuming model uniqs were populated from external source).
- */
- public function loadByModelUniqs() {
- $rows = $this->modelUniqsQuery( array_keys( $this->models ) );
- $this->setPkTableRows( $rows );
- }
- /**
- * Adds models loaded by source UNIQ's.
- * @note: previous elements are NOT lost.
- * Use constructor or ->clear() if you want clean load.
- * @param $sourceUniqs array
- * each element is source (not encoded) model's uniq;
- */
- public function loadBySrcUniqs( array $sourceUniqs ) {
- # get rows of pktable associated with each found uniq;
- $rows = $this->modelUniqsQuery(
- (count( $this->uniqFieldNames ) === 1) ?
- $sourceUniqs : array_map( array( $this, 'encodeUniq' ), $sourceUniqs )
- );
- $this->loadPkTableRows( $rows );
- }
- /**
- * Initializes collection with models loaded by PK's.
- * @note: previous elements are NOT lost.
- * Use constructor or ->clear() if you want clean load.
- * @param $pks array
- * array of ->pkTable primary integer keys
- */
- public function loadByPks( array $pks ) {
- global $appContext;
- # rows of pktable associated with each found pk;
- $rows = $appContext->dbw->query( ":SELECT * FROM",
- "^{$this->pkTable}", "WHERE", "\${$this->pkFieldName}",
- new DbwParams( array( $this->pkFieldName => $pks ) )
- );
- $this->loadPkTableRows( $rows );
- }
- /**
- * @param $row
- * row of ->pkTable
- * @return array
- * value of source UNIQ for $row
- * @return array
- * source UNIQ is always associative array,
- * encoded UNIQ is always a string.
- */
- public function getModelUniqByRow( \stdClass $row ) {
- $sourceUniq = array();
- foreach ( $this->uniqFieldNames as $uniqFieldName ) {
- $sourceUniq[$uniqFieldName] = $row->{$uniqFieldName};
- }
- return $sourceUniq;
- }
- /**
- * INSERT ON DUPLICATE KEY UPDATE of ->pkTable rows from updating models
- * into ->pkTable.
- * @param $pk_table_rows array
- * values are rows of ->pkTable to insert
- * @return array
- * rows of ->pkTable with populated surrogate PK's matching to $pk_table_rows
- * with amount of fields enough to be processable by ->getModelUniqByRow()
- */
- protected function insertIntoPkTable( array $pk_table_rows ) {
- $dbw = $GLOBALS['appContext']->dbw;
- # update ->pkTable via UNIQ ->models[] key
- $dbw->insert( $this->pkTable, $pk_table_rows, 0, $this->uniqFieldNames );
- # get model id's for every model via UNIQ (->pkTable UNIQUE KEY);
- return $this->modelUniqsQuery( $this->updateModelUniqs );
- }
- /**
- * Load all PKs for previousely stored models.
- * Non-stored models PKs will not be initialized (assumed to be null).
- * @param $modelUniqs array
- * values are UNIQ of selected models
- */
- public function loadPKs( array $modelUniqs ) {
- # Initially consider all requested models not found in DB.
- foreach ( $modelUniqs as $uniq ) {
- $this->setPkCache( $uniq, false );
- }
- $rows = $this->modelUniqsQuery( $modelUniqs );
- foreach ( $rows as $row ) {
- $sourceUniq = $this->getModelUniqByRow( $row );
- $uniq = $this->encodeUniq( $sourceUniq );
- $model = $this->models[$uniq];
- $model->setPKbyRow( $row );
- $pkVal = $model->getPK();
- # Model was found, store it's PK into pkCache.
- $this->setPkCache( $uniq, $pkVal );
- # table PK lookup
- $this->modelPKs[$pkVal] = $model;
- }
- }
- /** end of abstract PK retrieval ***/
- /*** abstract deletion ***/
- /**
- * Delete previousely selected models via ->deleteModelUniqs[]
- */
- protected function deletePendingModels() {
- global $appContext;
- # Dbg\log('this->deleteModelUniqs',$this->deleteModelUniqs);
- if ( count( $this->deleteModelUniqs ) === 0 ) {
- # nothing to delete
- return;
- }
- # check, whether PK was already set
- foreach ( $this->deleteModelUniqs as $uniq ) {
- }
- # Populate DB ->pkTable table surrogate PKs of ->$deleteModelUniqs;
- # (only single field PK is supported for performance reasons).
- $deletePks = $this->getPksByUniqs( $this->deleteModelUniqs );
- # delete models which are pending to be deleted
- if ( count( $deletePks ) === 0 ) {
- # nothing to delete
- return;
- }
- # delete model rows from every table via their PK's
- foreach ( $this->modelTables as $tableName ) {
- $tableDef = $appContext->schema->getSubProp( 'tableList', $tableName );
- if ( array_key_exists( $this->pkFieldName, $tableDef[Schema::FIELD_DEF] ) ) {
- $this->deletePKrows( $tableName, $deletePks );
- }
- }
- }
- /**
- * Delete rows from table with PK's specified.
- * @param $tableName string
- * DB table name;
- * @param $deletePks array
- * val int: pk's for selected DB table;
- */
- protected function deletePKrows( $tableName, array $deletePks ) {
- global $appContext;
- $appContext->dbw->query( "DELETE FROM", "^{$tableName}", "WHERE", "\${$this->pkFieldName}",
- new DbwParams( array( $this->pkFieldName => $deletePks ) ) );
- }
- /**
- * Unset DB-deleted models from collection.
- */
- protected function unsetPendingModels() {
- # Remove instances of already DB-deleted models from collection.
- # note: lpModel::destroy() also deletes unused photos,
- # freeing up disk space.
- foreach ( $this->deleteModelUniqs as $uniq ) {
- $pkVal = $this->getPkFromCache( $uniq );
- $this->models[$uniq]->destroy();
- unset( $this->models[$uniq] );
- if ( $pkVal !== null && $pkVal !== false ) {
- unset( $this->modelPKs[$pkVal] );
- }
- }
- # done, empty the arrays
- $this->deleteModelUniqs = array();
- $this->sphinxDeleteUniqs = array();
- }
- /**
- * Deletes _pending_ models both in DB and from ->models[].
- * note: all of models should already have isPendingDelete() = true;
- * otherwise see side-effect in ->setPendingModelUniqs().
- */
- public function delete() {
- $this->setPendingModelUniqs();
- if ( $this->hasPendingDelete() ) {
- $this->deletePendingModels();
- $this->deleteSphinx();
- };
- $this->unsetPendingModels();
- }
- /**
- * @uniqList array
- * value: UNIQ of model which PK must be get
- * @return array
- * value: PKs for each model in $uniqList
- */
- protected function getPksByUniqs( array $uniqList ) {
- $pkVals = array();
- $missingUniqs = array();
- foreach ( $uniqList as $uniq ) {
- $pkVal = $this->getPkFromCache( $uniq );
- if ( $pkVal === null || $pkVal === false ) {
- $missingUniqs[] = $uniq;
- } elseif ( $pkVal !== false ) {
- $pkVals[] = $pkVal;
- }
- }
- if ( count( $missingUniqs ) > 0 ) {
- $this->loadPKs( $missingUniqs );
- foreach ( $missingUniqs as $uniq ) {
- $pkVal = $this->getPkFromCache( $uniq );
- if ( $pkVal === null ) {
- SdvException::throwError(
- 'Bug in ->loadPKs(), missing load result for UNIQ',
- __METHOD__,
- $uniq
- );
- }
- # We do not throw an exception here because some models
- # might be in pendingDelete state while never stored into DB
- # (eg. just loaded from XML source).
- if ( $pkVal !== false ) {
- $pkVals[] = $pkVal;
- }
- }
- }
- return $pkVals;
- }
- public function deleteSphinx() {
- global $appContext;
- # Dbg\log(__METHOD__.':sphinxDeleteUniqs',$this->sphinxDeleteUniqs);
- $deletePks = $this->getPksByUniqs( $this->sphinxDeleteUniqs );
- if ( count( $deletePks ) === 0 ) {
- return;
- }
- Dbg\log(__METHOD__.':deletePks',$deletePks);
- $appContext->sphinx->query(
- "DELETE FROM",
- "^" . static::$rtIndexName, "WHERE",
- ':id in (' . implode( ',', array_map( 'intval', $deletePks ) ) .')'
- /*
- # Commented out because SphinxQL rejects proper syntax.
- "\$id",
- new DbwParams( array( 'id' => $deletePks ) )
- */
- );
- }
- /*** end of abstract deletion ***/
- /*** abstract update ***/
- /**
- * Write-synchronize collection of models to DB.
- */
- public function update() {
- global $appContext;
- $this->setPendingModelUniqs();
- if ( $this->hasPendingUpdate() || $this->hasPendingDelete() ) {
- # todo: begin / commit interval is long; ->pkTable is probably
- # locked for too long time; how to improve?
- # lp: (separate tables for cities?)
- $appContext->dbw->begin();
- try {
- $this->deletePendingModels();
- $this->updatePendingModels();
- } catch ( \Exception $e ) {
- $appContext->dbw->rollback();
- throw $e;
- }
- $appContext->dbw->commit();
- }
- $this->updateSphinx();
- $this->deleteSphinx();
- $this->unsetPendingModels();
- }
- public function bufferizedUpdate() {
- if ( static::$flushThreshold === 0 ||
- $this->count() < static::$flushThreshold ) {
- return false;
- }
- $this->update();
- $this->clear();
- return true;
- }
- /**
- * Must be called only after ->updatePendingModels(), because
- * at earlier stage model PKs are not populated
- */
- public function updateSphinx() {
- global $appContext;
- # Dbg\log(__METHOD__.':sphinxReplaceRows',$this->sphinxReplaceRows);
- $rows = array();
- foreach ( $this->sphinxReplaceRows as $uniq => $row ) {
- $pkVal = $this->getPkFromCache( $uniq );
- if ( $pkVal === null || $pkVal === false ) {
- SdvException::throwError(
- 'Please call ->updatePendingModels() or ->loadByModelUniqs() first',
- __METHOD__,
- $uniq
- );
- }
- $row['id'] = $pkVal;
- $rows[] = $row;
- }
- if ( count( $rows ) === 0 ) {
- return;
- }
- # Dbg\log(__METHOD__.':rows',$rows);
- $appContext->sphinx->replace( static::$rtIndexName, $rows );
- }
- /**
- * INSERT ON DUPLICATE KEY UPDATE for every model's table
- * by list of UNIQs from ->updateModelUniqs[]
- */
- protected function updateTablesByPKs() {
- global $appContext;
- # Update via PK for every table.
- foreach ( $this->modelTables as $tableName ) {
- $tableDef = $appContext->schema->getSubProp( 'tableList', $tableName );
- if ( $tableName === $this->pkTable ) {
- # Already updated via ->insertIntoPkTable().
- continue;
- }
- # $rows will contain current $tableName rows to insert for each model.
- $rows = array();
- # Keys for empty table rows to delete.
- $deletePks = array();
- foreach ( $this->updateModelUniqs as $uniq ) {
- $model = $this->models[$uniq];
- $pkVal = $this->getPkFromCache( $uniq );
- if ( $pkVal === null || $pkVal === false ) {
- SdvException::throwError(
- 'Please call ->updatePendingModels() or ->loadByModelUniqs() first',
- __METHOD__,
- $uniq
- );
- }
- if ( !$model->hasProp( $tableName ) ) {
- # Current optional table has no properties set in current model.
- # Add it's pkVal to the deletion list for current table.
- $deletePks[] = $pkVal;
- continue;
- }
- /**
- * Get single row (stdClass), or
- * multiple rows (2D array of key/values) of data
- * for $tableName from current model.
- */
- $tableData = $model->getProp( $tableName );
- $isTwoDim = is_array( $tableData );
- $tableData = (array) $tableData;
- $rowDimension = 0;
- if ( $isTwoDim ) {
- if ( count( $tableData ) === 0 ) {
- # Current optional table has no rows for current model.
- # Add it's pkVal to the deletion list for current table.
- $deletePks[] = $pkVal;
- continue;
- }
- # Add multiple rows with PK.
- foreach ( $tableData as $row ) {
- if ( !is_array( $row ) ) {
- SdvException::throwError(
- 'Multi-row (two-dimensional) table field value should be array',
- __METHOD__,
- array(
- 'tableData' => $tableData,
- 'row' => $row,
- 'tableName' => $tableName,
- )
- );
- }
- $row[$this->pkFieldName] = $pkVal;
- $rows[] = $row;
- if ( $rowDimension === 0 || count( $row ) < $rowDimension ) {
- $rowDimension = count( $row );
- }
- }
- } else {
- if ( count( $tableData ) === 0 ) {
- /**
- * Empty single row.
- * Currently it is not deleted, to allow forms with "incomplete" models.
- * @todo: Make flexible choice whether such empty row has to be skipped
- * or to be added into $deletePks[].
- */
- continue;
- }
- # Add single row with PK.
- $tableData[$this->pkFieldName] = $pkVal;
- $rows[] = $tableData;
- $rowDimension = count( $tableData );
- }
- if ( $rowDimension !== $appContext->schema->fieldsCount( $tableName ) ) {
- # Do not allow to insert incomplete rows, otherwise the missing fields
- # will incorrectly have outdated values.
- SdvException::throwError( 'Count of rows fields does not match count of table fields',
- __METHOD__,
- array(
- 'pkFieldName' => $this->pkFieldName,
- 'pkVal' => $pkVal,
- 'tableName' => $tableName,
- 'tableData' => $tableData,
- 'rowDimension' => $rowDimension,
- 'fieldsCount' => $appContext->schema->fieldsCount( $tableName ),
- 'isTwoDim' => $isTwoDim,
- 'rows' => $rows,
- )
- );
- }
- }
- # Dbg\log(__METHOD__.":rows $tableName",$rows);
- if ( $appContext->schema->fieldIsPK( $tableName, $this->pkFieldName ) ) {
- # Simple INSERT ON DUPLICATE KEY is enough when table has ->pkFieldName as PK.
- $appContext->dbw->insert( $tableName, $rows, 0, array( $this->pkFieldName ) );
- } else {
- # SELECT / INSERT sequence is required because ->pkFieldName is not PK of table.
- $appContext->dbw->setUniqueDataForField( $tableName, $rows, $this->pkFieldName );
- }
- if ( count( $deletePks ) > 0 ) {
- $this->deletePKrows( $tableName, $deletePks );
- }
- }
- }
- /**
- * Updates models from RAM into DB
- */
- protected function updatePendingModels() {
- global $appContext;
- if ( count( $this->updateModelUniqs ) < 1 ) {
- # nothing to update
- return;
- }
- # Create pk_table_rows[] array of DB ->pkTable rows for each model pending update.
- $pk_table_rows = array();
- foreach ( $this->updateModelUniqs as $uniq ) {
- $pk_table_row = (array) $this->models[$uniq]->getProp( $this->pkTable );
- $pkVal = $this->models[$uniq]->getPK();
- if ( $pkVal !== null ) {
- # If there was already a PK, use it for subsequent INSERT
- # to keep external keys consistency.
- $pk_table_row[$this->pkFieldName] = $pkVal;
- }
- $pk_table_rows[] = $pk_table_row;
- }
- # Dbg\log('pk_table_rows',$pk_table_rows);
- $rows = $this->insertIntoPkTable( $pk_table_rows );
- # Populate PK's for models pending update.
- foreach ( $rows as $row ) {
- $sourceUniq = $this->getModelUniqByRow( $row );
- $uniq = $this->encodeUniq( $sourceUniq );
- $model = $this->models[$uniq];
- $model->setPKbyRow( $row );
- $pkVal = $model->getPK();
- $this->setPkCache( $uniq, $pkVal );
- $this->modelPKs[$pkVal] = $model;
- }
- $this->updateTablesByPKs();
- }
- /*** end of abstract update ***/
- /*** begin of abstract load ***/
- /**
- * Populate all properties of each model in collection from all tables but primary table
- * after models in collection were initialized with full rows from primary table
- * (usually by calling $model->loadTableRow( $pk, $row ) on each model in collection).
- * note: automatically calls $model->loadTableRow() for all tables but primary.
- */
- public function loadAllByPkTable() {
- global $appContext;
- $dbw = $appContext->dbw;
- if ( count( $this->models ) < 1 ) {
- return;
- }
- $pks = array();
- foreach ( array_keys( $this->models ) as $uniq ) {
- $pkVal = $this->getPkFromCache( $uniq );
- if ( $pkVal === null || $pkVal === false ) {
- SdvException::throwError(
- 'Please call ->updatePendingModels() or ->loadByModelUniqs() first',
- __METHOD__,
- $uniq
- );
- }
- $pks[] = $pkVal;
- }
- foreach ( $this->modelTables as $tableName ) {
- if ( $tableName !== $this->pkTable ) {
- $rows = $dbw->query(
- "SELECT", "^$tableName", ":.* FROM",
- "^{$tableName}", "WHERE", "\${$this->pkFieldName}",
- new DbwParams( array( $this->pkFieldName => $pks ) )
- );
- # Dbg\log(__METHOD__.':rows',$rows);
- foreach ( $rows as $row ) {
- $model = $this->modelPKs[intval( $row->{$this->pkFieldName} )];
- $model->loadTableRow( $tableName, $row );
- }
- }
- }
- foreach ( $this->models as $model ) {
- $model->afterLoadAll();
- }
- }
- /*** end of abstract load ***/
- } /* end of AbstractModelCollection class */