Completed
Push — master ( 8c953e...7fbcf6 )
by Thomas
37s
created

EntityFetcher::createRelatedJoin()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
ccs 8
cts 8
cp 1
rs 9.4285
cc 2
eloc 9
nc 2
nop 2
crap 2
1
<?php
2
3
namespace ORM;
4
5
use ORM\Exceptions\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
     * @throws Exceptions\InvalidConfiguration
57
     * @throws Exceptions\InvalidName
58
     */
59 56
    public function __construct(EntityManager $entityManager, $class)
60
    {
61 56
        $this->entityManager = $entityManager;
62 56
        $this->class         = $class;
63
64 56
        $this->tableName = $entityManager->escapeIdentifier($class::getTableName());
65 56
        $this->alias = 't0';
66 56
        $this->columns = ['t0.*'];
67 56
        $this->modifier = ['DISTINCT'];
68
69 56
        $this->classMapping['byClass'][$class] = 't0';
70 56
        $this->classMapping['byAlias']['t0'] = $class;
71 56
    }
72
73
    /** @return self
74
     * @internal
75
     */
76 1
    public function columns(array $columns = null)
77
    {
78 1
        return $this;
79
    }
80
81
    /** @return self
82
     * @internal
83
     */
84 1
    public function column($column, $args = [], $alias = '')
85
    {
86 1
        return $this;
87
    }
88
89
    /**
90
     * Replaces questionmarks in $expression with $args
91
     *
92
     * Additionally this method replaces "ClassName::var" with "alias.col" and "alias.var" with "alias.col" if
93
     * $translateCols is true (default).
94
     *
95
     * @param string      $expression    Expression with placeholders
96
     * @param array|mixed $args          Argument(s) to insert
97
     * @param bool        $translateCols Whether or not column names should be translated
98
     * @return string
99
     * @throws Exceptions\NoConnection
100
     * @throws Exceptions\NotScalar
101
     */
102 39
    protected function convertPlaceholders($expression, $args, $translateCols = true)
103
    {
104 39
        if ($translateCols) {
105 36
            $expression = preg_replace_callback(
106
                '/(?<b>^| |\()' .
107
                '((?<class>[A-Za-z_][A-Za-z0-9_\\\\]*)::|(?<alias>[A-Za-z_][A-Za-z0-9_]+)\.)?' .
108
                '(?<column>[A-Za-z_][A-Za-z0-9_]*)' .
109 36
                '(?<a>$| |,|\))/',
110 36
                function ($match) {
111 36
                    if ($match['class']) {
112 2
                        if (!isset($this->classMapping['byClass'][$match['class']])) {
113 1
                            throw new NotJoined("Class " . $match['class'] . " not joined");
114
                        }
115 1
                        $class = $match['class'];
116 1
                        $alias = $this->classMapping['byClass'][$match['class']];
117 34
                    } elseif ($match['alias']) {
118 19
                        if (!isset($this->classMapping['byAlias'][$match['alias']])) {
119 2
                            return $match[0];
120
                        }
121 18
                        $alias = $match['alias'];
122 18
                        $class = $this->classMapping['byAlias'][$match['alias']];
123
                    } else {
124 15
                        if ($match['column'] === strtoupper($match['column'])) {
125 1
                            return $match['b'] . $match['column'] . $match['a'];
126
                        }
127 15
                        $class = $this->class;
128 15
                        $alias = $this->alias;
129
                    }
130
131
                    /** @var Entity|string $class */
132 34
                    return $match['b'] . $this->entityManager->escapeIdentifier(
133 34
                        $alias . '.' . $class::getColumnName($match['column'])
134 34
                    ) . $match['a'];
135 36
                },
136
                $expression
137
            );
138
        }
139
140 38
        return parent::convertPlaceholders($expression, $args);
141
    }
142
143
    /**
144
     * Common implementation for *Join methods
145
     *
146
     * Additionally this method replaces class name with table name and forces an alias.
147
     *
148
     * @param string      $join       The join type (e. g. `LEFT JOIN`)
149
     * @param string      $class      Class to join
150
     * @param string      $expression Expression to use in on clause or single column for USING
151
     * @param string      $alias      Alias for the table
152
     * @param array|mixed $args       Arguments to use in $expression
153
     * @param bool        $empty      Create an empty join (without USING and ON)
154
     * @return EntityFetcher|ParenthesisInterface
155
     * @throws Exceptions\InvalidConfiguration
156
     * @throws Exceptions\InvalidName
157
     * @throws Exceptions\NoConnection
158
     * @throws Exceptions\NotScalar
159
     * @internal
160
     */
161 18
    protected function createJoin($join, $class, $expression, $alias, $args, $empty)
162
    {
163 18
        if (class_exists($class)) {
164
            /** @var Entity|string $class */
165 14
            $tableName = $this->entityManager->escapeIdentifier($class::getTableName());
166 14
            $alias = $alias ?: 't' . count($this->classMapping['byAlias']);
167
168 14
            $this->classMapping['byClass'][$class] = $alias;
169 14
            $this->classMapping['byAlias'][$alias] = $class;
170
        } else {
171 6
            $tableName = $class;
172
        }
173
174 18
        return parent::createJoin(
175
            $join,
176
            $tableName,
177
            $expression,
178
            $alias,
179
            $args,
180
            $empty
181
        );
182
    }
183
184
    /**
185
     * Create the join with $join type
186
     *
187
     * @param $join
188
     * @param $relation
189
     * @return $this
190
     */
191 10
    public function createRelatedJoin($join, $relation)
192
    {
193 10
        if (strpos($relation, '.') !== false) {
194 1
            list($alias, $relation) = explode('.', $relation);
195 1
            $class = $this->classMapping['byAlias'][$alias];
196
        } else {
197 10
            $class = $this->class;
198 10
            $alias = $this->alias;
199
        }
200
201 10
        call_user_func([$class, 'getRelation'], $relation)->addJoin($this, $join, $alias);
202 10
        return $this;
203
    }
204
205
    /**
206
     * Join $relation
207
     *
208
     * @param $relation
209
     * @return $this
210
     */
211 6
    public function joinRelated($relation)
212
    {
213 6
        return $this->createRelatedJoin('join', $relation);
214
    }
215
216
    /**
217
     * Left outer join $relation
218
     *
219
     * @param $relation
220
     * @return $this
221
     */
222 4
    public function leftJoinRelated($relation)
223
    {
224 4
        return $this->createRelatedJoin('leftJoin', $relation);
225
    }
226
227
228
    /**
229
     * Fetch one entity
230
     *
231
     * If there is no more entity in the result set it returns null.
232
     *
233
     * @return Entity
234
     * @throws Exceptions\IncompletePrimaryKey
235
     * @throws Exceptions\InvalidConfiguration
236
     * @throws Exceptions\NoConnection
237
     */
238 14
    public function one()
239
    {
240 14
        $result = $this->getStatement();
241 14
        if (!$result) {
242 7
            return null;
243
        }
244
245 7
        $data = $result->fetch(\PDO::FETCH_ASSOC);
246
247 7
        if (!$data) {
248 1
            return null;
249
        }
250
251 6
        $c         = $this->class;
252 6
        $newEntity = new $c($data, $this->entityManager, true);
253 6
        $entity    = $this->entityManager->map($newEntity);
254
255 6
        if ($newEntity !== $entity) {
256 4
            $dirty = $entity->isDirty();
257 4
            $entity->setOriginalData($data);
258 4
            if (!$dirty && $entity->isDirty()) {
259 2
                $entity->reset();
260
            }
261
        }
262
263 6
        return $entity;
264
    }
265
266
    /**
267
     * Fetch an array of entities
268
     *
269
     * When no $limit is set it fetches all entities in result set.
270
     *
271
     * @param int $limit Maximum number of entities to fetch
272
     * @return Entity[]
273
     * @throws Exceptions\IncompletePrimaryKey
274
     * @throws Exceptions\InvalidConfiguration
275
     * @throws Exceptions\NoConnection
276
     */
277 4
    public function all($limit = 0)
278
    {
279 4
        $result = [];
280
281 4
        while ($entity = $this->one()) {
282 3
            $result[] = $entity;
283 3
            if ($limit && count($result) >= $limit) {
284 1
                break;
285
            }
286
        }
287
288 4
        return $result;
289
    }
290
291
    /**
292
     * Query database and return result
293
     *
294
     * Queries the database with current query and returns the resulted PDOStatement.
295
     *
296
     * If query failed it returns false. It also stores this failed result and to change the query afterwards will not
297
     * change the result.
298
     *
299
     * @return \PDOStatement
300
     * @throws Exceptions\NoConnection
301
     */
302 14
    private function getStatement()
303
    {
304 14
        if ($this->result === null) {
305 14
            $this->result = $this->entityManager->getConnection()->query($this->getQuery());
306
        }
307 14
        return $this->result;
308
    }
309
310
    /** {@inheritdoc} */
311 44
    public function getQuery()
312
    {
313 44
        if ($this->query) {
314 4
            return $this->query instanceof  QueryBuilderInterface ? $this->query->getQuery() : $this->query;
315
        }
316 40
        return parent::getQuery();
317
    }
318
319
    /**
320
     * Set a raw query or use different QueryBuilder
321
     *
322
     * For easier use and against sql injection it allows question mark placeholders.
323
     *
324
     * @param string|QueryBuilderInterface $query Raw query string or a QueryBuilderInterface
325
     * @param array                        $args  The arguments for placeholders
326
     * @return $this
327
     * @throws Exceptions\NoConnection
328
     * @throws Exceptions\NotScalar
329
     */
330 4
    public function setQuery($query, $args = null)
331
    {
332 4
        if (!$query instanceof QueryBuilderInterface) {
333 3
            $query = $this->convertPlaceholders($query, $args, false);
334
        }
335
336 4
        $this->query = $query;
337 4
        return $this;
338
    }
339
}
340