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