Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.30% covered (success)
96.30%
78 / 81
83.33% covered (warning)
83.33%
10 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
QueryBuilder
96.30% covered (success)
96.30%
78 / 81
83.33% covered (warning)
83.33%
10 / 12
38
0.00% covered (danger)
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% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createTable
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
6
 findAll
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findAllCount
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 fieldNames
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 select
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
5.27
 joins
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 where
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 groupBy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 orderBy
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 limit
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 currentColumn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Dynart\Micro\Entities;
4
5use Dynart\Micro\ConfigInterface;
6use Dynart\Micro\Entities\Attribute\Column;
7
8abstract 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}