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