Completed
Push — master ( 32964b...ce7d35 )
by Anton
05:22
created

RecordSelector::count()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 3
eloc 4
nc 2
nop 1
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
    const DEFAULT_COUNTING_FIELD = '*';
42
43
    /**
44
     * Selector provides set of profiling functionality helps to understand what is going on with
45
     * query and data parsing.
46
     */
47
    use LoggerTrait, BenchmarkTrait, SaturateTrait;
48
49
    /**
50
     * Class name of record to be loaded.
51
     *
52
     * @var string
53
     */
54
    protected $class = '';
55
56
    /**
57
     * Data columns are set of columns automatically created by inner loaders using
58
     * generateColumns() method, this is not the same column set as one provided by user using
59
     * columns() method. Do not define columns using generateColumns() method outside of loaders.
60
     *
61
     * @see generateColumns()
62
     * @var array
63
     */
64
    protected $dataColumns = [];
65
66
    /**
67
     * We have to track count of loader columns to define correct offsets.
68
     *
69
     * @var int
70
     */
71
    protected $countColumns = 0;
72
73
    /**
74
     * Primary selection loader.
75
     *
76
     * @var Loader
77
     */
78
    protected $loader = null;
79
80
    /**
81
     * @invisible
82
     * @var ORM
83
     */
84
    protected $orm = null;
85
86
    /**
87
     * @param string $class
88
     * @param ORM    $orm
89
     * @param Loader $loader
90
     */
91
    public function __construct($class, ORM $orm = null, Loader $loader = null)
92
    {
93
        $this->class = $class;
94
        $this->orm = $this->saturate($orm, ORM::class);
95
        $this->columns = $this->dataColumns = [];
96
97
        //We aways need primary loader
98
        if (empty($this->loader = $loader)) {
99
            //Selector always need primary data loaded to define data structure and perform query
100
            //parsing, in most of cases we can easily use RootLoader associated with primary record
101
            //schema
102
            $this->loader = new RootLoader($this->orm, null, $this->orm->schema($class));
103
        }
104
105
        //Every ORM loader has ability to declare it's primary database, we are going to use
106
        //primary loader database to initiate selector
107
        $database = $this->loader->dbalDatabase();
108
109
        //AbstractSelect construction
110
        parent::__construct($database, $database->driver()->queryCompiler($database->getPrefix()));
111
    }
112
113
    /**
114
     * Primary selection table.
115
     *
116
     * @return string
117
     */
118
    public function primaryTable()
119
    {
120
        return $this->loader->getTable();
121
    }
122
123
    /**
124
     * Primary alias points to table related to parent record.
125
     *
126
     * @return string
127
     */
128
    public function primaryAlias()
