DocumentSelector   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 363
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 7
dl 0
loc 363
rs 10
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getODM() 0 4 1
A getClass() 0 4 1
A find() 0 4 1
A where() 0 10 2
A sortBy() 0 6 1
A orderBy() 0 4 1
A findOne() 0 18 3
A count() 0 8 1
A fetchAll() 0 4 1
A getProjection() 0 4 1
A getIterator() 0 8 1
A jsonSerialize() 0 4 1
A __debugInfo() 0 11 1
A __destruct() 0 8 1
A createCursor() 0 19 3
A createOptions() 0 9 1
A normalizeDates() 0 11 2
A iocContainer() 0 9 2
1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ODM\Entities;
9
10
use MongoDB\BSON\UTCDateTime;
11
use MongoDB\Collection;
12
use MongoDB\Driver\Cursor;
13
use Spiral\Core\Component;
14
use Spiral\ODM\CompositableInterface;
15
use Spiral\ODM\ODMInterface;
16
use Spiral\Pagination\PaginatorAwareInterface;
17
use Spiral\Pagination\Traits\LimitsTrait;
18
use Spiral\Pagination\Traits\PaginatorTrait;
19
20
/**
21
 * Provides fluent interface to build document selections.
22
 */
23
class DocumentSelector extends Component implements
24
    \Countable,
25
    \IteratorAggregate,
26
    \JsonSerializable,
27
    PaginatorAwareInterface
