• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visavi / rotor / 28340133337

28 Jun 2026 11:47PM UTC coverage: 16.561% (+0.09%) from 16.474%
28340133337

push

github

visavi
Ядро и модули переведены на datetime, удалена константа SITETIME

18 of 95 new or added lines in 31 files covered. (18.95%)

7 existing lines in 6 files now uncovered.

989 of 5972 relevant lines covered (16.56%)

2.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

24.04
/app/Models/User.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Models;
6

7
use App\Casts\HtmlCast;
8
use App\Classes\Registry;
9
use App\Traits\SearchableTrait;
10
use App\Traits\SortableTrait;
11
use App\Traits\UploadTrait;
12
use Carbon\CarbonImmutable;
13
use Illuminate\Auth\Authenticatable;
14
use Illuminate\Auth\MustVerifyEmail;
15
use Illuminate\Auth\Passwords\CanResetPassword;
16
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
17
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
18
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
19
use Illuminate\Database\Eloquent\Collection;
20
use Illuminate\Database\Eloquent\Factories\HasFactory;
21
use Illuminate\Database\Eloquent\Model;
22
use Illuminate\Database\Eloquent\Relations\BelongsTo;
23
use Illuminate\Database\Eloquent\Relations\HasMany;
24
use Illuminate\Database\Eloquent\Relations\HasOne;
25
use Illuminate\Database\Query\JoinClause;
26
use Illuminate\Foundation\Auth\Access\Authorizable;
27
use Illuminate\Notifications\Notifiable;
28
use Illuminate\Support\Facades\Cache;
29
use Illuminate\Support\Facades\DB;
30
use Illuminate\Support\HtmlString;
31
use Illuminate\Support\Str;
32

33
/**
34
 * Class User
35
 *
36
 * @property int                  $id
37
 * @property string               $login
38
 * @property string               $password
39
 * @property string               $email
40
 * @property string               $level
41
 * @property string               $name
42
 * @property string               $country
43
 * @property string               $city
44
 * @property string               $language
45
 * @property string               $info
46
 * @property string               $site
47
 * @property string               $phone
48
 * @property string               $gender
49
 * @property string               $birthday
50
 * @property int                  $newprivat
51
 * @property string               $themes
52
 * @property string               $timezone
53
 * @property int                  $point
54
 * @property int                  $money
55
 * @property string               $status
56
 * @property string               $color
57
 * @property string               $avatar
58
 * @property string               $picture
59
 * @property int                  $rating
60
 * @property int                  $posrating
61
 * @property int                  $negrating
62
 * @property int                  $sendprivatmail
63
 * @property int                  $newchat
64
 * @property bool                 $notify_mention
65
 * @property bool                 $notify_reply
66
 * @property bool                 $notify_comment
67
 * @property string               $apikey
68
 * @property string|null          $subscribe
69
 * @property string               $remember_token
70
 * @property string               $confirm_token
71
 * @property CarbonImmutable|null $timeban
72
 * @property CarbonImmutable|null $timebonus
73
 * @property CarbonImmutable|null $updated_at
74
 * @property CarbonImmutable      $created_at
75
 * @property-read Collection<UserData> $data
76
 */
