Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.87% covered (warning)
78.87%
56 / 71
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
GrammarAbstract
78.87% covered (warning)
78.87%
56 / 71
37.50% covered (danger)
37.50%
3 / 8
46.55
0.00% covered (danger)
0.00%
0 / 1
 setDateTimeFormat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 compileQuery
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 compilePostQuerys
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 compileComponents
n/a
0 / 0
n/a
0 / 0
0
 getDateFormat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expressionizeTableColumn
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
14.01
 compileSystem
89.47% covered (warning)
89.47%
17 / 19
0.00% covered (danger)
0.00%
0 / 1
5.03
 compileValue
80.65% covered (warning)
80.65%
25 / 31
0.00% covered (danger)
0.00%
0 / 1
16.63
 compileColumnQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\DataStorage\Database
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\DataStorage\Database;
16
17use phpOMS\Contract\SerializableInterface;
18use phpOMS\DataStorage\Database\Query\Column;
19use phpOMS\DataStorage\Database\Query\Parameter;
20
21/**
22 * Grammar.
23 *
24 * @package phpOMS\DataStorage\Database
25 * @license OMS License 2.0
26 * @link    https://jingga.app
27 * @since   1.0.0
28 */
29abstract class GrammarAbstract
30{
31    /**
32     * Comment style.
33     *
34     * @var string
35     * @since 1.0.0
36     */
37    public string $comment = '--';
38
39    /**
40     * String quotes style.
41     *
42     * @var string
43     * @since 1.0.0
44     */
45    public string $valueQuotes = '\'';
46
47    /**
48     * System identifier.
49     *
50     * @var string
51     * @since 1.0.0
52     */
53    public string $systemIdentifierStart = '"';
54
55    /**
56     * System identifier.
57     *
58     * @var string
59     * @since 1.0.0
60     */
61    public string $systemIdentifierEnd = '"';
62
63    /**
64     * And operator.
65     *
66     * @var string
67     * @since 1.0.0
68     */
69    public string $and = 'AND';
70
71    /**
72     * Or operator.
73     *
74     * @var string
75     * @since 1.0.0
76     */
77    public string $or = 'OR';
78
79    /**
80     * Special keywords.
81     *
82     * @var string[]
83     * @since 1.0.0
84     */
85    public array $specialKeywords = [
86        'COUNT(',
87        'MAX(',
88        'MIN(',
89        'SUM(',
90        'DATE(',
91        'YEAR(',
92        'MONTH(',
93    ];
94
95    /**
96     * Datetime format.
97     *
98     * @var string
99     * @since 1.0.0
100     */
101    public string $datetimeFormat = 'Y-m-d H:i:s';
102
103    /**
104     * Set the datetime format
105     *
106     * @param string $format Datetime format
107     *
108     * @return void
109     *
110     * @since 1.0.0
111     */
112    public function setDateTimeFormat(string $format) : void
113    {
114        $this->datetimeFormat = $format;
115    }
116
117    /**
118     * Compile to query.
119     *
120     * @param BuilderAbstract $query Builder
121     *
122     * @return string
123     *
124     * @since 1.0.0
125     */
126    public function compileQuery(BuilderAbstract $query) : string
127    {
128        $components  = $this->compileComponents($query);
129        $queryString = '';
130
131        foreach ($components as $component) {
132            if ($component !== '') {
133                $queryString .= $component . ' ';
134            }
135        }
136
137        return \substr($queryString, 0, -1) . ';';
138    }
139
140    /**
141     * Compile post querys.
142     *
143     * These are queries, which should be run after the main query (e.g. table alters, trigger definitions etc.)
144     *
145     * @param BuilderAbstract $query Builder
146     *
147     * @return string[]
148     *
149     * @since 1.0.0
150     */
151    public function compilePostQuerys(BuilderAbstract $query) : array
152    {
153        return [];
154    }
155
156    /**
157     * Compile components.
158     *
159     * @param BuilderAbstract $query Builder
160     *
161     * @return string[]
162     *
163     * @throws \InvalidArgumentException
164     *
165     * @since 1.0.0
166     */
167    abstract protected function compileComponents(BuilderAbstract $query) : array;
168
169    /**
170     * Get date format.
171     *
172     * @return string
173     *
174     * @since 1.0.0
175     */
176    public function getDateFormat() : string
177    {
178        return 'Y-m-d H:i:s';
179    }
180
181    /**
182     * Expressionize elements.
183     *
184     * @param array $elements Elements
185     * @param bool  $column   Is column?
186     *
187     * @return string
188     *
189     * @throws \InvalidArgumentException
190     *
191     * @since 1.0.0
192     */
193    protected function expressionizeTableColumn(array $elements, bool $column = true) : string
194    {
195        $expression = '';
196
197        foreach ($elements as $key => $element) {
198            if (\is_string($element)) {
199                $expression .= $this->compileSystem($element)
200                    . (\is_string($key) ? ' as ' . $key : '') . ', ';
201            } elseif ($element instanceof \Closure) {
202                $expression .= $element() . (\is_string($key) ? ' as ' . $key : '') . ', ';
203            } elseif ($element instanceof BuilderAbstract) {
204                $expression .= $element->toSql() . (\is_string($key) ? ' as ' . $key : '') . ', ';
205            } else {
206                throw new \InvalidArgumentException();
207            }
208        }
209
210        return \rtrim($expression, ', ');
211    }
212
213    /**
214     * Compile system.
215     *
216     * A system is a table, a sub query or special keyword.
217     *
218     * @param string $system System
219     *
220     * @return string
221     *
222     * @since 1.0.0
223     */
224    protected function compileSystem(string $system) : string
225    {
226        $identifierStart = $this->systemIdentifierStart;
227        $identifierEnd   = $this->systemIdentifierEnd;
228
229        if (\stripos($system, '(') !== false) {
230            $identifierStart = '';
231            $identifierEnd   = '';
232        }
233
234        // The following code could have been handled with \explode more elegantly but \explode needs more memory and more time
235        // Normally this wouldn't be a problem but in this case there are so many function calls to this routine,
236        // that it makes sense to make this "minor" improvement.
237        if (($pos = \stripos($system, '.')) !== false) {
238            $split = [\substr($system, 0, $pos), \substr($system, $pos + 1)];
239
240            $identifierTwoStart = $identifierStart;
241            $identifierTwoEnd   = $identifierEnd;
242
243            if ($split[1] === '*') {
244                $identifierTwoStart = '';
245                $identifierTwoEnd   = '';
246            }
247
248            return $identifierStart . $split[0] . $identifierEnd
249                . '.'
250                . $identifierTwoStart . $split[1] . $identifierTwoEnd;
251        }
252
253        if ($system === '*') {
254            $identifierStart = '';
255            $identifierEnd   = '';
256        }
257
258        return $identifierStart . $system . $identifierEnd;
259    }
260
261    /**
262     * Compile value.
263     *
264     * @param BuilderAbstract $query Query builder
265     * @param mixed           $value Value
266     *
267     * @return string returns a string representation of the value
268     *
269     * @throws \InvalidArgumentException throws this exception if the value to compile is not supported by this function
270     *
271     * @since 1.0.0
272     */
273    protected function compileValue(BuilderAbstract $query, mixed $value) : string
274    {
275        if (\is_string($value)) {
276            return $query->quote($value);
277        } elseif (\is_int($value)) {
278            return (string) $value;
279        } elseif (\is_array($value)) {
280            $value  = \array_values($value);
281            $count  = \count($value) - 1;
282            $values = '(';
283
284            for ($i = 0; $i < $count; ++$i) {
285                $values .= $this->compileValue($query, $value[$i]) . ', ';
286            }
287
288            return $values . $this->compileValue($query, $value[$count]) . ')';
289        } elseif ($value instanceof \DateTime) {
290            return $query->quote($value->format($this->datetimeFormat));
291        } elseif ($value === null) {
292            return 'NULL';
293        } elseif (\is_bool($value)) {
294            return (string) ((int) $value);
295        } elseif (\is_float($value)) {
296            return \rtrim(\rtrim(\number_format($value, 5, '.', ''), '0'), '.');
297        } elseif ($value instanceof Column) {
298            return '(' . \rtrim($this->compileColumnQuery($value), ';') . ')';
299        } elseif ($value instanceof BuilderAbstract) {
300            return '(' . \rtrim($value->toSql(), ';') . ')';
301        } elseif ($value instanceof \JsonSerializable) {
302            $encoded = \json_encode($value);
303
304            return $encoded ? $encoded : 'NULL';
305        } elseif ($value instanceof SerializableInterface) {
306            return $value->serialize();
307        } elseif ($value instanceof Parameter) {
308            return $value->__toString();
309        } else {
310            throw new \InvalidArgumentException(\gettype($value));
311        }
312    }
313
314    /**
315     * Compile column query.
316     *
317     * @param Column $column Where query
318     *
319     * @return string
320     *
321     * @since 1.0.0
322     */
323    protected function compileColumnQuery(Column $column) : string
324    {
325        return $column->toSql();
326    }
327}