You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

376 lines
13 KiB

  1. <?php
  2. /**
  3. * Класс модели данных
  4. *
  5. * @copyright NetMonsters <team@netmonsters.ru>
  6. * @link http://netmonsters.ru
  7. * @package Majestic
  8. * @subpackage Model
  9. * @since 2011-11-11
  10. */
  11. /**
  12. * @property SqlDbDriver $db
  13. */
  14. abstract class SqlModel extends Model
  15. {
  16. const SELECT = 10;
  17. const DISTINCT = 11;
  18. const WHERE = 12;
  19. const ORDER = 13;
  20. const LIMIT = 15;
  21. const OFFSET = 16;
  22. const ORDER_DESC = 20;
  23. const ORDER_ASC = 21;
  24. const ASSOC_BY_FIELD = 30;
  25. const ASSOC_AS_ARRAY = 31;
  26. const ASSOC_BY_FIELD_IN_ARRAY = 32;
  27. protected static $criteria_data_keys = array(
  28. self::SELECT,
  29. self::DISTINCT,
  30. self::WHERE,
  31. self::ORDER,
  32. self::LIMIT,
  33. /** self::OFFSET // that key no to be a process in foreach criteria_data @see SqlModel::find() */
  34. );
  35. /**
  36. * @param string $ident
  37. * @return string Quoted identifier.
  38. */
  39. public function identify($ident)
  40. {
  41. return $this->db->quoteIdentifier($ident);
  42. }
  43. /**
  44. * @param mixed $value
  45. * @return string Quoted value.
  46. */
  47. public function quote($value)
  48. {
  49. return $this->db->quote($value);
  50. }
  51. /**
  52. * @param int $id
  53. * @return object
  54. */
  55. public function get($id)
  56. {
  57. $sql = 'SELECT * FROM :table WHERE :pk=?';
  58. return $this->fetch($sql, $id);
  59. }
  60. /**
  61. * @param array $data
  62. * @param mixed $where
  63. * @return int Number of affected rows
  64. */
  65. public function update($data, $where)
  66. {
  67. if (is_int($where) || $where === (string) (int) $where) {
  68. $where = $this->identify($this->key) . '=' . (int) $where;
  69. }
  70. return parent::update($data, $where);
  71. }
  72. /**
  73. * @param int $id Int id
  74. * @return int Number of affected rows
  75. */
  76. public function delete($id)
  77. {
  78. $where = $this->identify($this->key) . '=' . (int) $id;
  79. return $this->db->delete($this->table(), $where);
  80. }
  81. /**
  82. * Creates order sql string
  83. *
  84. * @param array $params
  85. * @param array $sortable
  86. * @return string
  87. */
  88. protected function order($params, $sortable = array('id'))
  89. {
  90. $sql = '';
  91. if (isset($params['sort'])) {
  92. $order = (isset($params['order']) && $params['order'] == 'desc') ? 'DESC' : 'ASC';
  93. if (in_array($params['sort'], $sortable)) {
  94. $sql = ' ORDER BY ' . $this->identify($params['sort']) . ' ' . $order;
  95. }
  96. }
  97. return $sql;
  98. }
  99. /**
  100. * Searches using like
  101. *
  102. * @param array $params
  103. * @param array $searchable
  104. * @param string $table_prefix
  105. * @return string
  106. */
  107. protected function search($params, $searchable = array('id'), $table_prefix = '')
  108. {
  109. $sql = '';
  110. if (isset($params['q']) && isset($params['qt']) && in_array($params['qt'], $searchable)) {
  111. if ($table_prefix) {
  112. $sql = $table_prefix . '.';
  113. }
  114. $sql .= $this->identify($params['qt']) . ' LIKE ' . $this->quote('%' . $params['q'] . '%');
  115. }
  116. return $sql;
  117. }
  118. /**
  119. * This method appends to params table and primary key.
  120. * So they can be accessed through `:table` and `:pk` placeholders.
  121. *
  122. * @param string $sql
  123. * @param array $params
  124. * @return DbStatement
  125. */
  126. protected function query($sql, $params = array())
  127. {
  128. if (!is_array($params)) {
  129. $params = array($params);
  130. }
  131. $params = array(
  132. 'table' => new DbExpr($this->identify($this->table())),
  133. 'pk' => new DbExpr($this->identify($this->key)),
  134. ) + $params;
  135. return $this->db->query($sql, $params);
  136. }
  137. /**
  138. * @param string $data Request
  139. * @param array $params Request parameters
  140. * @param string $field Requested field name
  141. * @param CacheKey $cache_key Key for caching in
  142. * @return mixed
  143. */
  144. protected function fetchField($data, $params = array(), $field, $cache_key = null)
  145. {
  146. if (!$cache_key || !$result = $cache_key->get()) {
  147. $result = $this->query($data, $params)->fetchField($field);
  148. if ($cache_key) {
  149. $cache_key->set($result);
  150. }
  151. }
  152. return $result;
  153. }
  154. /**
  155. * @param string $data Request
  156. * @param array $params Request parameters
  157. * @param CacheKey $cache_key Key for caching in
  158. * @return mixed
  159. */
  160. protected function fetch($data, $params = array(), $cache_key = null)
  161. {
  162. if (!$cache_key || !$result = $cache_key->get()) {
  163. $result = $this->query($data, $params)->fetch();
  164. if ($cache_key) {
  165. $cache_key->set($result);
  166. }
  167. }
  168. return $result;
  169. }
  170. /**
  171. * @param string $data
  172. * @param array $params
  173. * @param CacheKey $cache_key
  174. * @return array
  175. */
  176. protected function fetchAll($data, $params = array(), $cache_key = null)
  177. {
  178. if (!$cache_key || !$result = $cache_key->get()) {
  179. $result = $this->query($data, $params)->fetchAll();
  180. if ($cache_key) {
  181. $cache_key->set($result);
  182. }
  183. }
  184. return $result;
  185. }
  186. /**
  187. * @param $criteria SqlCriteria
  188. * @return string Sql string expression
  189. */
  190. protected function criteria($criteria)
  191. {
  192. if (!is_array($criteria->select)) {
  193. $criteria->select = array(new DbExpr($criteria->select));
  194. }
  195. foreach ($criteria->select as $field => &$term) {
  196. if (is_int($field)) {
  197. if ($term instanceof DbExpr) {
  198. $term = (string) $term;
  199. } else {
  200. $term = $this->db->quoteIdentifier($term);
  201. }
  202. } else {
  203. $term = $this->db->quoteIdentifier($field) . ' as ' . $this->db->quoteIdentifier($term);
  204. }
  205. }
  206. foreach ($criteria->where as $cond => &$term) {
  207. if (is_int($cond)) {
  208. if ($term instanceof DbExpr) {
  209. $term = (string) $term;
  210. }
  211. } else {
  212. if (is_array($term)) {
  213. $term = new DbExpr('(' . implode(',', array_map(function($item){return intval($item);}, $term)) . ')');
  214. }
  215. $term = $this->db->quoteInto($cond, $term);
  216. }
  217. }
  218. foreach ($criteria->order as $field => $term) {
  219. if (is_int($field)) {
  220. $term = $this->db->quoteIdentifier($term);
  221. } else {
  222. $order_direction = '';
  223. switch ($term) {
  224. case self::ORDER_DESC:
  225. $order_direction .= ' DESC';
  226. break;
  227. case self::ORDER_ASC:
  228. $order_direction .= ' ASC';
  229. break;
  230. }
  231. $term = $this->db->quoteIdentifier($field) . $order_direction;
  232. }
  233. }
  234. if ($criteria->distinct != '') {
  235. $criteria->distinct = 'DISTINCT ' . $this->db->quoteIdentifier($criteria->distinct);
  236. }
  237. if ($criteria->limit != '') {
  238. $criteria->limit = implode(',',array_map(function($item){return intval($item);},explode(',',$criteria->limit)));
  239. }
  240. return 'SELECT ' . (($criteria->distinct) ? $criteria->distinct : implode(',', $criteria->select))
  241. . ' FROM ' . $this->table()
  242. . (($criteria->where) ? (' WHERE ' . implode(' AND ', $criteria->where)) : '')
  243. . (($criteria->order) ? (' ORDER BY ' . implode(',', $criteria->order)) : '')
  244. . (($criteria->limit) ? (' LIMIT ' . $criteria->limit) : '');
  245. }
  246. /**
  247. * @param $criteria_data array
  248. * @ex array(
  249. * SqlModel::SELECT => '*',
  250. * SqlModel::WHERE => array('field=?', 1),
  251. * SqlModel::ORDER => array('last_name', 'first_name' => SqlModel::ORDER_DESC),
  252. * SqlModel::LIMIT => 10)
  253. * @return mixed|DbStatement[]
  254. */
  255. public function find($criteria_data)
  256. {
  257. $criteria = new SqlCriteria();
  258. foreach (self::$criteria_data_keys as $criteria_data_key) {
  259. if (isset($criteria_data[$criteria_data_key])) {
  260. $value = $criteria_data[$criteria_data_key];
  261. switch ($criteria_data_key) {
  262. case self::SELECT:
  263. $criteria->select($value);
  264. break;
  265. case self::DISTINCT:
  266. $criteria->distinct($value);
  267. break;
  268. case self::WHERE:
  269. $criteria->where($value);
  270. break;
  271. case self::ORDER:
  272. $criteria->order($value);
  273. break;
  274. case self::LIMIT:
  275. $criteria->limit($value, ((isset($criteria_data[self::OFFSET])) ? $criteria_data[self::OFFSET] : null));
  276. break;
  277. }
  278. }
  279. }
  280. $data = $this->criteria($criteria);
  281. if (isset($criteria_data[self::ASSOC_BY_FIELD])) {
  282. return self::fetchAllAssocByField($data, array(), $criteria_data[self::ASSOC_BY_FIELD], ((isset($criteria_data[self::ASSOC_AS_ARRAY])) ? $criteria_data[self::ASSOC_AS_ARRAY] : false), ((isset($criteria_data[self::ASSOC_BY_FIELD_IN_ARRAY])) ? $criteria_data[self::ASSOC_BY_FIELD_IN_ARRAY] : null));
  283. } else {
  284. return self::fetchAll($data);
  285. }
  286. }
  287. /**
  288. * @param $data string
  289. * @param $params array
  290. * @param null|string $field
  291. * @param bool $assoc_as_array
  292. * @param null|string $field_assoc_in_array
  293. * @return array
  294. * @throws ErrorException|GeneralException
  295. */
  296. protected function fetchAllAssocByField($data, $params, $field = null, $assoc_as_array = false, $field_assoc_in_array = null)
  297. {
  298. $items = self::fetchAll($data, $params);
  299. if (is_null($field)) {
  300. $field = $this->key;
  301. }
  302. return self::assocByField($items, $field, $assoc_as_array, $field_assoc_in_array);
  303. }
  304. /**
  305. * @param $items
  306. * @param $field
  307. * @param bool $assoc_as_array
  308. * @param $field_assoc_in_array null|string
  309. * @param $disable_assoc_in_array bool
  310. * @return array
  311. * @throws ErrorException|GeneralException
  312. */
  313. public function assocByField($items, $field, $assoc_as_array = false, $field_assoc_in_array = null, $disable_assoc_in_array = false)
  314. {
  315. $items_assoc_by_field = array();
  316. $primary_key_not_exists_in_items = false;
  317. foreach ($items as $item) {
  318. if (!isset($item->{$field})) {
  319. throw new GeneralException('Undefined field. ' . $field);
  320. }
  321. if ($assoc_as_array) {
  322. if (!isset($items_assoc_by_field[$item->{$field}])) {
  323. $items_assoc_by_field[$item->{$field}] = array();
  324. }
  325. $items_assoc_by_field[$item->{$field}][] = $item;
  326. if (!$primary_key_not_exists_in_items && $this->key && !isset($item->{$this->key})) {
  327. $primary_key_not_exists_in_items = true;
  328. }
  329. } else {
  330. if (isset($items_assoc_by_field[$item->{$field}])) {
  331. throw new ErrorException('Field not unique. May be use assoc_as_array. ' . $field);
  332. }
  333. $items_assoc_by_field[$item->{$field}] = $item;
  334. }
  335. }
  336. // Ассоциирование внутри каждого элемента массива по первичному ключу
  337. // Если первичного ключа нет (не указан в модели или нет поля с первичным ключом в результах выборки) - ассоциирование внутри не производится
  338. // Если указан $field_assoc_in_array - ассоциирование произовдится по этому полю (вернет ошибку, если этого поля нет в результатх выборки)
  339. // Параметр $disable_assoc_in_array отключает ассоциирование внутри результирующего массива
  340. if ($assoc_as_array && !$disable_assoc_in_array &&
  341. (($this->key && !$primary_key_not_exists_in_items) || $field_assoc_in_array)
  342. ) {
  343. foreach ($items_assoc_by_field as $key => $value) {
  344. $items_assoc_by_field[$key] = self::assocByField($items_assoc_by_field[$key], (($field_assoc_in_array) ? $field_assoc_in_array : $this->key));
  345. }
  346. }
  347. return $items_assoc_by_field;
  348. }
  349. public function assocByFieldAndGetKeys($items, $field)
  350. {
  351. $items = self::assocByField($items, $field, true, null, true);
  352. return array_keys($items);
  353. }
  354. }