Query::__construct()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 9.6666
cc 2
eloc 5
nc 2
nop 2
crap 2
1
<?php
2
3
/*
4
 * This file is part of the "RocketORM" package.
5
 *
6
 * https://github.com/RocketORM/ORM
7
 *
8
 * For the full license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Rocket\ORM\Model\Query;
13
14
use Rocket\ORM\Model\Map\TableMapInterface;
15
use Rocket\ORM\Model\Object\RocketObject;
16
use Rocket\ORM\Model\Query\Exception\RelationAliasNotFoundException;
17
use Rocket\ORM\Model\Query\Exception\RelationNotFoundException;
18
use Rocket\ORM\Model\Query\Hydrator\QueryHydratorInterface;
19
use Rocket\ORM\Model\Query\Hydrator\SimpleQueryHydrator;
20
use Rocket\ORM\Model\Query\Hydrator\ComplexQueryHydrator;
21
use Rocket\ORM\Rocket;
22
23
/**
24
 * @author Sylvain Lorinet <[email protected]>
25
 */
26
abstract class Query implements QueryInterface
27
{
28
    const JOIN_TYPE_INNER = 'INNER';
29
    const JOIN_TYPE_LEFT  = 'LEFT';
30
31
    /**
32
     * @var string
33
     */
34
    protected $alias;
35
36
    /**
37
     * @var string
38
     */
39
    protected $modelNamespace;
40
41
    /**
42
     * @var TableMapInterface
43
     */
44
    protected $tableMap;
45
46
    /**
47
     * @var array
48
     */
49
    protected $clauses = [];
50
51
    /**
52
     * @var int
53
     */
54
    protected $limit;
55
56
    /**
57
     * @var int
58
     */
59
    protected $offset;
60
61
    /**
62
     * @var array
63
     */
64
    protected $joins = [];
65
66
    /**
67
     * @var array All related tables that will inserted in the SELECT statement
68
     */
69
    protected $with = [];
70
71
72
    /**
73
     * @param string $alias
74
     * @param string $modelNamespace
75
     */
76 15
    public function __construct($alias, $modelNamespace)
77
    {
78 15
        if (null == $alias) {
79 1
            throw new \LogicException('The "' . get_called_class() . '" alias can be null');
80
        }
81
82 14
        $this->alias          = $alias;
83 14
        $this->modelNamespace = $modelNamespace;
84 14
    }
85
86
    /**
87
     * @param string     $clause
88
     * @param null|mixed $value  Can't be an array
89
     *
90
     * @return $this
91
     */
92 6
    public function where($clause, $value = null)
93
    {
94 6
        $this->doWhere($clause, $value, 'AND');
95
96 6
        return $this;
97
    }
98
99
    /**
100
     * @param string     $clause
101
     * @param null|mixed $value
102
     *
103
     * @return $this|Query
104
     *
105
     * @throws \Exception
106
     */
107 1
    public function orWhere($clause, $value = null)
108
    {
109 1
        $this->doWhere($clause, $value, 'OR');
110
111 1
        return $this;
112
    }
113
114
    /**
115
     * @param string $clause
116
     * @param mixed  $value
117
     * @param string $operator
118
     */
119 6
    protected function doWhere($clause, $value, $operator)
120
    {
121 6
        $this->clauses[] = [
122 6
            'clause'   => $clause,
123 6
            'value'    => $value,
124
            'operator' => $operator
125 6
        ];
126 6
    }
127
128
    /**
129
     * @param int      $limit
130
     * @param null|int $offset
131
     *
132
     * @return $this|Query
133
     */
134 4
    public function limit($limit, $offset = null)
135
    {
136 4
        $this->limit = $limit;
137 4
        if (null != $offset) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $offset of type null|integer against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
138 2
            $this->offset = $offset;
139 2
        }
140
141 4
        return $this;
142
    }
143
144
    /**
145
     * @param \PDO $con
0 ignored issues
show
Documentation introduced by
Should the type for parameter $con not be null|\PDO?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
146
     *
147
     * @return RocketObject|null
148
     */
149 1
    public function findOne(\PDO $con = null)
150
    {
151 1
        $this->limit(1);
152
153 1
        $objects = $this->find($con);
154
155 1
        if (isset($objects[0])) {
156 1
            return $objects[0];
157
        }
158
159 1
        return null;
160
    }
161
162
    /**
163
     * @param string      $relation
164
     * @param null|string $alias
165
     *
166
     * @return $this|Query
167
     *
168
     * @throws RelationNotFoundException
169
     * @throws RelationAliasNotFoundException
170
     */
171 3
    public function innerJoinWith($relation, $alias = null)
172
    {
173 3
        return $this->join($relation, $alias, self::JOIN_TYPE_INNER, true);
174
    }
175
176
    /**
177
     * @param string      $relation
178
     * @param null|string $alias
179
     *
180
     * @return $this|Query
181
     *
182
     * @throws RelationNotFoundException
183
     * @throws RelationAliasNotFoundException
184
     */