77
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract
78
{
79
    use Authenticatable;
80
    use Authorizable;
81
    use CanResetPassword;
82

83
    /** @use HasFactory<\Database\Factories\UserFactory> */
84
    use HasFactory;
85
    use MustVerifyEmail;
86
    use Notifiable;
87
    use SearchableTrait;
88
    use SortableTrait;
89
    use UploadTrait;
90

91
    public const string BOSS = 'boss';   // Владелец
92
    public const string ADMIN = 'admin';  // Админ
93
    public const string MODER = 'moder';  // Модератор
94
    public const string EDITOR = 'editor'; // Редактор
95
    public const string USER = 'user';   // Пользователь
96
    public const string PENDED = 'pended'; // Ожидающий
97
    public const string BANNED = 'banned'; // Забаненный
98

99
    /**
100
     * Администраторы
101
     */
102
    public const array ADMIN_GROUPS = [
103
        self::EDITOR,
104
        self::MODER,
105
        self::ADMIN,
106
        self::BOSS,
107
    ];
108

109
    /**
110
     * Участники
111
     */
112
    public const array USER_GROUPS = [
113
        self::USER,
114
        self::EDITOR,
115
        self::MODER,
116
        self::ADMIN,
117
        self::BOSS,
118
    ];
119

120
    /**
121
     * Все пользователи
122
     */
123
    public const array ALL_GROUPS = [
124
        self::BANNED,
125
        self::PENDED,
126
        self::USER,
127
        self::EDITOR,
128
        self::MODER,
129
        self::ADMIN,
130
        self::BOSS,
131
    ];
132

133
    /**
134
     * Genders
135
     */
136
    public const string MALE = 'male';
137
    public const string FEMALE = 'female';
138

139
    /**
140
     * The name of the "updated at" column.
141
     *
142
     * updated_at управляется вручную (время последнего визита, пишет Metrika),
143
     * поэтому Eloquent не должен трогать его на каждом update()
144
     */
145
    public const ?string UPDATED_AT = null;
146

147
    /**
148
     * The attributes that aren't mass assignable.
149
     */
150
    protected $guarded = [];
151

152
    /**
153
     * The attributes that should be hidden for arrays.
154
     */
155
    protected $hidden = [
156
        'password',
157
        'remember_token',
158
    ];
159

160
    /**
161
     * Директория загрузки файлов
162
     */
163
    public string $uploadPath = '/uploads/pictures';
164

165
    /**
166
     * Директория загрузки аватаров
167
     */
168
    public string $uploadAvatarPath = '/uploads/avatars';
169

170
    /**
171
     * Morph name
172
     */
173
    public static string $morphName = 'users';
174

175
    /**
176
     * Get the attributes that should be cast.
177
     */
178
    protected function casts(): array
109✔
179
    {
180
        return [
109✔
181
            'info'       => HtmlCast::class,
109✔
182
            'updated_at' => 'datetime',
109✔
183
            'timeban'    => 'datetime',
109✔
184
            'timebonus'  => 'datetime',
109✔
185
        ];
109✔
186
    }
187

188
    /**
189
     * Возвращает поля участвующие в поиске
190
     */
191
    public function searchableFields(): array
11✔
192
    {
193
        return ['login', 'name', 'info', 'site', 'status'];
11✔
194
    }
195

196
    /**
197
     * Get info
198
     */
199
    public function getInfo(): HtmlString
×
200
    {
201
        return renderHtml($this->info);
×
202
    }
203

204
    /**
205
     * Возвращает список сортируемых полей
206
     */
207
    protected static function sortableFields(): array
×
208
    {
209
        return [
×
210
            'point'   => ['field' => 'point', 'label' => __('users.assets')],
×
211
            'rating'  => ['field' => 'rating', 'label' => __('users.reputation')],
×
212
            'money'   => ['field' => 'money', 'label' => __('users.moneys')],
×
213
            'created' => ['field' => 'created_at', 'label' => __('main.registration_date')],
×
214
            'updated' => ['field' => 'updated_at', 'label' => __('users.last_visit')],
×
215
        ];
×
216
    }
217

218
    /**
219
     * Is admin
220
     */
221
    public function isAdmin(?string $level = null): bool
×
222
    {
223
        $level = $level ?? self::EDITOR;
×
224
        $levels = array_flip(self::ADMIN_GROUPS);
×
225

226
        return isset($levels[$this->level], $levels[$level])
×
227
            && $levels[$this->level] >= $levels[$level];
×
228
    }
229

230
    /**
231
     * Связь с таблицей online
232
     */
233
    public function online(): BelongsTo
×
234
    {
235
        return $this->belongsTo(Online::class, 'id', 'user_id')->withDefault();
×
236
    }
237

238
    /**
239
     * Возвращает последний бан
240
     */
241
    public function lastBan(): HasOne
×
242
    {
243
        return $this->hasOne(Banhist::class, 'user_id', 'id')
×
244
            ->whereIn('type', ['ban', 'change'])
×
245
            ->orderByDesc('created_at')
×
246
            ->withDefault();
×
247
    }
248

249
    /**
250
     * Возвращает дополнительные поля
251
     */
252
    public function data(): HasMany
×
253
    {
254
        return $this->hasMany(UserData::class, 'user_id');
×
255
    }
256

257
    /**
258
     * Возвращает имя или логин пользователя
259
     */
260
    public function getName(): string
2✔
261
    {
262
        if ($this->exists) {
2✔
263
            return $this->name ?: $this->login;
2✔
264
        }
265

266
        return setting('deleted_user');
×
267
    }
268

269
    /**
270
     * Возвращает ссылку на профиль пользователя
271
     */
272
    public function getProfile(): HtmlString
×
273
    {
274
        if ($this->id) {
×
275
            $admin = null;
×
276
            $name = check($this->getName());
×
277

278
            if ($this->color) {
×
279
                $name = '<span style="color:' . $this->color . '">' . $name . '</span>';
×
280
            }
281

282
            if (in_array($this->level, self::ADMIN_GROUPS, true)) {
×
283
                $admin = ' <i class="fas fa-xs fa-star text-info" title="' . $this->getLevel() . '"></i>';
×
284
            }
285

286
            $html = '<a class="section-author fw-bold" href="/users/' . $this->login . '" data-login="' . $this->login . '">' . $name . '</a>';
×
287

288
            return new HtmlString($html . $admin);
×
289
        }
290

291
        $html = '<span class="section-author fw-bold" data-login="' . setting('deleted_user') . '">' . setting('deleted_user') . '</span>';
×
292

293
        return new HtmlString($html);
×
294
    }
295

296
    /**
297
     * Возвращает пол пользователя
298
     */
299
    public function getGender(): HtmlString
×
300
    {
301
        if ($this->gender === 'female') {
×
302
            return new HtmlString('<i class="fa fa-female fa-lg"></i>');
×
303
        }
304

305
        return new HtmlString('<i class="fa fa-male fa-lg"></i>');
×
306
    }
307

308
    /**
309
     * Возвращает название уровня по ключу
310
     */
311
    public static function getLevelByKey(string $level): string
×
312
    {
313
        return match ($level) {
×
314
            self::BOSS   => __('main.boss'),
×
315
            self::ADMIN  => __('main.admin'),
×
316
            self::MODER  => __('main.moder'),
×
317
            self::EDITOR => __('main.editor'),
×
318
            self::USER   => __('main.user'),
×
319
            self::PENDED => __('main.pended'),
×
320
            self::BANNED => __('main.banned'),
×
321
            default      => setting('statusdef'),
×
322
        };
×
323
    }
324

325
    /**
326
     * Возвращает уровень пользователя
327
     */
328
    public function getLevel(): string
×
329
    {
330
        return self::getLevelByKey($this->level);
×
331
    }
332

333
    /**
334
     * Возвращает карту login => name для резолва упоминаний
335
     *
336
     * @return array<string, string>
337
     */
338
    public static function names(): array
×
339
    {
340
        static $names = null;
×
341

342
        return $names ??= Cache::rememberForever('users', static fn () => self::query()
×
343
            ->whereNotNull('name')
×
344
            ->where('name', '!=', '')
×
345
            ->pluck('name', 'login')
×
346
            ->all());
×
347
    }
348

349
    /**
350
     * Is user online
351
     */
352
    public function isOnline(): bool
×
353
    {
354
        if (! $this->id) {
×
355
            return false;
×
356
        }
357

358
        static $visits;
×
359

360
        if (! $visits) {
×
361
            $visits = Cache::remember('visit', 10, static function () {
×
362
                return Online::query()
×
363
                    ->whereNotNull('user_id')
×
364
                    ->pluck('user_id', 'user_id')
×
365
                    ->all();
×
366
            });
×
367
        }
368

369
        return isset($visits[$this->id]);
×
370
    }
371

372
    /**
373
     * User online status
374
     */
375
    public function getOnline(): HtmlString
×
376
    {
377
        $online = '';
×
378

379
        if ($this->isOnline()) {
×
380
            $online = '<div class="user-status bg-success" title="' . __('main.online') . '"></div>';
×
381
        }
382

383
        return new HtmlString($online);
×
384
    }
385

386
    /**
387
     * Get last visit
388
     */
389
    public function getVisit(): string
×
390
    {
391
        if ($this->isOnline()) {
×
392
            $visit = __('main.online');
×
393
        } else {
394
            $visit = dateFixed($this->updated_at);
×
395
        }
396

397
        return $visit;
×
398
    }
399

400
    /**
401
     * Возвращает статус пользователя
402
     */
403
    public function getStatus(): HtmlString|string
2✔
404
    {
405
        static $status;
2✔
406

407
        if (! $this->id) {
2✔
408
            return setting('statusdef');
×
409
        }
410

411
        if (! $status) {
2✔
412
            $status = $this->getStatuses(6 * 3600);
2✔
413
        }
414

415
        if (isset($status[$this->id])) {
2✔
416
            return new HtmlString($status[$this->id]);
×
417
        }
418

419
        return setting('statusdef');
2✔
420
    }
421

422
    /**
423
     * Возвращает аватар пользователя
424
     */
425
    public function getAvatar(): HtmlString
×
426
    {
427
        if (! $this->id) {
×
428
            return $this->getAvatarGuest();
×
429
        }
430

431
        if ($this->avatar && file_exists(public_path($this->avatar))) {
×
432
            $avatar = $this->getAvatarImage();
×
433
        } else {
434
            $avatar = $this->getAvatarDefault();
×
435
        }
436

437
        return new HtmlString('<a href="/users/' . $this->login . '">' . $avatar . '</a> ');
×
438
    }
439

440
    /**
441
     * Возвращает изображение аватара
442
     */
443
    public function getAvatarImage(): HtmlString
2✔
444
    {
445
        if (! $this->id) {
2✔
446
            return $this->getAvatarGuest();
×
447
        }
448

449
        if ($this->avatar && file_exists(public_path($this->avatar))) {
2✔
450
            return new HtmlString('<img class="avatar-default rounded-circle" src="' . $this->avatar . '" alt="">');
×
451
        }
452

453
        return $this->getAvatarDefault();
2✔
454
    }
455

456
    /**
457
     * Get guest avatar
458
     */
459
    public function getAvatarGuest(): HtmlString
×
460
    {
461
        return new HtmlString('<span class="avatar-default avatar-guest rounded-circle"><i class="fas fa-user"></i></span> ');
×
462
    }
463

464
    /**
465
     * Возвращает аватар для пользователя по умолчанию
466
     */
467
    private function getAvatarDefault(): HtmlString
2✔
468
    {
469
        $name = $this->getName();
2✔
470
        $color = '#' . substr(dechex(crc32($this->login)), 0, 6);
2✔
471
        $letter = mb_strtoupper(Str::substr($name, 0, 1), 'utf-8');
2✔
472

473
        return new HtmlString('<span class="avatar-default rounded-circle" style="background:' . $color . '">' . $letter . '</span>');
2✔
474
    }
475

476
    /**
477
     * Кеширует статусы пользователей
478
     */
479
    public function getStatuses(int $seconds): array
2✔
480
    {
481
        return Cache::remember('status', $seconds, static function () {
2✔
482
            $users = self::query()
2✔
483
                ->select('users.id', 'users.status', 'status.name', 'status.color')
2✔
484
                ->leftJoin('status', static function (JoinClause $join) {
2✔
485
                    $join->whereRaw('users.point between status.topoint and status.point');
2✔
486
                })
2✔
487
                ->where('users.point', '>', 0)
2✔
488
                ->toBase()
2✔
489
                ->get();
2✔
490

491
            $statuses = [];
2✔
492
            foreach ($users as $user) {
2✔
493
                if ($user->status) {
×
494
                    $statuses[$user->id] = '<span style="color:#ff0000">' . check($user->status) . '</span>';
×
495
                    continue;
×
496
                }
497

498
                if ($user->color) {
×
499
                    $statuses[$user->id] = '<span style="color:' . $user->color . '">' . check($user->name) . '</span>';
×
500
                    continue;
×
501
                }
502

503
                $statuses[$user->id] = check($user->name);
×
504
            }
505

506
            return $statuses;
2✔
507
        });
2✔
508
    }
509

510
    /**
511
     * Отправляет приватное сообщение
512
     */
513
    public function sendMessage(?self $author, string $text, bool $withAuthor = true): Message
×
514
    {
515
        return (new Message())->createDialogue($this, $author, $text, $withAuthor);
×
516
    }
517

518
    /**
519
     * Возвращает количество писем пользователя
520
     */
521
    public function getCountMessages(): int
×
522
    {
523
        return Dialogue::query()->where('user_id', $this->id)->count();
×
524
    }
525

526
    /**
527
     * Удаляет записи пользователя из всех таблиц
528
     */
529
    public function delete(): ?bool
×
530
    {
531
        return DB::transaction(function () {
×
532
            deleteFile(public_path($this->picture));
×
533
            deleteFile(public_path($this->avatar));
×
534

535
            Message::query()->where('user_id', $this->id)->delete();
×
536
            Dialogue::query()->where('user_id', $this->id)->delete();
×
537
            Rating::query()->where('user_id', $this->id)->delete();
×
538
            Banhist::query()->where('user_id', $this->id)->delete();
×
539

540
            foreach (Registry::$onDeleteUser as $callback) {
×
541
                $callback($this);
×
542
            }
543

544
            return parent::delete();
×
545
        });
×
546
    }
547

548
    /**
549
     * Updates count messages
550
     */
551
    public function updatePrivate(): void
×
552
    {
553
        if ($this->newprivat) {
×
554
            $countDialogues = Dialogue::query()
×
555
                ->where('user_id', $this->id)
×
556
                ->where('reading', 0)
×
557
                ->count();
×
558

559
            if ($countDialogues !== $this->newprivat) {
×
560
                $this->update([
×
561
                    'newprivat'      => $countDialogues,
×
562
                    'sendprivatmail' => 0,
×
563
                ]);
×
564
            }
565
        }
566
    }
567

568
    /**
569
     * Check user banned
570
     */
571
    public function isBanned(): bool
×
572
    {
573
        return $this->level === self::BANNED;
×
574
    }
575

576
    /**
577
     * Check user pended
578
     */
579
    public function isPended(): bool
×
580
    {
581
        return setting('regkeys') && $this->level === self::PENDED;
×
582
    }
583

584
    /**
585
     * Check user active
586
     */
587
    public function isActive(): bool
2✔
588
    {
589
        return in_array($this->level, self::USER_GROUPS, true);
2✔
590
    }
591

592
    /**
593
     * Getting daily bonus
594
     */
595
    public function gettingBonus(): void
×
596
    {
NEW
597
        if ($this->isActive() && (! $this->timebonus || $this->timebonus->lt(now()->subHours(23)))) {
×
598
            $this->increment('money', setting('bonusmoney'));
×
NEW
599
            $this->update(['timebonus' => now()]);
×
600

601
            setFlash('success', __('main.daily_bonus', ['money' => plural(setting('bonusmoney'), setting('moneyname'))]));
×
602
        }
603
    }
604
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc