Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.58% covered (success)
96.58%
113 / 117
96.55% covered (success)
96.55%
28 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
Base
96.58% covered (success)
96.58%
113 / 117
96.55% covered (success)
96.55%
28 / 29
54
0.00% covered (danger)
0.00%
0 / 1
 __construct
88.24% covered (warning)
88.24%
30 / 34
0.00% covered (danger)
0.00%
0 / 1
10.16
 __toString
n/a
0 / 0
n/a
0 / 0
4
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 addSelect
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setDistinctSelect
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setResolveLevel
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getResolveLevel
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getAlias
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getTablename
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addTable
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 addTableAlias
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 addRequiredJoins
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addResolvedReferences
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 setWithEntities
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 leftJoin
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 innerJoin
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getSql
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getParameters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReferenceMapping
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 expression
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addEntityMapping
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getPrefixed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrefixedList
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addReferenceMapping
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addLimit
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addValues
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 postProcess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 postProcessJoins
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 shouldLoadEntity
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace BO\Zmsbackend\Query;
4
5use BO\Zmsbackend\Query\Builder\Select;
6use BO\Zmsbackend\Query\Builder\Insert;
7use BO\Zmsbackend\Query\Builder\Update;
8use BO\Zmsbackend\Query\Builder\Delete;
9use BO\Zmsbackend\Query\Builder\Dialect\MySQL;
10use BO\Zmsbackend\Query\Builder\Expression;
11use BO\Zmsbackend\Query\Builder\Query as QueryBuilder;
12
13/**
14 * Base class to construct entity specific queries
15 * Usually used with the interface MappingInterface
16 * Further, it allows to react to resolveReferences as parameter to calling methods
17 */
18
19/**
20 * @SuppressWarnings(NumberOfChildren)
21 * @SuppressWarnings(Complexity)
22 *
23 */
24abstract class Base
25{
26    /**
27     * Identifier for the type of query
28     */
29    const string SELECT = 'SELECT';
30    const string INSERT = 'INSERT';
31    const string UPDATE = 'UPDATE';
32    const string REPLACE = 'REPLACE';
33    const string DELETE = 'DELETE';
34
35    /**
36     * Name of table in DB
37     */
38    const TABLE = null;
39    /**
40     * Alias used to access TABLE
41     */
42    const ALIAS = null;
43
44    /**
45     * Concrete builder instance (Select|Insert|Update|Delete), always set in the constructor.
46     */
47    protected QueryBuilder $query;
48
49    /**
50     * @var String $query
51     */
52    protected $prefix = '';
53
54    /**
55     * Name of the query used for caching
56     *
57     */
58    protected $name = false;
59
60    /**
61     * Level given ususally by parameter resolveReferences
62     *
63     */
64    protected $resolveLevel = null;
65
66    protected static $sqlCache = [];
67
68    protected string|null $currentSqlString = null;
69
70    /**
71     * List of joined aliasnames to avoid double joins
72     *
73     * @var list<string>
74     */
75    protected array $joinedAliasList = [];
76
77    /**
78     * List of joined queries to avoid double joins
79     *
80     * @var list<self>
81     */
82    protected array $joinedQueryList = [];
83
84    /** @var list<string> */
85    protected array $withEntities = [];
86
87    /**
88     * Create query builder if necessary
89     *
90     * @param Mixed $queryType one of the constants for a query type or of instance \BO\Zmsbackend\Query\Builder\Query
91     * @param String $prefix If used in a subquery, prefix results with this string
92     * @param string|false $name A named query has a cached SQL as soon as called first
93     * @param int|null $resolveLevel
94     * @param array $withEntities
95     */
96    public function __construct(
97        $queryType,
98        $prefix = '',
99        string|false $name = false,
100        mixed $resolveLevel = null,
101        array $withEntities = []
102    ) {
103        $this->prefix = $prefix;
104        $this->name = $name;
105        $this->withEntities = $withEntities;
106        $this->setResolveLevel($resolveLevel);
107        $dialect = new MySQL();
108        if (self::SELECT === $queryType) {
109            $this->query = new Select($dialect);
110            $this->addSelect();
111        } elseif (self::INSERT === $queryType) {
112            $this->query = new Insert($dialect);
113            $this->addTable();
114        } elseif (self::UPDATE === $queryType) {
115            $this->query = new Update($dialect);
116            $this->addTableAlias();
117        } elseif (self::REPLACE === $queryType) {
118            $this->query = new INSERT($dialect);
119            $this->query->queryBaseStatement('REPLACE INTO');
120            $this->addTable();
121        } elseif (self::DELETE === $queryType) {
122            $this->query = new Delete($dialect);
123            $this->query->queryBaseStatement('DELETE ' . $this::getAlias() . ' FROM');
124            $this->addTableAlias();
125        } elseif ($queryType instanceof self) {
126            $this->query = $queryType->query;
127            // Share join-alias state with the parent query (intentional by-ref).
128            /** @psalm-suppress UnsupportedPropertyReferenceUsage */
129            $this->joinedAliasList =& $queryType->joinedAliasList;
130            $this->resolveLevel = $queryType->resolveLevel - 1;
131        } elseif ($queryType instanceof QueryBuilder) {
132            $this->query = $queryType;
133        } else {
134            throw new \InvalidArgumentException(
135                'Unsupported query type for ' . static::class . ': '
136                . (is_object($queryType) ? $queryType::class : gettype($queryType))
137            );
138        }
139        if ($this->query instanceof Select) {
140            $this->addRequiredJoins();
141        }
142    }
143
144    /**
145     * @codeCoverageIgnore
146     */
147    public function __toString()
148    {
149        if ($this->name) {
150            $name = $this->name . '_' . $this->prefix . (string) $this->resolveLevel;
151            if (!isset(static::$sqlCache[$name])) {
152                static::$sqlCache[$name] = $this->getSql();
153            }
154            return static::$sqlCache[$name];
155        }
156        if ($this->currentSqlString) {
157            $sql = $this->currentSqlString;
158        } else {
159            $sql = $this->getSql();
160        }
161        return $sql;
162    }
163
164    public function getName()
165    {
166        return $this->name ? $this->name : get_class($this);
167    }
168
169    /**
170     * Add the from part to the queryBaseStatement
171     * This implementation tries to guess the syntax using the constant TABLE in the class
172     * Override the method for a special implementation or required joins
173     */
174    protected function addSelect(): static
175    {
176        $table = $this::getTablename();
177        $alias = $this::getAlias();
178        $this->query->from($table, $alias);
179        return $this;
180    }
181
182    public function setDistinctSelect(): void
183    {
184        $this->query->queryBaseStatement('SELECT DISTINCT');
185    }
186
187    public function setResolveLevel(int|null $resolveLevel): static
188    {
189        if ($resolveLevel !== null) {
190            $this->resolveLevel = $resolveLevel;
191        }
192        return $this;
193    }
194
195    public function getResolveLevel()
196    {
197        if (null === $this->resolveLevel) {
198            throw new \Exception("Required setting for resolveReferenceLevel missing in " . get_class($this));
199        }
200        return $this->resolveLevel;
201    }
202
203    /**
204     * Add the alias part to the queryBaseStatement
205     * This implementation tries to guess the syntax using the constant TABLE in the class
206     * Override the method for a special implementation or required joins
207     *
208     * @return self
209     */
210    public static function getAlias()
211    {
212        $class = get_called_class();
213        $alias = constant($class . '::ALIAS');
214        if (null === $alias) {
215            $alias = lcfirst(preg_replace('#^.*\\\#', '', $class));
216        }
217        return $alias;
218    }
219
220    /**
221     * Get the table name for the query
222     *
223     * @return string
224     */
225    public static function getTablename()
226    {
227        $class = get_called_class();
228        $table = constant($class . '::TABLE');
229        return $table;
230    }
231
232    /**
233     * Add the from part to the queryBaseStatement
234     * This implementation tries to guess the syntax using the constant TABLE in the class
235     * Override the method for a special implementation or required joins
236     */
237    protected function addTable(): static
238    {
239        $table = $this::getTablename();
240        $alias = $this::getAlias();
241        $this->query->table($table, $alias);
242        return $this;
243    }
244
245    /**
246     * Add the from part to the queryBaseStatement
247     * This implementation tries to guess the syntax using the constant TABLE in the class
248     * Override the method for a special implementation or required joins
249     */
250    protected function addTableAlias(): static
251    {
252        $table = $this::getTablename();
253        $alias = $this::getAlias();
254        $this->query->table(self::expression($table . ' ' . $alias));
255        return $this;
256    }
257
258    /**
259     * Add joins to table if required
260     * Override this method if join are required for a select
261     *
262     * @return void
263     */
264    protected function addRequiredJoins()
265    {
266    }
267
268    /**
269     * resolves references by joining tables defined in the method addJoin()
270     *
271     * @param  Int $depth Number of levels of sub references to resolve
272     */
273    public function addResolvedReferences($depth): static
274    {
275        $this->setResolveLevel($depth);
276        if ($depth > 0) {
277            $queryList = $this->addJoin();
278            foreach ($queryList as $query) {
279                $query->setResolveLevel($depth);
280                $query->setWithEntities($this->withEntities);
281                $query->addResolvedReferences($depth - 1);
282                $query->addEntityMapping();
283            }
284            $this->joinedQueryList = $queryList;
285        } else {
286            $this->addReferenceMapping();
287        }
288        return $this;
289    }
290
291    /**
292     * @psalm-api
293     */
294    public function setWithEntities($withEntities = []): void
295    {
296        $this->withEntities = $withEntities;
297    }
298
299    /**
300     * If resolveReferences is required, override this method
301     *
302     * @return Array of self
303     */
304    protected function addJoin()
305    {
306        return [];
307    }
308
309    protected function leftJoin(Alias $alias, string|Expression|null $left = null, string|null $operator = null, string|null $right = null): QueryBuilder
310    {
311        $aliasId = $alias->getAliasIdentifier();
312        if (!in_array($aliasId, $this->joinedAliasList)) {
313            $this->joinedAliasList[] = $aliasId;
314            $this->query->leftJoin($alias, $left, $operator, $right);
315        } else {
316            //throw new \Exception("Tried to add Alias ".$aliasId);
317        }
318        return $this->query;
319    }
320
321    protected function innerJoin(Alias $alias, string|null $left = null, string|null $operator = null, string|null $right = null): QueryBuilder
322    {
323        $aliasId = $alias->getAliasIdentifier();
324        if (!in_array($aliasId, $this->joinedAliasList)) {
325            $this->joinedAliasList[] = $aliasId;
326            $this->query->join($alias, $left, $operator, $right);
327        }
328        return $this->query;
329    }
330
331    /**
332     * get SQL-String
333     * Implement a simple caching routine to prevent multiple rebuilds
334     *
335     * @return string
336     */
337    public function getSql()
338    {
339        $this->currentSqlString = (string)$this->query;
340        return $this->currentSqlString;
341    }
342
343    /**
344     * List of parameters to use for a prepared statement
345     *
346     * @return Array
347     */
348    public function getParameters()
349    {
350        return $this->query->params();
351    }
352
353    /**
354     * @return array
355     *
356     */
357    public function getReferenceMapping()
358    {
359        return [
360        ];
361    }
362
363    /**
364     * Shortcut to create an SQL-Expression without quoting
365     *
366     * @return \BO\Zmsbackend\Query\Builder\Expression
367     */
368    protected static function expression(string $string)
369    {
370        return new Expression($string);
371    }
372
373    /**
374     * Add a select part to the query containing a mapping from the db schema to the entity schema
375     *
376     * @param mixed $type
377     * @return static
378     */
379    public function addEntityMapping(mixed $type = null): static
380    {
381        // Concrete query classes provide getEntityMapping(); not all declare MappingInterface.
382        /** @psalm-suppress UndefinedMethod, TooManyArguments */
383        $entityMapping = $this->getPrefixedList($this->getEntityMapping($type));
384        $this->query->select($entityMapping);
385        return $this;
386    }
387
388    protected function getPrefixed($prefix): string
389    {
390        return $this->prefix . $prefix;
391    }
392
393    protected function getPrefixedList(array $unprefixedList): array
394    {
395        $prefixed = [];
396        foreach ($unprefixedList as $key => $value) {
397            $prefixed[$this->getPrefixed($key)] = $value;
398        }
399        return $prefixed;
400    }
401
402    /**
403     * Add a select part to the query containing references if no resolveReferences is given
404     */
405    protected function addReferenceMapping(): static
406    {
407        $referenceMapping = $this->getPrefixedList($this->getReferenceMapping());
408        $this->query->select($referenceMapping);
409        return $this;
410    }
411
412    public function addLimit($count, $offset = null): static
413    {
414        $this->query->limit($count);
415        if ($offset) {
416            $this->query->offset($offset);
417        }
418        return $this;
419    }
420
421    /**
422     * Add values to a insert or update query
423     */
424    public function addValues(array $values): static
425    {
426        $this->query->values($values);
427        return $this;
428    }
429
430    /**
431     * postProcess data if necessary
432     *
433     */
434    public function postProcess($data)
435    {
436        return $data;
437    }
438
439    /**
440     * postProcess data including joined queries if necessary
441     *
442     * @param (null|scalar)[] $data
443     *
444     */
445    public function postProcessJoins(array $data)
446    {
447        $data = $this->postProcess($data);
448        foreach ($this->joinedQueryList as $query) {
449            $data = $query->postProcess($data);
450        }
451        return $data;
452    }
453
454    public function shouldLoadEntity(string $name): bool
455    {
456        if (empty($this->withEntities)) {
457            return true;
458        }
459
460        return in_array($name, $this->withEntities);
461    }
462}