1: <?php
2:
3: namespace Charcoal\Source;
4:
5:
6: use \Exception;
7: use \InvalidArgumentException;
8:
9:
10: use \PDO;
11:
12:
13: use \Charcoal\Model\ModelInterface;
14:
15:
16: use \Charcoal\Source\AbstractSource;
17: use \Charcoal\Source\DatabaseSourceConfig;
18: use \Charcoal\Source\DatabaseSourceInterface;
19: use \Charcoal\Source\Database\DatabaseFilter;
20: use \Charcoal\Source\Database\DatabaseOrder;
21: use \Charcoal\Source\Database\DatabasePagination;
22:
23: 24: 25:
26: class DatabaseSource extends AbstractSource implements DatabaseSourceInterface
27: {
28: const DEFAULT_DB_HOSTNAME = 'localhost';
29: const DEFAULT_DB_TYPE = 'mysql';
30:
31: 32: 33:
34: private $pdo;
35:
36: 37: 38:
39: private $table = null;
40:
41: 42: 43:
44: private static $db;
45:
46: 47: 48:
49: public function __construct(array $data)
50: {
51: $this->pdo = $data['pdo'];
52:
53: parent::__construct($data);
54: }
55:
56: 57: 58: 59: 60: 61: 62:
63: public function setTable($table)
64: {
65: if (!is_string($table)) {
66: throw new InvalidArgumentException(
67: sprintf(
68: 'DatabaseSource::setTable() expects a string as table. (%s given). [%s]',
69: gettype($table),
70: get_class($this->model())
71: )
72: );
73: }
74:
75:
76: if (!preg_match('/[A-Za-z0-9_]/', $table)) {
77: throw new InvalidArgumentException(
78: sprintf('Table name "%s" is invalid: must be alphanumeric / underscore.', $table)
79: );
80: }
81: $this->table = $table;
82:
83: return $this;
84: }
85:
86: 87: 88: 89: 90: 91:
92: public function table()
93: {
94: if ($this->table === null) {
95: throw new Exception(
96: 'Table was not set.'
97: );
98: }
99: return $this->table;
100: }
101:
102: 103: 104: 105: 106:
107: public function createTable()
108: {
109: if ($this->tableExists()) {
110:
111: return true;
112: }
113:
114: $dbDriver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
115:
116: $model = $this->model();
117: $metadata = $model->metadata();
118: $fields = $this->getModelFields($model);
119: $fieldsSql = [];
120: foreach ($fields as $field) {
121: $fieldsSql[] = $field->sql();
122: }
123:
124: $q = 'CREATE TABLE `'.$this->table().'` ('."\n";
125: $q .= implode(',', $fieldsSql);
126: $key = $model->key();
127: if ($key) {
128: $q .= ', PRIMARY KEY (`'.$key.'`) '."\n";
129: }
130:
131: if ($dbDriver === 'mysql') {
132: $engine = 'InnoDB';
133: $q .= ') ENGINE='.$engine.' DEFAULT CHARSET=utf8 COMMENT=\''.addslashes($metadata['name']).'\';';
134: } else {
135: $q .= ');';
136: }
137: $this->logger->debug($q);
138: $this->db()->query($q);
139:
140: return true;
141: }
142:
143: 144: 145: 146: 147:
148: public function alterTable()
149: {
150: if (!$this->tableExists()) {
151: return false;
152: }
153:
154: $fields = $this->getModelFields($this->model());
155:
156: $cols = $this->tableStructure();
157:
158: foreach ($fields as $field) {
159: $ident = $field->ident();
160:
161: if (!array_key_exists($ident, $cols)) {
162:
163: $q = 'ALTER TABLE `'.$this->table().'` ADD '.$field->sql();
164: $this->logger->debug($q);
165: $this->db()->query($q);
166: } else {
167:
168: $col = $cols[$ident];
169: $alter = true;
170: if (strtolower($col['Type']) != strtolower($field->sqlType())) {
171: $alter = true;
172: }
173: if ((strtolower($col['Null']) == 'no') && !$field->allowNull()) {
174: $alter = true;
175: }
176: if ((strtolower($col['Null']) != 'no') && $field->allowNull()) {
177: $alter = true;
178: }
179: if ($col['Default'] != $field->defaultVal()) {
180: $alter = true;
181: }
182:
183: if ($alter === true) {
184: $q = 'ALTER TABLE `'.$this->table().'` CHANGE `'.$ident.'` '.$field->sql();
185: $this->logger->debug($q);
186: $this->db()->query($q);
187: }
188: }
189: }
190:
191: return true;
192: }
193:
194: 195: 196:
197: public function tableExists()
198: {
199: $dbDriver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
200: if ($dbDriver === 'sqlite') {
201: $q = 'SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\''.$this->table().'\';';
202: } else {
203: $q = 'SHOW TABLES LIKE \''.$this->table().'\'';
204: }
205: $this->logger->debug($q);
206: $res = $this->db()->query($q);
207: $tableExists = $res->fetchColumn(0);
208:
209:
210: return !!$tableExists;
211: }
212:
213: 214: 215: 216: 217:
218: public function tableStructure()
219: {
220: $dbDriver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
221: if ($dbDriver === 'sqlite') {
222: $q = 'PRAGMA table_info(\''.$this->table().'\') ';
223: } else {
224: $q = 'SHOW COLUMNS FROM `'.$this->table().'`';
225: }
226: $this->logger->debug($q);
227: $res = $this->db()->query($q);
228: $cols = $res->fetchAll((PDO::FETCH_GROUP|PDO::FETCH_UNIQUE|PDO::FETCH_ASSOC));
229: if ($dbDriver === 'sqlite') {
230: $ret = [];
231: foreach ($cols as $c) {
232:
233: $ret[$c['name']] = [
234: 'Type' => $c['type'],
235: 'Null' => !!$c['notnull'] ? 'NO' : 'YES',
236: 'Default' => $c['dflt_value'],
237: 'Key' => !!$c['pk'] ? 'PRI' : '',
238: 'Extra' => ''
239: ];
240: }
241: return $ret;
242: } else {
243: return $cols;
244: }
245: }
246:
247: 248: 249: 250: 251:
252: public function tableIsEmpty()
253: {
254: $q = 'SELECT NULL FROM `'.$this->table().'` LIMIT 1';
255: $this->logger->debug($q);
256: $res = $this->db()->query($q);
257: return ($res->rowCount() === 0);
258: }
259:
260: 261: 262: 263:
264: public function db()
265: {
266: return $this->pdo;
267: }
268:
269: 270: 271: 272: 273: 274: 275: 276:
277: private function getModelFields(ModelInterface $model, $properties = null)
278: {
279: if ($properties === null) {
280:
281: $properties = array_keys($model->metadata()->properties());
282: } else {
283:
284: $properties = array_merge($properties, [ $model->key() ]);
285: }
286:
287: $fields = [];
288: foreach ($properties as $propertyIdent) {
289: $p = $model->p($propertyIdent);
290: if (!$p || !$p->active() || !$p->storable()) {
291: continue;
292: }
293:
294: $v = $model->propertyValue($propertyIdent);
295: foreach ($p->fields($v) as $fieldIdent => $field) {
296: $fields[$field->ident()] = $field;
297: }
298: }
299: return $fields;
300: }
301:
302: 303: 304: 305: 306:
307: public function loadItem($ident, StorableInterface $item = null)
308: {
309: $key = $this->model()->key();
310:
311: return $this->loadItemFromKey($key, $ident, $item);
312: }
313:
314: 315: 316: 317: 318: 319: 320: 321: 322:
323: public function loadItemFromKey($key, $ident, StorableInterface $item = null)
324: {
325: if ($item !== null) {
326: $this->setModel($item);
327: } else {
328: $class = get_class($this->model());
329: $item = new $class;
330: }
331:
332:
333: if (!$key || !$ident) {
334: return $item;
335: }
336:
337: $q = '
338: SELECT
339: *
340: FROM
341: `'.$this->table().'`
342: WHERE
343: `'.$key.'`=:ident
344: LIMIT
345: 1';
346:
347: $binds = [
348: 'ident' => $ident
349: ];
350:
351: return $this->loadItemFromQuery($q, $binds, $item);
352: }
353:
354: 355: 356: 357: 358: 359: 360:
361: public function loadItemFromQuery($query, array $binds = [], StorableInterface $item = null)
362: {
363: if ($item !== null) {
364: $this->setModel($item);
365: } else {
366: $class = get_class($this->model());
367: $item = new $class;
368: }
369:
370:
371: if (!$query) {
372: return $item;
373: }
374:
375: $sth = $this->dbQuery($query, $binds);
376: if ($sth === false) {
377: throw new Exception('Error');
378: }
379:
380: $data = $sth->fetch(PDO::FETCH_ASSOC);
381: if ($data) {
382: $item->setFlatData($data);
383: }
384:
385: return $item;
386: }
387:
388: 389: 390: 391: 392:
393: public function loadItems(StorableInterface $item = null)
394: {
395: if ($item !== null) {
396: $this->setModel($item);
397: }
398:
399: $q = $this->sqlLoad();
400: return $this->loadItemsFromQuery($q, [], $item);
401: }
402:
403: 404: 405: 406: 407: 408: 409: 410: 411:
412: public function loadItemsFromQuery($q, array $binds = [], StorableInterface $item = null)
413: {
414: if ($item !== null) {
415: $this->setModel($item);
416: }
417:
418:
419: $items = [];
420:
421: $model = $this->model();
422: $db = $this->db();
423:
424: $this->logger->debug($q);
425: $sth = $db->prepare($q);
426:
427:
428: if (!empty($binds)) {
429:
430: unset($binds);
431: }
432:
433: $sth->execute();
434: $sth->setFetchMode(PDO::FETCH_ASSOC);
435:
436: $classname = get_class($model);
437: while ($objData = $sth->fetch()) {
438: $obj = new $classname;
439: $obj->setFlatData($objData);
440: $items[] = $obj;
441: }
442:
443: return $items;
444: }
445:
446: 447: 448: 449: 450: 451: 452:
453: public function saveItem(StorableInterface $item)
454: {
455: if ($this->tableExists() === false) {
456:
457: $this->createTable();
458: }
459:
460: if ($item !== null) {
461: $this->setModel($item);
462: }
463: $model = $this->model();
464:
465: $tableStructure = array_keys($this->tableStructure());
466:
467: $fields = $this->getModelFields($model);
468:
469: $keys = [];
470: $values = [];
471: $binds = [];
472: $binds_types = [];
473: foreach ($fields as $f) {
474: $k = $f->ident();
475: if (in_array($k, $tableStructure)) {
476: $keys[] = '`'.$k.'`';
477: $values[] = ':'.$k.'';
478: $binds[$k] = $f->val();
479: $binds_types[$k] = $f->sqlPdoType();
480: }
481: }
482:
483: $q = '
484: INSERT
485: INTO
486: `'.$this->table().'`
487: ('.implode(', ', $keys).')
488: VALUES
489: ('.implode(', ', $values).')';
490:
491: $res = $this->dbQuery($q, $binds, $binds_types);
492:
493: if ($res === false) {
494: throw new Exception(
495: 'Could not save item.'
496: );
497: } else {
498: if ($model->id()) {
499: return $model->id();
500: } else {
501: return $this->db()->lastInsertId();
502: }
503: }
504: }
505:
506: 507: 508: 509: 510: 511: 512:
513: public function updateItem(StorableInterface $item, array $properties = null)
514: {
515: if ($item !== null) {
516: $this->setModel($item);
517: }
518: $model = $this->model();
519:
520: $tableStructure = array_keys($this->tableStructure());
521: $fields = $this->getModelFields($model, $properties);
522:
523: $updates = [];
524: $binds = [];
525: $binds_types = [];
526: foreach ($fields as $f) {
527: $k = $f->ident();
528: if (in_array($k, $tableStructure)) {
529: if ($k !== $model->key()) {
530: $updates[] = '`'.$k.'` = :'.$k;
531: }
532: $binds[$k] = $f->val();
533: $binds_types[$k] = $f->sqlPdoType();
534: } else {
535: $this->logger->debug(
536: sprintf('Field %s not in table structure', $k)
537: );
538: }
539: }
540: if (empty($updates)) {
541: $this->logger->warning('Could not update items. No valid fields were set / available in database table.', [
542: 'properties' => $properties,
543: 'structure' => $tableStructure
544: ]);
545: return false;
546: }
547:
548: $binds[$model->key()] = $model->id();
549: $binds_types[$model->key()] = PDO::PARAM_STR;
550:
551: $q = '
552: UPDATE
553: `'.$this->table().'`
554: SET
555: '.implode(", \n\t", $updates).'
556: WHERE
557: `'.$model->key().'`=:'.$model->key().'';
558:
559: $dbDriver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
560: if ($dbDriver == 'mysql') {
561: $q .= "\n".'LIMIT 1';
562: }
563:
564: $res = $this->dbQuery($q, $binds, $binds_types);
565:
566: if ($res === false) {
567: return false;
568: } else {
569: return true;
570: }
571: }
572:
573: 574: 575: 576: 577: 578: 579:
580: public function deleteItem(StorableInterface $item = null)
581: {
582: if ($item !== null) {
583: $this->setModel($item);
584: }
585: $model = $this->model();
586:
587: if (!$model->id()) {
588: throw new Exception(
589: sprintf('Can not delete "%s" item. No ID.', get_class($this))
590: );
591: }
592:
593: $q = '
594: DELETE FROM
595: `'.$this->table().'`
596: WHERE
597: `'.$model->key().'` = :id';
598:
599: $dbDriver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
600: if ($dbDriver == 'mysql') {
601: $q .= "\n".'LIMIT 1';
602: }
603:
604: $binds = [
605: 'id' => $model->id()
606: ];
607:
608: $res = $this->dbQuery($q, $binds);
609:
610: if ($res === false) {
611: return false;
612: } else {
613: return true;
614: }
615: }
616:
617: 618: 619: 620: 621: 622: 623: 624: 625: 626:
627: public function dbQuery($q, array $binds = [], array $binds_types = [])
628: {
629: $this->logger->debug($q, $binds);
630: $sth = $this->db()->prepare($q);
631: if (!$sth) {
632: return false;
633: }
634: if (!empty($binds)) {
635: foreach ($binds as $k => $v) {
636: if ($binds[$k] === null) {
637: $binds_types[$k] = PDO::PARAM_NULL;
638: } elseif (!is_scalar($binds[$k])) {
639: $binds[$k] = json_encode($binds[$k]);
640: }
641: $type = (isset($binds_types[$k]) ? $binds_types[$k] : PDO::PARAM_STR);
642: $sth->bindParam(':'.$k, $binds[$k], $type);
643: }
644: }
645:
646: $ret = $sth->execute();
647: if ($ret === false) {
648: return false;
649: }
650:
651: return $sth;
652: }
653:
654: 655: 656: 657:
658: public function sqlLoad()
659: {
660: $table = $this->table();
661: if (!$table) {
662: throw new Exception(
663: 'Can not get SQL. No table defined.'
664: );
665: }
666:
667: $selects = $this->sqlSelect();
668: $tables = '`'.$table.'` AS objTable';
669: $filters = $this->sqlFilters();
670: $orders = $this->sqlOrders();
671: $limits = $this->sqlPagination();
672:
673: $q = 'SELECT '.$selects.' FROM '.$tables.$filters.$orders.$limits;
674: return $q;
675: }
676:
677: 678: 679: 680: 681: 682:
683: public function sqlLoadCount()
684: {
685: $table = $this->table();
686: if (!$table) {
687: throw new Exception(
688: 'Can not get count SQL. No table defined.s'
689: );
690: }
691:
692: $tables = '`'.$table.'` AS objTable';
693: $filters = $this->sqlFilters();
694: $q = 'SELECT COUNT(*) FROM '.$tables.$filters;
695: return $q;
696: }
697:
698: 699: 700:
701: public function sqlSelect()
702: {
703: $properties = $this->properties();
704: if (empty($properties)) {
705: return 'objTable.*';
706: }
707:
708: $sql = '';
709: $propsSql = [];
710: foreach ($properties as $p) {
711: $propsSql[] = 'objTable.`'.$p.'`';
712: }
713: if (!empty($propsSql)) {
714: $sql = implode(', ', $propsSql);
715: }
716:
717: return $sql;
718: }
719:
720: 721: 722: 723:
724: public function sqlFilters()
725: {
726: $sql = '';
727:
728: $filters = $this->filters();
729: if (empty($filters)) {
730: return '';
731: }
732:
733:
734: $filtersSql = [];
735: foreach ($filters as $f) {
736: $fSql = $f->sql();
737: if ($fSql) {
738: $filtersSql[] = [
739: 'sql' => $f->sql(),
740: 'operand' => $f->operand()
741: ];
742: }
743: }
744: if (empty($filtersSql)) {
745: return '';
746: }
747:
748: $sql .= ' WHERE';
749: $i = 0;
750:
751: foreach ($filtersSql as $f) {
752: if ($i > 0) {
753: $sql .= ' '.$f['operand'];
754: }
755: $sql .= ' '.$f['sql'];
756: $i++;
757: }
758: return $sql;
759: }
760:
761: 762: 763:
764: public function sqlOrders()
765: {
766: $sql = '';
767:
768: if (!empty($this->orders)) {
769: $ordersSql = [];
770: foreach ($this->orders as $o) {
771: $ordersSql[] = $o->sql();
772: }
773: if (!empty($ordersSql)) {
774: $sql = ' ORDER BY '.implode(', ', $ordersSql);
775: }
776: }
777:
778: return $sql;
779: }
780:
781: 782: 783:
784: public function sqlPagination()
785: {
786: return $this->pagination()->sql();
787: }
788:
789: 790: 791:
792: protected function createFilter()
793: {
794: $filter = new DatabaseFilter();
795: return $filter;
796: }
797:
798: 799: 800:
801: protected function createOrder()
802: {
803: $order = new DatabaseOrder();
804: return $order;
805: }
806:
807: 808: 809:
810: protected function createPagination()
811: {
812: $pagination = new DatabasePagination();
813: return $pagination;
814: }
815:
816: 817: 818: 819: 820: 821: 822: 823:
824: public function createConfig(array $data = null)
825: {
826: $config = new DatabaseSourceConfig($data);
827: return $config;
828: }
829: }
830: