EntityFetcher   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 303
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 5
dl 0
loc 303
rs 9.6
c 0
b 0
f 0
ccs 96
cts 96
cp 1

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
A columns() 0 4 1
A column() 0 4 1
B convertPlaceholders() 0 40 7
A createJoin() 0 15 3
A createRelatedJoin() 0 13 2
A joinRelated() 0 4 1
A leftJoinRelated() 0 4 1
B one() 0 27 6
A all() 0 13 4
A count() 0 11 1
A getStatement() 0 7 2
A getQuery() 0 7 3
A setQuery() 0 9 2
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
use PDO;
10
use PDOStatement;
11
12
/**
13
 * Fetch entities from database
14
 *
15
 * If you need more specific queries you write them yourself. If you need just more specific where clause you can pass
16
 * them to the *where() methods.
17
 *
18
 * Supported:
19
 *  - joins with on clause (and alias)
20
 *  - joins with using (and alias)
21
 *  - where conditions
22
 *  - parenthesis
23
 *  - order by one or more columns / expressions
24
 *  - group by one or more columns / expressions
25
 *  - limit and offset
26
 *  - modifiers
27
 *
28
 * @package ORM
29
 * @author  Thomas Flori <[email protected]>
30
 */
31
class EntityFetcher extends QueryBuilder
32
{
33
    /** The entity class that we want to fetch
34
     * @var string|Entity */
35
    protected $class;
36
37
    /** The result object from PDO
38
     * @var PDOStatement */
39
    protected $result;
40
41
    /** The query to execute (overwrites other settings)
42
     * @var string|QueryBuilderInterface */
43
    protected $query;
44
45
    /** The class to alias mapping and vise versa
46
     * @var string[][] */
47
    protected $classMapping = [
48
        'byClass' => [],
49
        'byAlias' => [],
50
    ];
51
52
    /** @noinspection PhpMissingParentConstructorInspection */
53
    /**
54
     * Constructor
55
     *
56
     * @param EntityManager $entityManager EntityManager where to store the fetched entities
57
     * @param Entity|string $class Class to fetch
58
     */
59 67
    public function __construct(EntityManager $entityManager, $class)
60
    {
61 67
        $this->entityManager = $entityManager;
62 67
        $this->class         = $class;
63
64 67
        $this->tableName = $entityManager->escapeIdentifier($class::getTableName());
65 67
        $this->alias     = 't0';
66 67
        $this->columns   = [ 't0.*' ];
67 67
        $this->modifier  = [ 'DISTINCT' ];
68
69 67
        $this->classMapping['byClass'][$class] = 't0';
70 67
        $this->classMapping['byAlias']['t0']   = $class;
71 67
    }
72
73
    /** @return static
74
     * @internal
75
     */
76 1
    public function columns(array $columns = null)
77
    {
78 1
        return $this;
79
    }
80
81
    /** @return static
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
     */
100
    protected function convertPlaceholders($expression, $args, $translateCols = true)
101
    {
102 41
        if ($translateCols) {
103
            $expression = preg_replace_callback(
104 41
                '/(?<b>^| |\()' .
105 38
                '((?<class>[A-Za-z_][A-Za-z0-9_\\\\]*)::|(?<alias>[A-Za-z_][A-Za-z0-9_]+)\.)?' .
106
                '(?<column>[A-Za-z_][A-Za-z0-9_]*)' .
107
                '(?<a>$| |,|\))/',
108
                function ($match) {
109 38
                    if ($match['class']) {
110 38
                        if (!isset($this->classMapping['byClass'][$match['class']])) {
111 38
                            throw new NotJoined("Class " . $match['class'] . " not joined");
112 2
                        }
113 1
                        $class = $match['class'];
114
                        $alias = $this->classMapping['byClass'][$match['class']];
115 1
                    } elseif ($match['alias']) {
116 1
                        if (!isset($this->classMapping['byAlias'][$match['alias']])) {
117 36
                            return $match[0];
118 21
                        }
119 2
                        $alias = $match['alias'];
120
                        $class = $this->classMapping['byAlias'][$match['alias']];
121 20
                    } else {
122 20
                        if ($match['column'] === strtoupper($match['column'])) {
123
                            return $match['b'] . $match['column'] . $match['a'];
124 15
                        }
125 1
                        $class = $this->class;
126
                        $alias = $this->alias;
127 15
                    }
128 15
129
                    /** @var Entity|string $class */
130
                    return $match['b'] . $this->entityManager->escapeIdentifier(
131
                        $alias . '.' . $class::getColumnName($match['column'])
132 36
                    ) . $match['a'];
133 36
                },
134 36
                $expression
135 38
            );
136
        }
137
138
        return parent::convertPlaceholders($expression, $args);
139
    }
140 40
141
    /**
142
     * Common implementation for *Join methods
143
     *
144
     * Additionally this method replaces class name with table name and forces an alias.
145
     *
146
     * @param string $join The join type (e. g. `LEFT JOIN`)
147
     * @param string $class Class to join
148
     * @param string|boolean $expression Expression, single column name or boolean to create an empty join
149
     * @param string $alias Alias for the table
150
     * @param array $args Arguments for expression
151
     * @return EntityFetcher|ParenthesisInterface
152
     * @internal
153
     */
154
    protected function createJoin($join, $class, $expression = '', $alias = '', $args = [])
155
    {
156
        if (class_exists($class)) {
157
            /** @var Entity|string $class */
158
            $tableName = $this->entityManager->escapeIdentifier($class::getTableName());
159
            $alias     = $alias ?: 't' . count($this->classMapping['byAlias']);
160
161 20
            $this->classMapping['byClass'][$class] = $alias;
162
            $this->classMapping['byAlias'][$alias] = $class;
163 20
        } else {
164
            $tableName = $class;
165 14
        }
166 14
167
        return parent::createJoin($join, $tableName, $expression, $alias, $args);
168 14
    }
169 14
170
    /**
171 8
     * Create the join with $join type
172
     *
173
     * @param $join
174 20
     * @param $relation
175
     * @return $this
176
     */
177
    public function createRelatedJoin($join, $relation)
178
    {
179
        if (strpos($relation, '.') !== false) {
180
            list($alias, $relation) = explode('.', $relation);
181
            $class = $this->classMapping['byAlias'][$alias];
182
        } else {
183
            $class = $this->class;
184 10
            $alias = $this->alias;
185
        }
186 10
187 1
        call_user_func([ $class, 'getRelation' ], $relation)->addJoin($this, $join, $alias);
188 1
        return $this;
189
    }
190 10
191 10
    /**
192
     * Join $relation
193
     *
194 10
     * @param $relation
195 10
     * @return $this
196
     */
197
    public function joinRelated($relation)
198
    {
199
        return $this->createRelatedJoin('join', $relation);
200
    }
201
202
    /**
203
     * Left outer join $relation
204 6
     *
205
     * @param $relation
206 6
     * @return $this
207
     */
208
    public function leftJoinRelated($relation)
209
    {
210
        return $this->createRelatedJoin('leftJoin', $relation);
211
    }
212
213
214
    /**
215 4
     * Fetch one entity
216
     *
217 4
     * If there is no more entity in the result set it returns null.
218
     *
219
     * @return Entity
220
     */
221
    public function one()
222
    {
223
        $result = $this->getStatement();
224
        if (!$result) {
225
            return null;
226
        }
227
228
        $data = $result->fetch(PDO::FETCH_ASSOC);
229
230
        if (!$data) {
231 15
            return null;
232
        }
233 15
234 14
        $class         = $this->class;
235 7
        $newEntity = new $class($data, $this->entityManager, true);
236
        $entity    = $this->entityManager->map($newEntity);
237
238 7
        if ($newEntity !== $entity) {
239
            $dirty = $entity->isDirty();
240 7
            $entity->setOriginalData($data);
241 1
            if (!$dirty && $entity->isDirty()) {
242
                $entity->reset();
243
            }
244 6
        }
245 6
246 6
        return $entity;
247
    }
248 6
249 4
    /**
250 4
     * Fetch an array of entities
251 4
     *
252 2
     * When no $limit is set it fetches all entities in result set.
253
     *
254
     * @param int $limit Maximum number of entities to fetch
255
     * @return Entity[]
256 6
     */
257
    public function all($limit = 0)
258
    {
259
        $result = [];
260
261
        while ($entity = $this->one()) {
262
            $result[] = $entity;
263
            if ($limit && count($result) >= $limit) {
264
                break;
265
            }
266
        }
267
268
        return $result;
269
    }
270 6
271
    /**
272 6
     * Get the count of the resulting items
273
     *
274 6
     * @return int
275 5
     */
276 5
    public function count()
277 1
    {
278
        // set the columns and reset after get query
279
        $this->columns  = [ 'COUNT(DISTINCT t0.*)' ];
280
        $this->modifier = [];
281 6
        $query          = $this->getQuery();
282
        $this->columns  = [ 't0.*' ];
283
        $this->modifier = [ 'DISTINCT' ];
284
285
        return (int) $this->entityManager->getConnection()->query($query)->fetchColumn();
286
    }
287
288
    /**
289 4
     * Query database and return result
290
     *
291
     * Queries the database with current query and returns the resulted PDOStatement.
292 4
     *
293 4
     * If query failed it returns false. It also stores this failed result and to change the query afterwards will not
294 4
     * change the result.
295 4
     *
296 4
     * @return PDOStatement|bool
297
     */
298 4
    private function getStatement()
299
    {
300
        if ($this->result === null) {
301
            $this->result = $this->entityManager->getConnection()->query($this->getQuery());
302
        }
303
        return $this->result;
304
    }
305
306
    /** {@inheritdoc} */
307
    public function getQuery()
308
    {
309
        if ($this->query) {
310
            return $this->query instanceof QueryBuilderInterface ? $this->query->getQuery() : $this->query;
311
        }
312 15
        return parent::getQuery();
313
    }
314 15
315 15
    /**
316
     * Set a raw query or use different QueryBuilder
317 14
     *
318
     * For easier use and against sql injection it allows question mark placeholders.
319
     *
320
     * @param string|QueryBuilderInterface $query Raw query string or a QueryBuilderInterface
321 48
     * @param array                        $args  The arguments for placeholders
322
     * @return $this
323 48
     */
324 4
    public function setQuery($query, $args = null)
325
    {
326 44
        if (!$query instanceof QueryBuilderInterface) {
327
            $query = $this->convertPlaceholders($query, $args, false);
328
        }
329
330
        $this->query = $query;
331
        return $this;
332
    }
333
}
334