PageRenderTime 45ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/dumbledorm.php

http://github.com/jasonmoo/DumbledORM
PHP | 504 lines | 230 code | 40 blank | 234 comment | 27 complexity | 4922c7eb11597040e8aed8c0be31e154 MD5 | raw file
  1. <?php
  2. /**
  3. *
  4. * DumbledORM
  5. *
  6. * @version 0.1.1
  7. * @author Jason Mooberry <jasonmoo@me.com>
  8. * @link http://github.com/jasonmoo/DumbledORM
  9. * @package DumbledORM
  10. *
  11. * DumbledORM is a novelty PHP ORM
  12. *
  13. * Copyright (c) 2010 Jason Mooberry
  14. *
  15. * Permission is hereby granted, free of charge, to any person obtaining a copy
  16. * of this software and associated documentation files (the "Software"), to deal
  17. * in the Software without restriction, including without limitation the rights
  18. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  19. * copies of the Software, and to permit persons to whom the Software is furnished
  20. * to do so, subject to the following conditions:
  21. *
  22. * The above copyright notice and this permission notice shall be included in all
  23. * copies or substantial portions of the Software.
  24. *
  25. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  26. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  27. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  28. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  29. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  30. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  31. * THE SOFTWARE.
  32. *
  33. */
  34. /**
  35. * exceptional moments defined here
  36. */
  37. class RecordNotFoundException extends Exception {}
  38. /**
  39. * Class for denoting sql that should be inserted into the query directly without escaping
  40. *
  41. */
  42. final class PlainSql {
  43. private $_sql;
  44. public function __construct($sql) { $this->_sql = $sql; }
  45. public function __toString() { return $this->_sql; }
  46. }
  47. /**
  48. * Builder class for required for generating base classes
  49. *
  50. */
  51. abstract class Builder {
  52. /**
  53. * simple cameCasing method
  54. *
  55. * @param string $string
  56. * @return string
  57. */
  58. public static function camelCase($string) {
  59. return ucfirst(preg_replace("/_(\w)/e","strtoupper('\\1')",strtolower($string)));
  60. }
  61. /**
  62. * simple un_camel_casing method
  63. *
  64. * @param string $string
  65. * @return string
  66. */
  67. public static function unCamelCase($string) {
  68. return strtolower(preg_replace("/(\w)([A-Z])/","\\1_\\2",$string));
  69. }
  70. /**
  71. * re/generates base classes for db schema
  72. *
  73. * @param string $prefix
  74. * @param string $dir
  75. * @return void
  76. */
  77. public static function generateBase($prefix=null,$dir='model') {
  78. $tables = array();
  79. foreach (Db::query('show tables',null,PDO::FETCH_NUM) as $row) {
  80. foreach (Db::query('show columns from `'.$row[0].'`') as $col) {
  81. if ($col['Key'] === 'PRI') {
  82. $tables[$row[0]]['pk'] = $col['Field']; break;
  83. }
  84. }
  85. }
  86. foreach (array_keys($tables) as $table) {
  87. foreach (Db::query('show columns from `'.$table.'`') as $col) {
  88. if (substr($col['Field'],-3,3) === '_id') {
  89. $rel = substr($col['Field'],0,-3);
  90. if (array_key_exists($rel,$tables)) {
  91. if ($table === "{$rel}_meta") {
  92. $tables[$rel]['meta']['class'] = self::camelCase($table);
  93. $tables[$rel]['meta']['field'] = $col['Field'];
  94. }
  95. $tables[$table]['relations'][$rel] = array('fk' => 'id', 'lk' => $col['Field']);
  96. $tables[$rel]['relations'][$table] = array('fk' => $col['Field'], 'lk' => 'id');
  97. }
  98. }
  99. }
  100. }
  101. $basetables = "<?php\nspl_autoload_register(function(\$class) { @include(__DIR__.\"/\$class.class.php\"); });\n";
  102. foreach ($tables as $table => $conf) {
  103. $relations = preg_replace('/[\n\t\s]+/','',var_export((array)@$conf['relations'],true));
  104. $meta = isset($conf['meta']) ? "\$meta_class = '{$conf['meta']['class']}', \$meta_field = '{$conf['meta']['field']}'," : '';
  105. $basetables .= "class ".$prefix.self::camelCase($table)."Base extends BaseTable { protected static \$table = '$table', \$pk = '{$conf['pk']}', $meta \$relations = $relations; }\n";
  106. }
  107. @mkdir("./$dir",0777,true);
  108. file_put_contents("./$dir/base.php",$basetables);
  109. foreach (array_keys($tables) as $table) {
  110. $file = "./$dir/$prefix".self::camelCase($table).'.class.php';
  111. if (!file_exists($file)) {
  112. file_put_contents($file,"<?php\nclass ".$prefix.self::camelCase($table).' extends '.$prefix.self::camelCase($table).'Base {}');
  113. }
  114. }
  115. }
  116. }
  117. /**
  118. * thin wrapper for PDO access
  119. *
  120. */
  121. abstract class Db {
  122. /**
  123. * singleton variable for PDO connection
  124. *
  125. */
  126. private static $_pdo;
  127. /**
  128. * singleton getter for PDO connection
  129. *
  130. * @return PDO
  131. */
  132. public static function pdo() {
  133. if (!self::$_pdo) {
  134. self::$_pdo = new PDO('mysql:host='.DbConfig::HOST.';port='.DbConfig::PORT.';dbname='.DbConfig::DBNAME, DbConfig::USER, DbConfig::PASSWORD);
  135. self::$_pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  136. }
  137. return self::$_pdo;
  138. }
  139. /**
  140. * execute sql as a prepared statement
  141. *
  142. * @param string $sql
  143. * @param mixed $params
  144. * @return PDOStatement
  145. */
  146. public static function execute($sql,$params=null) {
  147. $params = is_array($params) ? $params : array($params);
  148. if ($params) {
  149. // using preg_replace_callback ensures that any inserted PlainSql
  150. // with ?'s in it will not be confused for replacement markers
  151. $sql = preg_replace_callback('/\?/',function($a) use (&$params) {
  152. $a = array_shift($params);
  153. if ($a instanceof PlainSql) {
  154. return $a;
  155. }
  156. $params[] = $a;
  157. return '?';
  158. },$sql);
  159. }
  160. $stmt = self::pdo()->prepare($sql);
  161. $stmt->execute($params);
  162. return $stmt;
  163. }
  164. /**
  165. * execute sql as a prepared statement and return all records
  166. *
  167. * @param string $query
  168. * @param mixed $params
  169. * @param PDO constant $fetch_style
  170. * @return Array
  171. */
  172. public static function query($query,$params=null,$fetch_style=PDO::FETCH_ASSOC) {
  173. return self::execute($query,$params)->fetchAll($fetch_style);
  174. }
  175. /**
  176. * run a query and return the results as a ResultSet of BaseTable objects
  177. *
  178. * @param BaseTable $obj
  179. * @param string $query
  180. * @param mixed $params
  181. * @return ResultSet
  182. */
  183. public static function hydrate(BaseTable $obj,$query,$params=null) {
  184. $set = array();
  185. foreach (self::query($query,$params) as $record) {
  186. $clone = clone $obj;
  187. $clone->hydrate($record);
  188. $set[$clone->getId()] = $clone;
  189. }
  190. return new ResultSet($set);
  191. }
  192. }
  193. /**
  194. * class to manage result array more effectively
  195. *
  196. */
  197. final class ResultSet extends ArrayIterator {
  198. /**
  199. * magic method for applying called methods to all members of result set
  200. *
  201. * @param string $method
  202. * @param Array $params
  203. * @return $this
  204. */
  205. public function __call($method,$params=array()) {
  206. foreach ($this as $obj) {
  207. call_user_func_array(array($obj,$method),$params);
  208. }
  209. return $this;
  210. }
  211. }
  212. /**
  213. * base functionality available to all objects extending from a generated base class
  214. *
  215. */
  216. abstract class BaseTable {
  217. protected static
  218. /**
  219. * table name
  220. */
  221. $table,
  222. /**
  223. * primary key
  224. */
  225. $pk,
  226. /**
  227. * table relations array
  228. */
  229. $relations,
  230. /**
  231. * metadata class name
  232. */
  233. $meta_class,
  234. /**
  235. * metadata field
  236. */
  237. $meta_field;
  238. protected
  239. /**
  240. * record data array
  241. */
  242. $data,
  243. /**
  244. * metadata array
  245. */
  246. $meta,
  247. /**
  248. * relation data array
  249. */
  250. $relation_data,
  251. /**
  252. * record primary key value
  253. */
  254. $id,
  255. /**
  256. * array of data fields that have changed since hydration
  257. */
  258. $changed;
  259. /**
  260. * search for single record in self::$table
  261. *
  262. * @param Array $constraints
  263. * @return BaseTable
  264. */
  265. final public static function one(Array $constraints) {
  266. return self::select('`'.implode('` = ? and `',array_keys($constraints)).'` = ? limit 1',array_values($constraints))->current();
  267. }
  268. /**
  269. * search for any number of records in self::$table
  270. *
  271. * @param Array $constraints
  272. * @return ResultSet
  273. */
  274. final public static function find(Array $constraints) {
  275. return self::select('`'.implode('` = ? and `',array_keys($constraints)).'` = ?',array_values($constraints));
  276. }
  277. /**
  278. * execute a query in self::$table
  279. *
  280. * @param string $qs
  281. * @param mixed $params
  282. * @return ResultSet
  283. */
  284. final public static function select($qs,$params=null) {
  285. return Db::hydrate(new static,'select * from `'.static::$table.'` where '.$qs,$params);
  286. }
  287. /**
  288. * construct object and load supplied data or fetch data by supplied id
  289. *
  290. * @param mixed $val
  291. */
  292. public function __construct($val=null) {
  293. if (is_array($val)) {
  294. $this->data = $val;
  295. $this->changed = array_flip(array_keys($this->data));
  296. $this->_loadMeta();
  297. } else if (is_numeric($val)) {
  298. if (!$obj = self::one(array(static::$pk => $val))) {
  299. throw new RecordNotFoundException("Nothing to be found with id $val");
  300. }
  301. $this->hydrate($obj->toArray());
  302. }
  303. }
  304. /**
  305. * most of the magic in here makes it all work
  306. * - handles all getters and setters on columns and relations
  307. *
  308. * @param string $method
  309. * @param Array $params
  310. * @return mixed
  311. */
  312. final public function __call($method,$params=array()) {
  313. $name = Builder::unCamelCase(substr($method,3,strlen($method)));
  314. if (strpos($method,'get')===0) {
  315. if (array_key_exists($name,$this->data)) {
  316. return $this->data[$name];
  317. }
  318. if (isset(static::$relations[$name])) {
  319. $class = substr($method,3,strlen($method));
  320. if (count($params)) {
  321. if ($params[0] === true) {
  322. return @$this->relation_data[$name.'_all'] ?: $this->relation_data[$name.'_all'] = $class::find(array(static::$relations[$name]['fk'] => $this->getId()));
  323. }
  324. $qparams = array_merge(array($this->getId()),(array)@$params[1]);
  325. $qk = md5(serialize(array($name,$params[0],$qparams)));
  326. return @$this->relation_data[$qk] ?: $this->relation_data[$qk] = $class::select('`'.static::$relations[$name]['fk'].'` = ? and '.$params[0],$qparams);
  327. }
  328. return @$this->relation_data[$name] ?: $this->relation_data[$name] = $class::one(array(static::$relations[$name]['fk'] => $this->getId()));
  329. }
  330. }
  331. else if (strpos($method,'set')===0) {
  332. $this->changed[$name] = true;
  333. $this->data[$name] = array_shift($params);
  334. return $this;
  335. }
  336. throw new BadMethodCallException("No amount of magic can make $method work..");
  337. }
  338. /**
  339. * simple output object data as array
  340. *
  341. * @return Array
  342. */
  343. final public function toArray() {
  344. return $this->data;
  345. }
  346. /**
  347. * simple output object pk id
  348. *
  349. * @return integer
  350. */
  351. final public function getId() {
  352. return $this->id;
  353. }
  354. /**
  355. * store supplied data and bring object state to current
  356. *
  357. * @param Array $data
  358. * @return $this
  359. */
  360. final public function hydrate(Array $data) {
  361. $this->id = $data[static::$pk];
  362. $this->data = $data;
  363. $this->_loadMeta();
  364. $this->changed = array();
  365. return $this;
  366. }
  367. /**
  368. * create an object with a defined relation to this one.
  369. *
  370. * @param BaseTable $obj
  371. * @return BaseTable
  372. */
  373. final public function create(BaseTable $obj) {
  374. return $obj->{'set'.Builder::camelCase(static::$relations[Builder::unCamelCase(get_class($obj))]['fk'])}($this->id);
  375. }
  376. /**
  377. * insert or update modified object data into self::$table and any associated metadata
  378. *
  379. * @return void
  380. */
  381. public function save() {
  382. if (empty($this->changed)) return;
  383. $data = array_intersect_key($this->data,$this->changed);
  384. // use proper sql NULL for values set to php null
  385. foreach ($data as $key => $value) {
  386. if ($value === null) {
  387. $data[$key] = new PlainSql('NULL');
  388. }
  389. }
  390. if ($this->id) {
  391. $query = 'update `'.static::$table.'` set `'.implode('` = ?, `',array_keys($data)).'` = ? where `'.static::$pk.'` = '.$this->id.' limit 1';
  392. }
  393. else {
  394. $query = 'insert into `'.static::$table.'` (`'.implode('`,`',array_keys($data))."`) values (".rtrim(str_repeat('?,',count($data)),',').")";
  395. }
  396. Db::execute($query,array_values($data));
  397. if ($this->id === null) {
  398. $this->id = Db::pdo()->lastInsertId();
  399. }
  400. $this->meta->{'set'.Builder::camelCase(static::$meta_field)}($this->id)->save();
  401. $this->hydrate(self::one(array(static::$pk => $this->id))->toArray());
  402. }
  403. /**
  404. * delete this object's record from self::$table and any associated meta data
  405. *
  406. * @return void
  407. */
  408. public function delete() {
  409. Db::execute('delete from `'.static::$table.'` where `'.static::$pk.'` = ? limit 1',$this->getId());
  410. $this->meta->delete();
  411. }
  412. /**
  413. * add an array of key/val to the metadata
  414. *
  415. * @param Array $data
  416. * @return $this
  417. */
  418. public function addMeta(Array $data) {
  419. foreach ($data as $field => $val) {
  420. $this->setMeta($field,$val);
  421. }
  422. return $this;
  423. }
  424. /**
  425. * set a field of metadata
  426. *
  427. * @param string $field
  428. * @param string $val
  429. * @return $this
  430. */
  431. public function setMeta($field,$val) {
  432. if (empty($this->meta[$field])) {
  433. $meta_class = static::$meta_class;
  434. $this->meta[$field] = new $meta_class(array('key' => $field,'val' => $val));
  435. }
  436. else {
  437. $this->meta[$field]->setVal($val);
  438. }
  439. return $this;
  440. }
  441. /**
  442. * get a field of metadata
  443. *
  444. * @param string $field
  445. * @return mixed
  446. */
  447. public function getMeta($field) {
  448. return isset($this->meta[$field]) ? $this->meta[$field]->getVal() : null;
  449. }
  450. /**
  451. * internally fetch and load any associated metadata
  452. *
  453. * @return void
  454. */
  455. private function _loadMeta() {
  456. if (!$meta_class = static::$meta_class) {
  457. return $this->meta = new ResultSet;
  458. }
  459. foreach ($meta_class::find(array(static::$meta_field => $this->getId())) as $obj) {
  460. $meta[$obj->getKey()] = $obj;
  461. }
  462. $this->meta = new ResultSet((array)@$meta);
  463. }
  464. }