Completed
Branch dev (bc6e47)
by Raffael
02:23
created

User::_getAttributeSummary()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 42
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 42
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 22
nc 2
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Balloon
7
 *
8
 * @author      Raffael Sahli <[email protected]>
9
 * @copyright   Copryright (c) 2012-2017 gyselroth GmbH (https://gyselroth.com)
10
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
11
 */
12
13
namespace Balloon\Server;
14
15
use Balloon\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\Server\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
16
use Balloon\Filesystem;
17
use Balloon\Filesystem\Node\Collection;
18
use Balloon\Helper;
19
use Balloon\Server;
20
use Micro\Auth\Identity;
21
use MongoDB\BSON\Binary;
22
use MongoDB\BSON\ObjectId;
23
use MongoDB\BSON\UTCDateTime;
24
use MongoDB\Database;
25
use Psr\Log\LoggerInterface;
26
27
class User
28
{
29
    /**
30
     * User unique id.
31
     *
32
     * @var ObjectId
33
     */
34
    protected $_id;
35
36
    /**
37
     * Username.
38
     *
39
     * @var string
40
     */
41
    protected $username;
42
43
    /**
44
     * Groups.
45
     *
46
     * @var array
47
     */
48
    protected $groups = [];
49
50
    /**
51
     * Last sync timestamp.
52
     *
53
     * @var UTCDateTime
54
     */
55
    protected $last_attr_sync;
56
57
    /**
58
     * Soft Quota.
59
     *
60
     * @var int
61
     */
62
    protected $soft_quota = 0;
63
64
    /**
65
     * Hard Quota.
66
     *
67
     * @var int
68
     */
69
    protected $hard_quota = 0;
70
71
    /**
72
     * Is user deleted?
73
     *
74
     * @var bool
75
     */
76
    protected $deleted = false;
77
78
    /**
79
     * Admin.
80
     *
81
     * @var bool
82
     */
83
    protected $admin = false;
84
85
    /**
86
     * Created.
87
     *
88
     * @var UTCDateTime
89
     */
90
    protected $created;
91
92
    /**
93
     * avatar.
94
     *
95
     * @var Binary
96
     */
97
    protected $avatar;
98
99
    /**
100
     * Namespace.
101
     *
102
     * @var string
103
     */
104
    protected $namespace;
105
106
    /**
107
     * Mail.
108
     *
109
     * @var string
110
     */
111
    protected $mail;
112
113
    /**
114
     * Db.
115
     *
116
     * @var Database
117
     */
118
    protected $db;
119
120
    /**
121
     * LoggerInterface.
122
     *
123
     * @var LoggerInterface
124
     */
125
    protected $logger;
126
127
    /**
128
     * Server.
129
     *
130
     * @var Server
131
     */
132
    protected $server;
133
134
    /**
135
     * Filesystem.
136
     *
137
     * @var Filesystem
138
     */
139
    protected $fs;
140
141
    /**
142
     * Valid attributes.
143
     *
144
     * @var array
145
     */
146
    protected static $valid_attributes = [
147
         'id',
148
         'username',
149
         'created',
150
         'soft_quota',
151
         'hard_quota',
152
         'mail',
153
         'namespace',
154
         'last_attr_sync',
155
         'avatar',
156
         'created',
157
         'admin',
158
     ];
159
160
    /**
161
     * Instance user.
162
     *
163
     * @param array           $attributes
164
     * @param Server          $server
165
     * @param Database        $db
166
     * @param LoggerInterface $logger
167
     */
168
    public function __construct(array $attributes, Server $server, Database $db, LoggerInterface $logger)
169
    {
170
        $this->server = $server;
171
        $this->db = $db;
172
        $this->logger = $logger;
173
174
        foreach ($attributes as $attr => $value) {
175
            $this->{$attr} = $value;
176
        }
177
    }
178
179
    /**
180
     * Return username as string.
181
     *
182
     * @return string
183
     */
184
    public function __toString(): string
185
    {
186
        return $this->username;
187
    }
188
189
    /**
190
     * Update user with identity attributes.
191
     *
192
     * @param Identity $identity
193
     *
194
     * @return bool
195
     */
196
    public function updateIdentity(Identity $identity): bool
197
    {
198
        $attr_sync = $identity->getAdapter()->getAttributeSyncCache();
199
        if ($attr_sync === -1) {
200
            return true;
201
        }
202
203
        $cache = ($this->last_attr_sync instanceof UTCDateTime ?
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
204
            $this->last_attr_sync->toDateTime()->format('U') : 0);
205
206
        if (time() - $attr_sync > $cache) {
207
            $this->logger->info('user attribute sync cache time expired, resync with auth attributes', [
208
                'category' => get_class($this),
209
            ]);
210
211
            $attributes = $identity->getAttributes();
212
            foreach ($attributes as $attr => $value) {
213
                $this->{$attr} = $value;
214
            }
215
216
            $this->last_attr_sync = new UTCDateTime();
217
218
            $save = array_keys($attributes);
219
            $save[] = 'last_attr_sync';
220
221
            return $this->save($save);
222
        }
223
        $this->logger->debug('user auth attribute sync cache is in time', [
224
                'category' => get_class($this),
225
            ]);
226
227
        return true;
228
    }
229
230
    /**
231
     * Set user attribute.
232
     *
233
     * @param array $attribute
234
     */
235
    public function setAttribute($attribute = [])
236
    {
237
        foreach ($attribute as $attr => $value) {
238
            if (!in_array($attr, self::$valid_attributes, true)) {
239
                throw new Exception\InvalidArgument('requested attribute '.$attr.' does not exists');
240
            }
241
242
            switch ($attr) {
243
                case 'id':
244
                    $this->_id = (string) $value;
0 ignored issues
show
Documentation Bug introduced by
It seems like (string) $value of type string is incompatible with the declared type object<MongoDB\BSON\ObjectId> of property $_id.

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...
245
246
                break;
247
                case 'avatar':
248
                    $this->avatar = new Binary(base64_decode($value, true));
249
250
                break;
251
                case 'created':
252
                case 'last_attr_sync':
253
                    $this->{$attr} = Helper::DateTimeToUnix($value);
254
255
                break;
256
                default:
257
                    $this->{$attr} = $value;
258
259
                break;
260
            }
261
        }
262
263
        return $this;
264
    }
265
266
    /**
267
     * Get user attribute.
268
     *
269
     * @param array|string $attribute
270
     *
271
     * @return mixed
272
     */
273
    public function getAttribute($attribute = null)
274
    {
275
        $default = [
276
            'id',
277
            'username',
278
            'namespace',
279
            'created',
280
            'soft_quota',
281
            'hard_quota',
282
            'mail',
283
        ];
284
285
        $requested = $default;
286
287
        if (empty($attribute)) {
288
            $requested = $default;
289
        } elseif (is_string($attribute)) {
290
            $requested = (array) $attribute;
291
        } elseif (is_array($attribute)) {
292
            $requested = $attribute;
293
        }
294
295
        $resolved = [];
296
        foreach ($requested as $attr) {
297
            if (!in_array($attr, self::$valid_attributes, true)) {
298
                throw new Exception\InvalidArgument('requested attribute '.$attr.' does not exists');
299
            }
300
301
            switch ($attr) {
302
                case 'id':
303
                    $resolved['id'] = (string) $this->_id;
304
305
                break;
306
                case 'avatar':
307
                    if ($this->avatar instanceof Binary) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\Binary does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
308
                        $resolved['avatar'] = base64_encode($this->avatar->getData());
309
                    }
310
311
                break;
312
                case 'created':
313
                case 'last_attr_sync':
314
                    $resolved[$attr] = Helper::DateTimeToUnix($this->{$attr});
315
316
                break;
317
                default:
318
                    $resolved[$attr] = $this->{$attr};
319
320
                break;
321
            }
322
        }
323
324
        if (is_string($attribute)) {
325
            return $resolved[$attribute];
326
        }
327
328
        return $resolved;
329
    }
330
331
    /**
332
     * Find all shares with membership.
333
     *
334
     * @param bool $string
335
     *
336
     * @return array
337
     */
338
    public function getShares(bool $string = false): array
339
    {
340
        $item = $this->db->storage->find([
341
            'deleted' => false,
342
            'shared' => true,
343
            'owner' => $this->_id,
344
        ], [
345
            '_id' => 1,
346
            'reference' => 1,
347
        ]);
348
349
        $found = [];
350
351
        foreach ($item as $child) {
352
            if (isset($child['reference']) && $child['reference'] instanceof ObjectId) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
353
                $share = $child['reference'];
354
            } else {
355
                $share = $child['_id'];
356
            }
357
358
            if (true === $string) {
359
                $found[] = (string) $share;
360
            } else {
361
                $found[] = $share;
362
            }
363
        }
364
365
        return $found;
366
    }