185 1
    public function leftJoinWith($relation, $alias = null)
186
    {
187 1
        return $this->join($relation, $alias, self::JOIN_TYPE_LEFT, true);
188
    }
189
190
    /**
191
     * @param string $relation
192
     * @param string $alias
193
     * @param string $joinType
194
     * @param bool   $with
195
     *
196
     * @return $this|Query
197
     *
198
     * @throws RelationNotFoundException
199
     */
200 4
    protected function join($relation, $alias, $joinType = self::JOIN_TYPE_INNER, $with = false)
201
    {
202 4
        $relationTable = $relation;
203 4
        $from = null;
204
205
        // Separate the link alias if exists : "From.Relation", keep "From" & "Relation"
206 4
        $pos = strpos($relation, '.');
207 4
        if (false !== $pos) {
208 3
            $relationTable = substr($relation, $pos + 1);
209 3
            $from = substr($relation, 0, $pos);
210 3
        }
211
212 4
        if (null == $alias) {
213 3
            $alias = $relationTable;
214 3
        }
215
216 4
        $tableMap = $this->getTableMap();
217 4
        $hasRelation = $tableMap->hasRelation($relationTable);
218
219 4
        if (!$hasRelation || $hasRelation && null !== $from && $this->alias !== $from) {
220 4
            if (null == $from) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $from of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
221 1
                throw new RelationNotFoundException(
222 1
                    'Unknown relation with "' . $relation . '" for model "' . $this->modelNamespace . '"'
223 1
                );
224
            }
225
226 3
            return $this->joinDeep($relation, $alias, $joinType, $with);
0 ignored issues
show
Documentation introduced by
$with is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
227
        }
228
229 3
        if ($with) {
230 3
            $this->with($alias);
231 3
        }
232
233 3
        $this->joins[$alias] = [
234 3
            'from'     => $this->alias,
235 3
            'relation' => $tableMap->getRelation($relationTable),
236
            'type'     => $joinType
237 3
        ];
238
239 3
        return $this;
240
    }
241
242
    /**
243
     * @param string $relation
244
     * @param string $alias
245
     * @param string $joinType
246
     * @param string $with
247
     *
248
     * @return $this|Query
249
     *
250
     * @throws RelationAliasNotFoundException
251
     */
252 3
    protected function joinDeep($relation, $alias, $joinType, $with)
253
    {
254 3
        $params = explode('.', $relation);
255 3
        if (!isset($this->joins[$params[0]])) {
256 1
            throw new RelationAliasNotFoundException(
257 1
                'Unknown alias for relation "' . $params[0] . '" for model "' . $this->modelNamespace . '"'
258 1
            );
259
        }
260
261 2
        $tableMap = Rocket::getTableMap($this->joins[$params[0]]['relation']['namespace']);
262 2
        if ($with) {
263 2
            $this->with($alias, $params[0]);
264 2
        }
265
266 2
        $this->joins[$alias] = [
267 2
            'from' => $params[0],
268 2
            'relation' => $tableMap->getRelation($params[1]),
269
            'type' => $joinType
270 2
        ];
271
272 2
        return $this;
273
    }
274
275
    /**
276
     * @param string      $alias
277
     * @param null|string $table
278
     */
279 3
    protected function with($alias, $table = null)
280
    {
281 3
        $this->with[$alias] = [
282 3
            'alias' => $alias,
283
            'from'  => $table
284 3
        ];
285 3
    }
286
287
    /**
288
     * @return string
289
     */
290 2
    protected function buildRelationWith()
291
    {
292 2
        $query = '';
293 2
        foreach ($this->with as $with) {
294
            /** @var TableMapInterface $relationTableMap */
295 2
            $relationTableMap = Rocket::getTableMap($this->joins[$with['alias']]['relation']['namespace']);
296 2
            foreach ($relationTableMap->getColumns() as $column) {
297 2
                $query .= ', ' . $with['alias'] . '.' . $column['name'] . ' AS "' . $with['alias'] . '.' . $column['name'] . '"';
298 2
            }
299
300 2
            unset($relationTableMap);
301 2
        }
302
303 2
        return $query;
304
    }
305
306
    /**
307
     * @return string
308
     */
309 2
    protected function buildRelationClauses()
310
    {
311 2
        $query = '';
312 2
        foreach ($this->joins as $alias => $join) {
313
            /** @var TableMapInterface $relationTableMap */
314 2
            $tableMap = Rocket::getTableMap($join['relation']['namespace']);
315 2
            $query .= sprintf(' %s JOIN `%s`.`%s` %s ON %s.%s = %s.%s',
316 2
                $join['type'],
317 2
                $tableMap->getDatabase(),
318 2
                $tableMap->getTableName(),
319 2
                $alias,
320 2
                $join['from'],
321 2
                $join['relation']['local'],
322 2
                $alias,
323 2
                $join['relation']['foreign']
324 2
            );
325
326 2
            unset($tableMap);
327 2
        }
328
329 2
        return $query;
330
    }
