Completed
Push — master ( 1e9746...1696ee )
by Anton
09:00
created

RecordSelector   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 485
Duplicated Lines 8.66 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 37
c 4
b 0
f 0
lcom 1
cbo 15
dl 42
loc 485
rs 7.8833

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 21 2
A primaryTable() 0 4 1
A primaryAlias() 0 4 1
A columns() 0 6 1
A generateColumns() 0 10 2
A load() 21 21 4
A with() 21 21 4
A findByPK() 0 13 2
A findOne() 0 14 4
B sqlStatement() 0 28 4
A getIterator() 0 19 3
A all() 0 4 1
B fetchData() 0 57 7
A __clone() 0 4 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\ORM\Entities;
9
10
use Psr\Log\LoggerAwareInterface;
11
use Spiral\Cache\CacheInterface;
12
use Spiral\Core\Traits\SaturateTrait;
13
use Spiral\Database\Builders\Prototypes\AbstractSelect;
14
use Spiral\Database\Entities\QueryCompiler;
15
use Spiral\Database\Query\QueryResult;
16
use Spiral\Debug\Traits\BenchmarkTrait;
17
use Spiral\Debug\Traits\LoggerTrait;
18
use Spiral\ORM\Entities\Loaders\RootLoader;
19
use Spiral\ORM\Exceptions\SelectorException;
20
use Spiral\ORM\ORM;
21
use Spiral\ORM\RecordEntity;
22
use Spiral\ORM\RecordInterface;
23
24
/**
25
 * Selectors provide QueryBuilder (see Database) like syntax and support for ORM records to be
26
 * fetched from database. In addition, selection uses set of internal data loaders dedicated to
27
 * every of record relation and used to pre-load (joins) or post-load (separate query) data for
28
 * this relations, including additional where conditions and using relation data for parent record
29
 * filtering queries.
30
 *
31
 * Selector loaders may not only be related to SQL databases, but might load data from external
32
 * sources.
33
 *
34
 * @see with()
35
 * @see load()
36
 * @see LoaderInterface
37
 * @see AbstractSelect
38
 */