367
368
    /**
369
     * Get node attribute usage.
370
     *
371
     * @param array|string $attributes
372
     * @param int          $limit
373
     *
374
     * @return array
375
     */
376
    public function getNodeAttributeSummary($attributes = [], int $limit = 25): array
377
    {
378
        $mongodb = $this->db->storage;
0 ignored issues
show
Unused Code introduced by
$mongodb is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
379
380
        $valid = [
381
            'mime' => 'string',
382
            'meta.tags' => 'array',
383
            'meta.author' => 'string',
384
            'meta.color' => 'string',
385
            'meta.license' => 'string',
386
            'meta.copyright' => 'string',
387
        ];
388
389
        if (empty($attributes)) {
390
            $attributes = array_keys($valid);
391
        } elseif (is_string($attributes)) {
392
            $attributes = [$attributes];
393
        }
394
395
        $filter = array_intersect_key($valid, array_flip($attributes));
396
        $result = [];
397
398
        foreach ($filter as $attribute => $type) {
399
            $result[$attribute] = $this->_getAttributeSummary($attribute, $type, $limit);
400
        }
401
402
        return $result;
403
    }
404
405
    /**
406
     * Get filesystem.
407
     *
408
     * @return Filesystem
409
     */
410
    public function getFilesystem(): Filesystem