28
{
29
    use LimitsTrait, PaginatorTrait;
30
31
    /**
32
     * Sort orders.
33
     */
34
    const ASCENDING  = 1;
35
    const DESCENDING = -1;
36
37
    /**
38
     * Default selection type map.
39
     */
40
    const TYPE_MAP = [
41
        'root'     => 'array',
42
        'document' => 'array',
43
        'array'    => 'array'
44
    ];
45
46
    /**
47
     * @var Collection
48
     */
49
    private $collection;
50
51
    /**
52
     * Document class being selected.
53
     *
54
     * @var string
55
     */
56
    private $class;
57
58
    /**
59
     * @var ODMInterface
60
     */
61
    private $odm;
62
63
    /**
64
     * Fields and conditions to query by.
65
     *
66
     * @link http://docs.mongodb.org/manual/tutorial/query-documents/
67
     *
68
     * @var array
69
     */
70
    private $query = [];
71
72
    /**
73
     * Fields to sort.
74
     *
75
     * @var array
76
     */
77
    private $sortBy = [];
78
79
    /**
80
     * @param Collection   $collection
81
     * @param string       $class
82
     * @param ODMInterface $odm
83
     */
84
    public function __construct(Collection $collection, string $class, ODMInterface $odm)
85
    {
86
        $this->collection = $collection;
87
        $this->class = $class;
88
        $this->odm = $odm;
89
    }
90
91
    /**
92
     * Associated ODM instance.
93
     *
94
     * @return ODMInterface
95
     */
96
    public function getODM(): ODMInterface
97
    {
98
        return $this->odm;
99
    }
100
101
    /**
102
     * Associated class name.
103
     *
104
     * @return string
105
     */
106
    public function getClass(): string
107
    {
108
        return $this->class;
109
    }
110
111
    /**
112
     * Set additional query, fields will be merged to currently existed request using array_merge.
113
     * Alias for query.
114
     *
115
     * Attention, MongoDB is strictly typed!
116
     *
117
     * @link http://docs.mongodb.org/manual/tutorial/query-documents/
118
     *
119
     * @see  query()
120
     *
121
     * @param array $query          Fields and conditions to query by.
122
     * @param bool  $normalizeDates When true (default) all DateTime objects will be converted into
123
     *                              MongoDate.
124
     *
125
     * @return self|$this
126
     */
127
    public function find(array $query = [], bool $normalizeDates = true): DocumentSelector
128
    {
129
        return $this->where($query, $normalizeDates);
130
    }
131
132
    /**
133
     * Set additional query, fields will be merged to currently existed request using array_merge.
134
     * Alias for query.
135
     *
136
     * Attention, MongoDB is strictly typed!
137
     *
138
     * @link http://docs.mongodb.org/manual/tutorial/query-documents/
139
     *
140
     * @see  find()
141
     *
142
     * @param array $query          Fields and conditions to query by.
143
     * @param bool  $normalizeDates When true (default) all DateTime objects will be converted into
144
     *                              MongoDate.
145
     *
146
     * @return self|$this
147
     */
148
    public function where(array $query = [], bool $normalizeDates = true): DocumentSelector
149
    {
150
        if ($normalizeDates) {
151
            $query = $this->normalizeDates($query);
152
        }
153
154
        $this->query = array_merge($this->query, $query);
155
156
        return $this;
157
    }
158
159
    /**
160
     * Sorts the results by given fields.
161
     *
162
     * @link http://www.php.net/manual/en/mongocursor.sort.php
163
     *
164
     * @param array $fields An array of fields by which to sort. Each element in the array has as
165
     *                      key the field name, and as value either 1 for ascending sort, or -1 for
166
     *                      descending sort.
167
     *
168
     * @return self|$this
169
     */
170
    public function sortBy(array $fields): DocumentSelector
171
    {
172
        $this->sortBy = $fields;
173
174
        return $this;
175
    }
176
177
    /**
178
     * Alias for sortBy.
179
     *
180
     * @param string $field
181
     * @param int    $direction
182
     *
183
     * @return self|$this
184
     */
185
    public function orderBy(string $field, int $direction = self::ASCENDING): DocumentSelector
186
    {
187
        return $this->sortBy($this->sortBy + [$field => $direction]);
188
    }
189
190
    /**
191
     * Select one document or it's fields from collection.
192
     *
193
     * Attention, MongoDB is strictly typed!
194
     *
195
     * @param array $query          Fields and conditions to query by. Query will not be added to an
196
     *                              existed query array.
197
     * @param bool  $normalizeDates When true (default) all DateTime objects will be converted into
198
     *                              MongoDate.
199
     *
200
     * @return CompositableInterface|null
201
     */
202
    public function findOne(array $query = [], bool $normalizeDates = true)
203
    {
204
        if ($normalizeDates) {
205
            $query = $this->normalizeDates($query);
206
        }
207
208
        //Working with projection (required to properly sort results)
209
        $result = $this->collection->findOne(
210
            array_merge($this->query, $query),
211
            $this->createOptions()
212
        );
213
214
        if (empty($result)) {
215
            return null;
216
        }
217
218
        return $this->odm->make($this->class, $result, false);
219
    }
220
221
    /**
222
     * Count collection. Attention, this method depends on current values set for limit and offset!
223
     *
224
     * Attention, MongoDB is strictly typed!
225
     *
226
     * @param array $query
227
     *
228
     * @return int
229
     */
230
    public function count(array $query = []): int
231
    {
232
        //Create options?
233
        return $this->collection->count(
234
            array_merge($this->query, $query),
235
            ['skip' => $this->offset, 'limit' => $this->limit]
236
        );
237
    }
238
239
    /**
240
     * Fetch all documents.
241
     *
242
     * @return CompositableInterface[]
243
     */
244
    public function fetchAll(): array
245
    {
246
        return $this->getIterator()->fetchAll();
247
    }
248
249
    /**
250
     * Create cursor with partial selection. Attention, you can not exclude _id from result!
251
     *
252
     * Example: $selector->fetchFields(['name', ...])->toArray();
253
     *
254
     * @param array $fields
255
     *
256
     * @return Cursor
257
     */
258
    public function getProjection(array $fields = []): Cursor
259
    {
260
        return $this->createCursor($fields);
261
    }
262
263
    /**
264
     * Create selection and wrap it using DocumentCursor.
265
     *
266
     * @return DocumentCursor
267
     */
268
    public function getIterator(): DocumentCursor
269
    {
270
        return new DocumentCursor(
271
            $this->createCursor(),
272
            $this->class,
273
            $this->odm
274
        );
275
    }
276
277
    /**
278
     * {@inheritdoc}
279
     */
280
    public function jsonSerialize()
281
    {
282
        return $this->fetchAll();
283
    }
284
285
    /**
286
     * @return array
287
     */
288
    public function __debugInfo()
289
    {
290
        return [
291
            'database'   => $this->collection->getDatabaseName(),
292
            'collection' => $this->collection->getCollectionName(),
293
            'query'      => $this->query,
294
            'limit'      => $this->limit,
295
            'offset'     => $this->offset,
296
            'sort'       => $this->sortBy
297
        ];
298
    }
299
300
    /**
301
     * Destructing.
302
     */
303
    public function __destruct()
304
    {
305
        $this->collection = null;
306
        $this->odm = null;
307
        $this->paginator = null;
308
        $this->query = [];
309
        $this->sortBy = [];
310
    }
311
312
    /**
313
     * @param array $fields Fields to be selected (keep empty to select all).
314
     *
315
     * @return Cursor
316
     */
317
    protected function createCursor(array $fields = [])
318
    {
319
        if ($this->hasPaginator()) {
320
            $paginator = $this->getPaginator(true);
321
322
            //Paginate in isolation
323
            $selector = clone $this;
324
            $selector->limit($paginator->getLimit())->offset($paginator->getOffset());
325
            $options = $selector->createOptions();
326
        } else {
327
            $options = $this->createOptions();
328
        }
329
330
        if (!empty($fields)) {
331
            $options['projection'] = array_fill_keys($fields, 1);
332
        }
333
334
        return $this->collection->find($this->query, $options);
335
    }
336
337
    /**
338
     * Options to be send to find() method of MongoDB\Collection. Options are based on how selector
339
     * was configured.
340
     **
341
     *
342
     * @return array
343
     */
344
    protected function createOptions(): array
345
    {
346
        return [
347
            'skip'    => $this->offset,
348
            'limit'   => $this->limit,
349
            'sort'    => $this->sortBy,
350
            'typeMap' => self::TYPE_MAP
351
        ];
352
    }
353
354
    /**
355
     * Converts DateTime objects into MongoDatetime.
356
     *
357
     * @param array $query
358
     *
359
     * @return array
360
     */
361
    protected function normalizeDates(array $query): array
362
    {
363
        array_walk_recursive($query, function (&$value) {
364
            if ($value instanceof \DateTime) {
365
                //MongoDate is always UTC, which is good :)
366
                $value = new UTCDateTime($value);
367
            }
368
        });
369
370
        return $query;
371
    }
372
373
    /**
374
     * @return \Interop\Container\ContainerInterface|null
375
     */
376
    protected function iocContainer()
377
    {
378
        if ($this->odm instanceof Component) {
379
            //Forwarding container scope
380
            return $this->odm->iocContainer();
381
        }
382
383
        return parent::iocContainer();
384
    }
385
}