129
    {
130
        return $this->loader->getAlias();
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    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...
137
    {
138
        $this->columns = $this->fetchIdentifiers(func_get_args());
139
140
        return $this;
141
    }
142
143
    /**
144
     * Automatically generate set of columns for specified table or alias, method used by loaders
145
     * in cases where data is joined.
146
     *
147
     * @param string $table   Source table name or alias.
148
     * @param array  $columns Original set of record columns.
149
     * @return int
150
     */
151
    public function generateColumns($table, array $columns)
152
    {
153
        $offset = count($this->dataColumns);
154
        foreach ($columns as $column) {
155
            $columnAlias = 'c' . (++$this->countColumns);
156
            $this->dataColumns[] = $table . '.' . $column . ' AS ' . $columnAlias;
157
        }
158
159
        return $offset;
160
    }
161
162
    /**
163
     * Request primary selector loader to pre-load relation name. Any type of loader can be used
164
     * for
165
     * data preloading. ORM loaders by default will select the most efficient way to load related
166
     * data which might include additional select query or left join. Loaded data will
167
     * automatically pre-populate record relations. You can specify nested relations using "."
168
     * separator.
169
     *
170
     * Examples:
171
     *
172
     * //Select users and load their comments (will cast 2 queries, HAS_MANY comments)
173
     * User::find()->with('comments');
174
     *
175
     * //You can load chain of relations - select user and load their comments and post related to
176
     * //comment
177
     * User::find()->with('comments.post');
178
     *
179
     * //We can also specify custom where conditions on data loading, let's load only public
180
     * comments. User::find()->load('comments', [
181
     *      'where' => ['{@}.status' => 'public']
182
     * ]);
183
     *
184
     * Please note using "{@}" column name, this placeholder is required to prevent collisions and
185
     * it will be automatically replaced with valid table alias of pre-loaded comments table.
186
     *
187
     * //In case where your loaded relation is MANY_TO_MANY you can also specify pivot table
188
     * conditions,
189
     * //let's pre-load all approved user tags, we can use same placeholder for pivot table alias
190
     * User::find()->load('tags', [
191
     *      'wherePivot' => ['{@}.approved' => true]
192
     * ]);
193
     *
194
     * //In most of cases you don't need to worry about how data was loaded, using external query
195
     * or
196
     * //left join, however if you want to change such behaviour you can force load method to
197
     * INLOAD
198
     * User::find()->load('tags', [
199
     *      'method'     => Loader::INLOAD,
200
     *      'wherePivot' => ['{@}.approved' => true]
201
     * ]);
202
     *
203
     * Attention, you will not be able to correctly paginate in this case and only ORM loaders
204
     * support different loading types.
205
     *
206
     * You can specify multiple loaders using array as first argument.
207
     *
208
     * Example:
209
     * User::find()->load(['posts', 'comments', 'profile']);
210
     *
211
     * @see with()
212
     * @param string $relation
213
     * @param array  $options
214
     * @return $this
215
     */
216 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...
217
    {
218
        if (is_array($relation)) {
219
            foreach ($relation as $name => $subOption) {
220
                if (is_string($subOption)) {
221
                    //Array of relation names
222
                    $this->load($subOption, $options);
223
                } else {
224
                    //Multiple relations or relation with addition load options
225
                    $this->load($name, $subOption + $options);
226
                }
227
            }
228
229
            return $this;
230
        }
231
232
        //We are requesting primary loaded to pre-load nested relation
233
        $this->loader->loader($relation, $options);
234
235
        return $this;
236
    }
237
238
    /**
239
     * With method is very similar to load() one, except it will always include related data to
240
     * parent query using INNER JOIN, this method can be applied only to ORM loaders and relations
241
     * using same database as parent record.
242
     *
243
     * Method generally used to filter data based on some relation condition.
244
     * Attention, with() method WILL NOT load relation data, it will only make it accessible in
245
     * query.
246
     *
247
     * By default joined tables will be available in query based on realtion name, you can change
248
     * joined table alias using relation option "alias".
249
     *
250
     * Do not forget to set DISTINCT flag while including HAS_MANY and MANY_TO_MANY relations. In
251
     * other scenario you will not able to paginate data well.
252
     *
253
     * Examples:
254
     *
255
     * //Find all users who have comments comments
256
     * User::find()->with('comments');
257
     *
258
     * //Find all users who have approved comments (we can use comments table alias in where
259
     * statement).
260
     * User::find()->with('comments')->where('comments.approved', true);
261
     *
262
     * //Find all users who have posts which have approved comments
263
     * User::find()->with('posts.comments')->where('posts_comments.approved', true);
264
     *
265
     * //Custom join alias for post comments relation
266
     * $user->with('posts.comments', [
267
     *      'alias' => 'comments'
268
     * ])->where('comments.approved', true);
269
     *
270
     * //If you joining MANY_TO_MANY relation you will be able to use pivot table used as relation
271
     * name
272
     * //plus "_pivot" postfix. Let's load all users with approved tags.
273
     * $user->with('tags')->where('tags_pivot.approved', true);
274
     *
275
     * //You can also use custom alias for pivot table as well
276
     * User::find()->with('tags', [
277
     *      'pivotAlias' => 'tags_connection'
278
     * ])
279
     * ->where('tags_connection.approved', false);
280
     *
281
     * You can safely combine with() and load() methods.
282
     *
283
     * //Load all users with approved comments and pre-load all their comments
284
     * User::find()->with('comments')->where('comments.approved', true)
285
     *             ->load('comments');
286
     *
287
     * //You can also use custom conditions in this case, let's find all users with approved
288
     * comments
289
     * //and pre-load such approved comments
290
     * User::find()->with('comments')->where('comments.approved', true)
291
     *             ->load('comments', [
292
     *                  'where' => ['{@}.approved' => true]
293
     *              ]);
294
     *
295
     * //As you might notice previous construction will create 2 queries, however we can simplify
296
     * //this construction to use already joined table as source of data for relation via "using"
297
     * //keyword
298
     * User::find()->with('comments')->where('comments.approved', true)
299
     *             ->load('comments', ['using' => 'comments']);
300
     *
301
     * //You will get only one query with INNER JOIN, to better understand this example let's use
302
     * //custom alias for comments in with() method.
303
     * User::find()->with('comments', ['alias' => 'commentsR'])->where('commentsR.approved', true)
304
     *             ->load('comments', ['using' => 'commentsR']);
305
     *
306
     * @see load()
307
     * @param string $relation
308
     * @param array  $options
309
     * @return $this
310
     */
311 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...
312
    {
313
        if (is_array($relation)) {
314
            foreach ($relation as $name => $options) {
315
                if (is_string($options)) {
316
                    //Array of relation names
317
                    $this->with($options, []);
318
                } else {
319
                    //Multiple relations or relation with addition load options
320
                    $this->with($name, $options);
321
                }
322
            }
323
324
            return $this;
325
        }
326
327
        //Requesting primary loader to join nested relation, will only work for ORM loaders
328
        $this->loader->joiner($relation, $options);
329
330
        return $this;
331
    }
332
333
    /**
334
     * Fetch one record from database using it's primary key. You can use INLOAD and JOIN_ONLY
335
     * loaders with HAS_MANY or MANY_TO_MANY relations with this method as no limit were used.
336
     *
337
     * @see findOne()
338
     * @param mixed $id Primary key value.
339
     * @return RecordEntity|null
340
     * @throws SelectorException
341
     */
342
    public function findByPK($id)
343
    {
344
        $primaryKey = $this->loader->getPrimaryKey();
345
346
        if (empty($primaryKey)) {
347
            throw new SelectorException(
348
                "Unable to fetch data by primary key, no primary key found."
349
            );
350
        }
351
352
        //No limit here
353
        return $this->findOne([$primaryKey => $id], false);
354
    }
355
356
    /**
357
     * Fetch one record from database. Attention, LIMIT statement will be used, meaning you can not
358
     * use loaders for HAS_MANY or MANY_TO_MANY relations with data inload (joins), use default
359
     * loading method.
360
     *
361
     * @see findByPK()
362
     * @param array $where     Selection WHERE statement.
363
     * @param bool  $withLimit Use limit 1.
364
     * @return RecordEntity|null
365
     */
366
    public function findOne(array $where = [], $withLimit = true)
367
    {
368
        if (!empty($where)) {
369
            $this->where($where);
370
        }
371
372
        $data = $this->limit($withLimit ? 1 : null)->fetchData();
373
        if (empty($data)) {
374
            return null;
375
        }
376
377
        //Letting ORM to do it's job
378
        return $this->orm->record($this->class, $data[0]);
379
    }
380
381
    /**
382
     * {@inheritdoc}
383
     */
384
    public function sqlStatement(QueryCompiler $compiler = null)
385
    {
386
        if (empty($compiler)) {
387
            $compiler = $this->compiler->resetQuoter();
388
        }
389
390
        //Primary loader may add custom conditions to select query
391
        $this->loader->configureSelector($this);
392
393
        if (empty($columns = $this->columns)) {
394
            //If no user columns were specified we are going to use columns defined by our loaders
395
            //in addition it will return RecordIterator instance as result instead of QueryResult
396
            $columns = !empty($this->dataColumns) ? $this->dataColumns : ['*'];
397
        }
398
399
        return $compiler->compileSelect(
400
            ["{$this->primaryTable()} AS {$this->primaryAlias()}"],
401
            $this->distinct,
402
            $columns,
403
            $this->joinTokens,
404
            $this->whereTokens,
405
            $this->havingTokens,
406
            $this->grouping,
407
            $this->ordering,
408
            $this->limit,
409
            $this->offset
410
        );
411
    }
412
413
    /**
414
     * {@inheritdoc}
415
     *
416
     * Return type will depend if custom columns set were used.
417
     *
418
     * @param array $callbacks Callbacks to be used in record iterator as magic methods.
419
     * @return QueryResult|RecordIterator
420
     */
421
    public function getIterator(array $callbacks = [])
422
    {
423
        if (!empty($this->columns) || !empty($this->grouping)) {
424
            //QueryResult for user requests
425
            return $this->run();
426
        }
427
428
        /*
429
         * We are getting copy of ORM with cloned cache, so all our entities are isolated in it.
430
         */
431
432
        return new RecordIterator(
433
            clone $this->orm,
434
            $this->class,
435
            $this->fetchData(),
436
            true,
437
            $callbacks
438
        );
439
    }
440
441
    /**
442
     * All records.
443
     *
444
     * @return RecordInterface[]
445
     */
446
    public function all()
447
    {
448
        return $this->getIterator()->all();
449
    }
450
451
    /**
452
     * Execute query and every related query to compile records data in tree form - every relation
453
     * data will be included as sub key.
454
     *
455
     * Attention, Selector will cache compiled data tree and not query itself to keep data integrity
456
     * and to skip data compilation on second query.
457
     *
458
     * @return array
459
     */
460
    public function fetchData()
461
    {
462
        //Pagination!
463
        $this->applyPagination();
464
465
        //Generating statement
466
        $statement = $this->sqlStatement();
467
468
        if (!empty($this->cacheLifetime)) {
469
            $cacheKey = $this->cacheKey ?: md5(serialize([$statement, $this->getParameters()]));
470
471
            if (empty($this->cacheStore)) {
472
                $this->cacheStore = $this->orm->container()->get(CacheInterface::class)->store();
473
            }
474
475
            if ($this->cacheStore->has($cacheKey)) {
476
                $this->logger()->debug("Selector result were fetched from cache.");
477
478
                //We are going to store parsed result, not queries
479
                return $this->cacheStore->get($cacheKey);
480
            }
481
        }
482
483
        //We are bypassing run() method here to prevent query caching, we will prefer to cache
484
        //parsed data rather that database response
485
        $result = $this->database->query($statement, $this->getParameters());
486
487
        //In many cases (too many inloads, too complex queries) parsing can take significant amount
488
        //of time, so we better profile it
489
        $benchmark = $this->benchmark('parseResult', $statement);
490
491
        //Here we are feeding selected data to our primary loaded to parse it and and create
492
        //data tree for our records
493
        $this->loader->parseResult($result, $rowsCount);
494
495
        $this->benchmark($benchmark);
496
497
        //Memory freeing
498
        $result->close();
499
500
        //This must force loader to execute all post loaders (including ODM and etc)
501
        $this->loader->loadData();
502
503
        //Now we can request our primary loader for compiled data
504
        $data = $this->loader->getResult();
505
506
        //Memory free! Attention, it will not reset columns aliases but only make possible to run
507
        //query again
508
        $this->loader->clean();
509
510
        if (!empty($this->cacheLifetime) && !empty($cacheKey)) {
511
            //We are caching full records tree, not queries
512
            $this->cacheStore->set($cacheKey, $data, $this->cacheLifetime);
513
        }
514
515
        return $data;
516
    }
517
518
    /**
519
     * Cloning selector presets
520
     */
521
    public function __clone()
522
    {
523
        $this->loader = clone $this->loader;
524
    }
525
526
    /**
527
     * {@inheritdoc}
528
     */
529
    public function count($column = self::DEFAULT_COUNTING_FIELD)
530
    {
531
        if ($column == self::DEFAULT_COUNTING_FIELD && !empty($this->loader->getPrimaryKey())) {
532
            $column = 'DISTINCT(' . $this->loader->getPrimaryKey().')';
533
        }
534
535
        return parent::count($column);
536
    }
537
}