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

visavi / rotor / 27390430840

12 Jun 2026 02:23AM UTC coverage: 14.157% (-0.002%) from 14.159%
27390430840

push

github

visavi
Вынес некотоые методы в трейты, удалил лишние файлы

0 of 24 new or added lines in 4 files covered. (0.0%)

1 existing line in 1 file now uncovered.

816 of 5764 relevant lines covered (14.16%)

1.64 hits per line

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

22.78
/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
    {
NEW
347
        if (! $this->id) {
×
NEW
348
            return false;
×
349
        }
350

UNCOV
351
        static $visits;
×
352

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

362
        return isset($visits[$this->id]);
×
363
    }
364

365
    /**
366
     * User online status
367
     */
368
    public function getOnline(): HtmlString
×
369
    {
370
        $online = '';
×
371

372
        if ($this->isOnline()) {
×
373
            $online = '<div class="user-status bg-success" title="' . __('main.online') . '"></div>';
×
374
        }
375

376
        return new HtmlString($online);
×
377
    }
378

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

390
        return $visit;
×
391
    }
392

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

400
        if (! $this->id) {
2✔
401
            return setting('statusdef');
×
402
        }
403

404
        if (! $status) {
2✔
405
            $status = $this->getStatuses(6 * 3600);
2✔
406
        }
407

408
        if (isset($status[$this->id])) {
2✔
409
            return new HtmlString($status[$this->id]);
×
410
        }
411

412
        return setting('statusdef');
2✔
413
    }
414

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

424
        if ($this->avatar && file_exists(public_path($this->avatar))) {
×
425
            $avatar = $this->getAvatarImage();
×
426
        } else {
427
            $avatar = $this->getAvatarDefault();
×
428
        }
429

430
        return new HtmlString('<a href="/users/' . $this->login . '">' . $avatar . '</a> ');
×
431
    }
432

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

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

446
        return $this->getAvatarDefault();
2✔
447
    }
448

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

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

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

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

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

491
                if ($user->color) {
×
492
                    $statuses[$user->id] = '<span style="color:' . $user->color . '">' . check($user->name) . '</span>';
×
493
                    continue;
×
494
                }
495

496
                $statuses[$user->id] = check($user->name);
×
497
            }
498

499
            return $statuses;
2✔
500
        });
2✔
501
    }
502

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

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

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

528
            Message::query()->where('user_id', $this->id)->delete();
×
529
            Dialogue::query()->where('user_id', $this->id)->delete();
×
530
            Rating::query()->where('user_id', $this->id)->delete();
×
531
            Banhist::query()->where('user_id', $this->id)->delete();
×
532

533
            foreach (Registry::$onDeleteUser as $callback) {
×
534
                $callback($this);
×
535
            }
536

537
            return parent::delete();
×
538
        });
×
539
    }
540

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

552
            if ($countDialogues !== $this->newprivat) {
×
553
                $this->update([
×
554
                    'newprivat'      => $countDialogues,
×
555
                    'sendprivatmail' => 0,
×
556
                ]);
×
557
            }
558
        }
559
    }
560

561
    /**
562
     * Check user banned
563
     */
564
    public function isBanned(): bool
×
565
    {
566
        return $this->level === self::BANNED;
×
567
    }
568

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

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

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

594
            setFlash('success', __('main.daily_bonus', ['money' => plural(setting('bonusmoney'), setting('moneyname'))]));
×
595
        }
596
    }
597
}
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