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

visavi / rotor / 27388066632

12 Jun 2026 01:15AM UTC coverage: 14.172%. Remained the same
27388066632

push

github

visavi
Исправил ошибки phpstan

0 of 36 new or added lines in 9 files covered. (0.0%)

4 existing lines in 3 files now uncovered.

816 of 5758 relevant lines covered (14.17%)

1.64 hits per line

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

23.03
/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 Illuminate\Auth\Authenticatable;
13
use Illuminate\Auth\MustVerifyEmail;
14
use Illuminate\Auth\Passwords\CanResetPassword;
15
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
16
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
17
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
18
use Illuminate\Database\Eloquent\Collection;
19
use Illuminate\Database\Eloquent\Factories\HasFactory;
20
use Illuminate\Database\Eloquent\Model;
21
use Illuminate\Database\Eloquent\Relations\BelongsTo;
22
use Illuminate\Database\Eloquent\Relations\HasMany;
23
use Illuminate\Database\Eloquent\Relations\HasOne;
24
use Illuminate\Database\Query\JoinClause;
25
use Illuminate\Foundation\Auth\Access\Authorizable;
26
use Illuminate\Notifications\Notifiable;
27
use Illuminate\Support\Facades\Cache;
28
use Illuminate\Support\Facades\DB;
29
use Illuminate\Support\HtmlString;
30
use Illuminate\Support\Str;
31

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

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

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

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

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

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

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

138
    /**
139
     * Indicates if the model should be timestamped.
140
     */
141
    public $timestamps = false;
142

143
    /**
144
     * The attributes that aren't mass assignable.
145
     */
146
    protected $guarded = [];
147

148
    /**
149
     * The attributes that should be hidden for arrays.
150
     */
151
    protected $hidden = [
152
        'password',
153
        'remember_token',
154
    ];
155

156
    /**
157
     * Директория загрузки файлов
158
     */
159
    public string $uploadPath = '/uploads/pictures';
160

161
    /**
162
     * Директория загрузки аватаров
163
     */
164
    public string $uploadAvatarPath = '/uploads/avatars';
165

166
    /**
167
     * Morph name
168
     */
169
    public static string $morphName = 'users';
170

171
    /**
172
     * Get the attributes that should be cast.
173
     */
174
    protected function casts(): array
73✔
175
    {
176
        return [
73✔
177
            'info' => HtmlCast::class,
73✔
178
        ];
73✔
179
    }
180

181
    /**
182
     * Возвращает поля участвующие в поиске
183
     */
184
    public function searchableFields(): array
3✔
185
    {
186
        return ['login', 'name', 'info', 'site', 'status'];
3✔
187
    }
188

189
    /**
190
     * Get info
191
     */
192
    public function getInfo(): HtmlString
×
193
    {
194
        return renderHtml($this->info);
×
195
    }
196

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

211
    /**
212
     * Is admin
213
     */
214
    public function isAdmin(?string $level = null): bool
×
215
    {
216
        $level = $level ?? self::EDITOR;
×
217
        $levels = array_flip(self::ADMIN_GROUPS);
×
218

219
        return isset($levels[$this->level], $levels[$level])
×
220
            && $levels[$this->level] >= $levels[$level];
×
221
    }
222

223
    /**
224
     * Связь с таблицей online
225
     */
226
    public function online(): BelongsTo
×
227
    {
228
        return $this->belongsTo(Online::class, 'id', 'user_id')->withDefault();
×
229
    }
230

231
    /**
232
     * Возвращает последний бан
233
     */
234
    public function lastBan(): HasOne
×
235
    {
236
        return $this->hasOne(Banhist::class, 'user_id', 'id')
×
237
            ->whereIn('type', ['ban', 'change'])
×
238
            ->orderByDesc('created_at')
×
239
            ->withDefault();
×
240
    }
241

242
    /**
243
     * Возвращает дополнительные поля
244
     */
245
    public function data(): HasMany
×
246
    {
247
        return $this->hasMany(UserData::class, 'user_id');
×
248
    }
249

250
    /**
251
     * Возвращает имя или логин пользователя
252
     */
253
    public function getName(): string
2✔
254
    {
255
        if ($this->exists) {
2✔
256
            return $this->name ?: $this->login;
2✔
257
        }
258

259
        return setting('deleted_user');
×
260
    }
261

262
    /**
263
     * Возвращает ссылку на профиль пользователя
264
     */
265
    public function getProfile(): HtmlString
