AbstractModel.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  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. return static::createMultiple($result->fetch_all(MYSQLI_ASSOC));
  166. }
  167. /**
  168. * @param string $attribute
  169. * @param mixed $value
  170. */
  171. public function setAttribute($attribute, $value)
  172. {
  173. $this->data[$attribute] = $value;
  174. }
  175. /**
  176. * @param string $attribute
  177. *
  178. * @return mixed|null
  179. */
  180. public function getAttribute($attribute)
  181. {
  182. if(isset($this->data[$attribute])){
  183. if(is_array($this->data[$attribute])){
  184. return array_map('strip_tags', $this->data[$attribute]);
  185. }
  186. else{
  187. return strip_tags($this->data[$attribute]);
  188. }
  189. }
  190. return null;
  191. }
  192. /**
  193. * @return mixed
  194. */
  195. public function getId()
  196. {
  197. return $this->getAttribute('id');
  198. }
  199. /**
  200. * @param mixed $value
  201. */
  202. protected function setId($value)
  203. {
  204. $this->setAttribute('id', $value);
  205. }
  206. /**
  207. * @param array $attributes
  208. *
  209. * @return string
  210. */
  211. protected static function sqlHelperAttributeList($attributes)
  212. {
  213. $sql = "%s";
  214. $values = array();
  215. $keywords = array('AS', 'ASC', 'DESC');
  216. foreach($attributes as $val){
  217. if(!is_array($val)){
  218. // raw
  219. $values[] = $val;
  220. continue;
  221. }
  222. switch(count($val)){
  223. case 1:
  224. $values[] = "`{$val[0]}`";
  225. break;
  226. case 2:
  227. if(in_array(strtoupper($val[1]), $keywords)){
  228. $values[] = "`{$val[0]}` {$val[1]}";
  229. }
  230. else{
  231. $values[] = "`{$val[0]}`.`{$val[1]}`";
  232. }
  233. break;
  234. case 3:
  235. if(in_array(strtoupper($val[1]), $keywords)){
  236. $values[] = "`{$val[0]}` {$val[1]} `{$val[2]}`";
  237. }
  238. elseif(in_array(strtoupper($val[2]), $keywords)){
  239. $values[] = "`{$val[0]}`.`{$val[1]}` {$val[2]}";
  240. }
  241. break;
  242. case 4:
  243. if(in_array(strtoupper($val[1]), $keywords)){
  244. $values[] = "`{$val[0]}` {$val[1]} `{$val[2]}`.`{$val[3]}`";
  245. }
  246. elseif(in_array(strtoupper($val[2]), $keywords)){
  247. $values[] = "`{$val[0]}`.`{$val[1]}` {$val[2]} `{$val[3]}`";
  248. }
  249. else{
  250. $values[] = "`{$val[0]}`.`{$val[1]}` `{$val[2]}`.`{$val[3]}`";
  251. }
  252. break;
  253. case 5:
  254. if(in_array(strtoupper($val[2]), $keywords)){
  255. $values[] = "`{$val[0]}`.`{$val[1]}` {$val[2]} `{$val[3]}`.`{$val[4]}`";
  256. }
  257. break;
  258. }
  259. }
  260. return sprintf($sql, implode(', ', $values));
  261. }
  262. /**
  263. * @param mixed $value
  264. *
  265. * @return string
  266. */
  267. protected static function sqlHelperValue($value)
  268. {
  269. global $db;
  270. if(is_null($value) || (is_string($value) && strtoupper($value) === 'NULL')){
  271. return "NULL";
  272. }
  273. elseif(is_array($value)){
  274. return static::sqlHelperValueList($value);
  275. }
  276. return "'{$db->escape_string($value)}'";
  277. }
  278. /**
  279. * @param array $values
  280. *
  281. * @return string
  282. */
  283. protected static function sqlHelperValueList($values)
  284. {
  285. $sql = "(%s)";
  286. $sqlValues = array();
  287. foreach($values as $val){
  288. $sqlValues[] = static::sqlHelperValue($val);
  289. }
  290. return sprintf($sql, implode(', ', $sqlValues));
  291. }
  292. /**
  293. * @param array $conditions
  294. * array('attr', '=', '3') => "`attr` = '3'"
  295. * array(
  296. * array('`attr` = '3') (raw SQL) => `attr` = '3'
  297. * array('attr', 3) => `attr` = '3'
  298. * array('attr', '=', '3') => `attr` = '3'
  299. * array('attr', '<=', 3) => `attr` <= '3'
  300. * array('attr', 'LIKE', '%asd') => `attr` LIKE '%asd'
  301. * array('attr', 'IS', null) => `attr` IS NULL
  302. * array('attr', 'IS NOT', null) => `attr` IS NOT NULL
  303. * )
  304. * @param string $conditionConnector AND, OR
  305. *
  306. * @return string
  307. */
  308. protected static function sqlHelperConditionList($conditions, $conditionConnector = 'AND')
  309. {
  310. $values = array();
  311. // detect non nested array
  312. if(count($conditions) > 0 && !is_array($conditions[0])){
  313. $conditions = array($conditions);
  314. }
  315. $conditionConnector = strtoupper($conditionConnector);
  316. if(in_array($conditionConnector, array('AND', 'OR'))){
  317. $conditionConnector = " ".$conditionConnector;
  318. }
  319. $sql = "`%s` %s %s";
  320. foreach($conditions as $val){
  321. switch(count($val)){
  322. case 1:
  323. // raw
  324. $values[] = $val;
  325. break;
  326. case 2:
  327. $v = static::sqlHelperValue($val[1]);
  328. $values[] = sprintf($sql, $val[0], "=", $v);
  329. break;
  330. case 3:
  331. $v = static::sqlHelperValue($val[2]);
  332. $values[] = sprintf($sql, $val[0], strtoupper($val[1]), $v);
  333. break;
  334. }
  335. }
  336. return implode($conditionConnector." ", $values);
  337. }
  338. /**
  339. * @param array $conditions
  340. * @param string $conditionConnector AND, OR
  341. *
  342. * @return string
  343. */
  344. protected static function sqlHelperWhere($conditions, $conditionConnector = 'AND')
  345. {
  346. if(count($conditions) > 0){
  347. $sql = " WHERE %s";
  348. return sprintf($sql, static::sqlHelperConditionList($conditions, $conditionConnector));
  349. }
  350. return "";
  351. }
  352. /**
  353. * @param array|null $orderBy Examples below:
  354. * null => ""
  355. * array() => ""
  356. * array('attr1' => 'asc', 'attr2' => 'desc') => " ORDER BY `attr1` ASC, `attr2` DESC "
  357. * array('attr1') => " ORDER BY `attr1` ASC "
  358. *
  359. * @return string
  360. */
  361. protected static function sqlHelperOrderBy($orderBy = null)
  362. {
  363. if(!is_null($orderBy) && count($orderBy) > 0){
  364. $sql = " ORDER BY %s";
  365. $values = array();
  366. foreach($orderBy as $key => $val){
  367. if(is_int($key)){
  368. $values[] = array($val);
  369. }
  370. else{
  371. $values[] = array($key, strtoupper($val));
  372. }
  373. }
  374. return sprintf($sql, static::sqlHelperAttributeList($values));
  375. }
  376. return "";
  377. }
  378. /**
  379. * @param int|array $limit
  380. * 0 => ""
  381. * 3 => " LIMIT 3 "
  382. * array(3, 4) => " LIMIT 3,4 "
  383. *
  384. * @return string
  385. */
  386. protected static function sqlHelperLimit($limit = 0)
  387. {
  388. $sql = " LIMIT %s";
  389. if(is_string($limit) || (is_int($limit) && $limit > 0)){
  390. return sprintf($sql, $limit);
  391. }
  392. elseif(is_array($limit) && count($limit) == 2){
  393. return sprintf($sql, $limit[0].",".$limit[1]);
  394. }
  395. return "";
  396. }
  397. /**
  398. * Find all models by raw sql
  399. *
  400. * @param $sql
  401. * @param null|string $useSpecificModel
  402. *
  403. * @return ModelCollection|static[]
  404. */
  405. public static function findAllRaw($sql, $useSpecificModel = null)
  406. {
  407. global $db;
  408. if(!$result = $db->query($sql)){
  409. dbError($db->error, $sql);
  410. }
  411. if(is_null($useSpecificModel)){
  412. return static::createMultipleFromDbResult($result);
  413. }
  414. elseif(class_exists($useSpecificModel)){
  415. return call_user_func_array(array($useSpecificModel, 'createMultipleFromDbResult'), array($result));
  416. }
  417. return new ModelCollection();
  418. }
  419. /**
  420. * Find a model by raw sql
  421. *
  422. * @param $sql
  423. * @param null|string $useSpecificModel
  424. *
  425. * @return AbstractModel
  426. */
  427. public static function findRaw($sql, $useSpecificModel = null)
  428. {
  429. global $db;
  430. if(!$result = $db->query($sql)){
  431. dbError($db->error, $sql);
  432. }
  433. if(is_null($useSpecificModel)){
  434. return static::createFromDbResult($result);
  435. }
  436. elseif(class_exists($useSpecificModel)){
  437. return call_user_func_array(array($useSpecificModel, 'createFromDbResult'), array($result));
  438. }
  439. return null;
  440. }
  441. /**
  442. * Find all models
  443. *
  444. * @param array|null $orderBy see sqlHelperOrderBy
  445. *
  446. * @return ModelCollection|static[]
  447. */
  448. public static function findAll($orderBy = null)
  449. {
  450. $sql = "SELECT * FROM `".static::$table."`"
  451. .static::sqlHelperOrderBy($orderBy);
  452. return static::findAllRaw($sql);
  453. }
  454. /**
  455. * Find models by a condition
  456. *
  457. * @param array $conditions see sqlHelperConditionArray
  458. * @param string $conditionConnector see sqlHelperConditionArray
  459. * @param array|null $orderBy
  460. * @param int $limit see sqlHelperLimit
  461. *
  462. * @return ModelCollection|static[]|AbstractModel|null
  463. */
  464. public static function findWhere($conditions = array(), $conditionConnector = 'AND', $orderBy = null, $limit = 0)
  465. {
  466. $sql = "SELECT * FROM `".static::$table."`"
  467. .static::sqlHelperWhere($conditions, $conditionConnector)
  468. .static::sqlHelperOrderBy($orderBy)
  469. .static::sqlHelperLimit($limit);
  470. if($limit === 1){
  471. return static::findRaw($sql);
  472. }
  473. return static::findAllRaw($sql);
  474. }
  475. /**
  476. * Find first model matching a condition
  477. *
  478. * @param array $conditions see sqlHelperConditionArray
  479. * @param string $conditionConnector see sqlHelperConditionArray
  480. * @param array|null $orderBy
  481. *
  482. * @return AbstractModel|null
  483. */
  484. public static function findWhereFirst($conditions = array(), $conditionConnector = 'AND', $orderBy = null)
  485. {
  486. return static::findWhere($conditions, $conditionConnector, $orderBy, 1);
  487. }
  488. /**
  489. * Find a model by id
  490. *
  491. * @param mixed $id
  492. *
  493. * @return AbstractModel|null
  494. */
  495. public static function find($id)
  496. {
  497. return static::findWhereFirst(array(static::$idAttribute, $id));
  498. }
  499. /**
  500. * Save model data to database
  501. *
  502. * @return bool
  503. */
  504. public function save()
  505. {
  506. global $db;
  507. $data = $this->preSave($this->data);
  508. if(is_null($this->getId())){
  509. // insert
  510. $attributes = array();
  511. $values = array();
  512. foreach($this->attributeDbAttributeMapping as $attribute => $sqlAttribute){
  513. if($sqlAttribute === static::$idAttribute){
  514. continue;
  515. }
  516. $attributes[] = array($sqlAttribute);
  517. $values[] = $data[$attribute];
  518. }
  519. $sql = "INSERT INTO `".static::$table."`"
  520. ." (".static::sqlHelperAttributeList($attributes).")"
  521. ." VALUES ".static::sqlHelperValueList($values);
  522. }
  523. else{
  524. // update
  525. $values = array();
  526. foreach($this->attributeDbAttributeMapping as $attribute => $sqlAttribute){
  527. if($sqlAttribute === static::$idAttribute){
  528. continue;
  529. }
  530. $values[] = array($sqlAttribute, '=', $data[$attribute]);
  531. }
  532. $sql = "UPDATE `".static::$table."`"
  533. ." SET ".static::sqlHelperConditionList($values, ',')
  534. .static::sqlHelperWhere(array(static::$idAttribute, $this->getId()));
  535. }
  536. if($stmt = $db->prepare($sql)){
  537. if($stmt->execute()){
  538. if(is_null($this->getId())){
  539. $this->setId(intval($db->insert_id));
  540. }
  541. return true;
  542. }
  543. else{
  544. dbError($db->error, $sql);
  545. }
  546. }
  547. return false;
  548. }
  549. /**
  550. * Delete model from database
  551. *
  552. * @return bool
  553. */
  554. public function delete()
  555. {
  556. global $db;
  557. if(!is_null($this->getId())){
  558. $sql = "DELETE FROM `".static::$table."`"
  559. .static::sqlHelperWhere(array(static::$idAttribute, $this->getId()));
  560. if($stmt = $db->prepare($sql)){
  561. if($stmt->execute()){
  562. return true;
  563. }
  564. else{
  565. dbError($db->error, $sql);
  566. }
  567. }
  568. }
  569. return false;
  570. }
  571. }