331
332
    /**
333
     * @return string
334
     */
335 4
    protected function buildClauses()
336
    {
337 4
        $query = ' WHERE ';
338
339
        // FIXME handle the case when a clause need to be encapsulated by parentheses
0 ignored issues
show
Coding Style introduced by
Comment refers to a FIXME task "handle the case when a clause need to be encapsulated by parentheses"
Loading history...
340 4
        foreach ($this->clauses as $i => $clauseParams) {
341 4
            if (0 == $i) {
342 4
                if (null != $clauseParams['value']) {
343
                    // foo = :param_0
344 4
                    $query .= sprintf('%s :param_%d', trim(substr($clauseParams['clause'], 0, -1)), $i);
345 4
                } else {
346 1
                    $query .= $clauseParams['clause'];
347
                }
348 4
            } else {
349 1
                if (null != $clauseParams['value']) {
350
                    // AND foo = :param_1
351 1
                    $query .= sprintf(' %s %s :param_%d', $clauseParams['operator'], trim(substr($clauseParams['clause'], 0, -1)), $i);
352 1
                } else {
353 1
                    $query .= ' ' . $clauseParams['operator'] . ' ' . $clauseParams['clause'];
354
                }
355
            }
356 4
        }
357
358 4
        return $query;
359
    }
360
361
    /**
362
     * @return string
363
     */
364 7
    protected function buildLimit()
365
    {
366 7
        $query = '';
367 7
        if (null != $this->limit) {
368 3
            $query .= ' LIMIT ' . $this->limit;
369
370 3
            if (null != $this->offset) {
371 1
                $query .= ',' . $this->offset;
372 1
            }
373 3
        }
374
375 7
        return $query;
376
    }
377
378
    /**
379
     * @param \PDOStatement $stmt
380
     *
381
     * @return array|RocketObject[]
0 ignored issues
show
Documentation introduced by
Should the return type not be array|RocketObject|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
382
     */
383 3
    protected function hydrate(\PDOStatement $stmt)
384
    {
385
        // FIXME do sub query when limit == 1 and there is many_to_* relation
0 ignored issues
show
Coding Style introduced by
Comment refers to a FIXME task "do sub query when limit == 1 and there is many_to_* relation"
Loading history...
386
387 3
        $hasRelation = 0 < sizeof($this->with);
388 3
        if (!$hasRelation) {
389 2
            $objects = $this->getSimpleQueryHydrator()->hydrate($stmt);
390 2
        } else {
391 1
            $objects = $this->getComplexQueryHydrator()->hydrate($stmt);
392
        }
393
394 3
        $stmt->closeCursor();
395
396 3
        return $objects;
397
    }
398
399
    /**
400
     * @return string
401
     */
402 6
    public function getSqlQuery()
403
    {
404 6
        $query = $this->buildQuery();
405 6
        if (0 < sizeof($this->clauses)) {
406 3
            foreach ($this->clauses as $i => $clauseParams) {
407 3
                if (is_string($clauseParams['value'])) {
408 3
                    $query = str_replace(':param_' . $i, "'" . $clauseParams['value'] . "'", $query);
409 3
                } else {
410 2
                    $query = str_replace(':param_' . $i, $clauseParams['value'], $query);
411
                }
412 3
            }
413 3
        }
414
415 6
        return $query;
416
    }
417
418
    /**
419
     * Clear all values
420
     */
421 3
    protected function clear()
422
    {
423 3
        unset($this->clauses, $this->joins, $this->with, $this->limit, $this->offset);
424
425 3
        $this->clauses = [];
426 3
        $this->joins   = [];
427 3
        $this->with    = [];
428 3
        $this->limit   = null;
429 3
        $this->offset  = null;
430 3
    }
431
432
    /**
433
     * @return TableMapInterface
434
     */
435 6
    protected function getTableMap()
436
    {
437 6
        if (!isset($this->tableMap)) {
438 6
            $this->tableMap = Rocket::getTableMap($this->modelNamespace);
439 6
        }
440
441 6
        return $this->tableMap;
442
    }
443
444
    /**
445
     * @return QueryHydratorInterface
446
     */
447 1
    protected function getSimpleQueryHydrator()
448
    {
449 1
        return new SimpleQueryHydrator($this->modelNamespace);
450
    }
451
452
    /**
453
     * @return QueryHydratorInterface
454
     */
455 1
    protected function getComplexQueryHydrator()
456
    {
457 1
        return new ComplexQueryHydrator($this->modelNamespace, $this->alias, $this->joins);
458
    }
459
460
    /**
461
     * @param \PDO $con
0 ignored issues
show
Documentation introduced by
Should the type for parameter $con not be null|\PDO?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
462
     *
463
     * @return mixed
464
     */
465
    public abstract function find(\PDO $con = null);
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
466
467
    /**
468
     * @return string
469
     */
470
    protected abstract function buildQuery();
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
471
}
472