×
266
    {
267
        if ($this->id) {
×
268
            $admin = null;
×
269
            $name = check($this->getName());
×
270

271
            if ($this->color) {
×
272
                $name = '<span style="color:' . $this->color . '">' . $name . '</span>';
×
273
            }
274

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

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

281
            return new HtmlString($html . $admin);
×
282
        }
283

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

286
        return new HtmlString($html);
×
287
    }
288

289
    /**
290
     * Возвращает пол пользователя
291
     */
292
    public function getGender(): HtmlString
×
293
    {
294
        if ($this->gender === 'female') {
×
295
            return new HtmlString('<i class="fa fa-female fa-lg"></i>');
×
296
        }
297

298
        return new HtmlString('<i class="fa fa-male fa-lg"></i>');
×
299
    }
300

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

318
    /**
319
     * Возвращает уровень пользователя
320
     */
321
    public function getLevel(): string
×
322
    {
323
        return self::getLevelByKey($this->level);
×
324
    }
325

326
    /**
327
     * Возвращает карту login => name для резолва упоминаний
328
     *
329
     * @return array<string, string>
330
     */
331
    public static function names(): array
×
332
    {
333
        static $names = null;
×
334

335
        return $names ??= Cache::rememberForever('users', static fn () => self::query()
×
336
            ->whereNotNull('name')
×
337
            ->where('name', '!=', '')
×
338
            ->pluck('name', 'login')
×
339
            ->all());
×
340
    }
341

342
    /**
343
     * Is user online
344
     */
345
    public function isOnline(): bool
×
346
    {
347
        static $visits;
×
348

349
        if (! $visits) {
×
350
            $visits = Cache::remember('visit', 10, static function () {
×
351
                return Online::query()
×
352
                    ->whereNotNull('user_id')
×
353
                    ->pluck('user_id', 'user_id')
×
354
                    ->all();
×
355
            });
×
356
        }
357

358
        return isset($visits[$this->id]);
×
359
    }
360

361
    /**
362
     * User online status
363
     */
364
    public function getOnline(): HtmlString
×
365
    {
366
        $online = '';
×
367

368
        if ($this->isOnline()) {
×
369
            $online = '<div class="user-status bg-success" title="' . __('main.online') . '"></div>';
×
370
        }
371

372
        return new HtmlString($online);
×
373
    }
374

375
    /**
376
     * Get last visit
377
     */
378
    public function getVisit(): string
×
379
    {
380
        if ($this->isOnline()) {
×
381
            $visit = __('main.online');
×
382
        } else {
383
            $visit = dateFixed($this->updated_at);
×
384
        }
385

386
        return $visit;
×
387
    }
388

389
    /**
390
     * Возвращает статус пользователя
391
     */
392
    public function getStatus(): HtmlString|string
2✔
393
    {
394
        static $status;
2✔
395

396
        if (! $this->id) {
2✔
397
            return setting('statusdef');
×
398
        }
399

400
        if (! $status) {
2✔
401
            $status = $this->getStatuses(6 * 3600);
2✔
402
        }
403

404
        if (isset($status[$this->id])) {
2✔
405
            return new HtmlString($status[$this->id]);
×
406
        }
407

408
        return setting('statusdef');
2✔
409
    }
410

411
    /**
412
     * Возвращает аватар пользователя
413
     */
414
    public function getAvatar(): HtmlString
×
415
    {
416
        if (! $this->id) {
×
NEW
417
            return $this->getAvatarGuest();
×
418
        }
419

420
        if ($this->avatar && file_exists(public_path($this->avatar))) {
×
421
            $avatar = $this->getAvatarImage();
×
422
        } else {
UNCOV
423
            $avatar = $this->getAvatarDefault();
×
424
        }
425

426
        return new HtmlString('<a href="/users/' . $this->login . '">' . $avatar . '</a> ');
×
427
    }
428

429
    /**
430
     * Возвращает изображение аватара
431
     */
432
    public function getAvatarImage(): HtmlString
2✔
433
    {
434
        if (! $this->id) {
2✔
435
            return $this->getAvatarGuest();
×
436
        }
437

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

442
        return $this->getAvatarDefault();
2✔
443
    }
444

445
    /**
446
     * Get guest avatar
447
     */
448
    public function getAvatarGuest(): HtmlString
×
449
    {
450
        return new HtmlString('<span class="avatar-default avatar-guest rounded-circle"><i class="fas fa-user"></i></span> ');
×
451
    }
452

453
    /**
454
     * Возвращает аватар для пользователя по умолчанию
455
     */
456
    private function getAvatarDefault(): HtmlString
