$mods * @property-read Collection $followers * @property-read Collection $following * @property-read Collection $oAuthConnections */ class User extends Authenticatable implements MustVerifyEmail { use Bannable; use HasApiTokens; use HasCoverPhoto; /** @use HasFactory */ use HasFactory; use HasProfilePhoto; use Notifiable; use Searchable; use TwoFactorAuthenticatable; protected $hidden = [ 'password', 'remember_token', 'two_factor_recovery_codes', 'two_factor_secret', ]; protected $appends = [ 'profile_photo_url', ]; /** * Get the storage path for profile photos. */ public static function profilePhotoStoragePath(): string { return 'profile-photos'; } /** * The relationship between a user and their mods. * * @return BelongsToMany */ public function mods(): BelongsToMany { return $this->belongsToMany(Mod::class); } /** * The relationship between a user and users that follow them. * * @return BelongsToMany */ public function followers(): BelongsToMany { return $this->belongsToMany(User::class, 'user_follows', 'following_id', 'follower_id') ->withTimestamps(); } /** * Follow another user. */ public function follow(User|int $user): void { $userId = $user instanceof User ? $user->id : $user; if ($this->id === $userId) { // Don't allow following yourself. return; } $this->following()->syncWithoutDetaching([$userId]); } /** * The relationship between a user and users they follow. * * @return BelongsToMany */ public function following(): BelongsToMany { return $this->belongsToMany(User::class, 'user_follows', 'follower_id', 'following_id') ->withTimestamps(); } /** * Unfollow another user. */ public function unfollow(User|int $user): void { $userId = $user instanceof User ? $user->id : $user; if ($this->isFollowing($userId)) { $this->following()->detach($userId); } } /** * Check if the user is following another user. */ public function isFollowing(User|int $user): bool { $userId = $user instanceof User ? $user->id : $user; return $this->following()->where('following_id', $userId)->exists(); } /** * The data that is searchable by Scout. * * @return array */ public function toSearchableArray(): array { return [ 'id' => (int) $this->id, 'name' => $this->name, ]; } /** * Determine if the model instance should be searchable. */ public function shouldBeSearchable(): bool { $this->load(['bans']); return $this->isNotBanned(); } /** * Check if the user has the role of a moderator. */ public function isMod(): bool { return Str::lower($this->role?->name) === 'moderator'; } /** * Check if the user has the role of an administrator. */ public function isAdmin(): bool { return Str::lower($this->role?->name) === 'administrator'; } /** * Overwritten to instead use the queued version of the VerifyEmail notification. */ public function sendEmailVerificationNotification(): void { $this->notify(new VerifyEmail); } /** * Overwritten to instead use the queued version of the ResetPassword notification. */ public function sendPasswordResetNotification(#[SensitiveParameter] $token): void { $this->notify(new ResetPassword($token)); } /** * Get the relative URL to the user's profile page. */ public function profileUrl(): string { return route('user.show', [ 'user' => $this->id, 'username' => $this->slug(), ]); } /** * Get the slug of the user's name. */ public function slug(): string { return Str::lower(Str::slug($this->name)); } /** * Assign a role to the user. */ public function assignRole(UserRole $userRole): bool { $this->role()->associate($userRole); return $this->save(); } /** * The relationship between a user and their role. * * @return BelongsTo */ public function role(): BelongsTo { return $this->belongsTo(UserRole::class, 'user_role_id'); } /** * Scope a query by applying QueryFilter filters. * * @param Builder $builder * @return Builder */ public function scopeFilter(Builder $builder, QueryFilter $queryFilter): Builder { return $queryFilter->apply($builder); } /** * The relationship between a user and their OAuth providers. * * @return HasMany */ public function oAuthConnections(): HasMany { return $this->hasMany(OAuthConnection::class); } /** * Handle the about default value if empty. Thanks, MySQL! * * @return Attribute */ protected function about(): Attribute { return Attribute::make( set: function ($value) { // MySQL will not allow you to set a default value of an empty string for a (LONG)TEXT column. *le sigh* // NULL is the default. If NULL is saved, we'll swap it out for an empty string. if (is_null($value)) { return ''; } return $value; }, ); } /** * Get the disk that profile photos should be stored on. */ protected function profilePhotoDisk(): string { return config('filesystems.asset_upload', 'public'); } /** * The attributes that should be cast to native types. */ protected function casts(): array { return [ 'id' => 'integer', 'hub_id' => 'integer', 'email_verified_at' => 'datetime', 'password' => 'hashed', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; } }