411
    {
412
        if ($this->fs instanceof Filesystem) {
413
            return $this->fs;
414
        }
415
416
        return $this->fs = $this->server->getFilesystem($this);
417
    }
418
419
    /**
420
     * Is Admin user?
421
     *
422
     * @return bool
423
     */
424
    public function isAdmin(): bool
425
    {
426
        return $this->admin;
427
    }
428
429
    /**
430
     * Check if user has share.
431
     *
432
     * @param Collection $node
433
     *
434
     * @return bool
435
     */
436
    public function hasShare(Collection $node): bool
437
    {
438
        $result = $this->db->storage->count([
439
            'reference' => $node->getId(),
440
            'directory' => true,
441
            'owner' => $this->_id,
442
        ]);
443
444
        return 1 === $result;
445
    }
446
447
    /**
448
     * Find new shares and create reference.
449
     *
450
     * @return bool
451
     */
452
    public function findNewShares(): bool
453
    {
454
        $item = $this->db->storage->find([
455
            'deleted' => false,
456
            'shared' => true,
457
            'directory' => true,
458
            '$or' => [
459
                ['acl' => [
460
                    '$elemMatch' => [
461
                        'id' => (string) $this->_id,
462
                        'type' => 'user',
463
                    ],
464
                ]],
465
                ['acl' => [
466
                    '$elemMatch' => [
467
                        '$in' => $this->groups,
468
                    ],
469
                ]],
470
            ],
471
        ]);
472
473
        $found = [];
474
        $list = [];
475
        foreach ($item as $child) {
476
            $found[] = $child['_id'];
477
            $list[(string) $child['_id']] = $child;
478
        }
479
480
        if (empty($found)) {
481
            return false;
482
        }
483
484
        //check for references
485
        $item = $this->db->storage->find([
486
            'directory' => true,
487
            'shared' => true,
488
            'owner' => $this->_id,
489
            'reference' => ['$exists' => 1],
490
        ]);
491
492
        $exists = [];
493
        foreach ($item as $child) {
494
            if (!in_array($child['reference'], $found, true)) {
495
                $this->logger->debug('found dead reference ['.$child['_id'].'] pointing to share ['.$child['reference'].']', [
496
                    'category' => get_class($this),
497
                ]);
498
499
                try {
500
                    $this->getFilesystem()->findNodeById($child['_id'])->delete(true);
501
                } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
502
                }
503
            } else {
504
                $this->logger->debug('found existing share reference ['.$child['_id'].'] pointing to share ['.$child['reference'].']', [
505
                    'category' => get_class($this),
506
                ]);
507
508
                $exists[] = $child['reference'];
509
            }
510
        }
