PageRenderTime 70ms CodeModel.GetById 35ms RepoModel.GetById 0ms app.codeStats 1ms

/model/AbstractModelCollection.php

https://bitbucket.org/sdvpartnership/questpc-framework
PHP | 925 lines | 538 code | 65 blank | 322 comment | 66 complexity | 630a9fdc81cd5fe9156475a47002ec5f MD5 | raw file
  1. <?php
  2. namespace QuestPC;
  3. /**
  4. * Provides container for AbstractModel descendant instances.
  5. *
  6. * New separate model instance factory via ->newSeparateModel() with
  7. * optional ->modelInitHook[] called before new separate model returned.
  8. *
  9. * Use ->addModel() to add separate model into collection with
  10. * optional ->modelAddHook[] called before model becomes part of collection.
  11. *
  12. * foreach() iteration of models in collection.
  13. *
  14. * SQL CRUD processing of models via ->delete() ->update().
  15. * Do not forget to use ->setPendingDelete() on models beforehand.
  16. *
  17. * Model loading can be speed up via ->loadPkTableRows(), which loads
  18. * only single row of primary table of each model, which is enough in some cases.
  19. * Complete read of all tables might be performed by ->loadAllByPkTable().
  20. *
  21. * Model tables INSERT UNIQUE KEY might contain multiple fields.
  22. * INSERT surrogate key (PK) must be integer only to simplify and
  23. * to make code run faster. However also because SphinxSearch
  24. * id (kind of PK) can only be integer single field.
  25. *
  26. * Supports optional SphinxSearch index creation / update.
  27. *
  28. */
  29. class AbstractModelCollection
  30. extends FeaturesObject
  31. implements \Countable, \Iterator {
  32. /**
  33. * How many models should be added into collection before flushing
  34. * (bufferized update) will occur.
  35. * Please note that currently bufferized updates should be initiated
  36. * manually via ->bufferizedUpdate() call.
  37. * @default: disable bufferized updates
  38. */
  39. protected static $flushThreshold = 0;
  40. /**
  41. * key: serialized array
  42. * model UNIQ (source data unique key w/o surrogate keys);
  43. * they are available in RAM before new records are inserted.
  44. * value: instanceof AbstractModel
  45. * AbstractModel descendants
  46. */
  47. protected $models = array();
  48. /**
  49. * Reverse lookup of model by surrogate PK.
  50. * Surrogate PK's are available only when model records are already in DB;
  51. * they are automatically generated by SQL server for new records.
  52. * We assume that surrogate key `primary_table.primary_key` is a single field,
  53. * which is true in most circumstances.
  54. */
  55. protected $modelPKs = array();
  56. /**
  57. * string: sphinx rtindex name
  58. * boolean: false - collection does not use sphinx index
  59. */
  60. protected static $rtIndexName = false;
  61. /**
  62. * array
  63. * key: string
  64. * UNIQ of model
  65. * value: mixed
  66. * null : no PK load attempt
  67. * false : no PK found in DB
  68. * int: PK value for model with specified UNIQ key
  69. */
  70. protected $pkCache = array();
  71. /**
  72. * @note:
  73. * When there's only one UNIQUE KEY field for ->pkTable,
  74. * count( $model->uniqFieldNames ) === 1.
  75. * Encoded UNIQ's are matching source UNIQ's (string) in such case.
  76. *
  77. * When there's multiple UNIQUE KEY fields for ->pkTable,
  78. * encoded UNIQ's are serialized strings of ('field' => value, ...) array.
  79. *
  80. * Source UNIQ's variable naming convention is $sourceUniq.
  81. * All of the rest uniq variable names must be encoded UNIQ's.
  82. */
  83. # which ->models[] UNIQs has to be updated into DB
  84. protected $updateModelUniqs = array();
  85. # UNIQs of which models should be deleted
  86. protected $deleteModelUniqs = array();
  87. protected $sphinxReplaceRows = array();
  88. protected $sphinxDeleteUniqs = array();
  89. /*** begin of iterator methods ***/
  90. public function count() {
  91. return count( $this->models );
  92. }
  93. function rewind() {
  94. reset( $this->models );
  95. }
  96. function current() {
  97. return current( $this->models );
  98. }
  99. function key() {
  100. return key( $this->models );
  101. }
  102. function next() {
  103. next( $this->models );
  104. }
  105. function valid() {
  106. return key( $this->models ) !== null;
  107. }
  108. /*** end of iterator methods ***/
  109. function clear() {
  110. $this->models = array();
  111. $this->modelPKs = array();
  112. $this->pkCache = array();
  113. }
  114. /*** model hooks ***/
  115. /**
  116. * Optionally supports "fixed" hooks via ['method' => $instance].
  117. */
  118. /**
  119. * Optionally called after new separate model created by ->newSeparateModel().
  120. * ->modelInitHook[] callback does not return value.
  121. */
  122. public $modelInitHook = array();
  123. /**
  124. * Optionally called before separate model added to collection.
  125. * ->modelAddHook[] callback should return false to skip
  126. * (do not add) model (after additional checks).
  127. */
  128. public $modelAddHook = array();
  129. /*** end of model hooks ***/
  130. /*** new separate model creation ***/
  131. protected $modelMixinClassNames = array();
  132. public function addModelMixinClass( $className ) {
  133. $this->modelMixinClassNames[AutoLoader::getFqnClassName( $className )] = true;
  134. }
  135. /**
  136. * Creates suitable model instance, which is separated
  137. * (not included to collection yet).
  138. */
  139. protected function createSeparateModel() {
  140. return new AbstractModel();
  141. }
  142. protected function applyModelMixins( AbstractModel $model ) {
  143. foreach ( array_keys( $this->modelMixinClassNames ) as $mixinClassName ) {
  144. $model->addMixin( new $mixinClassName() );
  145. }
  146. return $model;
  147. }
  148. protected function setFixedHook( $method, &$hook ) {
  149. if ( is_string( $method ) &&
  150. is_array( $hook ) &&
  151. count( $hook ) === 1 &&
  152. is_object( $hook[0] ) ) {
  153. # "fixed" method hook
  154. $hook[1] = $method;
  155. }
  156. }
  157. public function newSeparateModel() {
  158. $model = $this->applyModelMixins( $this->createSeparateModel() );
  159. foreach ( $this->modelInitHook as $method => $hook ) {
  160. $this->setFixedHook( $method, $hook );
  161. if ( !is_callable( $hook ) ) {
  162. SdvException::throwError( 'Hook is not callable', __METHOD__, $hook );
  163. }
  164. call_user_func( $hook, $model );
  165. }
  166. return $model;
  167. }
  168. /*** end of new separate model creation ***/
  169. /*** abstract PK retrieval ***/
  170. # DB table name where PK's are created by INSERT
  171. protected $pkTable;
  172. # DB table PK field name
  173. protected $pkFieldName;
  174. # list of DB tables used by model
  175. protected $modelTables;
  176. # array
  177. # value: UNIQUE KEY fields of ->pkTable for new rows,
  178. # where is no surrogate key yet.
  179. protected $uniqFieldNames;
  180. function __construct() {
  181. $model = $this->newSeparateModel();
  182. # todo: make late static binding
  183. $this->pkTable = $model->getPkTable();
  184. $this->pkFieldName = $model->getPkFieldName();
  185. $this->modelTables = $model->getTables();
  186. $this->uniqFieldNames = $model->getUniqFieldNames();
  187. }
  188. protected function setPkCache( $uniq, $pkVal ) {
  189. $this->pkCache[$uniq] = $pkVal;
  190. }
  191. protected function getPkFromCache( $uniq ) {
  192. if ( array_key_exists( $uniq, $this->pkCache ) ) {
  193. return $this->pkCache[$uniq];
  194. }
  195. $pkVal = $this->models[$uniq]->getPK();
  196. $this->pkCache[$uniq] = $pkVal;
  197. return $pkVal;
  198. }
  199. /**
  200. * Load collection from ->pkTable rows supplied.
  201. * @param $rows array
  202. */
  203. public function loadPkTableRows( array $rows ) {
  204. foreach ( $rows as $row ) {
  205. $model = $this->newSeparateModel();
  206. $model->loadPrimaryRow( $row );
  207. $this->addModel( $model );
  208. }
  209. }
  210. /**
  211. * Load collection from ->pkTable rows supplied.
  212. * @param $rows array
  213. */
  214. public function setPkTableRows( array $rows ) {
  215. foreach ( $rows as $row ) {
  216. $model = $this->newSeparateModel();
  217. $model->loadPrimaryRow( $row );
  218. $this->addModel( $model, true );
  219. }
  220. }
  221. /**
  222. * Removes all models which are pending to delete from collection _only_.
  223. * No db or related files removal.
  224. */
  225. public function reducePendingModels() {
  226. $deleteModelKeys = array();
  227. foreach ( $this->models as $uniq => $model ) {
  228. if ( $model->isPendingDelete() ) {
  229. $deleteModelKeys[$uniq] = $this->getPkFromCache( $uniq );
  230. }
  231. }
  232. foreach ( $deleteModelKeys as $uniq => $pkVal ) {
  233. # @note: Do not uncomment next line.
  234. # We are virtually reducing, NOT removing from storage.
  235. # $this->models[$uniq]->destroy();
  236. unset( $this->models[$uniq] );
  237. if ( $pkVal !== null && $pkVal !== false ) {
  238. unset( $this->modelPKs[$pkVal] );
  239. }
  240. }
  241. }
  242. /**
  243. * Converts source array UNIQ to internal string UNIQ.
  244. * @param $sourceUniq array
  245. * key: string
  246. * ->pkTable field name that is part of non-surrogate UNIQUE KEY
  247. * value: mixed
  248. * value of the field
  249. * @return string
  250. */
  251. protected function encodeUniq( array &$sourceUniq ) {
  252. if ( count( $this->uniqFieldNames ) === 1 ) {
  253. foreach ( $sourceUniq as $uniq ) {
  254. return $uniq;
  255. }
  256. } else {
  257. ksort( $sourceUniq );
  258. return serialize( $sourceUniq );
  259. }
  260. }
  261. public function addModel( AbstractModel $model, $replaceUniq = false ) {
  262. $sourceUniq = $model->getUniq();
  263. $uniq = $this->encodeUniq( $sourceUniq );
  264. if ( array_key_exists( $uniq, $this->models ) ) {
  265. if ( $replaceUniq ) {
  266. $pkVal = $this->getPkFromCache( $uniq );
  267. # @note: Do not uncomment next line.
  268. # We are virtually reducing, NOT removing from storage.
  269. # $this->models[$uniq]->destroy();
  270. unset( $this->models[$uniq] );
  271. if ( $pkVal !== null && $pkVal !== false ) {
  272. unset( $this->modelPKs[$pkVal] );
  273. }
  274. } else {
  275. SdvException::throwRecoverable( 'Non-unique model UNIQ', __METHOD__, $uniq );
  276. }
  277. }
  278. # @todo: Check whether it is good to call this hook again when $replaceUniq === true.
  279. foreach ( $this->modelAddHook as $method => $hook ) {
  280. $this->setFixedHook( $method, $hook );
  281. if ( !is_callable( $hook ) ) {
  282. SdvException::throwError( 'Hook is not callable', __METHOD__, $hook );
  283. }
  284. if ( call_user_func( $hook, $model ) === false ) {
  285. return;
  286. }
  287. }
  288. # model uniq lookup
  289. $this->models[$uniq] = $model;
  290. # table PK (surrogate key) lookup
  291. $pkVal = $this->getPkFromCache( $uniq );
  292. if ( $pkVal !== null && $pkVal !== false ) {
  293. $this->modelPKs[$pkVal] = $model;
  294. }
  295. }
  296. protected function hasPendingUpdate() {
  297. return count( $this->updateModelUniqs ) > 0;
  298. }
  299. protected function hasPendingDelete() {
  300. return count( $this->deleteModelUniqs ) > 0;
  301. }
  302. /**
  303. * Set ->models[] UNIQ values pending to delete and to update.
  304. * note: has side effect of calling $model->preload() for models
  305. * which are not already pending to delete.
  306. */
  307. protected function setPendingModelUniqs() {
  308. # Dbg\log(__METHOD__.':className',get_class( $this ));
  309. $this->deleteModelUniqs = array();
  310. $this->updateModelUniqs = array();
  311. # A subset of ->updateModelUniqs which have
  312. # ->getRtIndexRow() !== false.
  313. $this->sphinxReplaceRows = array();
  314. # A superset of ->deleteModelUniqs which also have
  315. # ->getRtIndexRow() === false.
  316. $this->sphinxDeleteUniqs = array();
  317. # collect model UNIQ to delete / update
  318. foreach ( $this->models as $uniq => $model ) {
  319. # Make sure ->afterLoadAll() was performed on models created from XML / in code.
  320. $model->afterLoadAll();
  321. if ( !$model->isFinallyPendingDelete() ) {
  322. # if the download of photo was unsuccessful AND
  323. # there was no previous photo of model, will
  324. # trigger $model->isPendingDelete() = true;
  325. $model->preload();
  326. }
  327. if ( $model->isPendingDelete() ) {
  328. # for DELETE
  329. $this->deleteModelUniqs[] = $uniq;
  330. } else {
  331. # for INSERT ON DUPLICATE KEY UPDATE
  332. $this->updateModelUniqs[] = $uniq;
  333. }
  334. if ( static::$rtIndexName !== false ) {
  335. if ( $model->isPendingDelete() ) {
  336. $this->sphinxDeleteUniqs[] = $uniq;
  337. } else {
  338. $row = $model->getRtIndexRow();
  339. if ( $row !== false ) {
  340. $this->sphinxReplaceRows[$uniq] = $row;
  341. } else {
  342. $this->sphinxDeleteUniqs[] = $uniq;
  343. }
  344. }
  345. }
  346. }
  347. # Dbg\log(__METHOD__.':deleteModelUniqs',$this->deleteModelUniqs);
  348. # Dbg\log(__METHOD__.':updateModelUniqs',$this->updateModelUniqs);
  349. }
  350. /**
  351. * @param $modelUniqs array
  352. * values are UNIQ of selected models
  353. * @return array
  354. * rows of ->pkTable matching to _existing_ $modelUniqs which should be processable by
  355. * ->getModelUniqByRow()
  356. */
  357. protected function modelUniqsQuery( array $modelUniqs ) {
  358. # Dbg\log(__METHOD__,$modelUniqs);
  359. $dbw = $GLOBALS['appContext']->dbw;
  360. # Retrieve $uniqFieldName(s) value(s) to easily match
  361. # surrogate PK to row in ->getModelUniqByRow().
  362. if ( count( $this->uniqFieldNames ) === 1 ) {
  363. $uniqFieldName = $this->uniqFieldNames[0];
  364. return $dbw->query(
  365. ":SELECT * FROM", "^{$this->pkTable}",
  366. "WHERE", "\${$uniqFieldName}",
  367. new DbwParams( array( $uniqFieldName => $modelUniqs ) )
  368. );
  369. } else {
  370. $query = array( ":SELECT * FROM", "^{$this->pkTable}",
  371. "WHERE", "*uniq2d",
  372. new DbwParams( array(
  373. 'uniq2d' => new Dbw2dFieldSet( array_map( 'unserialize', $modelUniqs ) )
  374. ) )
  375. );
  376. return call_user_func_array( array( $dbw, 'query' ), $query );
  377. }
  378. }
  379. /**
  380. * Loads primary rows of models by their uniqs (assuming model uniqs were populated from external source).
  381. */
  382. public function loadByModelUniqs() {
  383. $rows = $this->modelUniqsQuery( array_keys( $this->models ) );
  384. $this->setPkTableRows( $rows );
  385. }
  386. /**
  387. * Adds models loaded by source UNIQ's.
  388. * @note: previous elements are NOT lost.
  389. * Use constructor or ->clear() if you want clean load.
  390. * @param $sourceUniqs array
  391. * each element is source (not encoded) model's uniq;
  392. */
  393. public function loadBySrcUniqs( array $sourceUniqs ) {
  394. # get rows of pktable associated with each found uniq;
  395. $rows = $this->modelUniqsQuery(
  396. (count( $this->uniqFieldNames ) === 1) ?
  397. $sourceUniqs : array_map( array( $this, 'encodeUniq' ), $sourceUniqs )
  398. );
  399. $this->loadPkTableRows( $rows );
  400. }
  401. /**
  402. * Initializes collection with models loaded by PK's.
  403. * @note: previous elements are NOT lost.
  404. * Use constructor or ->clear() if you want clean load.
  405. * @param $pks array
  406. * array of ->pkTable primary integer keys
  407. */
  408. public function loadByPks( array $pks ) {
  409. global $appContext;
  410. # rows of pktable associated with each found pk;
  411. $rows = $appContext->dbw->query( ":SELECT * FROM",
  412. "^{$this->pkTable}", "WHERE", "\${$this->pkFieldName}",
  413. new DbwParams( array( $this->pkFieldName => $pks ) )
  414. );
  415. $this->loadPkTableRows( $rows );
  416. }
  417. /**
  418. * @param $row
  419. * row of ->pkTable
  420. * @return array
  421. * value of source UNIQ for $row
  422. * @return array
  423. * source UNIQ is always associative array,
  424. * encoded UNIQ is always a string.
  425. */
  426. public function getModelUniqByRow( \stdClass $row ) {
  427. $sourceUniq = array();
  428. foreach ( $this->uniqFieldNames as $uniqFieldName ) {
  429. $sourceUniq[$uniqFieldName] = $row->{$uniqFieldName};
  430. }
  431. return $sourceUniq;
  432. }
  433. /**
  434. * INSERT ON DUPLICATE KEY UPDATE of ->pkTable rows from updating models
  435. * into ->pkTable.
  436. * @param $pk_table_rows array
  437. * values are rows of ->pkTable to insert
  438. * @return array
  439. * rows of ->pkTable with populated surrogate PK's matching to $pk_table_rows
  440. * with amount of fields enough to be processable by ->getModelUniqByRow()
  441. */
  442. protected function insertIntoPkTable( array $pk_table_rows ) {
  443. $dbw = $GLOBALS['appContext']->dbw;
  444. # update ->pkTable via UNIQ ->models[] key
  445. $dbw->insert( $this->pkTable, $pk_table_rows, 0, $this->uniqFieldNames );
  446. # get model id's for every model via UNIQ (->pkTable UNIQUE KEY);
  447. return $this->modelUniqsQuery( $this->updateModelUniqs );
  448. }
  449. /**
  450. * Load all PKs for previousely stored models.
  451. * Non-stored models PKs will not be initialized (assumed to be null).
  452. * @param $modelUniqs array
  453. * values are UNIQ of selected models
  454. */
  455. public function loadPKs( array $modelUniqs ) {
  456. # Initially consider all requested models not found in DB.
  457. foreach ( $modelUniqs as $uniq ) {
  458. $this->setPkCache( $uniq, false );
  459. }
  460. $rows = $this->modelUniqsQuery( $modelUniqs );
  461. foreach ( $rows as $row ) {
  462. $sourceUniq = $this->getModelUniqByRow( $row );
  463. $uniq = $this->encodeUniq( $sourceUniq );
  464. $model = $this->models[$uniq];
  465. $model->setPKbyRow( $row );
  466. $pkVal = $model->getPK();
  467. # Model was found, store it's PK into pkCache.
  468. $this->setPkCache( $uniq, $pkVal );
  469. # table PK lookup
  470. $this->modelPKs[$pkVal] = $model;
  471. }
  472. }
  473. /** end of abstract PK retrieval ***/
  474. /*** abstract deletion ***/
  475. /**
  476. * Delete previousely selected models via ->deleteModelUniqs[]
  477. */
  478. protected function deletePendingModels() {
  479. global $appContext;
  480. # Dbg\log('this->deleteModelUniqs',$this->deleteModelUniqs);
  481. if ( count( $this->deleteModelUniqs ) === 0 ) {
  482. # nothing to delete
  483. return;
  484. }
  485. # check, whether PK was already set
  486. foreach ( $this->deleteModelUniqs as $uniq ) {
  487. }
  488. # Populate DB ->pkTable table surrogate PKs of ->$deleteModelUniqs;
  489. # (only single field PK is supported for performance reasons).
  490. $deletePks = $this->getPksByUniqs( $this->deleteModelUniqs );
  491. # delete models which are pending to be deleted
  492. if ( count( $deletePks ) === 0 ) {
  493. # nothing to delete
  494. return;
  495. }
  496. # delete model rows from every table via their PK's
  497. foreach ( $this->modelTables as $tableName ) {
  498. $tableDef = $appContext->schema->getSubProp( 'tableList', $tableName );
  499. if ( array_key_exists( $this->pkFieldName, $tableDef[Schema::FIELD_DEF] ) ) {
  500. $this->deletePKrows( $tableName, $deletePks );
  501. }
  502. }
  503. }
  504. /**
  505. * Delete rows from table with PK's specified.
  506. * @param $tableName string
  507. * DB table name;
  508. * @param $deletePks array
  509. * val int: pk's for selected DB table;
  510. */
  511. protected function deletePKrows( $tableName, array $deletePks ) {
  512. global $appContext;
  513. $appContext->dbw->query( "DELETE FROM", "^{$tableName}", "WHERE", "\${$this->pkFieldName}",
  514. new DbwParams( array( $this->pkFieldName => $deletePks ) ) );
  515. }
  516. /**
  517. * Unset DB-deleted models from collection.
  518. */
  519. protected function unsetPendingModels() {
  520. # Remove instances of already DB-deleted models from collection.
  521. # note: lpModel::destroy() also deletes unused photos,
  522. # freeing up disk space.
  523. foreach ( $this->deleteModelUniqs as $uniq ) {
  524. $pkVal = $this->getPkFromCache( $uniq );
  525. $this->models[$uniq]->destroy();
  526. unset( $this->models[$uniq] );
  527. if ( $pkVal !== null && $pkVal !== false ) {
  528. unset( $this->modelPKs[$pkVal] );
  529. }
  530. }
  531. # done, empty the arrays
  532. $this->deleteModelUniqs = array();
  533. $this->sphinxDeleteUniqs = array();
  534. }
  535. /**
  536. * Deletes _pending_ models both in DB and from ->models[].
  537. * note: all of models should already have isPendingDelete() = true;
  538. * otherwise see side-effect in ->setPendingModelUniqs().
  539. */
  540. public function delete() {
  541. $this->setPendingModelUniqs();
  542. if ( $this->hasPendingDelete() ) {
  543. $this->deletePendingModels();
  544. $this->deleteSphinx();
  545. };
  546. $this->unsetPendingModels();
  547. }
  548. /**
  549. * @uniqList array
  550. * value: UNIQ of model which PK must be get
  551. * @return array
  552. * value: PKs for each model in $uniqList
  553. */
  554. protected function getPksByUniqs( array $uniqList ) {
  555. $pkVals = array();
  556. $missingUniqs = array();
  557. foreach ( $uniqList as $uniq ) {
  558. $pkVal = $this->getPkFromCache( $uniq );
  559. if ( $pkVal === null || $pkVal === false ) {
  560. $missingUniqs[] = $uniq;
  561. } elseif ( $pkVal !== false ) {
  562. $pkVals[] = $pkVal;
  563. }
  564. }
  565. if ( count( $missingUniqs ) > 0 ) {
  566. $this->loadPKs( $missingUniqs );
  567. foreach ( $missingUniqs as $uniq ) {
  568. $pkVal = $this->getPkFromCache( $uniq );
  569. if ( $pkVal === null ) {
  570. SdvException::throwError(
  571. 'Bug in ->loadPKs(), missing load result for UNIQ',
  572. __METHOD__,
  573. $uniq
  574. );
  575. }
  576. # We do not throw an exception here because some models
  577. # might be in pendingDelete state while never stored into DB
  578. # (eg. just loaded from XML source).
  579. if ( $pkVal !== false ) {
  580. $pkVals[] = $pkVal;
  581. }
  582. }
  583. }
  584. return $pkVals;
  585. }
  586. public function deleteSphinx() {
  587. global $appContext;
  588. # Dbg\log(__METHOD__.':sphinxDeleteUniqs',$this->sphinxDeleteUniqs);
  589. $deletePks = $this->getPksByUniqs( $this->sphinxDeleteUniqs );
  590. if ( count( $deletePks ) === 0 ) {
  591. return;
  592. }
  593. Dbg\log(__METHOD__.':deletePks',$deletePks);
  594. $appContext->sphinx->query(
  595. "DELETE FROM",
  596. "^" . static::$rtIndexName, "WHERE",
  597. ':id in (' . implode( ',', array_map( 'intval', $deletePks ) ) .')'
  598. /*
  599. # Commented out because SphinxQL rejects proper syntax.
  600. "\$id",
  601. new DbwParams( array( 'id' => $deletePks ) )
  602. */
  603. );
  604. }
  605. /*** end of abstract deletion ***/
  606. /*** abstract update ***/
  607. /**
  608. * Write-synchronize collection of models to DB.
  609. */
  610. public function update() {
  611. global $appContext;
  612. $this->setPendingModelUniqs();
  613. if ( $this->hasPendingUpdate() || $this->hasPendingDelete() ) {
  614. # todo: begin / commit interval is long; ->pkTable is probably
  615. # locked for too long time; how to improve?
  616. # lp: (separate tables for cities?)
  617. $appContext->dbw->begin();
  618. try {
  619. $this->deletePendingModels();
  620. $this->updatePendingModels();
  621. } catch ( \Exception $e ) {
  622. $appContext->dbw->rollback();
  623. throw $e;
  624. }
  625. $appContext->dbw->commit();
  626. }
  627. $this->updateSphinx();
  628. $this->deleteSphinx();
  629. $this->unsetPendingModels();
  630. }
  631. public function bufferizedUpdate() {
  632. if ( static::$flushThreshold === 0 ||
  633. $this->count() < static::$flushThreshold ) {
  634. return false;
  635. }
  636. $this->update();
  637. $this->clear();
  638. return true;
  639. }
  640. /**
  641. * Must be called only after ->updatePendingModels(), because
  642. * at earlier stage model PKs are not populated
  643. */
  644. public function updateSphinx() {
  645. global $appContext;
  646. # Dbg\log(__METHOD__.':sphinxReplaceRows',$this->sphinxReplaceRows);
  647. $rows = array();
  648. foreach ( $this->sphinxReplaceRows as $uniq => $row ) {
  649. $pkVal = $this->getPkFromCache( $uniq );
  650. if ( $pkVal === null || $pkVal === false ) {
  651. SdvException::throwError(
  652. 'Please call ->updatePendingModels() or ->loadByModelUniqs() first',
  653. __METHOD__,
  654. $uniq
  655. );
  656. }
  657. $row['id'] = $pkVal;
  658. $rows[] = $row;
  659. }
  660. if ( count( $rows ) === 0 ) {
  661. return;
  662. }
  663. # Dbg\log(__METHOD__.':rows',$rows);
  664. $appContext->sphinx->replace( static::$rtIndexName, $rows );
  665. }
  666. /**
  667. * INSERT ON DUPLICATE KEY UPDATE for every model's table
  668. * by list of UNIQs from ->updateModelUniqs[]
  669. */
  670. protected function updateTablesByPKs() {
  671. global $appContext;
  672. # Update via PK for every table.
  673. foreach ( $this->modelTables as $tableName ) {
  674. $tableDef = $appContext->schema->getSubProp( 'tableList', $tableName );
  675. if ( $tableName === $this->pkTable ) {
  676. # Already updated via ->insertIntoPkTable().
  677. continue;
  678. }
  679. # $rows will contain current $tableName rows to insert for each model.
  680. $rows = array();
  681. # Keys for empty table rows to delete.
  682. $deletePks = array();
  683. foreach ( $this->updateModelUniqs as $uniq ) {
  684. $model = $this->models[$uniq];
  685. $pkVal = $this->getPkFromCache( $uniq );
  686. if ( $pkVal === null || $pkVal === false ) {
  687. SdvException::throwError(
  688. 'Please call ->updatePendingModels() or ->loadByModelUniqs() first',
  689. __METHOD__,
  690. $uniq
  691. );
  692. }
  693. if ( !$model->hasProp( $tableName ) ) {
  694. # Current optional table has no properties set in current model.
  695. # Add it's pkVal to the deletion list for current table.
  696. $deletePks[] = $pkVal;
  697. continue;
  698. }
  699. /**
  700. * Get single row (stdClass), or
  701. * multiple rows (2D array of key/values) of data
  702. * for $tableName from current model.
  703. */
  704. $tableData = $model->getProp( $tableName );
  705. $isTwoDim = is_array( $tableData );
  706. $tableData = (array) $tableData;
  707. $rowDimension = 0;
  708. if ( $isTwoDim ) {
  709. if ( count( $tableData ) === 0 ) {
  710. # Current optional table has no rows for current model.
  711. # Add it's pkVal to the deletion list for current table.
  712. $deletePks[] = $pkVal;
  713. continue;
  714. }
  715. # Add multiple rows with PK.
  716. foreach ( $tableData as $row ) {
  717. if ( !is_array( $row ) ) {
  718. SdvException::throwError(
  719. 'Multi-row (two-dimensional) table field value should be array',
  720. __METHOD__,
  721. array(
  722. 'tableData' => $tableData,
  723. 'row' => $row,
  724. 'tableName' => $tableName,
  725. )
  726. );
  727. }
  728. $row[$this->pkFieldName] = $pkVal;
  729. $rows[] = $row;
  730. if ( $rowDimension === 0 || count( $row ) < $rowDimension ) {
  731. $rowDimension = count( $row );
  732. }
  733. }
  734. } else {
  735. if ( count( $tableData ) === 0 ) {
  736. /**
  737. * Empty single row.
  738. * Currently it is not deleted, to allow forms with "incomplete" models.
  739. * @todo: Make flexible choice whether such empty row has to be skipped
  740. * or to be added into $deletePks[].
  741. */
  742. continue;
  743. }
  744. # Add single row with PK.
  745. $tableData[$this->pkFieldName] = $pkVal;
  746. $rows[] = $tableData;
  747. $rowDimension = count( $tableData );
  748. }
  749. if ( $rowDimension !== $appContext->schema->fieldsCount( $tableName ) ) {
  750. # Do not allow to insert incomplete rows, otherwise the missing fields
  751. # will incorrectly have outdated values.
  752. SdvException::throwError( 'Count of rows fields does not match count of table fields',
  753. __METHOD__,
  754. array(
  755. 'pkFieldName' => $this->pkFieldName,
  756. 'pkVal' => $pkVal,
  757. 'tableName' => $tableName,
  758. 'tableData' => $tableData,
  759. 'rowDimension' => $rowDimension,
  760. 'fieldsCount' => $appContext->schema->fieldsCount( $tableName ),
  761. 'isTwoDim' => $isTwoDim,
  762. 'rows' => $rows,
  763. )
  764. );
  765. }
  766. }
  767. # Dbg\log(__METHOD__.":rows $tableName",$rows);
  768. if ( $appContext->schema->fieldIsPK( $tableName, $this->pkFieldName ) ) {
  769. # Simple INSERT ON DUPLICATE KEY is enough when table has ->pkFieldName as PK.
  770. $appContext->dbw->insert( $tableName, $rows, 0, array( $this->pkFieldName ) );
  771. } else {
  772. # SELECT / INSERT sequence is required because ->pkFieldName is not PK of table.
  773. $appContext->dbw->setUniqueDataForField( $tableName, $rows, $this->pkFieldName );
  774. }
  775. if ( count( $deletePks ) > 0 ) {
  776. $this->deletePKrows( $tableName, $deletePks );
  777. }
  778. }
  779. }
  780. /**
  781. * Updates models from RAM into DB
  782. */
  783. protected function updatePendingModels() {
  784. global $appContext;
  785. if ( count( $this->updateModelUniqs ) < 1 ) {
  786. # nothing to update
  787. return;
  788. }
  789. # Create pk_table_rows[] array of DB ->pkTable rows for each model pending update.
  790. $pk_table_rows = array();
  791. foreach ( $this->updateModelUniqs as $uniq ) {
  792. $pk_table_row = (array) $this->models[$uniq]->getProp( $this->pkTable );
  793. $pkVal = $this->models[$uniq]->getPK();
  794. if ( $pkVal !== null ) {
  795. # If there was already a PK, use it for subsequent INSERT
  796. # to keep external keys consistency.
  797. $pk_table_row[$this->pkFieldName] = $pkVal;
  798. }
  799. $pk_table_rows[] = $pk_table_row;
  800. }
  801. # Dbg\log('pk_table_rows',$pk_table_rows);
  802. $rows = $this->insertIntoPkTable( $pk_table_rows );
  803. # Populate PK's for models pending update.
  804. foreach ( $rows as $row ) {
  805. $sourceUniq = $this->getModelUniqByRow( $row );
  806. $uniq = $this->encodeUniq( $sourceUniq );
  807. $model = $this->models[$uniq];
  808. $model->setPKbyRow( $row );
  809. $pkVal = $model->getPK();
  810. $this->setPkCache( $uniq, $pkVal );
  811. $this->modelPKs[$pkVal] = $model;
  812. }
  813. $this->updateTablesByPKs();
  814. }
  815. /*** end of abstract update ***/
  816. /*** begin of abstract load ***/
  817. /**
  818. * Populate all properties of each model in collection from all tables but primary table
  819. * after models in collection were initialized with full rows from primary table
  820. * (usually by calling $model->loadTableRow( $pk, $row ) on each model in collection).
  821. * note: automatically calls $model->loadTableRow() for all tables but primary.
  822. */
  823. public function loadAllByPkTable() {
  824. global $appContext;
  825. $dbw = $appContext->dbw;
  826. if ( count( $this->models ) < 1 ) {
  827. return;
  828. }
  829. $pks = array();
  830. foreach ( array_keys( $this->models ) as $uniq ) {
  831. $pkVal = $this->getPkFromCache( $uniq );
  832. if ( $pkVal === null || $pkVal === false ) {
  833. SdvException::throwError(
  834. 'Please call ->updatePendingModels() or ->loadByModelUniqs() first',
  835. __METHOD__,
  836. $uniq
  837. );
  838. }
  839. $pks[] = $pkVal;
  840. }
  841. foreach ( $this->modelTables as $tableName ) {
  842. if ( $tableName !== $this->pkTable ) {
  843. $rows = $dbw->query(
  844. "SELECT", "^$tableName", ":.* FROM",
  845. "^{$tableName}", "WHERE", "\${$this->pkFieldName}",
  846. new DbwParams( array( $this->pkFieldName => $pks ) )
  847. );
  848. # Dbg\log(__METHOD__.':rows',$rows);
  849. foreach ( $rows as $row ) {
  850. $model = $this->modelPKs[intval( $row->{$this->pkFieldName} )];
  851. $model->loadTableRow( $tableName, $row );
  852. }
  853. }
  854. }
  855. foreach ( $this->models as $model ) {
  856. $model->afterLoadAll();
  857. }
  858. }
  859. /*** end of abstract load ***/
  860. } /* end of AbstractModelCollection class */