PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/pronto/core/model.php

https://github.com/jvinet/pronto
PHP | 520 lines | 235 code | 42 blank | 243 comment | 28 complexity | 2b992749a1c5f68b79063e30f05ef099 MD5 | raw file
  1. <?php
  2. /**
  3. * PRONTO WEB FRAMEWORK
  4. * @copyright Copyright (C) 2006, Judd Vinet
  5. * @author Judd Vinet <jvinet@zeroflux.org>
  6. *
  7. * Description: Base class for all data models. Many of the methods here
  8. * will be overridden by subclasses.
  9. *
  10. * NOTE: This class is now deprecated in favor of the newer RecordModel
  11. * and RecordSelector classes. Please use those instead.
  12. *
  13. **/
  14. class Model_Base
  15. {
  16. var $db = null;
  17. var $web = null;
  18. var $cache = null;
  19. var $depends = null;
  20. var $plugins = null;
  21. /**
  22. * This can be set by the page controller acting on the model.
  23. * It defines the context with which a record is being modified.
  24. * For example, a Page_CRUD controller may set the model's context
  25. * to 'ADMIN', indicating that an authorized administrator is
  26. * modifying a record from a backend admin console, and so regular
  27. * validation should be bypassed.
  28. *
  29. * The model can then act on this setting in its overridden
  30. * methods (such as validate, insert, update, etc.)
  31. *
  32. * This setting is optional.
  33. */
  34. var $context = null;
  35. /*
  36. * Variables below are to be overridden by subclasses
  37. */
  38. /**
  39. * Name of database table this model will use
  40. */
  41. var $table = null;
  42. /**
  43. * Name of the PK column in this table (one column only)
  44. */
  45. var $pk = 'id';
  46. /**
  47. * Default column to sort by
  48. */
  49. var $default_sort = 'id';
  50. /**
  51. * Default number of items to show per page
  52. */
  53. var $per_page = 50;
  54. /**
  55. * Enable caching for this entity. Only enable this if you plan to
  56. * be diligent about _only_ inserting/updating/fetching/deleting this
  57. * entity _through_ the model interface. If you interact with the data
  58. * yourself (eg, through $this->db in a controller) then you will
  59. * undermine the caching layer.
  60. */
  61. var $enable_cache = false;
  62. /**
  63. * An array of files associated with this model, used by
  64. * Page_CRUD to handle file uploads. The 'type' element determines
  65. * the acceptable MIME type(s). Use an array for multiple types, or
  66. * "image" to accept normal image types. Omit it to accept any file.
  67. *
  68. * If type==image, then images will be converted to JPEG for consistency.
  69. * If type==image, then use 'max_width' and 'max_height' to optionally
  70. * resize images, maintaining aspect ratio.
  71. *
  72. * The filename parameter can include any values from the record
  73. * returned from the model's get() method. Each data key should be
  74. * surrounded in angle brackets (eg, "<name>").
  75. *
  76. * Array keys in $files must match those of the create/edit form defined
  77. * in the template.
  78. *
  79. * Example:
  80. @code
  81. var $files = array(
  82. 'image' => array(
  83. 'type' => 'image',
  84. 'max_width' => '640', // used for type==image only
  85. 'max_height' => '800', // used for type==image only
  86. 'webroot' => DIR_WS_DATA_IMG,
  87. 'fileroot' => DIR_FS_DATA_IMG,
  88. 'filename' => "<id>.jpg"
  89. )
  90. );
  91. @endcode
  92. */
  93. var $files = null;
  94. /**
  95. * Constructor for all models
  96. *
  97. */
  98. function Model_Base()
  99. {
  100. $this->db =& Registry::get('pronto:db:main');
  101. $this->cache =& Registry::get('pronto:cache');
  102. $this->web =& Registry::get('pronto:web');
  103. $this->validator =& Registry::get('pronto:validator');
  104. // this isn't a very smart inflector -- best to explicitly set the
  105. // table name in the model itself
  106. if(is_null($this->table) || empty($this->table)) {
  107. $this->table = strtolower(substr(get_class($this),1)).'s';
  108. }
  109. // load plugins
  110. $this->plugins =& Registry::get('pronto:plugins');
  111. if(method_exists($this, '__init__')) {
  112. $this->__init__();
  113. }
  114. }
  115. /**
  116. * Load another model as a dependency of this one.
  117. *
  118. * @param string $model_name
  119. */
  120. function depend($model)
  121. {
  122. foreach(func_get_args() as $name) {
  123. if(isset($this->depends->$name)) continue;
  124. $this->depends->$name =& Factory::model($name);
  125. }
  126. }
  127. /**
  128. * Import a plugin (aka "page plugin").
  129. * @param string $name Plugin name
  130. */
  131. function &import_plugin($name)
  132. {
  133. foreach(func_get_args() as $name) {
  134. $class =& Factory::plugin($name, 'page');
  135. if($class === false) {
  136. trigger_error("Plugin $name does not exist");
  137. die;
  138. }
  139. }
  140. // reload $this->plugins
  141. $this->plugins =& Registry::get('pronto:plugins');
  142. if($class) return $class;
  143. }
  144. /**
  145. * Validate data for an INSERT. To be overridden by subclasses.
  146. *
  147. * @param array $data
  148. * @return array Associative array of errors
  149. */
  150. function validate_for_insert($data)
  151. {
  152. return false;
  153. }
  154. /**
  155. * Validate data for an UPDATE. To be overridden by subclasses.
  156. *
  157. * @param array $data
  158. * @return array Associative array of errors
  159. */
  160. function validate_for_update($data)
  161. {
  162. return false;
  163. }
  164. /**
  165. * Perform the correct validation routine depending on insert/update mode.
  166. *
  167. * @param array $data
  168. * @param bool $is_update Whether this data manipulation is an UPDATE or not
  169. * @return array Associative array of errors
  170. */
  171. function validate($data, $is_update=false)
  172. {
  173. if($is_update) {
  174. return $this->validate_for_update($data);
  175. }
  176. return $this->validate_for_insert($data);
  177. }
  178. /**
  179. * Use the HTML checker to remove any possible XSS attacks (eg, <script> tags)
  180. *
  181. * @param array $data
  182. * @return array
  183. */
  184. function purify($data)
  185. {
  186. require_once(DIR_FS_PRONTO.DS.'extlib'.DS.'safehtml'.DS.'safehtml.php');
  187. foreach($data as $k=>$v) {
  188. if(is_array($v)) {
  189. // PHP4 doesn't like self::purify()
  190. $data[$k] = Model::purify($v);
  191. } else if(class_exists('safehtml')) {
  192. $purifier = new safehtml();
  193. $data[$k] = $purifier->parse($v);
  194. }
  195. }
  196. return $data;
  197. }
  198. /**
  199. * Used to do any necessary sanitization on the data before putting it
  200. * in the DB. addslashes() stuff is not necessary.
  201. *
  202. * @param array $data
  203. * @return array
  204. */
  205. function sanitize($data)
  206. {
  207. $data = $this->purify($data);
  208. return $data;
  209. }
  210. /**
  211. * Return a new/fresh entity. This method is responsible for returning
  212. * a brand new, un-edited record that will be used to prepopulate form
  213. * fields in a "Create" form for this data entity.
  214. *
  215. * @return array
  216. */
  217. function create()
  218. {
  219. return array();
  220. }
  221. /**
  222. * Return a single record by PK. Normally, you won't want to override this method.
  223. * Instead, override the get_record() method, which does the actual fetching/assembling
  224. * of a data record for use by other objects.
  225. *
  226. * @param int $id
  227. * @return array
  228. */
  229. function get($id)
  230. {
  231. if($this->enable_cache && $this->cache) {
  232. $key = 'model:'.get_class($this).":{$this->pk}:$id";
  233. $item =& $this->cache->get($key);
  234. if(!$item) {
  235. $item = $this->get_record($id);
  236. $this->cache->set($key, $item);
  237. }
  238. return $item;
  239. }
  240. return $this->get_record($id);
  241. }
  242. /**
  243. * Remove an entry from the cache
  244. *
  245. * @param mixed $id PK of the record to be invalidated from the cache.
  246. * Use an array if invalidating multiple records.
  247. */
  248. function invalidate($id)
  249. {
  250. if(!is_array($id)) $id = array($id);
  251. foreach($id as $i) {
  252. if($this->enable_cache && $this->cache) {
  253. $key = 'model:'.get_class($this).":{$this->pk}:$i";
  254. $this->cache->delete($key);
  255. }
  256. }
  257. }
  258. /**
  259. * Remove all cache entries for this data entity.
  260. */
  261. function invalidate_all()
  262. {
  263. if($this->cache) {
  264. $re = '^model:'.get_class($this).':.*$';
  265. $this->cache->delete_by_regex("/$re/");
  266. }
  267. }
  268. /**
  269. * Return a full record for this entity, looked up by PK. Normally
  270. * you will want to override this method to do any additional work on
  271. * the record before returning it to the caller.
  272. *
  273. * @param int $id
  274. * @return array
  275. */
  276. function get_record($id)
  277. {
  278. return $this->get_bare($id);
  279. }
  280. /**
  281. * Return a single bare record by PK. Returns a single row from the
  282. * table, no other data manipulation is performed. Normally the
  283. * Model::get_record() method is overridden to do additional data work, so
  284. * this method serves to retain a plain record-getter.
  285. *
  286. * @param int $id
  287. * @return array
  288. */
  289. function get_bare($id)
  290. {
  291. return $this->db->get_item("SELECT * FROM {$this->table} WHERE \"{$this->pk}\"=%i LIMIT 1", array($id));
  292. }
  293. /**
  294. * Return multiple records by PK, optionally ignoring empty/false records.
  295. *
  296. * @param array $ids
  297. * @param boolean $ignore_empty Ignore empty records.
  298. * @return array
  299. */
  300. function get_multi($ids, $ignore_empty=true)
  301. {
  302. $data = array();
  303. foreach($ids as $id) {
  304. $rec = $this->get($id);
  305. if($rec || !$ignore_empty) $data[] = $rec;
  306. }
  307. return $data;
  308. }
  309. /**
  310. * Return a single column from a single entity by PK
  311. *
  312. * @param int $id
  313. * @param string $column
  314. * @return array
  315. */
  316. function get_value($id, $column)
  317. {
  318. return $this->db->get_value("SELECT \"$column\" FROM {$this->table} WHERE \"{$this->pk}\"=%i LIMIT 1", array($id));
  319. }
  320. /**
  321. * Fetch a row by a specified column. If $column and $value are
  322. * arrays, then "AND" all the values in the WHERE clause.
  323. *
  324. * @param mixed $column
  325. * @param mixed $value
  326. * @return array
  327. */
  328. function get_by($column, $value)
  329. {
  330. if(!is_array($column)) $column = array($column);
  331. if(!is_array($value)) $value = array($value);
  332. $where = array();
  333. foreach($column as $c) $where[] = "\"{$c}\"='%s'";
  334. $where = implode(' AND ', $where);
  335. $id = $this->db->get_value("SELECT \"{$this->pk}\" FROM {$this->table} WHERE $where LIMIT 1", $value);
  336. return $this->get($id);
  337. }
  338. /**
  339. * Fetch all rows by a specified column. If $column and $value are
  340. * arrays, then "AND" all the values in the WHERE clause.
  341. *
  342. * @param mixed $column
  343. * @param mixed $value
  344. * @return array
  345. */
  346. function get_all_by($column, $value)
  347. {
  348. if(!is_array($column)) $column = array($column);
  349. if(!is_array($value)) $value = array($value);
  350. $where = array();
  351. foreach($column as $c) $where[] = "\"{$c}\"='%s'";
  352. $where = implode(' AND ', $where);
  353. $ids = $this->db->get_values("SELECT \"{$this->pk}\" FROM {$this->table} WHERE $where ORDER BY {$this->default_sort}", $value);
  354. return $this->get_multi($ids);
  355. }
  356. /**
  357. * Fetch all records.
  358. *
  359. * @return array
  360. */
  361. function get_all()
  362. {
  363. $ids = $this->db->get_values("SELECT \"{$this->pk}\" FROM {$this->table} ORDER BY {$this->default_sort}");
  364. return $this->get_multi($ids);
  365. }
  366. /**
  367. * Return an entity or throw a 404 page if not found
  368. *
  369. * @param int $id
  370. * @return array
  371. */
  372. function get_or_404($id)
  373. {
  374. $data = $this->get($id);
  375. if($data === false) {
  376. $this->web->notfound();
  377. }
  378. return $data;
  379. }
  380. /**
  381. * Delete a single entity by PK
  382. *
  383. * @param int $id
  384. */
  385. function delete($id)
  386. {
  387. $this->invalidate($id);
  388. return $this->db->execute("DELETE FROM {$this->table} WHERE \"{$this->pk}\"=%i", array($id));
  389. }
  390. /**
  391. * Insert a new entity into the table
  392. *
  393. * @param array $data
  394. * @return int The insert ID of the newly-inserted row
  395. */
  396. function insert($data)
  397. {
  398. $data = $this->sanitize($data);
  399. return $this->db->insert_row($this->table, $data);
  400. }
  401. /**
  402. * Update an entity
  403. *
  404. * @param array $data
  405. */
  406. function update($data)
  407. {
  408. $this->invalidate($data['id']);
  409. $data = $this->sanitize($data);
  410. return $this->db->update_row($this->table, $data, $this->pk."='%s'", array($data[$this->pk]));
  411. }
  412. /**
  413. * Return some data used for generating smart SQL for lists/grids.
  414. *
  415. * @return array
  416. */
  417. function list_params()
  418. {
  419. return array(
  420. 'from' => $this->table,
  421. 'exprs' => array(),
  422. 'gexprs' => array(),
  423. 'select' => '*',
  424. 'where' => '',
  425. 'group_by' => '',
  426. 'having' => '',
  427. 'order' => $this->default_sort,
  428. 'limit' => $this->per_page,
  429. );
  430. }
  431. /**
  432. * TODO: PHP5 only!
  433. *
  434. * Call like so: $this->set_mycolname($id, $value)
  435. * and: $this->get_mycolname($id)
  436. */
  437. function __call($name, $args)
  438. {
  439. switch(true) {
  440. case substr($name, 0, 4) == 'get_':
  441. $field = substr($name, 4);
  442. return $this->db->get_value("SELECT \"$field\" FROM {$this->table} WHERE \"{$this->pk}\"='%s'", array($args[0]));
  443. case substr($name, 0, 4) == 'set_':
  444. $field = substr($name, 4);
  445. $this->db->execute("UPDATE {$this->table} SET \"$field\"='%s' WHERE \"{$this->pk}\"='%s'", array($args[1],$args[0]));
  446. $this->invalidate($args[0]);
  447. return true;
  448. }
  449. trigger_error("Method does not exist: $name");
  450. }
  451. /**
  452. * BACKWARDS COMPATIBILITY
  453. *
  454. * This makes the basic model functions compatible with the newer RecordModel
  455. * class.
  456. */
  457. function save($data)
  458. {
  459. if($data['id']) {
  460. return $this->update($data);
  461. } else {
  462. return $this->insert($data);
  463. }
  464. }
  465. function load($id)
  466. {
  467. return $this->get($id);
  468. }
  469. function create_record()
  470. {
  471. return $this->create();
  472. }
  473. function enum_schema()
  474. {
  475. return $this->list_params();
  476. }
  477. }
  478. ?>