511
512
        $new = array_diff($found, $exists);
513
        foreach ($new as $add) {
0 ignored issues
show
Bug introduced by
The expression $new of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
514
            $node = $list[(string) $add];
515
516
            $this->logger->info('found new share ['.$node['_id'].']', [
517
                'category' => get_class($this),
518
            ]);
519
520
            if ($node['owner'] == $this->_id) {
521
                $this->logger->debug('skip creating reference to share ['.$node['_id'].'] cause share owner ['.$node['owner'].'] is the current user', [
522
                    'category' => get_class($this),
523
                ]);
524
525
                continue;
526
            }
527
528
            $attrs = [
529
                'shared' => true,
530
                'parent' => null,
531
                'reference' => $node['_id'],
532
            ];
533
534
            $dir = $this->getFilesystem()->getRoot();
535
536
            try {
537
                $dir->addDirectory($node['name'], $attrs);
538
            } catch (Exception\Conflict $e) {
539
                $new = $node['name'].' ('.substr(uniqid('', true), -4).')';
540
                $dir->addDirectory($new, $attrs);
541
            } catch (\Exception $e) {
542
                $this->logger->error('failed create new share reference to share ['.$node['_id'].']', [
543
                    'category' => get_class($this),
544
                    'exception' => $e,
545
                ]);
546
547
                throw $e;
548
            }
549
550
            $this->logger->info('created new share reference to share ['.$node['_id'].']', [
551
                'category' => get_class($this),
552
            ]);
553
        }
554
555
        return true;
556
    }
557
558
    /**
559
     * Get unique id.
560
     *
561
     * @return ObjectId
562
     */
563
    public function getId(): ObjectId
564
    {
565
        return $this->_id;
566
    }
567
568
    /**
569
     * Get namespace.
570
     *
571
     * @return string
572
     */
573
    public function getNamespace(): ?string
574
    {
575
        return $this->namespace;
576
    }
577
578
    /**
579
     * Get hard quota.
580
     *
581
     * @return int
582
     */
583
    public function getHardQuota(): int
584
    {
585
        return $this->hard_quota;
586
    }
587
588
    /**
589
     * Set hard quota.
590
     *
591
     * @param int $quota In Bytes
592
     *
593
     * @return User
594
     */
595
    public function setHardQuota(int $quota): self
596
    {
597
        $this->hard_quota = (int) $quota;
598
        $this->save(['hard_quota']);
599
600
        return $this;
601
    }
602
603
    /**
604
     * Set soft quota.
605
     *
606
     * @param int $quota In Bytes
607
     *
608
     * @return User
609
     */
610
    public function setSoftQuota(int $quota): self
611
    {
612
        $this->soft_quota = (int) $quota;
613
        $this->save(['soft_quota']);
614
615
        return $this;
616
    }
617
618
    /**
619
     * Save.
620
     *
621
     * @param array $attributes
622
     *
623
     * @throws Exception\InvalidArgument if a given argument is not valid
624
     *
625
     * @return bool
626
     */
627
    public function save(array $attributes = []): bool
628
    {
629
        $set = [];
630
        foreach ($attributes as $attr) {
631
            if (!in_array($attr, self::$valid_attributes, true)) {
632
                throw new Exception\InvalidArgument('attribute '.$attr.' is not valid');
633
            }
634
            $set[$attr] = $this->{$attr};
635
        }
636
637
        $result = $this->db->user->updateOne([
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
638
            '_id' => $this->_id,
639
        ], [
640
            '$set' => $set,
641
        ]);
642
643
        return true;
644
    }
