table.class.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. <?php
  2. namespace Juju\PDO;
  3. require_once(realpath(dirname(__DIR__).'/pdo.class.php'));
  4. use Juju\{PDO, PDO\Transaction};
  5. class Table {
  6. private $pdo;
  7. private $name;
  8. private $exists;
  9. private $columns;
  10. private $columns_removed;
  11. private $primaryKey;
  12. private $index;
  13. private $foreignKey;
  14. public function __construct($pdo, string $name){
  15. if($pdo instanceof PDO || $pdo instanceof Transaction){
  16. $this->pdo = $pdo;
  17. $this->name = $name;
  18. $this->rollback();
  19. }else{
  20. throw new \Exception("Invalid pdo argument");
  21. }
  22. }
  23. public function __get($name){
  24. switch($name){
  25. case 'exists':case 'name':case 'columns':
  26. return $this->$name;
  27. break;
  28. default:
  29. throw new \Exception("Invalid property {$name}");
  30. }
  31. }
  32. public function exists(){
  33. $pdo = $this->pdo;
  34. try{
  35. $count = $pdo->exec("select count(1) from `{$this->name}` limit 1");
  36. $this->exists = $count > 0;
  37. }catch(\Exception $e){
  38. $this->exists = false;
  39. }
  40. return $this->exists;
  41. }
  42. public function describe(){
  43. $this->columns = [];
  44. $this->primaryKey = [];
  45. $this->index = [];
  46. $this->foreignKeys = [];
  47. if($this->exists){
  48. $pdo = $this->pdo;
  49. $query = $pdo->query("show create table `{$this->name}`");
  50. $sql = $query->fetch()['create table'];
  51. $query->closeCursor();
  52. if(preg_match_all('/ PRIMARY KEY \(((?:`[^`]+`,?)+)\)/s', $sql, $primaryKey, PREG_SET_ORDER) > 0){
  53. $this->primaryKey = array_map(
  54. function($item){
  55. return preg_replace('/`([^`]+)`/', '\\1', $item);
  56. },
  57. array_merge(
  58. ...array_map(
  59. function($item){
  60. return explode(',', $item[1]);
  61. }, $primaryKey
  62. )
  63. )
  64. );
  65. }
  66. unset($primaryKey);
  67. if(preg_match_all('/ ((?:UNIQUE)? ?KEY) `([^`]+)` \(((?:`[^`]+`,?)+)\)/s', $sql, $indexes, PREG_SET_ORDER) > 0){
  68. $this->indexes = array_reduce($indexes, function($indexes, $item){
  69. $indexes[$item[2]] = [
  70. 'unique'=> $item[1] == 'UNIQUE KEY',
  71. 'columns'=> array_map(function($item){
  72. return preg_replace('/`([^`]+)`/', '\\1', $item);
  73. }, explode(',', $item[3])),
  74. 'dirty'=> false
  75. ];
  76. return $indexes;
  77. });
  78. }
  79. unset($indexes);
  80. if(preg_match_all('/ CONSTRAINT `([^`]+)` FOREIGN KEY \(((?:`[^`]+`(?:, )?)+)\) REFERENCES `([^`]+)` \(((?:`[^`]+`(?:, )?)+)\)/s', $sql, $foreignKeys, PREG_SET_ORDER, 0) > 0){
  81. $this->foreignKeys = array_reduce($foreignKeys, function($foreignKeys, $item) use($sql){
  82. $columns = explode(',', $item[2]);
  83. $matches = explode(',', $item[4]);
  84. foreach($columns as $key => &$column){
  85. $column = [
  86. preg_replace('/`([^`]+)`/', '\\1', $column),
  87. preg_replace('/`([^`]+)`/', '\\1', $matches[$key])
  88. ];
  89. }
  90. $foreignKeys[$item[1]] = [
  91. 'references'=> $item[3],
  92. 'columns'=> $columns
  93. ];
  94. return $foreignKeys;
  95. });
  96. }
  97. unset($foreignKeys);
  98. if(preg_match_all('/ `([^`]+)` ([^\(]+\([^\)]+\))[^,\n]*,?/s', $sql, $columns, PREG_SET_ORDER) > 0){
  99. foreach($columns as $column){
  100. $default = null;
  101. $line = $column[0];
  102. $line .= substr($line, -1, 1) === ',' ? '' : ',';
  103. if(preg_match('/ `[^`]+` [^\(]+\([^\)]+\)[^D$]+DEFAULT (?!NULL)(.+)(?: AUTO_INCREMENT|UNIQUE|KEY|PRIMARY|COMMENT|COLUMN_FORMAT|STORAGE|REFERENCES|,)/u', $line, $match) == 1){
  104. $default = preg_replace("/'(.+)'/", "\\1", $match[1]);
  105. }
  106. $this->column(
  107. $column[1], // name
  108. $column[2], // type
  109. $default, // default
  110. preg_match('/`[^`]+` [^\(]+\([^\)]+\) NOT NULL/', $column[0]) === 1, // null
  111. preg_match('/ `[^`]+` [^\(]+\([^\)]+\)[^A$]+AUTO_INCREMENT,?/u', $line) == 1 // increment
  112. );
  113. $this->columns[$column[1]]['dirty'] = false;
  114. }
  115. }
  116. unset($columns);
  117. }
  118. }
  119. public function commit(){
  120. $pdo = $this->pdo;
  121. if(!$this->exists){
  122. $columns = '';
  123. foreach($this->columns as $name => $column){
  124. $columns .= "{$pdo->stringColumn($name, $column)},";
  125. }
  126. if(count($columns) > 0){
  127. $columns = rtrim($columns, ',');
  128. }
  129. $pk = $this->primaryKey();
  130. if(count($pk)){
  131. $pk = ", primary key (".implode(',', $pk).")";
  132. }
  133. $index = '';
  134. foreach($this->index as $name => $idx){
  135. $index .=", {$pdo->stringIndex($name, $idx)}";
  136. }
  137. $fk = '';
  138. foreach($this->foreignKeys as $name => $k){
  139. $fk .= ", {$pdo->stringForeignKey($name, $k)}";
  140. }
  141. $count = $pdo->exec("create table `{$this->name}` ({$columns}{$pk}{$index}{$fk})");
  142. if($count === false){
  143. throw $pdo->getError();
  144. }
  145. if(!$this->exists()){
  146. throw new \Exception("Unable to create table {$this->name}");
  147. }
  148. $this->exists();
  149. }else{
  150. $columns = array_filter($this->columns, function($item){
  151. return $item['dirty'];
  152. });
  153. if(count($columns) + count($this->columns_removed) > 0){
  154. $sql = "alter table `{$this->name}`";
  155. foreach($this->columns_removed as $name){
  156. $sql .= " drop column {$name},";
  157. }
  158. if(count($columns) > 0){
  159. $sql .= " add (";
  160. foreach($columns as $name => $column){
  161. $sql .= "{$this->stringColumn($name, $column)},";
  162. }
  163. $sql = rtrim($sql , ',') . "),";
  164. }
  165. unset($columns);
  166. $sql = rtrim($sql, ',');
  167. if($pdo->exec($sql) === false){
  168. throw new \Exception("Unable to update table {$this->name}\n{$this->pdo->getError()}");
  169. }
  170. }
  171. $sql = "alter table `{$this->name}`";
  172. if(count($this->primaryKey) > 0){
  173. if($pdo->exec("show index from `{$this->name}` where KEY_name = 'PRIMARY'") > 0){
  174. $sql .= " drop primary key,";
  175. }
  176. $sql .= " add primary key (".implode(',', $this->primaryKey)."),";
  177. }
  178. foreach(array_filter($this->index, function($item){
  179. return $item['dirty'];
  180. }) as $name => $idx){
  181. $sql .= " add {$this->stringIndex($name, $idx)},";
  182. }
  183. foreach(array_filter($this->foreignKeys, function($item){
  184. return $item['dirty'];
  185. }) as $name => $fk){
  186. $sql .= " add {$this->stringForeignKey($name, $fk)},";
  187. }
  188. $sql = rtrim($sql, ',');
  189. if($pdo->exec($sql) === false){
  190. throw new \Exception("Unable to update table {$this->name}\n{$this->pdo->getError()}");
  191. }
  192. }
  193. $this->describe();
  194. }
  195. public function rollback(){
  196. $this->columns_removed = [];
  197. $this->exists();
  198. $this->describe();
  199. }
  200. public function drop(string $column = null){
  201. if(!is_null($column)){
  202. if(isset($this->columns[$column])){
  203. if($this->exists){
  204. $this->columns_removed = array_merge($this->columns_removed, [$column]);
  205. }
  206. unset($this->columns[$column]);
  207. }
  208. }else{
  209. if($this->exists){
  210. $this->pdo->exec("drop table `{$this->name}`");
  211. }
  212. $this->rollback();
  213. }
  214. return $this;
  215. }
  216. public function column(string $name, string $type = null, string $default = null, bool $null = false, bool $increment = false){
  217. if(!is_null($type)){
  218. $new = [
  219. 'type'=> $type,
  220. 'default'=> $default,
  221. 'null'=> $null,
  222. 'increment'=> $increment,
  223. 'dirty'=> false
  224. ];
  225. if(isset($this->columns[$name])){
  226. $old = $this->columns[$name];
  227. foreach($new as $key => $val){
  228. if($key != 'dirty' && $old[$key] !== $val){
  229. $new['dirty'] = true;
  230. break;
  231. }
  232. }
  233. }else{
  234. $new['dirty'] = true;
  235. }
  236. $this->columns[$name] = $new;
  237. return $this;
  238. }else{
  239. return isset($this->columns[$name]) ? $this->columns[$name] : null;
  240. }
  241. }
  242. public function index(string $name, array $columns = null, bool $unique = false){
  243. if(!is_null($columns)){
  244. foreach($columns as $column){
  245. if(!isset($this->columns[$column])){
  246. throw new \Exception("Can't add index. Column {$this->name}.{$column} doesn't exist");
  247. }
  248. }
  249. $this->index[$name] = [
  250. 'columns'=> $columns,
  251. 'unique'=> $unique,
  252. 'dirty'=>true
  253. ];
  254. return $this;
  255. }else{
  256. return isset($this->index[$name]) ? $this->index[$name] : null;
  257. }
  258. }
  259. public function addToIndex(string $name, string $column){
  260. if(!isset($this->index[$name])){
  261. throw new \Exception("Can't add column to index. Index {$this->name}.{$name} doesn't exist");
  262. }
  263. if(!isset($this->columns[$column])){
  264. throw new \Exception("Can't add column to index. Column {$this->name}.{$column} doesn't exist");
  265. }
  266. $this->index[$name]['columns'] = array_merge($this->index[$name]['columns'], [$column]);
  267. }
  268. public function foreignKey(string $name, string $references = null, array $columns = []){
  269. if(!is_null($references)){
  270. $table = $this->pdo->table($references);
  271. if(!$table->exists){
  272. throw new \Exception("Can't create foreign key {$name}. Table {$references} does not exist.");
  273. }
  274. foreach($columns as $column){
  275. if(is_null($this->column($column[0]))){
  276. throw new \Exception("Can't create foreign key {$name}. Column {$this->name}.{$column[0]} does not exist");
  277. }
  278. if(is_null($table->column($column[1]))){
  279. throw new \Exception("Can't create foreign key {$name}. Column {$references}.{$column[1]} does not exist");
  280. }
  281. }
  282. $this->foreignKeys[$name] = [
  283. 'references'=> $references,
  284. 'columns'=> $columns
  285. ];
  286. return $this;
  287. }else{
  288. return isset($this->foreignKeys[$name]) ? $this->foreignKeys[$name] : null;
  289. }
  290. }
  291. public function primaryKey(...$columns){
  292. if(count($columns) > 0){
  293. foreach($columns as $column){
  294. if(!isset($this->columns[$column])){
  295. throw new \Exception("Can't add Primary key. Column {$this->name}.{$column} doesn't exist");
  296. }
  297. }
  298. $this->primaryKey = $columns;
  299. return $this;
  300. }else{
  301. return $this->primaryKey;
  302. }
  303. }
  304. public function insert(array $data){
  305. return $this->exists ? $this->pdo->exec("insert into `{$this->name}` {$this->pdo->stringSet($data)}") : 0;
  306. }
  307. public function update(array $data, array $filter = null){
  308. return $this->exists ? $this->pdo->exec("update `{$this->name} {$this->pdo->stringSet($data)} {$this->pdo->stringFilter($filter)}`") : 0;
  309. }
  310. public function delete(array $filter = null){
  311. return $this->exists ? $this->pdo->exec("delete from `{$this->name}` {$this->pdo->stringFilter($filter)}") : 0;
  312. }
  313. public function fetch(array $filter = null){
  314. if($this->exists){
  315. $query = $this->pdo->query("select * from `{$this->name}` {$this->pdo->stringFilter($filter)}");
  316. $results = $query->fetchAll();
  317. $query->closeCursor();
  318. }else{
  319. $results = [];
  320. }
  321. return $results;
  322. }
  323. public function count(array $filter = null){
  324. return $this->exists ? $this->pdo->exec("select 1 from `{$this->name}` {$this->pdo->stringFilter($filter)}") : 0;
  325. }
  326. }
  327. ?>