39
class RecordSelector extends AbstractSelect implements LoggerAwareInterface
40
{
41
    /**
42
     * Selector provides set of profiling functionality helps to understand what is going on with
43
     * query and data parsing.
44
     */
45
    use LoggerTrait, BenchmarkTrait, SaturateTrait;
46
47
    /**
48
     * Class name of record to be loaded.
49
     *
50
     * @var string
51
     */
52
    protected $class = '';
53
54
    /**
55
     * Data columns are set of columns automatically created by inner loaders using
56
     * generateColumns() method, this is not the same column set as one provided by user using
57
     * columns() method. Do not define columns using generateColumns() method outside of loaders.
58
     *
59
     * @see generateColumns()
60
     * @var array
61
     */
62
    protected $dataColumns = [];
63
64
    /**
65
     * We have to track count of loader columns to define correct offsets.
66
     *
67
     * @var int
68
     */
69
    protected $countColumns = 0;
70
71
    /**
72
     * Primary selection loader.
73
     *
74
     * @var Loader
75
     */
76
    protected $loader = null;
77
78
    /**
79
     * @invisible
80
     * @var ORM
81
     */
82
    protected $orm = null;
83
84
    /**
85
     * @param string $class
86
     * @param ORM    $orm
87
     * @param Loader $loader
88
     */
89
    public function __construct($class, ORM $orm = null, Loader $loader = null)
90
    {
91
        $this->class = $class;
92
        $this->orm = $this->saturate($orm, ORM::class);
93
        $this->columns = $this->dataColumns = [];
94
95
        //We aways need primary loader
96
        if (empty($this->loader = $loader)) {
97
            //Selector always need primary data loaded to define data structure and perform query
98
            //parsing, in most of cases we can easily use RootLoader associated with primary record
99
            //schema
100
            $this->loader = new RootLoader($this->orm, null, $this->orm->schema($class));
101
        }
102
103
        //Every ORM loader has ability to declare it's primary database, we are going to use
104
        //primary loader database to initiate selector
105
        $database = $this->loader->dbalDatabase();
106
107
        //AbstractSelect construction
108
        parent::__construct($database, $database->driver()->queryCompiler($database->getPrefix()));
109
    }
110
111
    /**
112
     * Primary selection table.
113
     *
114
     * @return string
115
     */
116
    public function primaryTable()
117
    {
118
        return $this->loader->getTable();
119
    }
120
121
    /**
122
     * Primary alias points to table related to parent record.
123
     *
124
     * @return string
125
     */
126
    public function primaryAlias()
127
    {
128
        return $this->loader->getAlias();
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    public function columns($columns = ['*'])
0 ignored issues
show
Unused Code introduced by
The parameter $columns is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
135
    {
136
        $this->columns = $this->fetchIdentifiers(func_get_args());
137
138
        return $this;
139
    }
140
141
    /**
142
     * Automatically generate set of columns for specified table or alias, method used by loaders
143
     * in cases where data is joined.
144
     *
145
     * @param string $table   Source table name or alias.
146
     * @param array  $columns Original set of record columns.
147
     * @return int
148
     */
149
    public function generateColumns($table, array $columns)
150
    {
151
        $offset = count($this->dataColumns);
152
        foreach ($columns as $column) {
153
            $columnAlias = 'c' . (++$this->countColumns);
154
            $this->dataColumns[] = $table . '.' . $column . ' AS ' . $columnAlias;
155
        }
156
157
        return $offset;
158
    }
159
160
    /**
161
     * Request primary selector loader to pre-load relation name. Any type of loader can be used
162
     * for
163
     * data preloading. ORM loaders by default will select the most efficient way to load related
164
     * data which might include additional select query or left join. Loaded data will
165
     * automatically pre-populate record relations. You can specify nested relations using "."
166
     * separator.
167
     *
168
     * Examples:
169
     *
170
     * //Select users and load their comments (will cast 2 queries, HAS_MANY comments)
171
     * User::find()->with('comments');
172
     *
173
     * //You can load chain of relations - select user and load their comments and post related to
174
     * //comment
175
     * User::find()->with('comments.post');
176
     *
177
     * //We can also specify custom where conditions on data loading, let's load only public
178
     * comments. User::find()->load('comments', [
179
     *      'where' => ['{@}.status' => 'public']
180
     * ]);
181
     *
182
     * Please note using "{@}" column name, this placeholder is required to prevent collisions and
183
     * it will be automatically replaced with valid table alias of pre-loaded comments table.
184
     *
185
     * //In case where your loaded relation is MANY_TO_MANY you can also specify pivot table
186
     * conditions,
187
     * //let's pre-load all approved user tags, we can use same placeholder for pivot table alias
188
     * User::find()->load('tags', [
189
     *      'wherePivot' => ['{@}.approved' => true]
190
     * ]);
191
     *
192
     * //In most of cases you don't need to worry about how data was loaded, using external query
193
     * or
194
     * //left join, however if you want to change such behaviour you can force load method to
195
     * INLOAD
196
     * User::find()->load('tags', [
197
     *      'method'     => Loader::INLOAD,
198
     *      'wherePivot' => ['{@}.approved' => true]
199
     * ]);
200
     *
201
     * Attention, you will not be able to correctly paginate in this case and only ORM loaders
202
     * support different loading types.
203
     *
204
     * You can specify multiple loaders using array as first argument.
205
     *
206
     * Example:
207
     * User::find()->load(['posts', 'comments', 'profile']);
208
     *
209
     * @see with()
210
     * @param string $relation
211
     * @param array  $options
212
     * @return $this
213
     */
214 View Code Duplication
    public function load($relation, array $options = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
215
    {
216
        if (is_array($relation)) {
217
            foreach ($relation as $name => $subOption) {
218
                if (is_string($subOption)) {
219
                    //Array of relation names
220
                    $this->load($subOption, $options);
221
                } else {
222
                    //Multiple relations or relation with addition load options
223
                    $this->load($name, $subOption + $options);
224
                }
225
            }
226
227
            return $this;
228
        }
229
230
        //We are requesting primary loaded to pre-load nested relation
231
        $this->loader->loader($relation, $options);
232
233
        return $this;
234
    }
235
236
    /**
237
     * With method is very similar to load() one, except it will always include related data to
238
     * parent query using INNER JOIN, this method can be applied only to ORM loaders and relations
239
     * using same database as parent record.
240
     *
241
     * Method generally used to filter data based on some relation condition.
242
     * Attention, with() method WILL NOT load relation data, it will only make it accessible in
243
     * query.
244
     *
245
     * By default joined tables will be available in query based on realtion name, you can change
246
     * joined table alias using relation option "alias".
247
     *
248
     * Do not forget to set DISTINCT flag while including HAS_MANY and MANY_TO_MANY relations. In
249
     * other scenario you will not able to paginate data well.
250
     *
251
     * Examples:
252
     *
253
     * //Find all users who have comments comments
254
     * User::find()->with('comments');
255
     *
256
     * //Find all users who have approved comments (we can use comments table alias in where
257
     * statement).
258
     * User::find()->with('comments')->where('comments.approved', true);
259
     *
260
     * //Find all users who have posts which have approved comments
261
     * User::find()->with('posts.comments')->where('posts_comments.approved', true);
262
     *
263
     * //Custom join alias for post comments relation
264
     * $user->with('posts.comments', [
265
     *      'alias' => 'comments'
266
     * ])->where('comments.approved', true);
267
     *
268
     * //If you joining MANY_TO_MANY relation you will be able to use pivot table used as relation
269
     * name
270
     * //plus "_pivot" postfix. Let's load all users with approved tags.
271
     * $user->with('tags')->where('tags_pivot.approved', true);
272
     *
273
     * //You can also use custom alias for pivot table as well
274
     * User::find()->with('tags', [
275
     *      'pivotAlias' => 'tags_connection'
276
     * ])
277
     * ->where('tags_connection.approved', false);
278
     *
279
     * You can safely combine with() and load() methods.
280
     *
281
     * //Load all users with approved comments and pre-load all their comments
282
     * User::find()->with('comments')->where('comments.approved', true)
283
     *             ->load('comments');
284
     *
285
     * //You can also use custom conditions in this case, let's find all users with approved
286
     * comments
287
     * //and pre-load such approved comments
288
     * User::find()->with('comments')->where('comments.approved', true)
289
     *             ->load('comments', [
290
     *                  'where' => ['{@}.approved' => true]
291
     *              ]);
292
     *
293
     * //As you might notice previous construction will create 2 queries, however we can simplify
294
     * //this construction to use already joined table as source of data for relation via "using"
295
     * //keyword
296
     * User::find()->with('comments')->where('comments.approved', true)
297
     *             ->load('comments', ['using' => 'comments']);
298
     *
299
     * //You will get only one query with INNER JOIN, to better understand this example let's use
300
     * //custom alias for comments in with() method.
301
     * User::find()->with('comments', ['alias' => 'commentsR'])->where('commentsR.approved', true)
302
     *             ->load('comments', ['using' => 'commentsR']);
303
     *
304
     * @see load()
305
     * @param string $relation
306
     * @param array  $options
307
     * @return $this
308
     */
309 View Code Duplication
    public function with($relation, array $options = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
310
    {
311
        if (is_array($relation)) {
312
            foreach ($relation as $name => $options) {
313
                if (is_string($options)) {
314
                    //Array of relation names
315
                    $this->with($options, []);
316
                } else {
317
                    //Multiple relations or relation with addition load options
318
                    $this->with($name, $options);
319
                }
320
            }
321
322
            return $this;
323
        }
324
325
        //Requesting primary loader to join nested relation, will only work for ORM loaders
326
        $this->loader->joiner($relation, $options);
327
328
        return $this;
329
    }
330
331
    /**
332
     * Fetch one record from database using it's primary key. You can use INLOAD and JOIN_ONLY
333
     * loaders with HAS_MANY or MANY_TO_MANY relations with this method as no limit were used.
334
     *
335
     * @see findOne()
336
     * @param mixed $id Primary key value.
337
     * @return RecordEntity|null
338
     * @throws SelectorException
339
     */
340
    public function findByPK($id)
341
    {
342
        $primaryKey = $this->loader->getPrimaryKey();
343
344
        if (empty($primaryKey)) {
345
            throw new SelectorException(
346
                "Unable to fetch data by primary key, no primary key found."
347
            );
348
        }
349
350
        //No limit here
351
        return $this->findOne([$primaryKey => $id], false);
352
    }
353
354
    /**
355
     * Fetch one record from database. Attention, LIMIT statement will be used, meaning you can not
356
     * use loaders for HAS_MANY or MANY_TO_MANY relations with data inload (joins), use default
357
     * loading method.
358
     *
359
     * @see findByPK()
360
     * @param array $where     Selection WHERE statement.
361
     * @param bool  $withLimit Use limit 1.
362
     * @return RecordEntity|null
363
     */
364
    public function findOne(array $where = [], $withLimit = true)
365
    {
366
        if (!empty($where)) {
367
            $this->where($where);
368
        }
369
370
        $data = $this->limit($withLimit ? 1 : null)->fetchData();
371
        if (empty($data)) {
372
            return null;
373
        }
374
375
        //Letting ORM to do it's job
376
        return $this->orm->record($this->class, $data[0]);
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382
    public function sqlStatement(QueryCompiler $compiler = null)
383
    {
384
        if (empty($compiler)) {
385
            $compiler = $this->compiler->resetQuoter();
386
        }
387
388
        //Primary loader may add custom conditions to select query
389
        $this->loader->configureSelector($this);
390
391
        if (empty($columns = $this->columns)) {
392
            //If no user columns were specified we are going to use columns defined by our loaders
393
            //in addition it will return RecordIterator instance as result instead of QueryResult
394
            $columns = !empty($this->dataColumns) ? $this->dataColumns : ['*'];
395
        }
396
397
        return $compiler->compileSelect(
398
            ["{$this->primaryTable()} AS {$this->primaryAlias()}"],
399
            $this->distinct,
400
            $columns,
401
            $this->joinTokens,
402
            $this->whereTokens,
403
            $this->havingTokens,
404
            $this->grouping,
405
            $this->ordering,
406
            $this->limit,
407
            $this->offset
408
        );
409
    }
410
411
    /**
412
     * {@inheritdoc}
413
     *
414
     * Return type will depend if custom columns set were used.
415
     *
416
     * @param array $callbacks Callbacks to be used in record iterator as magic methods.
417
     * @return QueryResult|RecordIterator
418
     */
419
    public function getIterator(array $callbacks = [])
420
    {
421
        if (!empty($this->columns) || !empty($this->grouping)) {
422
            //QueryResult for user requests
423
            return $this->run();
424
        }
425
426
        /*
427
         * We are getting copy of ORM with cloned cache, so all our entities are isolated in it.
428
         */
429
430
        return new RecordIterator(
431
            clone $this->orm,
432
            $this->class,
433
            $this->fetchData(),
434
            true,
435
            $callbacks
436
        );
437
    }
438
439
    /**
440
     * All records.
441
     *
442
     * @return RecordInterface[]
443
     */
444
    public function all()
445
    {
446
        return $this->getIterator()->all();
447
    }
448
449
    /**
450
     * Execute query and every related query to compile records data in tree form - every relation
451
     * data will be included as sub key.
452
     *
453
     * Attention, Selector will cache compiled data tree and not query itself to keep data integrity
454
     * and to skip data compilation on second query.
455
     *
456
     * @return array
457
     */
458
    public function fetchData()
459
    {
460
        //Pagination!
461
        $this->applyPagination();
462
463
        //Generating statement
464
        $statement = $this->sqlStatement();
465
466
        if (!empty($this->cacheLifetime)) {
467
            $cacheKey = $this->cacheKey ?: md5(serialize([$statement, $this->getParameters()]));
468
469
            if (empty($this->cacheStore)) {
470
                $this->cacheStore = $this->orm->container()->get(CacheInterface::class)->store();
471
            }
472
473
            if ($this->cacheStore->has($cacheKey)) {
474
                $this->logger()->debug("Selector result were fetched from cache.");
475
476
                //We are going to store parsed result, not queries
477
                return $this->cacheStore->get($cacheKey);
478
            }
479
        }
480
481
        //We are bypassing run() method here to prevent query caching, we will prefer to cache
482
        //parsed data rather that database response
483
        $result = $this->database->query($statement, $this->getParameters());
484
485
        //In many cases (too many inloads, too complex queries) parsing can take significant amount
486
        //of time, so we better profile it
487
        $benchmark = $this->benchmark('parseResult', $statement);
488
489
        //Here we are feeding selected data to our primary loaded to parse it and and create
490
        //data tree for our records
491
        $this->loader->parseResult($result, $rowsCount);
492
493
        $this->benchmark($benchmark);
494
495
        //Memory freeing
496
        $result->close();
497
498
        //This must force loader to execute all post loaders (including ODM and etc)
499
        $this->loader->loadData();
500
501
        //Now we can request our primary loader for compiled data
502
        $data = $this->loader->getResult();
503
504
        //Memory free! Attention, it will not reset columns aliases but only make possible to run
505
        //query again
506
        $this->loader->clean();
507
508
        if (!empty($this->cacheLifetime) && !empty($cacheKey)) {
509
            //We are caching full records tree, not queries
510
            $this->cacheStore->set($cacheKey, $data, $this->cacheLifetime);
511
        }
512
513
        return $data;
514
    }
515
516
    /**
517
     * Cloning selector presets
518
     */
519
    public function __clone()
520
    {
521
        $this->loader = clone $this->loader;
522
    }
523
}