645
646
    /**
647
     * Get used qota.
648
     *
649
     * @return array
650
     */
651
    public function getQuotaUsage(): array
652
    {
653
        $result = $this->db->storage->find(
654
            [
655
                'owner' => $this->_id,
656
                'directory' => false,
657
                'deleted' => false,
658
            ],
659
            ['size']
660
        );
661
662
        $sum = 0;
663
        foreach ($result as $size) {
664
            if (isset($size['size'])) {
665
                $sum += $size['size'];
666
            }
667
        }
668
669
        return [
670
            'used' => $sum,
671
            'available' => ($this->hard_quota - $sum),
672
            'hard_quota' => $this->hard_quota,
673
            'soft_quota' => $this->soft_quota,
674
        ];
675
    }
676
677
    /**
678
     * Check quota.
679
     *
680
     * @param int $add Size in bytes
681
     *
682
     * @return bool
683
     */
684
    public function checkQuota(int $add): bool
685
    {
686
        $quota = $this->getQuotaUsage();
687
688
        if (($quota['used'] + $add) > $quota['hard_quota']) {
689
            return false;
690
        }
691
692
        return true;
693
    }
694
695
    /**
696
     * Delete user.
697
     *
698
     * @param bool $force
699
     *
700
     * @return bool
701
     */
702
    public function delete(bool $force = false): bool
703
    {
704
        if (false === $force) {
705
            $result_data = $this->getFilesystem()->getRoot()->delete();
706
            $this->deleted = true;
707
            $result_user = $this->save(['deleted']);
708
        } else {
709
            $result_data = $this->getFilesystem()->getRoot()->delete(true);
710
711
            $result = $this->db->user->deleteOne([
712
                '_id' => $this->_id,
713
            ]);
714
            $result_user = $result->isAcknowledged();
715
        }
716
717
        return $result_data && $result_user;
718
    }
719
720
    /**
721
     * Undelete user.
722
     *
723
     * @return bool
724
     */
725
    public function undelete(): bool
726
    {
727
        $this->deleted = false;
728
729
        return $this->save(['deleted']);
730
    }
731
732
    /**
733
     * Check if user is deleted.
734
     *
735
     * @return bool
736
     */
737
    public function isDeleted(): bool
738
    {
739
        return $this->deleted;
740
    }
741
742
    /**
743
     * Get Username.
744
     *
745
     * @return string
746
     */
747
    public function getUsername(): string
748
    {
749
        return $this->username;
750
    }
751
752
    /**
753
     * Get groups.
754
     *
755
     * @return array
756
     */
757
    public function getGroups(): array
758
    {
759
        return $this->groups;
760
    }
761
762
    /**
763
     * Get attribute usage summary.
764
     *
765
     * @param string $attribute
766
     * @param string $type
767
     * @param int    $limit
768
     *
769
     * @return array
770
     */
771
    protected function _getAttributeSummary(string $attribute, string $type = 'string', int $limit = 25): array
772
    {
773
        $mongodb = $this->db->storage;
774
775
        $ops = [
776
            [
777
                '$match' => [
778
                    '$and' => [
779
                        ['owner' => $this->_id],
780
                        ['deleted' => false],
781
                        [$attribute => ['$exists' => true]],
782
                    ],
783
                ],
784
            ],
785
        ];
786
787
        if ('array' === $type) {
788
            $ops[] = [
789
                '$unwind' => '$'.$attribute,
790
            ];
791
        }
792
793
        $ops[] = [
794
            '$group' => [
795
                '_id' => '$'.$attribute,
796
                'sum' => ['$sum' => 1],
797
            ],
798
        ];
799
800
        $ops[] = [
801
            '$sort' => [
802
               'sum' => -1,
803
               '_id' => 1,
804
            ],
805
        ];
806
807
        $ops[] = [
808
            '$limit' => $limit,
809
        ];
810
811
        return $mongodb->aggregate($ops)->toArray();
812
    }
813
}
814