PageRenderTime 40ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 0ms

/application/library/Db/Row/AbstractRow.php

https://gitlab.com/lcp0578/yaf.app
PHP | 667 lines | 363 code | 81 blank | 223 comment | 48 complexity | 43d2bc9ca49bf3c8b901beb6b9d1949c MD5 | raw file
  1. <?php
  2. /**
  3. * Yaf.app Framework
  4. *
  5. * @author xudianyang<120343758@qq.com>
  6. * @copyright Copyright (c) 2014 (http://www.phpboy.net)
  7. */
  8. namespace Db\Row;
  9. use ArrayAccess;
  10. use ArrayIterator;
  11. use Countable;
  12. use IteratorAggregate;
  13. use Db\Table\AbstractTable;
  14. use Traversable;
  15. use Db\Sql\Sql;
  16. use Validator\ValidatorChain;
  17. abstract class AbstractRow implements ArrayAccess, IteratorAggregate, Countable, RowInterface
  18. {
  19. const ON_CREATE = 'onCreate';
  20. const ON_UPDATE = 'onUpdate';
  21. /**
  22. * @var bool
  23. */
  24. protected $isInitialized = false;
  25. /**
  26. * @var array
  27. */
  28. protected $columns;
  29. /**
  30. * @var array
  31. */
  32. protected $primaryKey;
  33. /**
  34. * @var array
  35. */
  36. protected $primaryKeyData;
  37. /**
  38. * @var array
  39. */
  40. protected $cleanData = array();
  41. /**
  42. * @var array
  43. */
  44. protected $data = array();
  45. /**
  46. * @var Sql
  47. */
  48. protected $sql;
  49. /**
  50. * @var ValidatorChain[]
  51. */
  52. protected $validatorChains = array();
  53. /** @var array */
  54. protected $invalidColumns = array();
  55. /**
  56. * @var array
  57. */
  58. protected $autofills = array(
  59. self::ON_CREATE => array(),
  60. self::ON_UPDATE => array()
  61. );
  62. /**
  63. * initialize()
  64. */
  65. public function initialize()
  66. {
  67. if ($this->isInitialized) {
  68. return;
  69. }
  70. if (empty($this->columns)) {
  71. throw new Exception\RuntimeException('This row object must be setup columns.');
  72. } elseif (array_keys($this->columns) == range(0, count($this->columns) - 1)) {
  73. $this->columns = array_flip($this->columns);
  74. }
  75. if ($this->primaryKey == null) {
  76. throw new Exception\RuntimeException('This row object does not have a primary key column set.');
  77. } elseif (is_string($this->primaryKey)) {
  78. $this->primaryKey = (array)$this->primaryKey;
  79. }
  80. if (!$this->sql instanceof Sql) {
  81. throw new Exception\RuntimeException('This row object does not have a Sql object set.');
  82. }
  83. $this->isInitialized = true;
  84. }
  85. /**
  86. * @param AbstractTable $table
  87. * @return $this
  88. */
  89. public function setupWithTable(AbstractTable $table)
  90. {
  91. $this->sql = $table->getSql();
  92. $adapter = $this->sql->getAdapter();
  93. $table = $table->getTable();
  94. $metadata = $adapter->getMetadata();
  95. $this->columns = array_keys($metadata->getColumns($table->getTable(), $table->getSchema()));
  96. $this->primaryKey = $metadata->getPrimarys($table->getTable(), $table->getSchema());
  97. return $this;
  98. }
  99. /**
  100. * @param string $column
  101. * @return ValidatorChain
  102. * @throws Exception\InvalidArgumentException
  103. */
  104. public function getValidatorChain($column)
  105. {
  106. if (!isset($this->validatorChains[$column])) {
  107. $this->validatorChains[$column] = new ValidatorChain;
  108. }
  109. return $this->validatorChains[$column];
  110. }
  111. /**
  112. * Returns the columns failure on last validation
  113. *
  114. * @return array
  115. */
  116. public function getInvalidColumns()
  117. {
  118. return $this->invalidColumns;
  119. }
  120. /**
  121. * add validator
  122. *
  123. * @param string $column
  124. * @param string|\Validator\ValidatorInterface|callable $validator
  125. * @param null|string $message
  126. * @return $this
  127. */
  128. public function addValidator($column, $validator, $message = NULL)
  129. {
  130. $this->getValidatorChain($column)->addValidator($validator, $message);
  131. return $this;
  132. }
  133. /**
  134. * Valid modified data
  135. *
  136. * @param null|bool $breakChainOnFailure
  137. * @return bool
  138. */
  139. public function isValid($breakChainOnFailure = null)
  140. {
  141. $data = $this->getModified();
  142. $columns = $data;
  143. if (!$this->rowExistsInDatabase()) {
  144. $columns = $this->columns;
  145. }
  146. /** @var ValidatorChain[] $chains */
  147. $chains = array_intersect_key($this->validatorChains, $columns);
  148. if (empty($chains)) {
  149. return true;
  150. }
  151. $this->invalidColumns = array();
  152. foreach ($chains as $column => $chain) {
  153. if (!$chain->isValid(isset($this->data[$column]) ? $this->data[$column] : null, $breakChainOnFailure))
  154. {
  155. $this->invalidColumns[] = $column;
  156. if ($breakChainOnFailure === true) {
  157. return false;
  158. }
  159. }
  160. }
  161. return empty($this->invalidColumns);
  162. }
  163. /**
  164. * @param string $column
  165. * @param mixed $autofill
  166. * @param string $on
  167. * @return $this
  168. * @throws Exception\InvalidArgumentException
  169. */
  170. public function addAutofill($column, $autofill, $on = self::ON_CREATE)
  171. {
  172. if (!isset($this->autofills[$on])) {
  173. throw new Exception\InvalidArgumentException(sprintf(
  174. '$on must be ON_CREATE or ON_UPDATE; received "%s"', $on
  175. ));
  176. }
  177. $this->autofills[$on][$column] = $autofill;
  178. return $this;
  179. }
  180. /**
  181. * Populate Data
  182. *
  183. * @param array|object $rowData
  184. * @param bool $rowExistsInDatabase
  185. * @return $this
  186. */
  187. public function populate($rowData, $rowExistsInDatabase = false)
  188. {
  189. $this->initialize();
  190. $data = $this->pickupData($rowData);
  191. if ($rowExistsInDatabase) {
  192. $this->data = $data;
  193. $this->cleanData = $data;
  194. $this->processPrimaryKeyData();
  195. } else {
  196. $this->cleanData = array();
  197. $this->primaryKeyData = null;
  198. $this->data = array();
  199. foreach ($data as $column => $value) {
  200. $this->set($column, $value);
  201. }
  202. }
  203. return $this;
  204. }
  205. protected function pickupData($data)
  206. {
  207. if (is_object($data)) {
  208. if (method_exists($data, 'toArray')) {
  209. $data = $data->toArray();
  210. } elseif ($data instanceof Traversable) {
  211. $temp = array();
  212. foreach ($data as $key => $val) {
  213. $temp[$key] = $val;
  214. }
  215. $data = $temp;
  216. } else {
  217. $data = (array) $data;
  218. }
  219. }
  220. if (!is_array($data)) {
  221. throw new Exception\InvalidArgumentException(sprintf(
  222. '%s: expects an array, or Traversable argument; received "%s"',
  223. __METHOD__, gettype($data)
  224. ));
  225. }
  226. return array_intersect_key($data, $this->columns);
  227. }
  228. /**
  229. * @param mixed $array
  230. * @return $this
  231. */
  232. public function exchangeArray($array)
  233. {
  234. return $this->populate($array, true);
  235. }
  236. /**
  237. * Merge the new data
  238. *
  239. * @param array|Traversable|object $data
  240. * @return $this
  241. */
  242. public function mergeData($data)
  243. {
  244. $this->initialize();
  245. $data = $this->pickupData($data);
  246. foreach ($data as $column => $value) {
  247. $this->set($column, $value);
  248. }
  249. return $this;
  250. }
  251. /**
  252. * Save
  253. *
  254. * @param bool $ignoreValidation
  255. * @return int
  256. * @throws Exception\ValidateException
  257. */
  258. public function save($ignoreValidation = false)
  259. {
  260. $this->initialize();
  261. $on = $this->rowExistsInDatabase() ? self::ON_UPDATE : self::ON_CREATE;
  262. // autofill empty sets
  263. if (!empty($this->autofills[$on])) {
  264. foreach ($this->autofills[$on] as $column => $autofill) {
  265. if (!isset($this->columns[$column]) || $this->isModified($column) || !$this->isNull($column))
  266. {
  267. continue;
  268. }
  269. $this->set($column, is_callable($autofill) ? $autofill($column, $this) : $autofill);
  270. }
  271. }
  272. // validate data
  273. if (!$ignoreValidation && !$this->isValid(true)) {
  274. $invalidColumn = reset($this->invalidColumns);
  275. throw new Exception\ValidateException($this->getValidatorChain($invalidColumn)->getFirstMessage());
  276. }
  277. // get modified data
  278. $data = $this->getModified();
  279. if (empty($data)) {
  280. return 0;
  281. }
  282. if ($this->rowExistsInDatabase()) {
  283. // UPDATE
  284. $where = $this->processPrimaryKeyWhere();
  285. $statement = $this->sql->prepareStatement($this->sql->update()->set($data)->where($where));
  286. $result = $statement->execute();
  287. $rowsAffected = $result->getAffectedRows();
  288. unset($statement, $result); // cleanup
  289. } else {
  290. // INSERT
  291. $insert = $this->sql->insert();
  292. $insert->values($data);
  293. $statement = $this->sql->prepareStatement($insert);
  294. $result = $statement->execute();
  295. if (($primaryKeyValue = $result->getGeneratedValue()) && count($this->primaryKey) == 1) {
  296. $this->primaryKeyData = array($this->primaryKey[0] => $primaryKeyValue);
  297. } else {
  298. // make primary key data available so that $where can be complete
  299. $this->processPrimaryKeyData();
  300. }
  301. $rowsAffected = $result->getAffectedRows();
  302. unset($statement, $result); // cleanup
  303. }
  304. // refresh data
  305. $this->refresh();
  306. // return rows affected
  307. return $rowsAffected;
  308. }
  309. /**
  310. * Refresh data from database
  311. *
  312. * @return $this
  313. */
  314. public function refresh()
  315. {
  316. $this->initialize();
  317. if (!$this->rowExistsInDatabase()) {
  318. throw new Exception\RuntimeException('Cannot refresh the data not exists in database.');
  319. }
  320. $where = $this->processPrimaryKeyWhere();
  321. $statement = $this->sql->prepareStatement($this->sql->select()->where($where));
  322. $result = $statement->execute();
  323. $rowData = $result->current();
  324. unset($statement, $result); // cleanup
  325. // make sure data and original data are in sync after save
  326. $this->populate($rowData, true);
  327. return $this;
  328. }
  329. /**
  330. * Get modified data
  331. *
  332. * @return array
  333. */
  334. public function getModified()
  335. {
  336. if (empty($this->cleanData)) {
  337. return $this->data;
  338. }
  339. $data = $this->data;
  340. foreach ($data as $key => $val) {
  341. if (array_key_exists($key, $this->cleanData) && $val === $this->cleanData[$key]) {
  342. unset($data[$key]);
  343. }
  344. }
  345. return $data;
  346. }
  347. /**
  348. * Check the column if modified
  349. *
  350. * @param string $column
  351. * @return bool
  352. */
  353. public function isModified($column)
  354. {
  355. if (empty($this->cleanData) || !array_key_exists($column, $this->cleanData)) {
  356. return array_key_exists($column, $this->data);
  357. }
  358. return $this->cleanData[$column] !== $this->data[$column];
  359. }
  360. /**
  361. * Check column data is null or empty string
  362. *
  363. * @param string $column
  364. * @return bool
  365. */
  366. public function isNull($column)
  367. {
  368. return !isset($this->data[$column]) || !strlen($this->data[$column]);
  369. }
  370. /**
  371. * Delete
  372. *
  373. * @return int
  374. */
  375. public function delete()
  376. {
  377. $this->initialize();
  378. if (!$this->rowExistsInDatabase()) {
  379. throw new Exception\RuntimeException('Cannot refresh the data not exists in database.');
  380. }
  381. $where = $this->processPrimaryKeyWhere();
  382. $statement = $this->sql->prepareStatement($this->sql->delete()->where($where));
  383. $result = $statement->execute();
  384. $rowsAffected = $result->getAffectedRows();
  385. if ($rowsAffected == 1) {
  386. // detach from database
  387. $this->primaryKeyData = null;
  388. $this->data = array();
  389. $this->cleanData = array();
  390. }
  391. return $rowsAffected;
  392. }
  393. /**
  394. * Get a column value
  395. *
  396. * @param string $column
  397. * @return mixed
  398. * @throws Exception\InvalidArgumentException
  399. */
  400. public function get($column)
  401. {
  402. $this->initialize();
  403. if (array_key_exists($column, $this->columns)) {
  404. $accessor = '__get' . str_replace(' ', '', ucwords(str_replace(array('-', '_'), ' ', $column)));
  405. return method_exists($this, $accessor) ? $this->{$accessor}() : $this->data[$column];
  406. }
  407. throw new Exception\InvalidArgumentException('Not a valid column in this row: ' . $column);
  408. }
  409. /**
  410. * Set the column value
  411. *
  412. * @param string $column
  413. * @param mixed $value
  414. * @throws Exception\InvalidArgumentException
  415. */
  416. public function set($column, $value)
  417. {
  418. $this->initialize();
  419. if (array_key_exists($column, $this->columns)) {
  420. $mutator = '__set' . str_replace(' ', '', ucwords(str_replace(array('-', '_'), ' ', $column)));
  421. if (method_exists($this, $mutator)) {
  422. $this->{$mutator}($value);
  423. } else {
  424. $this->data[$column] = $value;
  425. }
  426. return;
  427. }
  428. throw new Exception\InvalidArgumentException('Not a valid column in this row: ' . $column);
  429. }
  430. /**
  431. * Exists the column
  432. *
  433. * @param string $column
  434. * @return bool
  435. */
  436. public function exists($column)
  437. {
  438. $this->initialize();
  439. return array_key_exists($column, $this->columns);
  440. }
  441. /**
  442. * Offset Exists
  443. *
  444. * @param string $offset
  445. * @return bool
  446. */
  447. public function offsetExists($offset)
  448. {
  449. return $this->exists($offset);
  450. }
  451. /**
  452. * Offset get
  453. *
  454. * @param string $offset
  455. * @return mixed
  456. */
  457. public function offsetGet($offset)
  458. {
  459. return $this->get($offset);
  460. }
  461. /**
  462. * Offset set
  463. *
  464. * @param string $offset
  465. * @param mixed $value
  466. */
  467. public function offsetSet($offset, $value)
  468. {
  469. $this->set($offset, $value);
  470. }
  471. /**
  472. * Offset unset
  473. *
  474. * @param string $offset
  475. */
  476. public function offsetUnset($offset)
  477. {
  478. $this->initialize();
  479. if (array_key_exists($offset, $this->columns)) {
  480. $this->data[$offset] = null;
  481. }
  482. }
  483. /**
  484. * @return int
  485. */
  486. public function count()
  487. {
  488. return count($this->data);
  489. }
  490. /**
  491. * To array
  492. *
  493. * @param bool $filter with accessor
  494. * @return array
  495. */
  496. public function toArray($filter = false)
  497. {
  498. $data = $this->data;
  499. if ($filter) {
  500. foreach ($data as $column => &$value) {
  501. $value = $this->get($column);
  502. }
  503. }
  504. return $data;
  505. }
  506. /**
  507. * __get
  508. *
  509. * @param string $name
  510. * @return mixed
  511. */
  512. public function __get($name)
  513. {
  514. return $this->get($name);
  515. }
  516. /**
  517. * __set
  518. *
  519. * @param string $name
  520. * @param mixed $value
  521. * @return void
  522. */
  523. public function __set($name, $value)
  524. {
  525. $this->set($name, $value);
  526. }
  527. /**
  528. * __isset
  529. *
  530. * @param string $name
  531. * @return bool
  532. */
  533. public function __isset($name)
  534. {
  535. return $this->exists($name);
  536. }
  537. /**
  538. * __unset
  539. *
  540. * @param string $name
  541. * @return void
  542. */
  543. public function __unset($name)
  544. {
  545. $this->offsetUnset($name);
  546. }
  547. /**
  548. * Required by interface IteratorAggregate, use for foreach
  549. *
  550. * @return ArrayIterator
  551. */
  552. public function getIterator()
  553. {
  554. return new ArrayIterator($this->data);
  555. }
  556. /**
  557. * @return bool
  558. */
  559. public function rowExistsInDatabase()
  560. {
  561. return ($this->primaryKeyData !== null);
  562. }
  563. /**
  564. * @throws Exception\RuntimeException
  565. */
  566. protected function processPrimaryKeyData()
  567. {
  568. $this->primaryKeyData = array();
  569. foreach ($this->primaryKey as $column) {
  570. if (!isset($this->data[$column])) {
  571. throw new Exception\RuntimeException(
  572. 'While processing primary key data, a known key ' . $column . ' was not found in the data array');
  573. }
  574. $this->primaryKeyData[$column] = $this->data[$column];
  575. }
  576. }
  577. protected function processPrimaryKeyWhere()
  578. {
  579. $where = array();
  580. // primary key is always an array even if its a single column
  581. foreach ($this->primaryKey as $column) {
  582. $where[$column] = $this->primaryKeyData[$column];
  583. }
  584. return $where;
  585. }
  586. }