2✔
457
    {
458
        $name = $this->getName();
2✔
459
        $color = '#' . substr(dechex(crc32($this->login)), 0, 6);
2✔
460
        $letter = mb_strtoupper(Str::substr($name, 0, 1), 'utf-8');
2✔
461

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

465
    /**
466
     * Кеширует статусы пользователей
467
     */
468
    public function getStatuses(int $seconds): array
2✔
469
    {
470
        return Cache::remember('status', $seconds, static function () {
2✔
471
            $users = self::query()
2✔
472
                ->select('users.id', 'users.status', 'status.name', 'status.color')
2✔
473
                ->leftJoin('status', static function (JoinClause $join) {
2✔
474
                    $join->whereRaw('users.point between status.topoint and status.point');
2✔
475
                })
2✔
476
                ->where('users.point', '>', 0)
2✔
477
                ->toBase()
2✔
478
                ->get();
2✔
479

480
            $statuses = [];
2✔
481
            foreach ($users as $user) {
2✔
482
                if ($user->status) {
×
483
                    $statuses[$user->id] = '<span style="color:#ff0000">' . check($user->status) . '</span>';
×
484
                    continue;
×
485
                }
486

487
                if ($user->color) {
×
488
                    $statuses[$user->id] = '<span style="color:' . $user->color . '">' . check($user->name) . '</span>';
×
489
                    continue;
×
490
                }
491

492
                $statuses[$user->id] = check($user->name);
×
493
            }
494

495
            return $statuses;
2✔
496
        });
2✔
497
    }
498

499
    /**
500
     * Отправляет приватное сообщение
501
     */
502
    public function sendMessage(?self $author, string $text, bool $withAuthor = true): Message
×
503
    {
504
        return (new Message())->createDialogue($this, $author, $text, $withAuthor);
×
505
    }
506

507
    /**
508
     * Возвращает количество писем пользователя
509
     */
510
    public function getCountMessages(): int
×
511
    {
512
        return Dialogue::query()->where('user_id', $this->id)->count();
×
513
    }
514

515
    /**
516
     * Удаляет записи пользователя из всех таблиц
517
     */
518
    public function delete(): ?bool
×
519
    {
520
        return DB::transaction(function () {
×
521
            deleteFile(public_path($this->picture));
×
522
            deleteFile(public_path($this->avatar));
×
523

524
            Message::query()->where('user_id', $this->id)->delete();
×
525
            Dialogue::query()->where('user_id', $this->id)->delete();
×
526
            Rating::query()->where('user_id', $this->id)->delete();
×
527
            Banhist::query()->where('user_id', $this->id)->delete();
×
528

529
            foreach (Registry::$onDeleteUser as $callback) {
×
530
                $callback($this);
×
531
            }
532

533
            return parent::delete();
×
534
        });
×
535
    }
536

537
    /**
538
     * Updates count messages
539
     */
540
    public function updatePrivate(): void
×
541
    {
542
        if ($this->newprivat) {
×
543
            $countDialogues = Dialogue::query()
×
544
                ->where('user_id', $this->id)
×
545
                ->where('reading', 0)
×
546
                ->count();
×
547

548
            if ($countDialogues !== $this->newprivat) {
×
549
                $this->update([
×
550
                    'newprivat'      => $countDialogues,
×
551
                    'sendprivatmail' => 0,
×
552
                ]);
×
553
            }
554
        }
555
    }
556

557
    /**
558
     * Check user banned
559
     */
560
    public function isBanned(): bool
×
561
    {
562
        return $this->level === self::BANNED;
×
563
    }
564

565
    /**
566
     * Check user pended
567
     */
568
    public function isPended(): bool
×
569
    {
570
        return setting('regkeys') && $this->level === self::PENDED;
×
571
    }
572

573
    /**
574
     * Check user active
575
     */
576
    public function isActive(): bool
2✔
577
    {
578
        return in_array($this->level, self::USER_GROUPS, true);
2✔
579
    }
580

581
    /**
582
     * Getting daily bonus
583
     */
584
    public function gettingBonus(): void
×
585
    {
586
        if ($this->isActive() && $this->timebonus < strtotime('-23 hours', SITETIME)) {
×
587
            $this->increment('money', setting('bonusmoney'));
×
588
            $this->update(['timebonus' => SITETIME]);
×
589

590
            setFlash('success', __('main.daily_bonus', ['money' => plural(setting('bonusmoney'), setting('moneyname'))]));
×
591
        }
592
    }
593
}
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