orm.abstract.class.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. <?php
  2. require_once('sql.class.php');
  3. require_once('relationship.class.php');
  4. abstract class ORM implements ArrayAccess, JsonSerializable {
  5. // Model definition
  6. protected static $table_name = null;
  7. protected static $primary_key = 'id';
  8. protected static $foreign_key_suffix = '_id';
  9. protected static $has_one = [];
  10. protected static $has_many = [];
  11. protected static $belongs_to = [];
  12. // Data tracking
  13. private $_data = [];
  14. private $_changed = [];
  15. private $_related = [];
  16. private static $aliases = [];
  17. private static $instances = [];
  18. private static $sql;
  19. // Magic functions
  20. private function __construct($idOrData){
  21. if(!isset(self::$aliases[$this->name])){
  22. foreach(static::$belongs_to as $alias => $details){
  23. self::$aliases[$this->name]['belongs_to'][$alias] = array_merge(
  24. [
  25. 'model'=>$alias,
  26. 'foreign_key'=>$this->name.static::foreign_key_suffix()
  27. ],
  28. $details
  29. );
  30. }
  31. foreach(static::$has_one as $alias => $details){
  32. self::$aliases[$this->name]['has_one'][$alias] = array_merge(
  33. [
  34. 'model'=>$alias,
  35. 'foreign_key'=>$alias.static::foreign_key_suffix()
  36. ],
  37. $details
  38. );
  39. }
  40. foreach(static::$has_many as $alias => $details){
  41. self::$aliases[$this->name]['has_many'][$alias] = array_merge(
  42. [
  43. 'model'=>$alias,
  44. 'foreign_key'=>$this->name.static::foreign_key_suffix(),
  45. 'through'=> null
  46. ],
  47. $details
  48. );
  49. }
  50. }
  51. // Clear relationship definitions to save memory
  52. static::$belongs_to = static::$has_one = static::$has_many =null;
  53. if(is_array($idOrData)){
  54. if(isset($idOrData[static::primary_key()]) && self::cached($idOrData[static::primary_key()])){
  55. throw new Exception('Instance already cached');
  56. }
  57. foreach($idOrData as $key => $val){
  58. $this->_data[$key] = $val;
  59. }
  60. }else{
  61. if(self::cached($idOrData)){
  62. throw new Exception('Instance already cached');
  63. }
  64. $this->_data[static::primary_key()] = (int)$idOrData;
  65. }
  66. self::$instances[] = $this;
  67. }
  68. private function __destruct(){
  69. $this->__sleep();
  70. $this->_changed = [];
  71. $this->_data = [];
  72. $this->_related = [];
  73. $key = array_search(self::$instances, $this);
  74. if($key !== false){
  75. array_splice(self::$instances, $key, 1);
  76. }
  77. }
  78. public function __get(string $name){
  79. switch($name){
  80. case 'name':
  81. return static::table_name();
  82. break;
  83. case 'primary_key':
  84. return static::primary_key();
  85. break;
  86. case 'foreign_key_suffix':
  87. return static::foreign_key_suffix();
  88. break;
  89. case 'has_one':case 'has_many':case 'belongs_to':
  90. return self::$aliases[$this->name][$name];
  91. break;
  92. case 'id':
  93. return $this->get(static::primary_key());
  94. break;
  95. default:
  96. throw new Exception('Unknown property '.$name);
  97. }
  98. }
  99. public function __set(string $name, $val){
  100. switch($name){
  101. case 'id':case 'name':
  102. throw new Exception('Property '.$name.' is read only');
  103. break;
  104. default:
  105. throw new Exception('Unknown property '.$name);
  106. }
  107. }
  108. public function __sleep(){
  109. $this->save();
  110. }
  111. public function __wakeup(){
  112. $this->reload(true);
  113. }
  114. public function __toString(){
  115. return $this->name.'('.$this->id.')';
  116. }
  117. public function __invoke(){
  118. return $this->_data;
  119. }
  120. public function __clone(){
  121. unset($this->$_data[static::primary_key()]);
  122. }
  123. // JsonSerializable
  124. public function jsonSerialize(){
  125. return $this->_data;
  126. }
  127. // ArrayAccess
  128. public function offsetSet($key, $val){
  129. $this->set($key, $val);
  130. }
  131. public function offsetExists($key){
  132. return $this->has($key);
  133. }
  134. public function offsetUnset($key){
  135. return $this->unset($key);
  136. }
  137. public function offsetGet($key){
  138. return $this->get($key);
  139. }
  140. // Main API
  141. public static function table_name(){
  142. if(is_null(static::$table_name)){
  143. $name = get_called_class();
  144. return substr($name, strrpos($name, '\\') + 1);
  145. }
  146. return static::$table_name;
  147. }
  148. public static function primary_key(){
  149. return static::$primary_key;
  150. }
  151. public static function foreign_key_suffix(){
  152. return static::$foreign_key_suffix;
  153. }
  154. public static function bind(SQL $sql){
  155. self::$sql = $sql;
  156. // @todo handle updating live instances
  157. }
  158. public static function query(...$args){
  159. return self::$sql->query(...$args);
  160. }
  161. public static function instance(int $id){
  162. $instance = self::cached_instance($id);
  163. if(!is_null($instance)){
  164. return $instance;
  165. }elseif(self::exists($id)){
  166. return new static($id);
  167. }
  168. return null;
  169. }
  170. public static function exists(int $id){
  171. return (int)self::query(
  172. "select count(1) as count ".
  173. "from ".static::table_name().' '.
  174. "where ".static::primary_key()." = ?",
  175. 'i',
  176. $id
  177. )->assoc_result["count"] > 0;
  178. }
  179. public static function cached_instance(int $id){
  180. $name = static::table_name();
  181. if(isset(self::$instances[$name])){
  182. $instances = array_filter(self::$instances[$name], function(&$instance){
  183. return $instance->id === $id;
  184. });
  185. return isset($instances[0]) ? $instances[0] : null;
  186. }
  187. return null;
  188. }
  189. public static function cached(int $id){
  190. return !is_null(self::cached_instance($id));
  191. }
  192. public static function delete(int $id){
  193. $query = self::query(
  194. "delete from ".static::table_name().' '.
  195. "where ".static::primary_key()." = ?",
  196. 'i',
  197. $id
  198. );
  199. return $query->execute() && $query->affected_rows > 0;
  200. }
  201. public static function each_cached(callable $fn){
  202. $name = static::table_name();
  203. if(self::$instances[$name]){
  204. array_walk(self::$instances[$name], $fn);
  205. }
  206. }
  207. public static function each_where(callable $fn, array $filter = null, int $start = null, int $amount = null){
  208. $limit = ' ';
  209. if(!is_null($start) && !is_null($amount)){
  210. $limit .= "limit {$start}, {$amount}";
  211. }
  212. $where = ' ';
  213. $types = null;
  214. $bindings = null;
  215. if(!is_null($filter)){
  216. $types = '';
  217. $bindings = array();
  218. foreach($filter as $key => $val){
  219. if(is_string($val)){
  220. $types .= 's';
  221. }elseif(is_double($val)){
  222. $types .= 'd';
  223. }elseif(is_int($val)){
  224. $types .= 'i';
  225. }else{
  226. throw new Exception("Unknown data type");
  227. }
  228. $where .= 'and {$key} = ? ';
  229. $bindings[] = $val;
  230. }
  231. $where = self::str_replace_first(' and ', ' ', $where);
  232. }
  233. self::query(
  234. "select ".static::primary_key().' id '.
  235. "from ".static::table_name().
  236. $where.
  237. $limit,
  238. $types,
  239. $bindings
  240. )->each_assoc(function($row) use($fn){
  241. $fn(self::instance((int)$row['id']));
  242. });
  243. }
  244. public static function each(callable $fn, int $start = null, int $amount = null){
  245. self::each_where($fn, null, $start, $amount);
  246. }
  247. public static function fetch(array $filter = null, int $start = null, int $amount = null){
  248. $results = [];
  249. self::each_where(function($item) use($results){
  250. $results[] = $item;
  251. }, $filter, $start, $amount);
  252. return $results;
  253. }
  254. public static function str_replace_first($search, $replace, $source) {
  255. $explode = explode($search, $source);
  256. $shift = array_shift($explode);
  257. $implode = implode($search, $explode);
  258. return $shift.$replace.$implode;
  259. }
  260. // Instance Api
  261. public function values($values){
  262. foreach($values as $key => $val){
  263. $this->set($key, $val);
  264. }
  265. return $this;
  266. }
  267. public function load(bool $force = false){
  268. if(!$force && $this->dirty()){
  269. throw new Exception('Cannot load, there are pending changes');
  270. }else{
  271. if(!is_null($this->id)){
  272. $data = self::query(
  273. "select * " .
  274. "from {$this->name} ".
  275. "where ".static::primary_key()." = ?",
  276. 'i',
  277. $this->id
  278. )->assoc_result;
  279. if($data === false){
  280. throw new Exception("{$this->name} with ".static::primary_key()." of {$this->id} does not exist");
  281. }
  282. $this->_data = $data;
  283. }
  284. }
  285. return $this;
  286. }
  287. public function save(){
  288. if($this->dirty()){
  289. $data = [];
  290. $set = "set ";
  291. $types = '';
  292. foreach($this->_changed as $key){
  293. if(isset($this->_data[$key]) || is_null($this->_data[$key])){
  294. $data[$key] = $this->_data[$key];
  295. }else{
  296. $set .= "{$key} = null";
  297. }
  298. }
  299. foreach($data as $key => $val){
  300. if(is_string($val)){
  301. $types .= 's';
  302. }elseif(is_double($val)){
  303. $types .= 'd';
  304. }elseif(is_int($val)){
  305. $types .= 'i';
  306. }else{
  307. throw new Exception('Unknown data type');
  308. }
  309. $set .= "{$key} = ? ";
  310. }
  311. if(!is_null($this->id) && !in_array(static::primary_key(), $this->_changed)){
  312. $data = array_merge(array_values($data), [$this->id]);
  313. self::query(
  314. "update {$this->name} {$set} where ".static::primary_key()." = ?",
  315. $types.'i',
  316. $data
  317. )->execute();
  318. }else{
  319. self::query(
  320. "insert {$this->name} {$set}"
  321. )->execute();
  322. $this->_data[static::primary_key()] = self::$sql->insert_id;
  323. }
  324. foreach($this->_related as $related){
  325. $related->save();
  326. }
  327. }
  328. // Always fetch again from the database in case saving
  329. // forces something to change at the database level
  330. return $this->reload(true);
  331. }
  332. public function reload(bool $force = false){
  333. if($force){
  334. $this->_changed = [];
  335. }
  336. return $this->load($force);
  337. }
  338. public function clear(){
  339. return $this->reload(true);
  340. }
  341. public function get($key){
  342. return $this->_data[$key];
  343. }
  344. public function set($key, $val){
  345. if($key === static::primary_key() && !is_null($this->id)){
  346. throw new Exception('You are not allowed to change the primary key');
  347. }
  348. $this->_data[$key] = $val;
  349. $this->_changed = array_merge($this->_changed, [$key]);
  350. return $this;
  351. }
  352. public function unset($key){
  353. unset($this->_data[$key]);
  354. return $this;
  355. }
  356. public function has($key){
  357. return isset($this->_data[$key]);
  358. }
  359. public function dirty(string $key = null){
  360. if(is_null($key)){
  361. return count($this->_changed) > 0;
  362. }else{
  363. return in_array($key, $this->_changed);
  364. }
  365. }
  366. public function related(string $name){
  367. if(!isset($this->_related[$name])){
  368. $aliases = self::$aliases[$this->name];
  369. if($aliases['belongs_to'][$name]){
  370. $alias = $aliases['has_many'][$name];
  371. $class = "Models\\{$alias['model']}";
  372. $this->_related[$name] = $class::fetch([$alias['foreign_key'] => $this->id])[0];
  373. }elseif($aliases['has_one'][$name]){
  374. $alias = $aliases['has_many'][$name];
  375. $class = "Models\\{$alias['model']}";
  376. $this->_related[$name] = $class::instance($this[$alias['foreign_key']]);
  377. }elseif($aliases['has_many'][$name]){
  378. $alias = $aliases['has_many'][$name];
  379. $class = "Models\\{$alias['model']}";
  380. $sql = "select ";
  381. if($alias['through']){
  382. $sql .= $class::table_name().$class::foreign_key_suffix()." id from {$alias['through']} ";
  383. }else{
  384. $sql .= $class::primary_key()." id from {$alias['model']} ";
  385. }
  386. $sql .= "where ".$alias['foreign_key']." = ?";
  387. $related = [];
  388. self::query(
  389. $sql,
  390. 'i',
  391. $this->id
  392. )->each_assoc(function($row) use(&$related, $class){
  393. $related[] = new $class($row['id']);
  394. });
  395. $this->_related[$name] = Relationship::from($this, $name, $alias, $related);
  396. }
  397. }
  398. if(isset($this->_related[$name])){
  399. return $this->_related[$name];
  400. }else{
  401. throw new Exception("Relationship {$name} does not exist");
  402. }
  403. }
  404. }
  405. ?>