Completed
Branch feature/pre-split (8b986a)
by Anton
06:31
created

DocumentSelector::findOne()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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