AbstractTable.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. <?php
  2. /**
  3. *
  4. * Cube Framework $Id$ u7R4F2jbFhaZUUCQKCw5dP2tKElL+a2HCx31dKAxQV4=
  5. *
  6. * @link http://codecu.be/framework
  7. * @copyright Copyright (c) 2017 CodeCube SRL
  8. * @license http://codecu.be/framework/license Commercial License
  9. *
  10. * @version 1.10 [rev.1.10.01]
  11. */
  12. namespace Cube\Db\Table;
  13. use Cube\Db,
  14. Cube\Db\Select,
  15. Cube\Db\Adapter\AbstractAdapter,
  16. Cube\Controller\Front,
  17. Cube\Cache\Adapter\AbstractAdapter as CacheAdapter;
  18. abstract class AbstractTable
  19. {
  20. /**
  21. * class constants
  22. */
  23. const NAME = 'name';
  24. const COLS = 'cols';
  25. const PRIMARY = 'primary';
  26. const METADATA = 'metadata';
  27. const REFERENCE_MAP = 'referenceMap';
  28. const DEPENDENT_TABLES = 'dependentTables';
  29. const COLUMNS = 'columns';
  30. const REF_TABLE_CLASS = 'refTableClass';
  31. const REF_COLUMNS = 'refColumns';
  32. const QUERIES_CACHE_EXPIRES = 300; // 5 minutes
  33. /**
  34. *
  35. * table name
  36. *
  37. * @var string
  38. */
  39. protected $_name;
  40. /**
  41. *
  42. * table prefix
  43. * (set in configuration)
  44. *
  45. * @var string
  46. */
  47. protected $_prefix;
  48. /**
  49. *
  50. * database adapter
  51. *
  52. * @var \Cube\Db\Adapter\AbstractAdapter
  53. */
  54. protected $_adapter;
  55. /**
  56. * table column names derived from
  57. * \Cube\Db\Adapter\AbstractAdapter::describeTable()
  58. *
  59. * @var array
  60. */
  61. protected $_cols = null;
  62. /**
  63. *
  64. * primary key column(s)
  65. * A compound key should be declared as an array
  66. *
  67. * @var string|array
  68. */
  69. protected $_primary = null;
  70. /**
  71. *
  72. * information provided by the adapter's describeTable() method
  73. *
  74. * @var array
  75. */
  76. protected $_metadata = array();
  77. /**
  78. *
  79. * class name for row
  80. *
  81. * @var string
  82. */
  83. protected $_rowClass = '\Cube\Db\Table\Row';
  84. /**
  85. *
  86. * class name for rowset
  87. *
  88. * @var string
  89. */
  90. protected $_rowsetClass = '\Cube\Db\Table\Rowset';
  91. /**
  92. * Associative array map of declarative referential integrity rules.
  93. * This array has one entry per foreign key in the current table.
  94. * Each key is a mnemonic name for one reference rule.
  95. *
  96. * Each value is also an associative array, with the following keys:
  97. * - columns = array of names of column(s) in the child table.
  98. * - refTableClass = class name of the parent table.
  99. * - refColumns = array of names of column(s) in the parent table,
  100. * in the same order as those in the 'columns' entry.
  101. * - onDelete = "cascade" means that a delete in the parent table also
  102. * causes a delete of referencing rows in the child table.
  103. * - onUpdate = "cascade" means that an update of primary key values in
  104. * the parent table also causes an update of referencing
  105. * rows in the child table.
  106. *
  107. * @var array
  108. */
  109. protected $_referenceMap = array();
  110. /**
  111. * Simple array of class names of tables that are "children" of the current
  112. * table, in other words tables that contain a foreign key to this one.
  113. * Array elements are not table names; they are class names of classes that
  114. * extend Zend_Db_Table_Abstract.
  115. *
  116. * @var array
  117. */
  118. protected $_dependentTables = array();
  119. /**
  120. *
  121. * flag that sets if table queries are cacheable
  122. * used to determine if queries cache is to be purged on a create/update/delete operation
  123. *
  124. * @var bool
  125. */
  126. protected $_cacheableQueries = false;
  127. /**
  128. *
  129. * cache object
  130. *
  131. * @var \Cube\Cache|false false if caching is disabled
  132. */
  133. protected $_cache = false;
  134. /**
  135. *
  136. * class constructor
  137. *
  138. * @param \Cube\Db\Adapter\AbstractAdapter $adapter
  139. *
  140. * @throws \RuntimeException
  141. */
  142. public function __construct(AbstractAdapter $adapter = null)
  143. {
  144. $bootstrap = Front::getInstance()->getBootstrap();
  145. $this->_cache = $bootstrap->getResource('cache');
  146. if ($adapter === null) {
  147. $adapter = $bootstrap->getResource('db');
  148. }
  149. if (!$adapter instanceof AbstractAdapter) {
  150. throw new \RuntimeException("Could not create table.
  151. The database adapter must be an instance of \Cube\Db\Adapter\AbstractAdapter");
  152. }
  153. if (empty($this->_name)) {
  154. $this->_name = strtolower(get_class());
  155. }
  156. $adapterConfig = $adapter->getConfig();
  157. if (isset($adapterConfig['prefix'])) {
  158. $this->_prefix = $adapterConfig['prefix'];
  159. }
  160. $this->setAdapter($adapter);
  161. }
  162. /**
  163. *
  164. * get table name
  165. *
  166. * @return string
  167. */
  168. public function getName()
  169. {
  170. return $this->_name;
  171. }
  172. /**
  173. *
  174. * get table prefix
  175. *
  176. * @return string
  177. */
  178. public function getPrefix()
  179. {
  180. return $this->_prefix;
  181. }
  182. /**
  183. *
  184. * set table prefix
  185. *
  186. * @param string $prefix
  187. *
  188. * @return $this
  189. */
  190. public function setPrefix($prefix = null)
  191. {
  192. $this->_prefix = $prefix;
  193. return $this;
  194. }
  195. /**
  196. *
  197. * get database adapter
  198. *
  199. * @return \Cube\Db\Adapter\AbstractAdapter
  200. */
  201. public function getAdapter()
  202. {
  203. return $this->_adapter;
  204. }
  205. /**
  206. *
  207. * set database adapter
  208. *
  209. * @param \Cube\Db\Adapter\AbstractAdapter $adapter
  210. *
  211. * @return $this
  212. * @throws \RuntimeException
  213. */
  214. public function setAdapter($adapter)
  215. {
  216. if (!$adapter instanceof AbstractAdapter) {
  217. throw new \RuntimeException("Could not create table.
  218. The database adapter must be an instance of \Cube\Db\Adapter\AbstractAdapter");
  219. }
  220. $this->_adapter = $adapter;
  221. return $this;
  222. }
  223. /**
  224. *
  225. * get row object class
  226. *
  227. * @return string
  228. */
  229. public function getRowClass()
  230. {
  231. return $this->_rowClass;
  232. }
  233. /**
  234. *
  235. * set row object class
  236. *
  237. * @param string $rowClass
  238. *
  239. * @return $this
  240. */
  241. public function setRowClass($rowClass)
  242. {
  243. $this->_rowClass = (string)$rowClass;
  244. return $this;
  245. }
  246. /**
  247. *
  248. * get rowset object class
  249. *
  250. * @return string
  251. */
  252. public function getRowsetClass()
  253. {
  254. return $this->_rowsetClass;
  255. }
  256. /**
  257. *
  258. * set rowset object class
  259. *
  260. * @param string $rowsetClass
  261. *
  262. * @return $this
  263. */
  264. public function setRowsetClass($rowsetClass)
  265. {
  266. $this->_rowsetClass = (string)$rowsetClass;
  267. return $this;
  268. }
  269. /**
  270. *
  271. * get the reference between the table and a requested table
  272. *
  273. * @param string $refTableClass
  274. * @param string $ruleKey
  275. *
  276. * @return array
  277. * @throws \RuntimeException
  278. */
  279. public function getReference($refTableClass, $ruleKey = null)
  280. {
  281. if ($ruleKey !== null) {
  282. if (!isset($this->_referenceMap[$ruleKey])) {
  283. throw new \RuntimeException(
  284. sprintf("A reference rule with the name '%s' does not exist in the definition of '%s'.", $ruleKey,
  285. get_class($this)));
  286. }
  287. if ($this->_referenceMap[$ruleKey][self::REF_TABLE_CLASS] != $refTableClass) {
  288. throw new \RuntimeException(
  289. sprintf("The reference rule '%s' does not reference the table '%s'.", $ruleKey, $refTableClass));
  290. }
  291. return $this->_referenceMap[$ruleKey];
  292. }
  293. foreach ($this->_referenceMap as $reference) {
  294. if ($reference[self::REF_TABLE_CLASS] == $refTableClass) {
  295. return $reference;
  296. }
  297. }
  298. throw new \RuntimeException(
  299. sprintf("There is no reference from table '%s' to table '%s'.", get_class($this), $refTableClass));
  300. }
  301. /**
  302. *
  303. * add a reference to the reference map of the table
  304. *
  305. * @param string $ruleKey
  306. * @param mixed $columns
  307. * @param string $refTableClass
  308. * @param mixed $refColumns
  309. *
  310. * @return $this
  311. */
  312. public function setReference($ruleKey, $columns, $refTableClass, $refColumns)
  313. {
  314. $reference = array(self::COLUMNS => (array)$columns,
  315. self::REF_TABLE_CLASS => $refTableClass,
  316. self::REF_COLUMNS => (array)$refColumns);
  317. $this->_referenceMap[$ruleKey] = $reference;
  318. return $this;
  319. }
  320. /**
  321. *
  322. * set the reference map of the table
  323. *
  324. * @param array $referenceMap
  325. *
  326. * @return $this
  327. */
  328. public function setReferenceMap(array $referenceMap)
  329. {
  330. $this->_referenceMap = $referenceMap;
  331. return $this;
  332. }
  333. /**
  334. *
  335. * get dependent tables
  336. *
  337. * @return array
  338. */
  339. public function getDependentTables()
  340. {
  341. return $this->_dependentTables;
  342. }
  343. /**
  344. *
  345. * set dependent tables
  346. *
  347. * @param array $dependentTables
  348. *
  349. * @return $this
  350. */
  351. public function setDependentTables(array $dependentTables)
  352. {
  353. $this->_dependentTables = $dependentTables;
  354. return $this;
  355. }
  356. /**
  357. *
  358. * create an instance of the select object
  359. *
  360. * @param array|string|\Cube\Db\Expr $cols The columns to select from this table.
  361. *
  362. * @return \Cube\Db\Select
  363. */
  364. public function select($cols = '*')
  365. {
  366. $select = new Select($this->_adapter);
  367. $select->setPrefix($this->getPrefix())
  368. ->from($this->_name, $cols);
  369. return $select;
  370. }
  371. /**
  372. *
  373. * Inserts a table row with specified data.
  374. *
  375. * @param array $data Column-value pairs.
  376. *
  377. * @return int the id of the inserted column.
  378. */
  379. public function insert(array $data)
  380. {
  381. $this->_purgeQueriesCache();
  382. $this->_adapter->insert($this->_prefix . $this->_name, $data);
  383. return $this->lastInsertId();
  384. }
  385. /**
  386. *
  387. * Updates table rows with specified data based on a WHERE clause.
  388. *
  389. * @param array $data Column-value pairs.
  390. * @param mixed $where UPDATE WHERE clause(s).
  391. *
  392. * @return int The number of affected rows.
  393. */
  394. public function update(array $data, $where)
  395. {
  396. $result = $this->_adapter->update($this->_prefix . $this->_name, $data, $where);
  397. if ($result > 0) {
  398. $this->_purgeQueriesCache();
  399. }
  400. return $result;
  401. }
  402. /**
  403. *
  404. * delete table rows based on a WHERE clause.
  405. *
  406. * @param mixed $where DELETE WHERE clause(s).
  407. *
  408. * @return int The number of affected rows.
  409. */
  410. public function delete($where)
  411. {
  412. $result = $this->_adapter->delete($this->_prefix . $this->_name, $where);
  413. if ($result > 0) {
  414. $this->_purgeQueriesCache();
  415. }
  416. return $result;
  417. }
  418. /**
  419. *
  420. * fetches all matched rows
  421. *
  422. * @param string|\Cube\Db\Select $where SQL where clause, or a select object
  423. * @param string|array $order
  424. * @param int $count
  425. * @param int $offset
  426. * @param string|array $cache
  427. *
  428. * @return \Cube\Db\Table\Rowset\AbstractRowset
  429. */
  430. public function fetchAll($where = null, $order = null, $count = null, $offset = null, $cache = null)
  431. {
  432. if (!$where instanceof Select) {
  433. $select = $this->select()
  434. ->where($where)
  435. ->order($order)
  436. ->limit($count, $offset);
  437. }
  438. else {
  439. $select = $where;
  440. }
  441. $cachedData = false;
  442. $cacheFile = null;
  443. $rows = null;
  444. // cacheQueries: if requested in method, if enabled for the table and if enabled globally
  445. $cacheQueries = ($cache !== null) ? $this->_getCache('cacheQueries') : false;
  446. if ($cacheQueries !== false) {
  447. $cacheFile = md5($select->assemble());
  448. if (($data = $this->_cache->read($cacheFile, CacheAdapter::QUERIES)) !== false) {
  449. $cacheCol = (is_array($cache)) ? $cache[CacheAdapter::CACHE_COL] : $cache;
  450. $data = empty($data) ? array(0) : $data;
  451. $select->reset(Select::WHERE)
  452. ->where("{$cacheCol} IN (?)", $data);
  453. $cachedData = true;
  454. }
  455. }
  456. $stmt = $this->_adapter->query($select);
  457. $rows = $stmt->fetchAll(Db::FETCH_ASSOC);
  458. if ($cachedData === false) {
  459. if ($cacheQueries !== false) {
  460. $data = array();
  461. $cacheWhere = (is_array($cache)) ? $cache[CacheAdapter::CACHE_WHERE] : $cache;
  462. foreach ($rows as $row) {
  463. $data[] = $row[$cacheWhere];
  464. }
  465. $this->_cache->write($cacheFile, CacheAdapter::QUERIES, $data, self::QUERIES_CACHE_EXPIRES);
  466. }
  467. }
  468. $data = array(
  469. 'table' => $this,
  470. 'data' => $rows,
  471. );
  472. return new $this->_rowsetClass($data);
  473. }
  474. /**
  475. *
  476. * fetch a single matched row from a result set
  477. *
  478. * @param string|\Cube\Db\Select $where SQL where clause, or a select object
  479. * @param string|array $order
  480. * @param int $offset
  481. * @param string $cacheId
  482. *
  483. * @return \Cube\Db\Table\Row\AbstractRow|null
  484. */
  485. public function fetchRow($where = null, $order = null, $offset = null, $cacheId = null)
  486. {
  487. return $this->fetchAll($where, $order, 1, $offset, $cacheId)->getRow(0);
  488. }
  489. /**
  490. *
  491. * get the id resulted from an insert operation
  492. *
  493. * @return int
  494. */
  495. public function lastInsertId()
  496. {
  497. return $this->_adapter->lastInsertId();
  498. }
  499. /**
  500. *
  501. * returns table information
  502. *
  503. * @param string $key specific info part to return
  504. *
  505. * @throws \InvalidArgumentException
  506. * @return mixed
  507. */
  508. public function info($key = null)
  509. {
  510. $this->_getPrimary();
  511. $info = array(
  512. self::NAME => $this->_name,
  513. self::COLS => $this->_getCols(),
  514. self::PRIMARY => (array)$this->_primary,
  515. self::METADATA => $this->_metadata,
  516. self::REFERENCE_MAP => $this->_referenceMap,
  517. self::DEPENDENT_TABLES => $this->_dependentTables,
  518. );
  519. if ($key === null) {
  520. return $info;
  521. }
  522. if (!array_key_exists($key, $info)) {
  523. throw new \InvalidArgumentException(
  524. sprintf("There is no table information for the key '%s'.", $key));
  525. }
  526. return $info[$key];
  527. }
  528. /**
  529. *
  530. * get table metadata
  531. * use cache if caching is enabled
  532. *
  533. * @return array
  534. */
  535. protected function _getMetadata()
  536. {
  537. if (!count($this->_metadata)) {
  538. $cachedData = false;
  539. $cacheMetadata = $this->_getCache('cacheMetadata');
  540. $cacheFile = null;
  541. if ($cacheMetadata !== false) {
  542. $cacheFile = md5("DESCRIBE " . $this->_prefix . $this->_name);
  543. }
  544. if ($cacheMetadata !== false) {
  545. if (($data = $this->_cache->read($cacheFile, CacheAdapter::METADATA)) !== false) {
  546. $this->_metadata = $data;
  547. $cachedData = true;
  548. }
  549. }
  550. if ($cachedData === false) {
  551. $this->_metadata = $this->_adapter->describeTable($this->_prefix . $this->_name);
  552. if ($cacheMetadata !== false) {
  553. $this->_cache->write($cacheFile, CacheAdapter::METADATA, $this->_metadata);
  554. }
  555. }
  556. }
  557. return $this->_metadata;
  558. }
  559. /**
  560. *
  561. * get cache
  562. *
  563. * @param string $key
  564. *
  565. * @return mixed
  566. */
  567. protected function _getCache($key = null)
  568. {
  569. if ($this->_cache !== false) {
  570. if ($key === null) {
  571. return $this->_cache;
  572. }
  573. else {
  574. $methodName = 'get' . ucfirst($key);
  575. if (method_exists($this->_cache, $methodName)) {
  576. if ($key != 'cacheQueries' || $this->_cacheableQueries) {
  577. return $this->_cache->$methodName();
  578. }
  579. }
  580. }
  581. }
  582. return false;
  583. }
  584. /**
  585. *
  586. * purge queries cache
  587. *
  588. * @return $this
  589. */
  590. protected function _purgeQueriesCache()
  591. {
  592. $cacheQueries = $this->_getCache('cacheQueries');
  593. if ($cacheQueries !== false) {
  594. $this->_cache->getAdapter()->purge(CacheAdapter::QUERIES, true);
  595. }
  596. return $this;
  597. }
  598. /**
  599. *
  600. * get table columns
  601. *
  602. * @return array
  603. */
  604. protected function _getCols()
  605. {
  606. if ($this->_cols === null) {
  607. $this->_cols = array_keys(
  608. $this->_getMetadata());
  609. }
  610. return $this->_cols;
  611. }
  612. /**
  613. *
  614. * get primary key(s)
  615. *
  616. * @throws \RuntimeException
  617. * @return array
  618. */
  619. protected function _getPrimary()
  620. {
  621. if (!$this->_primary) {
  622. $this->_getMetadata();
  623. $this->_primary = array();
  624. foreach ($this->_metadata as $col) {
  625. if ($col['PRIMARY']) {
  626. $this->_primary[$col['PRIMARY_POSITION']] = $col['COLUMN_NAME'];
  627. }
  628. }
  629. }
  630. else if (!is_array($this->_primary)) {
  631. $this->_primary = array(1 => $this->_primary);
  632. }
  633. else if (isset($this->_primary[0])) {
  634. array_unshift($this->_primary, null);
  635. unset($this->_primary[0]);
  636. }
  637. $cols = $this->_getCols();
  638. if (!array_intersect((array)$this->_primary, $cols) == (array)$this->_primary) {
  639. throw new \RuntimeException(
  640. sprintf("Invalid primary key column(s): %s.", implode(',', (array)$this->_primary)));
  641. }
  642. return $this->_primary;
  643. }
  644. }