Completed
Branch feature/pre-split (2ed6c7)
by Anton
04:25
created

RecordSelector::wherePK()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM\Entities;
8
9
use Psr\Cache\CacheItemPoolInterface;
10
use Spiral\Core\Component;
11
use Spiral\Core\Traits\SaturateTrait;
12
use Spiral\Database\Builders\SelectQuery;
13
use Spiral\Models\EntityInterface;
14
use Spiral\ORM\Entities\Loaders\RootLoader;
15
use Spiral\ORM\Entities\Nodes\RootNode;
16
use Spiral\ORM\Exceptions\SelectorException;
17
use Spiral\ORM\ORMInterface;
18
use Spiral\Pagination\PaginatorAwareInterface;
19
use Spiral\Pagination\PaginatorInterface;
20
21
/**
22
 * Attention, RecordSelector DOES NOT extends QueryBuilder but mocks it!
23
 *
24
 * @todo mocked methods
25
 */
26
class RecordSelector extends Component implements \IteratorAggregate, \Countable, PaginatorAwareInterface
27
{
28
    use SaturateTrait;
29
30
    /**
31
     * @var string
32
     */
33
    private $class;
34
35
    /**
36
     * @invisible
37
     * @var ORMInterface
38
     */
39
    private $orm;
40
41
    /**
42
     * @var RootLoader
43
     */
44
    private $loader;
45
46
    /**
47
     * @param string       $class
48
     * @param ORMInterface $orm
49
     */
50
    public function __construct(string $class, ORMInterface $orm)
51
    {
52
        $this->class = $class;
53
        $this->orm = $orm;
54
55
        $this->loader = new RootLoader(
56
            $class,
57
            $orm->define($class, ORMInterface::R_SCHEMA),
58
            $orm
59
        );
60
    }
61
62
    /**
63
     * Get associated class.
64
     *
65
     * @return string
66
     */
67
    public function getClass(): string
68
    {
69
        return $this->class;
70
    }
71
72
    /**
73
     * Request primary selector loader to pre-load relation name. Any type of loader can be used
74
     * for
75
     * data preloading. ORM loaders by default will select the most efficient way to load related
76
     * data which might include additional select query or left join. Loaded data will
77
     * automatically pre-populate record relations. You can specify nested relations using "."
78
     * separator.
79
     *
80
     * Examples:
81
     *
82
     * //Select users and load their comments (will cast 2 queries, HAS_MANY comments)
83
     * User::find()->with('comments');
84
     *
85
     * //You can load chain of relations - select user and load their comments and post related to
86
     * //comment
87
     * User::find()->with('comments.post');
88
     *
89
     * //We can also specify custom where conditions on data loading, let's load only public
90
     * comments. User::find()->load('comments', [
91
     *      'where' => ['{@}.status' => 'public']
92
     * ]);
93
     *
94
     * Please note using "{@}" column name, this placeholder is required to prevent collisions and
95
     * it will be automatically replaced with valid table alias of pre-loaded comments table.
96
     *
97
     * //In case where your loaded relation is MANY_TO_MANY you can also specify pivot table
98
     * conditions,
99
     * //let's pre-load all approved user tags, we can use same placeholder for pivot table alias
100
     * User::find()->load('tags', [
101
     *      'wherePivot' => ['{@}.approved' => true]
102
     * ]);
103
     *
104
     * //In most of cases you don't need to worry about how data was loaded, using external query
105
     * or
106
     * //left join, however if you want to change such behaviour you can force load method to
107
     * INLOAD
108
     * User::find()->load('tags', [
109
     *      'method'     => Loader::INLOAD,
110
     *      'wherePivot' => ['{@}.approved' => true]
111
     * ]);
112
     *
113
     * Attention, you will not be able to correctly paginate in this case and only ORM loaders
114
     * support different loading types.
115
     *
116
     * You can specify multiple loaders using array as first argument.
117
     *
118
     * Example:
119
     * User::find()->load(['posts', 'comments', 'profile']);
120
     *
121
     * @see with()
122
     *
123
     * @param string|array $relation
124
     * @param array        $options
125
     *
126
     * @return $this|RecordSelector
127
     */
128
    public function load($relation, array $options = []): self
129
    {
130
        if (is_array($relation)) {
131
            foreach ($relation as $name => $subOption) {
132
                if (is_string($subOption)) {
133
                    //Array of relation names
134
                    $this->load($subOption, $options);
135
                } else {
136
                    //Multiple relations or relation with addition load options
137
                    $this->load($name, $subOption + $options);
138
                }
139
            }
140
141
            return $this;
142
        }
143
144
        //We are requesting primary loaded to pre-load nested relation
145
        $this->loader->loadRelation($relation, $options);
146
147
        return $this;
148
    }
149
150
    /**
151
     * With method is very similar to load() one, except it will always include related data to
152
     * parent query using INNER JOIN, this method can be applied only to ORM loaders and relations
153
     * using same database as parent record.
154
     *
155
     * Method generally used to filter data based on some relation condition.
156
     * Attention, with() method WILL NOT load relation data, it will only make it accessible in
157
     * query.
158
     *
159
     * By default joined tables will be available in query based on relation name, you can change
160
     * joined table alias using relation option "alias".
161
     *
162
     * Do not forget to set DISTINCT flag while including HAS_MANY and MANY_TO_MANY relations. In
163
     * other scenario you will not able to paginate data well.
164
     *
165
     * Examples:
166
     *
167
     * //Find all users who have comments comments
168
     * User::find()->with('comments');
169
     *
170
     * //Find all users who have approved comments (we can use comments table alias in where
171
     * statement).
172
     * User::find()->with('comments')->where('comments.approved', true);
173
     *
174
     * //Find all users who have posts which have approved comments
175
     * User::find()->with('posts.comments')->where('posts_comments.approved', true);
176
     *
177
     * //Custom join alias for post comments relation
178
     * $user->with('posts.comments', [
179
     *      'alias' => 'comments'
180
     * ])->where('comments.approved', true);
181
     *
182
     * //If you joining MANY_TO_MANY relation you will be able to use pivot table used as relation
183
     * name
184
     * //plus "_pivot" postfix. Let's load all users with approved tags.
185
     * $user->with('tags')->where('tags_pivot.approved', true);
186
     *
187
     * //You can also use custom alias for pivot table as well
188
     * User::find()->with('tags', [
189
     *      'pivotAlias' => 'tags_connection'
190
     * ])
191
     * ->where('tags_connection.approved', false);
192
     *
193
     * You can safely combine with() and load() methods.
194
     *
195
     * //Load all users with approved comments and pre-load all their comments
196
     * User::find()->with('comments')->where('comments.approved', true)
197
     *             ->load('comments');
198
     *
199
     * //You can also use custom conditions in this case, let's find all users with approved
200
     * comments
201
     * //and pre-load such approved comments
202
     * User::find()->with('comments')->where('comments.approved', true)
203
     *             ->load('comments', [
204
     *                  'where' => ['{@}.approved' => true]
205
     *              ]);
206
     *
207
     * //As you might notice previous construction will create 2 queries, however we can simplify
208
     * //this construction to use already joined table as source of data for relation via "using"
209
     * //keyword
210
     * User::find()->with('comments')
211
     *             ->where('comments.approved', true)
212
     *             ->load('comments', ['using' => 'comments']);
213
     *
214
     * //You will get only one query with INNER JOIN, to better understand this example let's use
215
     * //custom alias for comments in with() method.
216
     * User::find()->with('comments', ['alias' => 'commentsR'])
217
     *             ->where('commentsR.approved', true)
218
     *             ->load('comments', ['using' => 'commentsR']);
219
     *
220
     * @see load()
221
     *
222
     * @param string|array $relation
223
     * @param array        $options
224
     *
225
     * @return $this|RecordSelector
226
     */
227
    public function with($relation, array $options = []): self
228
    {
229
        if (is_array($relation)) {
230
            foreach ($relation as $name => $options) {
231
                if (is_string($options)) {
232
                    //Array of relation names
233
                    $this->with($options, []);
234
                } else {
235
                    //Multiple relations or relation with addition load options
236
                    $this->with($name, $options);
237
                }
238
            }
239
240
            return $this;
241
        }
242
243
        //Requesting primary loader to join nested relation, will only work for ORM loaders
244
        $this->loader->loadRelation($relation, $options, true);
245
246
        return $this;
247
    }
248
249
    /**
250
     * Shortcut to where method to set AND condition for parent record primary key.
251
     *
252
     * @param string|int $id
253
     *
254
     * @return RecordSelector
255
     *
256
     * @throws SelectorException
257
     */
258
    public function wherePK($id): self
259
    {
260
        if (empty($this->loader->primaryKey())) {
261
            throw new SelectorException("Unable to set wherePK condition, no proper PK were defined");
262
        }
263
264
        $this->loader->initialQuery()->where([$this->loader->primaryKey() => $id]);
265
266
        return $this;
267
    }
268
269
    /**
270
     * Find one entity or return null.
271
     *
272
     * @param array|null $query
273
     *
274
     * @return EntityInterface|null
275
     */
276
    public function findOne(array $query = null)
277
    {
278
        $data = (clone $this)->where($query)->fetchData();
0 ignored issues
show
Documentation Bug introduced by
The method where does not exist on object<Spiral\ORM\Entities\RecordSelector>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
279
280
        if (empty($data[0])) {
281
            return null;
282
        }
283
284
        return $this->orm->make($this->class, $data[0], ORMInterface::STATE_LOADED, true);
285
    }
286
287
    /**
288
     * Get RecordIterator (entity iterator) for a requested data. Provide cache key and lifetime in
289
     * order to cache request data.
290
     *
291
     * @param string                      $cacheKey
292
     * @param int|\DateInterval           $ttl
293
     * @param CacheItemPoolInterface|null $pool
294
     *
295
     * @return RecordIterator
296
     */
297
    public function getIterator(
298
        string $cacheKey = '',
299
        $ttl = 0,
300
        CacheItemPoolInterface $pool = null
301
    ): RecordIterator {
302
        if (!empty($cacheKey)) {
303
            /**
304
             * When no pool is provided saturate it using container scope
305
             *
306
             * @var CacheItemPoolInterface $pool
307
             */
308
            $pool = $this->saturate($pool, CacheItemPoolInterface::class);
309
            $item = $pool->getItem($cacheKey);
310
311
            if ($item->isHit()) {
312
                $data = $item->get();
313
            } else {
314
                $data = $this->fetchData();
315
316
                $pool->save($item->set($data)->expiresAfter($ttl));
317
            }
318
        } else {
319
            $data = $this->fetchData();
320
        }
321
322
        return new RecordIterator($data, $this->class, $this->orm);
323
    }
324
325
    /**
326
     * @param string|null $column When column is null DISTINCT(PK) will be generated.
327
     *
328
     * @return int
329
     */
330
    public function count(string $column = null): int
331
    {
332
        if (is_null($column)) {
333
            if (!empty($this->loader->primaryKey())) {
334
                //@tuneyourserver solves the issue with counting on queries with joins.
335
                $column = "DISTINCT({$this->loader->primaryKey()})";
336
            } else {
337
                $column = '*';
338
            }
339
        }
340
341
        return $this->compileQuery()->count($column);
342
    }
343
344
    /**
345
     * Get compiled version of SelectQuery, attentionly only first level query access is allowed.
346
     *
347
     * @return SelectQuery
348
     */
349
    public function compileQuery(): SelectQuery
350
    {
351
        return $this->loader->compileQuery();
352
    }
353
354
    /**
355
     * Load data tree from databases and linked loaders in a form of array.
356
     *
357
     * @return array
358
     */
359
    public function fetchData(): array
360
    {
361
        /**
362
         * @var RootNode $node
363
         */
364
        $node = $this->loader->createNode();
365
366
        //Working with parser defined by loader itself
367
        $this->loader->loadData($node);
368
369
        return $node->getResult();
370
    }
371
372
    /**
373
     * {@inheritdoc}
374
     */
375
    public function hasPaginator(): bool
376
    {
377
        return $this->loader->initialQuery()->hasPaginator();
378
    }
379
380
    /**
381
     * {@inheritdoc}
382
     */
383
    public function setPaginator(PaginatorInterface $paginator)
384
    {
385
        $this->loader->initialQuery()->setPaginator($paginator);
386
    }
387
388
    /**
389
     * {@inheritdoc}
390
     */
391
    public function getPaginator(): PaginatorInterface
392
    {
393
        return $this->loader->initialQuery()->getPaginator();
394
    }
395
396
    /**
397
     * Bypassing call to primary select query.
398
     *
399
     * @param string $name
400
     * @param        $arguments
401
     *
402
     * @return $this|mixed
403
     */
404
    public function __call(string $name, array $arguments)
405
    {
406
        if (in_array(strtoupper($name), ['AVG', 'MIN', 'MAX', 'SUM'])) {
407
            //One of aggregation requests
408
            $result = call_user_func_array([$this->compileQuery(), $name], $arguments);
409
        } else {
410
            //Where condition or statement
411
            $result = call_user_func_array([$this->loader->initialQuery(), $name], $arguments);
412
        }
413
414
        if ($result === $this->loader->initialQuery()) {
415
            return $this;
416
        }
417
418
        return $result;
419
    }
420
421
    /**
422
     * Cloning with loader tree cloning.
423
     *
424
     * @attention at this moment binded query parameters would't be cloned!
425
     */
426
    public function __clone()
427
    {
428
        $this->loader = clone $this->loader;
429
    }
430
431
    /**
432
     * Remove nested loaders and clean ORM link.
433
     */
434
    public function __destruct()
435
    {
436
        $this->orm = null;
437
        $this->loader = null;
438
    }
439
440
    /**
441
     * @return \Interop\Container\ContainerInterface|null
442
     */
443
    protected function iocContainer()
444
    {
445
        if ($this->orm instanceof Component) {
446
            //Working inside ORM container scope
447
            return $this->orm->iocContainer();
448
        }
449
450
        return parent::iocContainer();
451
    }
452
}