PageRenderTime 48ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/wire/core/WireSaveableItems.php

https://bitbucket.org/webbear/processwire-base-installation
PHP | 405 lines | 181 code | 70 blank | 154 comment | 36 complexity | 63cb67a38228de8c8e1b75a448e14457 MD5 | raw file
  1. <?php
  2. /**
  3. * ProcessWire WireSaveableItems
  4. *
  5. * Wire Data Access Object, provides reusable capability for loading, saving, creating, deleting,
  6. * and finding items of descending class-defined types.
  7. *
  8. * ProcessWire 2.x
  9. * Copyright (C) 2013 by Ryan Cramer
  10. * Licensed under GNU/GPL v2, see LICENSE.TXT
  11. *
  12. * http://processwire.com
  13. *
  14. */
  15. abstract class WireSaveableItems extends Wire implements IteratorAggregate {
  16. /**
  17. * Return the WireArray that this DAO stores it's items in
  18. *
  19. */
  20. abstract public function getAll();
  21. /**
  22. * Return a new blank item
  23. *
  24. */
  25. abstract public function makeBlankItem();
  26. /**
  27. * Return the name of the table that this DAO stores item records in
  28. *
  29. */
  30. abstract public function getTable();
  31. /**
  32. * Return the default name of the field that load() should sort by (default is none)
  33. *
  34. * This is overridden by selectors if applied during the load method
  35. *
  36. */
  37. public function getSort() { return ''; }
  38. /**
  39. * Provides additions to the ___load query for when selectors or selector string are provided
  40. *
  41. */
  42. protected function getLoadQuerySelectors($selectors, DatabaseQuerySelect $query) {
  43. $database = $this->wire('database');
  44. if(is_object($selectors) && $selectors instanceof Selectors) {
  45. // iterable selectors
  46. } else if($selectors && is_string($selectors)) {
  47. // selector string, convert to iterable selectors
  48. $selectors = new Selectors($selectors);
  49. } else {
  50. // nothing provided, load all assumed
  51. return $query;
  52. }
  53. $functionFields = array(
  54. 'sort' => '',
  55. 'limit' => '',
  56. 'start' => '',
  57. );
  58. foreach($selectors as $selector) {
  59. if(!$database->isOperator($selector->operator))
  60. throw new WireException("Operator '{$selector->operator}' may not be used in {$this->className}::load()");
  61. if(in_array($selector->field, $functionFields)) {
  62. $functionFields[$selector->field] = $selector->value;
  63. continue;
  64. }
  65. if(!in_array($selector->field, $fields))
  66. throw new WireException("Field '{$selector->field}' is not valid for {$this->className}::load()");
  67. $selectorField = $database->escapeTableCol($selector->field);
  68. $value = $database->escapeStr($selector->value);
  69. $query->where("{$selectorField}{$selector->operator}'$value'"); // QA
  70. }
  71. if($functionFields['sort'] && in_array($functionFields['sort'], $fields)) $query->orderby("$functionFields[sort]");
  72. if($functionFields['limit']) $query->limit(($functionFields['start'] ? ((int) $functionFields['start']) . "," : '') . $functionFields['limit']);
  73. return $query;
  74. }
  75. /**
  76. * Get the DatabaseQuerySelect to perform the load operation of items
  77. *
  78. * @param Selectors|string|null $selectors Selectors or a selector string to find, or NULL to load all.
  79. * @return DatabaseQuerySelect
  80. *
  81. */
  82. protected function getLoadQuery($selectors = null) {
  83. $item = $this->makeBlankItem();
  84. $fields = array_keys($item->getTableData());
  85. $database = $this->wire('database');
  86. $table = $database->escapeTable($this->getTable());
  87. foreach($fields as $k => $v) {
  88. $v = $database->escapeCol($v);
  89. $fields[$k] = "$table.$v";
  90. }
  91. $query = new DatabaseQuerySelect();
  92. $query->select($fields)->from($table);
  93. if($sort = $this->getSort()) $query->orderby($sort);
  94. $this->getLoadQuerySelectors($selectors, $query);
  95. return $query;
  96. }
  97. /**
  98. * Load items from the database table and return them in the same type class that getAll() returns
  99. * A selector string or Selectors may be provided so that this can be used as a find() by descending classes that don't load all items at once.
  100. *
  101. * @param WireArray $items
  102. * @param Selectors|string|null $selectors Selectors or a selector string to find, or NULL to load all.
  103. * @return WireArray Returns the same type as specified in the getAll() method.
  104. *
  105. */
  106. protected function ___load(WireArray $items, $selectors = null) {
  107. $database = $this->wire('database');
  108. $sql = $this->getLoadQuery($selectors)->getQuery();
  109. $query = $database->prepare($sql);
  110. $query->execute();
  111. while($row = $query->fetch(PDO::FETCH_ASSOC)) {
  112. $item = $this->makeBlankItem();
  113. foreach($row as $field => $value) {
  114. if($field == 'data') {
  115. if($value) $value = $this->decodeData($value);
  116. else continue;
  117. }
  118. $item->$field = $value;
  119. }
  120. $item->setTrackChanges(true);
  121. $items->add($item);
  122. }
  123. $query->closeCursor();
  124. $items->setTrackChanges(true);
  125. return $items;
  126. }
  127. /**
  128. * Should the given item key/field be saved in the database?
  129. *
  130. * Template method used by ___save()
  131. *
  132. */
  133. protected function saveItemKey($key) {
  134. if($key == 'id') return false;
  135. return true;
  136. }
  137. /**
  138. * Save the provided item to database
  139. *
  140. * @param Saveable $item The item to save
  141. * @return bool Returns true on success, false on failure
  142. * @throws WireException
  143. *
  144. */
  145. public function ___save(Saveable $item) {
  146. $blank = $this->makeBlankItem();
  147. if(!$item instanceof $blank) throw new WireException("WireSaveableItems::save(item) requires item to be of type '" . $blank->className() . "'");
  148. $database = $this->wire('database');
  149. $table = $database->escapeTable($this->getTable());
  150. $sql = "`$table` SET ";
  151. $id = (int) $item->id;
  152. $this->saveReady($item);
  153. $data = $item->getTableData();
  154. foreach($data as $key => $value) {
  155. if(!$this->saveItemKey($key)) continue;
  156. if($key == 'data') {
  157. if(is_array($value)) {
  158. $value = $this->encodeData($value);
  159. } else $value = '';
  160. }
  161. $key = $database->escapeTableCol($key);
  162. $value = $database->escapeStr("$value");
  163. $sql .= "`$key`='$value', ";
  164. }
  165. $sql = rtrim($sql, ", ");
  166. if($id) {
  167. $query = $database->prepare("UPDATE $sql WHERE id=:id");
  168. $query->bindValue(":id", $id, PDO::PARAM_INT);
  169. $result = $query->execute();
  170. } else {
  171. $query = $database->prepare("INSERT INTO $sql");
  172. $result = $query->execute();
  173. if($result) {
  174. $item->id = $database->lastInsertId();
  175. $this->getAll()->add($item);
  176. $this->added($item);
  177. }
  178. }
  179. if($result) {
  180. $this->saved($item);
  181. $this->resetTrackChanges();
  182. }
  183. return $result;
  184. }
  185. /**
  186. * Delete the provided item from the database
  187. *
  188. * @param Saveable $item Item to save
  189. * @return bool Returns true on success, false on failure
  190. * @throws WireException
  191. *
  192. */
  193. public function ___delete(Saveable $item) {
  194. $blank = $this->makeBlankItem();
  195. if(!$item instanceof $blank) throw new WireException("WireSaveableItems::delete(item) requires item to be of type '" . $blank->className() . "'");
  196. $id = (int) $item->id;
  197. if(!$id) return false;
  198. $database = $this->wire('database');
  199. $this->deleteReady($item);
  200. $this->getAll()->remove($item);
  201. $table = $database->escapeTable($this->getTable());
  202. $query = $database->prepare("DELETE FROM `$table` WHERE id=:id LIMIT 1");
  203. $query->bindValue(":id", $id, PDO::PARAM_INT);
  204. $result = $query->execute();
  205. if($result) {
  206. $this->deleted($item);
  207. $item->id = 0;
  208. }
  209. return $result;
  210. }
  211. /**
  212. * Create and return a cloned copy of this item
  213. *
  214. * If the new item uses a 'name' field, it will contain a number at the end to make it unique
  215. *
  216. * @param Saveable $item Item to clone
  217. * @return bool|Saveable $item Returns the new clone on success, or false on failure
  218. *
  219. */
  220. public function ___clone(Saveable $item) {
  221. $item = clone $item;
  222. if(array_key_exists('name', $item->getTableData())) {
  223. // this item uses a 'name' field for identification, so we want to ensure it's unique
  224. $n = 0;
  225. $name = $item->name;
  226. // ensure the new name is unique
  227. while($this->get($name)) $name = $item->name . '_' . (++$n);
  228. $item->name = $name;
  229. }
  230. // id=0 forces the save() to create a new field
  231. $item->id = 0;
  232. if($this->save($item)) return $item;
  233. return false;
  234. }
  235. /**
  236. * Find items based on Selectors or selector string
  237. *
  238. * This is a delegation to the WireArray associated with this DAO.
  239. * This method assumes that all items are loaded. Desecending classes that don't load all items should
  240. * override this to the ___load() method instead.
  241. *
  242. * @param Selectors|string $selectors
  243. * @return WireArray
  244. *
  245. */
  246. public function ___find($selectors) {
  247. return $this->getAll()->find($selectors);
  248. }
  249. public function getIterator() {
  250. return $this->getAll();
  251. }
  252. public function get($key) {
  253. return $this->getAll()->get($key);
  254. }
  255. public function __get($key) {
  256. $value = $this->get($key);
  257. if(is_null($value)) $value = parent::__get($key);
  258. return $value;
  259. }
  260. public function has($item) {
  261. return $this->getAll()->has($item);
  262. }
  263. public function __isset($key) {
  264. return $this->get($key) !== null;
  265. }
  266. /**
  267. * Encode the 'data' portion of the table.
  268. *
  269. * This is a front-end to wireEncodeJSON so that it can be overridden if needed.
  270. *
  271. */
  272. protected function encodeData(array $value) {
  273. return wireEncodeJSON($value);
  274. }
  275. /**
  276. * Decode the 'data' portion of the table.
  277. *
  278. * This is a front-end to wireDecodeJSON that it can be overridden if needed.
  279. *
  280. */
  281. protected function decodeData($value) {
  282. return wireDecodeJSON($value);
  283. }
  284. /**
  285. * Enforce no locally-scoped fuel for this class
  286. *
  287. */
  288. public function useFuel($useFuel = null) {
  289. return false;
  290. }
  291. /**
  292. * Hook that runs right before item is to be saved.
  293. *
  294. * Unlike before(save), when this runs, it has already been confirmed that the item will indeed be saved.
  295. *
  296. * @param Saveable $item
  297. *
  298. */
  299. public function ___saveReady(Saveable $item) { }
  300. /**
  301. * Hook that runs right before item is to be deleted.
  302. *
  303. * Unlike before(delete), when this runs, it has already been confirmed that the item will indeed be deleted.
  304. *
  305. * @param Saveable $item
  306. *
  307. */
  308. public function ___deleteReady(Saveable $item) { }
  309. /**
  310. * Hook that runs right after an item has been saved.
  311. *
  312. * Unlike after(save), when this runs, it has already been confirmed that the item has been saved (no need to error check).
  313. *
  314. * @param Saveable $item
  315. *
  316. */
  317. public function ___saved(Saveable $item) { }
  318. /**
  319. * Hook that runs right after a new item has been added.
  320. *
  321. * @param Saveable $item
  322. *
  323. */
  324. public function ___added(Saveable $item) { }
  325. /**
  326. * Hook that runs right after an item has been deleted.
  327. *
  328. * Unlike after(delete), it has already been confirmed that the item was indeed deleted.
  329. *
  330. * @param Saveable $item
  331. *
  332. */
  333. public function ___deleted(Saveable $item) { }
  334. }