AbstractModel.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. <?php
  2. abstract class AbstractModel
  3. {
  4. /**
  5. * Db table for find methods
  6. *
  7. * @var string
  8. */
  9. public static $table;
  10. /**
  11. * Db id attribute for find methods
  12. *
  13. * @var string
  14. */
  15. public static $idAttribute;
  16. /**
  17. * Mapping model attributes and database attributes for saving
  18. *
  19. * @var array
  20. */
  21. protected $attributeDbAttributeMapping = array();
  22. /**
  23. * Setup db attribute mapping
  24. *
  25. * @param array $childMapping
  26. *
  27. * @return array
  28. *
  29. * @throws Exception
  30. */
  31. protected function setupDbMapping($childMapping = array())
  32. {
  33. return array_replace(
  34. array(
  35. 'id' => static::$idAttribute,
  36. ),
  37. $childMapping
  38. );
  39. }
  40. /**
  41. * Format or do other things before saving
  42. *
  43. * @param array $data
  44. *
  45. * @return array
  46. */
  47. protected function preSave($data)
  48. {
  49. return $data;
  50. }
  51. /**
  52. * Hold all data from a model
  53. *
  54. * @var mixed
  55. */
  56. protected $data = array();
  57. /**
  58. * Constructor.
  59. *
  60. * @param array $data
  61. */
  62. protected function __construct($data)
  63. {
  64. $this->attributeDbAttributeMapping = $this->setupDbMapping();
  65. if(isset($data[static::$idAttribute])){
  66. $id = is_numeric($data[static::$idAttribute]) && strpos($data[static::$idAttribute], ',') === false
  67. ? intval($data[static::$idAttribute])
  68. : $data[static::$idAttribute];
  69. $this->setId($id);
  70. }
  71. }
  72. /**
  73. * Create a model from data
  74. *
  75. * @param array $data
  76. *
  77. * @return static|null The Model
  78. */
  79. public static function create($data)
  80. {
  81. if(count($data) > 0){
  82. return new static($data);
  83. }
  84. return null;
  85. }
  86. /**
  87. * Create a model collection from data
  88. *
  89. * @param array $multiData
  90. *
  91. * @return ModelCollection|static[]
  92. */
  93. public static function createMultiple($multiData = array())
  94. {
  95. $collection = new ModelCollection();
  96. foreach($multiData as $data){
  97. $model = static::create($data);
  98. if(!is_null($model)){
  99. if(is_null($model->getId())){
  100. $collection->add($model);
  101. }
  102. else{
  103. $collection->add($model, $model->getId());
  104. }
  105. }
  106. }
  107. return $collection;
  108. }
  109. /**
  110. * @see create
  111. *
  112. * @param array $data
  113. *
  114. * @return AbstractModel|null
  115. */
  116. public static function createAndSave($data)
  117. {
  118. $model = static::create($data);
  119. if(!is_null($model) && $model->save()){
  120. return $model;
  121. }
  122. return null;
  123. }
  124. /**
  125. * @see createMultiple
  126. *
  127. * @param array $multiData
  128. *
  129. * @return ModelCollection|static[]
  130. */
  131. public static function createMultipleAndSave($multiData = array())
  132. {
  133. $collection = new ModelCollection();
  134. foreach($multiData as $data){
  135. $model = static::createAndSave($data);
  136. if(!is_null($model)){
  137. $collection->add($model);
  138. }
  139. }
  140. return $collection;
  141. }
  142. /**
  143. * Create a model from mysqli result
  144. *
  145. * @param mysqli_result $result
  146. *
  147. * @return static|null
  148. */
  149. public static function createFromDbResult($result)
  150. {
  151. if($result->num_rows === 0){
  152. return null;
  153. }
  154. return static::create($result->fetch_assoc());
  155. }
  156. /**
  157. * Create a model collection from mysqli result
  158. *
  159. * @param mysqli_result $result
  160. *
  161. * @return ModelCollection|static[]
  162. */
  163. public static function createMultipleFromDbResult($result)
  164. {
  165. $rows = array();
  166. while($row = $result->fetch_assoc()) {
  167. $rows[] = $row;
  168. }
  169. return static::createMultiple($rows);
  170. }
  171. /**
  172. * @param string $attribute
  173. * @param mixed $value
  174. */
  175. public function setAttribute($attribute, $value)
  176. {
  177. $this->data[$attribute] = $value;
  178. }
  179. /**
  180. * @param string $attribute
  181. *
  182. * @return mixed|null
  183. */
  184. public function getAttribute($attribute)
  185. {
  186. if(isset($this->data[$attribute])){
  187. if(is_array($this->data[$attribute])){
  188. return array_map('strip_tags', $this->data[$attribute]);
  189. }
  190. else{
  191. return strip_tags($this->data[$attribute]);
  192. }
  193. }
  194. return null;
  195. }
  196. /**
  197. * @return mixed
  198. */
  199. public function getId()
  200. {
  201. return $this->getAttribute('id');
  202. }
  203. /**
  204. * @param mixed $value
  205. */
  206. protected function setId($value)
  207. {
  208. $this->setAttribute('id', $value);
  209. }
  210. /**
  211. * @param array $attributes
  212. *
  213. * @return string
  214. */
  215. protected static function sqlHelperAttributeList($attributes)
  216. {
  217. $sql = "%s";
  218. $values = array();
  219. $keywords = array('AS', 'ASC', 'DESC');
  220. foreach($attributes as $val){
  221. if(!is_array($val)){
  222. // raw
  223. $values[] = $val;
  224. continue;
  225. }
  226. switch(count($val)){
  227. case 1:
  228. $values[] = "`{$val[0]}`";
  229. break;
  230. case 2:
  231. if(in_array(strtoupper($val[1]), $keywords)){
  232. $values[] = "`{$val[0]}` {$val[1]}";
  233. }
  234. else{
  235. $values[] = "`{$val[0]}`.`{$val[1]}`";
  236. }
  237. break;
  238. case 3:
  239. if(in_array(strtoupper($val[1]), $keywords)){
  240. $values[] = "`{$val[0]}` {$val[1]} `{$val[2]}`";
  241. }
  242. elseif(in_array(strtoupper($val[2]), $keywords)){
  243. $values[] = "`{$val[0]}`.`{$val[1]}` {$val[2]}";
  244. }
  245. break;
  246. case 4:
  247. if(in_array(strtoupper($val[1]), $keywords)){
  248. $values[] = "`{$val[0]}` {$val[1]} `{$val[2]}`.`{$val[3]}`";
  249. }
  250. elseif(in_array(strtoupper($val[2]), $keywords)){
  251. $values[] = "`{$val[0]}`.`{$val[1]}` {$val[2]} `{$val[3]}`";
  252. }
  253. else{
  254. $values[] = "`{$val[0]}`.`{$val[1]}` `{$val[2]}`.`{$val[3]}`";
  255. }
  256. break;
  257. case 5:
  258. if(in_array(strtoupper($val[2]), $keywords)){
  259. $values[] = "`{$val[0]}`.`{$val[1]}` {$val[2]} `{$val[3]}`.`{$val[4]}`";
  260. }
  261. break;
  262. }
  263. }
  264. return sprintf($sql, implode(', ', $values));
  265. }
  266. /**
  267. * @param mixed $value
  268. *
  269. * @return string
  270. */
  271. protected static function sqlHelperValue($value)
  272. {
  273. global $db;
  274. if(is_null($value) || (is_string($value) && strtoupper($value) === 'NULL')){
  275. return "NULL";
  276. }
  277. elseif(is_array($value)){
  278. return static::sqlHelperValueList($value);
  279. }
  280. return "'{$db->escape_string($value)}'";
  281. }
  282. /**
  283. * @param array $values
  284. *
  285. * @return string
  286. */
  287. protected static function sqlHelperValueList($values)
  288. {
  289. $sql = "(%s)";
  290. $sqlValues = array();
  291. foreach($values as $val){
  292. $sqlValues[] = static::sqlHelperValue($val);
  293. }
  294. return sprintf($sql, implode(', ', $sqlValues));
  295. }
  296. /**
  297. * @param array $conditions
  298. * array('attr', '=', '3') => "`attr` = '3'"
  299. * array(
  300. * array('`attr` = '3') (raw SQL) => `attr` = '3'
  301. * array('attr', 3) => `attr` = '3'
  302. * array('attr', '=', '3') => `attr` = '3'
  303. * array('attr', '<=', 3) => `attr` <= '3'
  304. * array('attr', 'LIKE', '%asd') => `attr` LIKE '%asd'
  305. * array('attr', 'IS', null) => `attr` IS NULL
  306. * array('attr', 'IS NOT', null) => `attr` IS NOT NULL
  307. * )
  308. * @param string $conditionConnector AND, OR
  309. *
  310. * @return string
  311. */
  312. protected static function sqlHelperConditionList($conditions, $conditionConnector = 'AND')
  313. {
  314. $values = array();
  315. // detect non nested array
  316. if(count($conditions) > 0 && !is_array($conditions[0])){
  317. $conditions = array($conditions);
  318. }
  319. $conditionConnector = strtoupper($conditionConnector);
  320. if(in_array($conditionConnector, array('AND', 'OR'))){
  321. $conditionConnector = " ".$conditionConnector;
  322. }
  323. $sql = "`%s` %s %s";
  324. foreach($conditions as $val){
  325. switch(count($val)){
  326. case 1:
  327. // raw
  328. $values[] = $val;
  329. break;
  330. case 2:
  331. $v = static::sqlHelperValue($val[1]);
  332. $values[] = sprintf($sql, $val[0], "=", $v);
  333. break;
  334. case 3:
  335. $v = static::sqlHelperValue($val[2]);
  336. $values[] = sprintf($sql, $val[0], strtoupper($val[1]), $v);
  337. break;
  338. }
  339. }
  340. return implode($conditionConnector." ", $values);
  341. }
  342. /**
  343. * @param array $conditions
  344. * @param string $conditionConnector AND, OR
  345. *
  346. * @return string
  347. */
  348. protected static function sqlHelperWhere($conditions, $conditionConnector = 'AND')
  349. {
  350. if(count($conditions) > 0){
  351. $sql = " WHERE %s";
  352. return sprintf($sql, static::sqlHelperConditionList($conditions, $conditionConnector));
  353. }
  354. return "";
  355. }
  356. /**
  357. * @param array|null $orderBy Examples below:
  358. * null => ""
  359. * array() => ""
  360. * array('attr1' => 'asc', 'attr2' => 'desc') => " ORDER BY `attr1` ASC, `attr2` DESC "
  361. * array('attr1') => " ORDER BY `attr1` ASC "
  362. *
  363. * @return string
  364. */
  365. protected static function sqlHelperOrderBy($orderBy = null)
  366. {
  367. if(!is_null($orderBy) && count($orderBy) > 0){
  368. $sql = " ORDER BY %s";
  369. $values = array();
  370. foreach($orderBy as $key => $val){
  371. if(is_int($key)){
  372. $values[] = array($val);
  373. }
  374. else{
  375. $values[] = array($key, strtoupper($val));
  376. }
  377. }
  378. return sprintf($sql, static::sqlHelperAttributeList($values));
  379. }
  380. return "";
  381. }
  382. /**
  383. * @param int|array $limit
  384. * 0 => ""
  385. * 3 => " LIMIT 3 "
  386. * array(3, 4) => " LIMIT 3,4 "
  387. *
  388. * @return string
  389. */
  390. protected static function sqlHelperLimit($limit = 0)
  391. {
  392. $sql = " LIMIT %s";
  393. if(is_string($limit) || (is_int($limit) && $limit > 0)){
  394. return sprintf($sql, $limit);
  395. }
  396. elseif(is_array($limit) && count($limit) == 2){
  397. return sprintf($sql, $limit[0].",".$limit[1]);
  398. }
  399. return "";
  400. }
  401. /**
  402. * Find all models by raw sql
  403. *
  404. * @param $sql
  405. * @param null|string $useSpecificModel
  406. *
  407. * @return ModelCollection|static[]
  408. */
  409. public static function findAllRaw($sql, $useSpecificModel = null)
  410. {
  411. global $db;
  412. if(!$result = $db->query($sql)){
  413. dbError($db->error, $sql);
  414. }
  415. if(is_null($useSpecificModel)){
  416. return static::createMultipleFromDbResult($result);
  417. }
  418. elseif(class_exists($useSpecificModel)){
  419. return call_user_func_array(array($useSpecificModel, 'createMultipleFromDbResult'), array($result));
  420. }
  421. return new ModelCollection();
  422. }
  423. /**
  424. * Find a model by raw sql
  425. *
  426. * @param $sql
  427. * @param null|string $useSpecificModel
  428. *
  429. * @return AbstractModel
  430. */
  431. public static function findRaw($sql, $useSpecificModel = null)
  432. {
  433. global $db;
  434. if(!$result = $db->query($sql)){
  435. dbError($db->error, $sql);
  436. }
  437. if(is_null($useSpecificModel)){
  438. return static::createFromDbResult($result);
  439. }
  440. elseif(class_exists($useSpecificModel)){
  441. return call_user_func_array(array($useSpecificModel, 'createFromDbResult'), array($result));
  442. }
  443. return null;
  444. }
  445. /**
  446. * Find all models
  447. *
  448. * @param array|null $orderBy see sqlHelperOrderBy
  449. *
  450. * @return ModelCollection|static[]
  451. */
  452. public static function findAll($orderBy = null)
  453. {
  454. $sql = "SELECT * FROM `".static::$table."`"
  455. .static::sqlHelperOrderBy($orderBy);
  456. return static::findAllRaw($sql);
  457. }
  458. /**
  459. * Find models by a condition
  460. *
  461. * @param array $conditions see sqlHelperConditionArray
  462. * @param string $conditionConnector see sqlHelperConditionArray
  463. * @param array|null $orderBy
  464. * @param int $limit see sqlHelperLimit
  465. *
  466. * @return ModelCollection|static[]|AbstractModel|null
  467. */
  468. public static function findWhere($conditions = array(), $conditionConnector = 'AND', $orderBy = null, $limit = 0)
  469. {
  470. $sql = "SELECT * FROM `".static::$table."`"
  471. .static::sqlHelperWhere($conditions, $conditionConnector)
  472. .static::sqlHelperOrderBy($orderBy)
  473. .static::sqlHelperLimit($limit);
  474. if($limit === 1){
  475. return static::findRaw($sql);
  476. }
  477. return static::findAllRaw($sql);
  478. }
  479. /**
  480. * Find first model matching a condition
  481. *
  482. * @param array $conditions see sqlHelperConditionArray
  483. * @param string $conditionConnector see sqlHelperConditionArray
  484. * @param array|null $orderBy
  485. *
  486. * @return AbstractModel|null
  487. */
  488. public static function findWhereFirst($conditions = array(), $conditionConnector = 'AND', $orderBy = null)
  489. {
  490. return static::findWhere($conditions, $conditionConnector, $orderBy, 1);
  491. }
  492. /**
  493. * Find a model by id
  494. *
  495. * @param mixed $id
  496. *
  497. * @return AbstractModel|null
  498. */
  499. public static function find($id)
  500. {
  501. return static::findWhereFirst(array(static::$idAttribute, $id));
  502. }
  503. /**
  504. * Save model data to database
  505. *
  506. * @return bool
  507. */
  508. public function save()
  509. {
  510. global $db;
  511. $data = $this->preSave($this->data);
  512. if(is_null($this->getId())){
  513. // insert
  514. $attributes = array();
  515. $values = array();
  516. foreach($this->attributeDbAttributeMapping as $attribute => $sqlAttribute){
  517. if($sqlAttribute === static::$idAttribute){
  518. continue;
  519. }
  520. $attributes[] = array($sqlAttribute);
  521. $values[] = $data[$attribute];
  522. }
  523. $sql = "INSERT INTO `".static::$table."`"
  524. ." (".static::sqlHelperAttributeList($attributes).")"
  525. ." VALUES ".static::sqlHelperValueList($values);
  526. }
  527. else{
  528. // update
  529. $values = array();
  530. foreach($this->attributeDbAttributeMapping as $attribute => $sqlAttribute){
  531. if($sqlAttribute === static::$idAttribute){
  532. continue;
  533. }
  534. $values[] = array($sqlAttribute, '=', $data[$attribute]);
  535. }
  536. $sql = "UPDATE `".static::$table."`"
  537. ." SET ".static::sqlHelperConditionList($values, ',')
  538. .static::sqlHelperWhere(array(static::$idAttribute, $this->getId()));
  539. }
  540. if($stmt = $db->prepare($sql)){
  541. if($stmt->execute()){
  542. if(is_null($this->getId())){
  543. $this->setId(intval($db->insert_id));
  544. }
  545. return true;
  546. }
  547. else{
  548. dbError($db->error, $sql);
  549. }
  550. }
  551. return false;
  552. }
  553. /**
  554. * Delete model from database
  555. *
  556. * @return bool
  557. */
  558. public function delete()
  559. {
  560. global $db;
  561. if(!is_null($this->getId())){
  562. $sql = "DELETE FROM `".static::$table."`"
  563. .static::sqlHelperWhere(array(static::$idAttribute, $this->getId()));
  564. if($stmt = $db->prepare($sql)){
  565. if($stmt->execute()){
  566. return true;
  567. }
  568. else{
  569. dbError($db->error, $sql);
  570. }
  571. }
  572. }
  573. return false;
  574. }
  575. }