Passed
Pull Request — master (#49)
by Thomas
03:25 queued 01:18
created

EntityFetcher::getQuery()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 0
crap 12
1
<?php
2
3
namespace ORM;
4
5
use ORM\Exception\NotJoined;
6
use ORM\QueryBuilder\ParenthesisInterface;
7
use ORM\QueryBuilder\QueryBuilder;
8
use ORM\QueryBuilder\QueryBuilderInterface;
9
10
/**
11
 * Fetch entities from database
12
 *
13
 * If you need more specific queries you write them yourself. If you need just more specific where clause you can pass
14
 * them to the *where() methods.
15
 *
16
 * Supported:
17
 *  - joins with on clause (and alias)
18
 *  - joins with using (and alias)
19
 *  - where conditions
20
 *  - parenthesis
21
 *  - order by one or more columns / expressions
22
 *  - group by one or more columns / expressions
23
 *  - limit and offset
24
 *  - modifiers
25
 *
26
 * @package ORM
27
 * @author  Thomas Flori <[email protected]>
28
 */
29
class EntityFetcher extends QueryBuilder
30
{
31
    /** The entity class that we want to fetch
32
     * @var string|Entity */
33
    protected $class;
34
35
    /** The result object from PDO
36
     * @var \PDOStatement */
37
    protected $result;
38
39
    /** The query to execute (overwrites other settings)
40
     * @var string|QueryBuilderInterface */
41
    protected $query;
42
43
    /** The class to alias mapping and vise versa
44
     * @var string[][] */
45
    protected $classMapping = [
46
        'byClass' => [],
47
        'byAlias' => [],
48
    ];
49
50
    /** @noinspection PhpMissingParentConstructorInspection */
51
    /**
52
     * Constructor
53
     *
54
     * @param EntityManager $entityManager EntityManager where to store the fetched entities
55
     * @param Entity|string $class Class to fetch
56
     */
57
    public function __construct(EntityManager $entityManager, $class)
58
    {
59 67
        $this->entityManager = $entityManager;
60
        $this->class         = $class;
61 67
62 67
        $this->tableName = $entityManager->escapeIdentifier($class::getTableName());
63
        $this->alias     = 't0';
64 67
        $this->columns   = [ 't0.*' ];
65 67
        $this->modifier  = [ 'DISTINCT' ];
66 67
67 67
        $this->classMapping['byClass'][$class] = 't0';
68
        $this->classMapping['byAlias']['t0']   = $class;
69 67
    }
70 67
71 67
    /** @return self
72
     * @internal
73
     */
74
    public function columns(array $columns = null)
75
    {
76 1
        return $this;
77
    }
78 1
79
    /** @return self
80
     * @internal
81
     */
82
    public function column($column, $args = [], $alias = '')
83
    {
84 1
        return $this;
85
    }
86 1
87
    /**
88
     * Replaces questionmarks in $expression with $args
89
     *
90
     * Additionally this method replaces "ClassName::var" with "alias.col" and "alias.var" with "alias.col" if
91
     * $translateCols is true (default).
92
     *
93
     * @param string      $expression    Expression with placeholders
94
     * @param array|mixed $args          Argument(s) to insert
95
     * @param bool        $translateCols Whether or not column names should be translated
96
     * @return string
97
     */
98
    protected function convertPlaceholders($expression, $args, $translateCols = true)
99
    {
100
        if ($translateCols) {
101
            $expression = preg_replace_callback(
102 41
                '/(?<b>^| |\()' .
103
                '((?<class>[A-Za-z_][A-Za-z0-9_\\\\]*)::|(?<alias>[A-Za-z_][A-Za-z0-9_]+)\.)?' .
104 41
                '(?<column>[A-Za-z_][A-Za-z0-9_]*)' .
105 38
                '(?<a>$| |,|\))/',
106
                function ($match) {
107
                    if ($match['class']) {
108
                        if (!isset($this->classMapping['byClass'][$match['class']])) {
109 38
                            throw new NotJoined("Class " . $match['class'] . " not joined");
110 38
                        }
111 38
                        $class = $match['class'];
112 2
                        $alias = $this->classMapping['byClass'][$match['class']];
113 1
                    } elseif ($match['alias']) {
114
                        if (!isset($this->classMapping['byAlias'][$match['alias']])) {
115 1
                            return $match[0];
116 1
                        }
117 36
                        $alias = $match['alias'];
118 21
                        $class = $this->classMapping['byAlias'][$match['alias']];
119 2
                    } else {
120
                        if ($match['column'] === strtoupper($match['column'])) {
121 20
                            return $match['b'] . $match['column'] . $match['a'];
122 20
                        }
123
                        $class = $this->class;
124 15
                        $alias = $this->alias;
125 1
                    }
126
127 15
                    /** @var Entity|string $class */
128 15
                    return $match['b'] . $this->entityManager->escapeIdentifier(
129
                        $alias . '.' . $class::getColumnName($match['column'])
130
                    ) . $match['a'];
131
                },
132 36
                $expression
133 36
            );
134 36
        }
135 38
136
        return parent::convertPlaceholders($expression, $args);
137
    }
138
139
    /**
140 40
     * Common implementation for *Join methods
141
     *
142
     * Additionally this method replaces class name with table name and forces an alias.
143
     *
144
     * @param string $join The join type (e. g. `LEFT JOIN`)
145
     * @param string $class Class to join
146
     * @param string $expression Expression to use in on clause or single column for USING
147
     * @param string $alias Alias for the table
148
     * @param array|mixed $args Arguments to use in $expression
149
     * @param bool $empty Create an empty join (without USING and ON)
150
     * @return EntityFetcher|ParenthesisInterface
151
     * @internal
152
     */
153
    protected function createJoin($join, $class, $expression, $alias, $args, $empty)
154
    {
155
        if (class_exists($class)) {
156
            /** @var Entity|string $class */
157
            $tableName = $this->entityManager->escapeIdentifier($class::getTableName());
158
            $alias     = $alias ?: 't' . count($this->classMapping['byAlias']);
159
160
            $this->classMapping['byClass'][$class] = $alias;
161 20
            $this->classMapping['byAlias'][$alias] = $class;
162
        } else {
163 20
            $tableName = $class;
164
        }
165 14
166 14
        return parent::createJoin($join, $tableName, $expression, $alias, $args, $empty);
167
    }
168 14
169 14
    /**
170
     * Create the join with $join type
171 8
     *
172
     * @param $join
173
     * @param $relation
174 20
     * @return $this
175
     */
176
    public function createRelatedJoin($join, $relation)
177
    {
178
        if (strpos($relation, '.') !== false) {
179
            list($alias, $relation) = explode('.', $relation);
180
            $class = $this->classMapping['byAlias'][$alias];
181
        } else {
182
            $class = $this->class;
183
            $alias = $this->alias;
184 10
        }
185
186 10
        call_user_func([ $class, 'getRelation' ], $relation)->addJoin($this, $join, $alias);
187 1
        return $this;
188 1
    }
189
190 10
    /**
191 10
     * Join $relation
192
     *
193
     * @param $relation
194 10
     * @return $this
195 10
     */
196
    public function joinRelated($relation)
197
    {
198
        return $this->createRelatedJoin('join', $relation);
199
    }
200
201
    /**
202
     * Left outer join $relation
203
     *
204 6
     * @param $relation
205
     * @return $this
206 6
     */
207
    public function leftJoinRelated($relation)
208
    {
209
        return $this->createRelatedJoin('leftJoin', $relation);
210
    }
211
212
213
    /**
214
     * Fetch one entity
215 4
     *
216
     * If there is no more entity in the result set it returns null.
217 4
     *
218
     * @return Entity
219
     */
220
    public function one()
221
    {
222
        $result = $this->getStatement();
223
        if (!$result) {
224
            return null;
225
        }
226
227
        $data = $result->fetch(\PDO::FETCH_ASSOC);
228
229
        if (!$data) {
230
            return null;
231 15
        }
232
233 15
        $class         = $this->class;
234 14
        $newEntity = new $class($data, $this->entityManager, true);
235 7
        $entity    = $this->entityManager->map($newEntity);
236
237
        if ($newEntity !== $entity) {
238 7
            $dirty = $entity->isDirty();
239
            $entity->setOriginalData($data);
240 7
            if (!$dirty && $entity->isDirty()) {
241 1
                $entity->reset();
242
            }
243
        }
244 6
245 6
        return $entity;
246 6
    }
247
248 6
    /**
249 4
     * Fetch an array of entities
250 4
     *
251 4
     * When no $limit is set it fetches all entities in result set.
252 2
     *
253
     * @param int $limit Maximum number of entities to fetch
254
     * @return Entity[]
255
     */
256 6
    public function all($limit = 0)
257
    {
258
        $result = [];
259
260
        while ($entity = $this->one()) {
261
            $result[] = $entity;
262
            if ($limit && count($result) >= $limit) {
263
                break;
264
            }
265
        }
266
267
        return $result;
268
    }
269
270 6
    /**
271
     * Get the count of the resulting items
272 6
     *
273
     * @return int
274 6
     */
275 5
    public function count()
276 5
    {
277 1
        // set the columns and reset after get query
278
        $this->columns  = [ 'COUNT(DISTINCT t0.*)' ];
279
        $this->modifier = [];
280
        $query          = $this->getQuery();
281 6
        $this->columns  = [ 't0.*' ];
282
        $this->modifier = [ 'DISTINCT' ];
283
284
        return (int) $this->entityManager->getConnection()->query($query)->fetchColumn();
285
    }
286
287
    /**
288
     * Query database and return result
289 4
     *
290
     * Queries the database with current query and returns the resulted PDOStatement.
291
     *
292 4
     * If query failed it returns false. It also stores this failed result and to change the query afterwards will not
293 4
     * change the result.
294 4
     *
295 4
     * @return \PDOStatement|bool
296 4
     */
297
    private function getStatement()
298 4
    {
299
        if ($this->result === null) {
300
            $this->result = $this->entityManager->getConnection()->query($this->getQuery());
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->entityManager->ge...uery($this->getQuery()) can also be of type boolean. However, the property $result is declared as type PDOStatement. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
301
        }
302
        return $this->result;
303
    }
304
305
    /** {@inheritdoc} */
306
    public function getQuery()
307
    {
308
        if ($this->query) {
309
            return $this->query instanceof QueryBuilderInterface ? $this->query->getQuery() : $this->query;
310
        }
311
        return parent::getQuery();
312 15
    }
313
314 15
    /**
315 15
     * Set a raw query or use different QueryBuilder
316
     *
317 14
     * For easier use and against sql injection it allows question mark placeholders.
318
     *
319
     * @param string|QueryBuilderInterface $query Raw query string or a QueryBuilderInterface
320
     * @param array                        $args  The arguments for placeholders
321 48
     * @return $this
322
     */
323 48
    public function setQuery($query, $args = null)
324 4
    {
325
        if (!$query instanceof QueryBuilderInterface) {
326 44
            $query = $this->convertPlaceholders($query, $args, false);
327
        }
328
329
        $this->query = $query;
330
        return $this;
331
    }
332
}
333