Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
96.30% |
78 / 81 |
|
83.33% |
10 / 12 |
CRAP | |
0.00% |
0 / 1 |
| QueryBuilder | |
96.30% |
78 / 81 |
|
83.33% |
10 / 12 |
38 | |
0.00% |
0 / 1 |
| columnDefinition | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| primaryKeyDefinition | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| foreignKeyDefinition | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| isTableExist | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| listTables | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| describeTable | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| columnsByTableDescription | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| createTable | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
6 | |||
| findAll | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| findAllCount | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| fieldNames | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
| select | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
5.27 | |||
| joins | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
| where | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| groupBy | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| orderBy | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
| limit | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
| currentColumn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace Dynart\Micro\Entities; |
| 4 | |
| 5 | use Dynart\Micro\ConfigInterface; |
| 6 | use Dynart\Micro\Entities\Attribute\Column; |
| 7 | |
| 8 | abstract class QueryBuilder { |
| 9 | |
| 10 | const CONFIG_MAX_LIMIT = 'entities.query_builder.max_limit'; |
| 11 | const DEFAULT_MAX_LIMIT = 1000; |
| 12 | |
| 13 | const INDENTATION = ' '; |
| 14 | |
| 15 | private static int $subQueryCounter = 0; |
| 16 | |
| 17 | protected string $currentClassNameForException = ''; |
| 18 | protected string $currentColumnNameForException = ''; |
| 19 | protected int $maxLimit; |
| 20 | |
| 21 | abstract public function columnDefinition(string $columnName, Column $column): string; |
| 22 | abstract public function primaryKeyDefinition(string $className): string; |
| 23 | abstract public function foreignKeyDefinition(string $columnName, Column $column): string; |
| 24 | abstract public function isTableExist(string $dbNameParam, string $tableNameParam): string; |
| 25 | abstract public function listTables(): string; |
| 26 | abstract public function describeTable(string $className): string; |
| 27 | abstract public function columnsByTableDescription(array $data): array; |
| 28 | |
| 29 | public function __construct( |
| 30 | ConfigInterface $config, |
| 31 | protected Database $db, |
| 32 | protected EntityManager $em, |
| 33 | ) { |
| 34 | $this->maxLimit = $config->get(self::CONFIG_MAX_LIMIT, self::DEFAULT_MAX_LIMIT); |
| 35 | } |
| 36 | |
| 37 | public function createTable(string $className, bool $ifNotExists = false): string { |
| 38 | $this->currentClassNameForException = $className; |
| 39 | $allColumnDef = []; |
| 40 | $allForeignKeyDef = []; |
| 41 | foreach ($this->em->tableColumns($className) as $columnName => $column) { |
| 42 | $this->currentColumnNameForException = $columnName; |
| 43 | $allColumnDef[] = self::INDENTATION . $this->columnDefinition($columnName, $column); |
| 44 | $foreignKeyDef = $this->foreignKeyDefinition($columnName, $column); |
| 45 | if ($foreignKeyDef) { |
| 46 | $allForeignKeyDef[] = self::INDENTATION . $foreignKeyDef; |
| 47 | } |
| 48 | } |
| 49 | $primaryKeyDef = $this->primaryKeyDefinition($className); |
| 50 | $safeTableName = $this->em->safeTableName($className); |
| 51 | $result = "create table "; |
| 52 | if ($ifNotExists) { |
| 53 | $result .= "if not exists "; |
| 54 | } |
| 55 | $result .= "$safeTableName (\n"; |
| 56 | $result .= join(",\n", $allColumnDef); |
| 57 | if ($primaryKeyDef) { |
| 58 | $result .= ",\n" . self::INDENTATION . $primaryKeyDef; |
| 59 | } |
| 60 | if (!empty($allForeignKeyDef)) { |
| 61 | $result .= ",\n" . join(",\n", $allForeignKeyDef); |
| 62 | } |
| 63 | $result .= "\n)"; |
| 64 | return $result; |
| 65 | } |
| 66 | |
| 67 | // TODO: public function findAllUnion(array $queries): string |
| 68 | |
| 69 | public function findAll(Query $query, array $fields = []): string { |
| 70 | $sql = $this->select($query, $fields); |
| 71 | $sql .= $this->joins($query); |
| 72 | $sql .= $this->where($query); |
| 73 | $sql .= $this->groupBy($query); |
| 74 | $sql .= $this->orderBy($query); |
| 75 | $sql .= $this->limit($query); |
| 76 | return $sql; |
| 77 | } |
| 78 | |
| 79 | public function findAllCount(Query $query): string { |
| 80 | $sql = $this->select($query, ['c' => ['count(1)']]); |
| 81 | $sql .= $this->joins($query); |
| 82 | $sql .= $this->where($query); |
| 83 | $sql .= $this->groupBy($query); |
| 84 | return $sql; |
| 85 | } |
| 86 | |
| 87 | public function fieldNames(array $fields): array { |
| 88 | $result = []; |
| 89 | foreach ($fields as $as => $name) { |
| 90 | $safeName = is_array($name) ? $name[0] : $this->db->escapeName($name); |
| 91 | if (is_int($as)) { |
| 92 | $result[] = $safeName; |
| 93 | } else { |
| 94 | $result[] = $safeName.' as '.$this->db->escapeName($as); |
| 95 | } |
| 96 | } |
| 97 | return $result; |
| 98 | } |
| 99 | |
| 100 | protected function select(Query $query, array $fields = []): string { |
| 101 | $queryFrom = $query->from(); |
| 102 | $selectFields = $fields ?: $query->fields(); |
| 103 | if (empty($selectFields) && is_string($queryFrom)) { |
| 104 | $selectFields = array_keys($this->em->tableColumns($queryFrom)); |
| 105 | } |
| 106 | if (is_subclass_of($queryFrom, Query::class)) { |
| 107 | self::$subQueryCounter++; // TODO: better solution? |
| 108 | $from = '('.$this->findAll($queryFrom, []).') S'.self::$subQueryCounter; |
| 109 | } else { |
| 110 | $from = $this->em->safeTableName($queryFrom); |
| 111 | } |
| 112 | return 'select '.join(', ', $this->fieldNames($selectFields)).' from '.$from; |
| 113 | } |
| 114 | |
| 115 | protected function joins(Query $query): string { |
| 116 | $joins = []; |
| 117 | foreach ($query->joins() as $join) { |
| 118 | [$type, $from, $condition] = $join; |
| 119 | $fromStr = is_array($from) |
| 120 | ? $this->em->safeTableName($from[0]).' as '.$this->db->escapeName($from[1]) |
| 121 | : $this->em->safeTableName($from); |
| 122 | $joins[] = $type.' join '.$fromStr.' on '.$condition; |
| 123 | } |
| 124 | return $joins ? join("\n", $joins) : ''; |
| 125 | } |
| 126 | |
| 127 | protected function where(Query $query): string { |
| 128 | return empty($query->conditions()) |
| 129 | ? '' |
| 130 | : ' where ('.join(') and (', $query->conditions()).')'; |
| 131 | } |
| 132 | |
| 133 | protected function groupBy(Query $query): string { |
| 134 | return empty($query->groupBy()) ? '' : ' group by '.join(', ', $query->groupBy()); |
| 135 | } |
| 136 | |
| 137 | protected function orderBy(Query $query): string { |
| 138 | $orders = []; |
| 139 | $fieldNames = array_keys($query->fields()); |
| 140 | foreach ($query->orderBy() as $orderBy) { |
| 141 | if (in_array($orderBy[0], $fieldNames)) { |
| 142 | $orders[] = $this->db->escapeName($orderBy[0]).' '.($orderBy[1] == 'desc' ? 'desc' : 'asc'); |
| 143 | } |
| 144 | } |
| 145 | return $orders ? ' order by '.join(', ', $orders) : ''; |
| 146 | } |
| 147 | |
| 148 | protected function limit(Query $query): string { |
| 149 | if ($query->offset() == -1 || $query->max() == -1) { |
| 150 | return ''; |
| 151 | } |
| 152 | $offset = $query->offset(); |
| 153 | $max = $query->max(); |
| 154 | if ($offset < 0) { |
| 155 | $offset = 0; |
| 156 | } |
| 157 | if ($max < 1) { |
| 158 | $max = 1; |
| 159 | } |
| 160 | if ($max > $this->maxLimit) { |
| 161 | $max = $this->maxLimit; |
| 162 | } |
| 163 | return ' limit '.$offset.', '.$max; |
| 164 | } |
| 165 | |
| 166 | protected function currentColumn(): string { |
| 167 | // TODO: better solution? |
| 168 | return $this->currentClassNameForException.'::'.$this->currentColumnNameForException; |
| 169 | } |
| 170 | } |