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

visavi / rotor / 26723576697

31 May 2026 08:27PM UTC coverage: 14.172% (-0.4%) from 14.618%
26723576697

push

github

visavi
Перенес файлы обновлений бд в отдельную папку

785 of 5539 relevant lines covered (14.17%)

1.26 hits per line

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

22.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 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 $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
    use HasFactory;
82
    use MustVerifyEmail;
83
    use Notifiable;
84
    use SearchableTrait;
85
    use SortableTrait;
86
    use UploadTrait;
87

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

257
        return setting('deleted_user');
×
258
    }
259

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

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

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

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

279
            return new HtmlString($html . $admin);
×
280
        }
281

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

284
        return new HtmlString($html);
×
285
    }
286

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

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

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

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

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

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

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

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

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

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

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

370
        return new HtmlString($online);
×
371
    }
372

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

384
        return $visit;
×
385
    }
386

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

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

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

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

406
        return setting('statusdef');
2✔
407
    }
408

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

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

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

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

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

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

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

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

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

464
    /**
465
     * Get gravatar
466
     */
467
    private function getGravatar(): HtmlString
×
468
    {
469
        $hash = hash('sha256', $this->email);
×
470

471
        return new HtmlString('<img class="avatar-default rounded-circle" src="//gravatar.com/avatar/' . $hash . '?d=initials&amp;name=' . $this->getName() . '" alt="">');
×
472
    }
473

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

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

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

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

504
            return $statuses;
2✔
505
        });
2✔
506
    }
507

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

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

524
    /**
525
     * Удаляет альбом пользователя
526
     */
527
    public function deleteAlbum(): void
×
528
    {
529
        $photos = Photo::query()->where('user_id', $this->id)->get();
×
530

531
        if ($photos->isNotEmpty()) {
×
532
            foreach ($photos as $photo) {
×
533
                $photo->delete();
×
534
            }
535
        }
536
    }
537

538
    /**
539
     * Удаляет записи пользователя из всех таблиц
540
     */
541
    public function delete(): ?bool
×
542
    {
543
        return DB::transaction(function () {
×
544
            deleteFile(public_path($this->picture));
×
545
            deleteFile(public_path($this->avatar));
×
546

547
            Message::query()->where('user_id', $this->id)->delete();
×
548
            Dialogue::query()->where('user_id', $this->id)->delete();
×
549
            Rating::query()->where('user_id', $this->id)->delete();
×
550
            Banhist::query()->where('user_id', $this->id)->delete();
×
551

552
            foreach (Registry::$onDeleteUser as $callback) {
×
553
                $callback($this);
×
554
            }
555

556
            return parent::delete();
×
557
        });
×
558
    }
559

560
    /**
561
     * Updates count messages
562
     */
563
    public function updatePrivate(): void
×
564
    {
565
        if ($this->newprivat) {
×
566
            $countDialogues = Dialogue::query()
×
567
                ->where('user_id', $this->id)
×
568
                ->where('reading', 0)
×
569
                ->count();
×
570

571
            if ($countDialogues !== $this->newprivat) {
×
572
                $this->update([
×
573
                    'newprivat'      => $countDialogues,
×
574
                    'sendprivatmail' => 0,
×
575
                ]);
×
576
            }
577
        }
578
    }
579

580
    /**
581
     * Check user banned
582
     */
583
    public function isBanned(): bool
×
584
    {
585
        return $this->level === self::BANNED;
×
586
    }
587

588
    /**
589
     * Check user pended
590
     */
591
    public function isPended(): bool
×
592
    {
593
        return setting('regkeys') && $this->level === self::PENDED;
×
594
    }
595

596
    /**
597
     * Check user active
598
     */
599
    public function isActive(): bool
2✔
600
    {
601
        return in_array($this->level, self::USER_GROUPS, true);
2✔
602
    }
603

604
    /**
605
     * Getting daily bonus
606
     */
607
    public function gettingBonus(): void
×
608
    {
609
        if ($this->isActive() && $this->timebonus < strtotime('-23 hours', SITETIME)) {
×
610
            $this->increment('money', setting('bonusmoney'));
×
611
            $this->update(['timebonus' => SITETIME]);
×
612

613
            setFlash('success', __('main.daily_bonus', ['money' => plural(setting('bonusmoney'), setting('moneyname'))]));
×
614
        }
615
    }
616
}
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