Completed
Branch feature/split-orm (fa2f8e)
by Anton
03:35
created

DocumentSelector::sortBy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * 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 class name.
92
     *
93
     * @return string
94
     */
95
    public function getClass(): string
96
    {
97
        return $this->class;
98
    }
99
100
    /**
101
     * Set additional query, fields will be merged to currently existed request using array_merge.
102
     * Alias for query.
103
     *
104
     * @link http://docs.mongodb.org/manual/tutorial/query-documents/
105
     *
106
     * @see  query()
107
     *
108
     * @param array $query          Fields and conditions to query by.
109
     * @param bool  $normalizeDates When true (default) all DateTime objects will be converted into
110
     *                              MongoDate.
111
     *
112
     * @return self|$this
113
     */
114
    public function find(array $query = [], bool $normalizeDates = true): DocumentSelector
115
    {
116
        return $this->where($query, $normalizeDates);
117
    }
118
119
    /**
120
     * Set additional query, fields will be merged to currently existed request using array_merge.
121
     * Alias for query.
122
     *
123
     * @link http://docs.mongodb.org/manual/tutorial/query-documents/
124
     *
125
     * @see  query()
126
     *
127
     * @param array $query          Fields and conditions to query by.
128
     * @param bool  $normalizeDates When true (default) all DateTime objects will be converted into
129
     *                              MongoDate.
130
     *
131
     * @return self|$this
132
     */
133
    public function where(array $query = [], bool $normalizeDates = true): DocumentSelector
134
    {
135
        if ($normalizeDates) {
136
            $query = $this->normalizeDates($query);
137
        }
138
139
        $this->query = array_merge($this->query, $query);
140
141
        return $this;
142
    }
143
144
    /**
145
     * Sorts the results by given fields.
146
     *
147
     * @link http://www.php.net/manual/en/mongocursor.sort.php
148
     *
149
     * @param array $fields An array of fields by which to sort. Each element in the array has as
150
     *                      key the field name, and as value either 1 for ascending sort, or -1 for
151
     *                      descending sort.
152
     *
153
     * @return self|$this
154
     */
155
    public function sortBy(array $fields): DocumentSelector
156
    {
157
        $this->sort = $fields;
158
159
        return $this;
160
    }
161
162
    /**
163
     * Alias for sortBy.
164
     *
165
     * @param string $field
166
     * @param int    $direction
167
     *
168
     * @return self|$this
169
     */
170
    public function orderBy(string $field, int $direction = self::ASCENDING): DocumentSelector
171
    {
172
        return $this->sortBy($this->sort + [$field => $direction]);
173
    }
174
175
    /**
176
     * Select one document or it's fields from collection.
177
     *
178
     * @param array $query          Fields and conditions to query by. Query will not be added to an
179
     *                              existed query array.
180
     * @param bool  $normalizeDates When true (default) all DateTime objects will be converted into
181
     *                              MongoDate.
182
     *
183
     * @return CompositableInterface|null
184
     */
185
    public function findOne(array $query = [], bool $normalizeDates = true)
186
    {
187
        if ($normalizeDates) {
188
            $query = $this->normalizeDates($query);
189
        }
190
191
        $result = $this->collection->findOne(
192
            array_merge($this->query, $query),
193
            $this->createOptions()
194
        );
195
196
        if (!is_null($result)) {
197
            $result = $this->odm->instantiate($this->class, $result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type object; however, Spiral\ODM\ODMInterface::instantiate() does only seem to accept array|object<ArrayAccess>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
198
        }
199
200
        return $result;
201
    }
202
203
    /**
204
     * Count collection. Attention, this method depends on current values set for limit and offset!
205
     *
206
     * @return int
207
     */
208
    public function count(): int
209
    {
210
        //Create options?
211
        return $this->collection->count(
212
            $this->query,
213
            ['skip' => $this->offset, 'limit' => $this->limit]
214
        );
215
    }
216
217
    /**
218
     * Fetch all documents.
219
     *
220
     * @return CompositableInterface[]
221
     */
222
    public function fetchAll(): array
223
    {
224
        return $this->getIterator()->fetchAll();
225
    }
226
227
    /**
228
     * Create cursor with partial selection. Attention, you can not exclude _id from result!
229
     *
230
     * Example: $selector->fetchFields(['name', ...])->toArray();
231
     *
232
     * @param array $fields
233
     *
234
     * @return Cursor
235
     */
236
    public function getProjection(array $fields = []): Cursor
237
    {
238
        return $this->createCursor($fields);
239
    }
240
241
    /**
242
     * Create selection and wrap it using DocumentCursor.
243
     *
244
     * @return DocumentCursor
245
     */
246
    public function getIterator(): DocumentCursor
247
    {
248
        return new DocumentCursor(
249
            $this->createCursor(),
250
            $this->class,
251
            $this->odm
252
        );
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258
    public function jsonSerialize()
259
    {
260
        return $this->fetchAll();
261
    }
262
263
    /**
264
     * @return array
265
     */
266
    public function __debugInfo()
267
    {
268
        return [
269
            'database'   => $this->collection->getDatabaseName(),
270
            'collection' => $this->collection->getCollectionName(),
271
            'query'      => $this->query,
272
            'limit'      => $this->limit,
273
            'offset'     => $this->offset,
274
            'sort'       => $this->sort
275
        ];
276
    }
277
278
    /**
279
     * Destructing.
280
     */
281
    public function __destruct()
282
    {
283
        $this->collection = null;
284
        $this->odm = null;
285
        $this->paginator = null;
286
        $this->query = [];
287
        $this->sort = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $sort.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
288
    }
289
290
    /**
291
     * @param array $fields Fields to be selected (keep empty to select all).
292
     *
293
     * @return Cursor
294
     */
295
    protected function createCursor(array $fields = [])
296
    {
297
        if ($this->hasPaginator()) {
298
            $paginator = $this->configurePaginator($this->count());
299
300
            //Paginate in isolation
301
            $selector = clone $this;
302
            $selector->limit($paginator->getLimit())->offset($paginator->getOffset());
303
            $options = $selector->createOptions();
304
        } else {
305
            $options = $this->createOptions();
306
        }
307
308
        if (!empty($fields)) {
309
            $options['projection'] = array_fill_keys($fields, 1);
310
        }
311
312
        return $this->collection->find($this->query, $options);
313
    }
314
315
    /**
316
     * Options to be send to find() method of MongoDB\Collection. Options are based on how selector
317
     * was configured.
318
     **
319
     *
320
     * @return array
321
     */
322
    protected function createOptions(): array
323
    {
324
        return [
325
            'skip'    => $this->offset,
326
            'limit'   => $this->limit,
327
            'sort'    => $this->sort,
328
            'typeMap' => self::TYPE_MAP
329
        ];
330
    }
331
332
    /**
333
     * Converts DateTime objects into MongoDatetime.
334
     *
335
     * @param array $query
336
     *
337
     * @return array
338
     */
339
    protected function normalizeDates(array $query): array
340
    {
341
        array_walk_recursive($query, function (&$value) {
342
            if ($value instanceof \DateTime) {
343
                //MongoDate is always UTC, which is good :)
344
                $value = new UTCDateTime($value);
345
            }
346
        });
347
348
        return $query;
349
    }
350
351
    /**
352
     * @return \Interop\Container\ContainerInterface|null
353
     */
354
    protected function iocContainer()
355
    {
356
        if ($this->odm instanceof Component) {
357
            //Forwarding container scope
358
            return $this->odm->iocContainer();
359
        }
360
361
        return parent::iocContainer();
362
    }
363
}