refactor: use custom query builders instead of scopes

This commit is contained in:
Phan An 2022-08-09 20:45:11 +02:00
parent 7704bef3ac
commit 9d9dc0b397
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
52 changed files with 373 additions and 351 deletions

View file

@ -0,0 +1,33 @@
<?php
namespace App\Builders;
use App\Models\Album;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Facades\DB;
class AlbumBuilder extends Builder
{
public function isStandard(): static
{
return $this->whereNot('albums.id', Album::UNKNOWN_ID);
}
public function withMeta(User $user): static
{
return $this->with('artist')
->leftJoin('songs', 'albums.id', '=', 'songs.album_id')
->leftJoin('interactions', static function (JoinClause $join) use ($user): void {
$join->on('songs.id', '=', 'interactions.song_id')->where('interactions.user_id', $user->id);
})
->groupBy('albums.id')
->select(
'albums.*',
DB::raw('CAST(SUM(interactions.play_count) AS UNSIGNED) AS play_count')
)
->withCount('songs AS song_count')
->withSum('songs AS length', 'length');
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Builders;
use App\Models\Artist;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Facades\DB;
class ArtistBuilder extends Builder
{
public function isStandard(): static
{
return $this->whereNotIn('artists.id', [Artist::UNKNOWN_ID, Artist::VARIOUS_ID]);
}
public function withMeta(User $user): static
{
return $this->leftJoin('songs', 'artists.id', '=', 'songs.artist_id')
->leftJoin('interactions', static function (JoinClause $join) use ($user): void {
$join->on('interactions.song_id', '=', 'songs.id')->where('interactions.user_id', $user->id);
})
->groupBy('artists.id')
->select([
'artists.*',
DB::raw('CAST(SUM(interactions.play_count) AS UNSIGNED) AS play_count'),
DB::raw('COUNT(DISTINCT songs.album_id) AS album_count'),
])
->withCount('songs AS song_count')
->withSum('songs AS length', 'length');
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Builders;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
class SongBuilder extends Builder
{
public function inDirectory(string $path): static
{
// Make sure the path ends with a directory separator.
$path = rtrim(trim($path), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
return $this->where('path', 'LIKE', "$path%");
}
public function withMeta(User $user): static
{
return $this
->with('artist', 'album', 'album.artist')
->leftJoin('interactions', static function (JoinClause $join) use ($user): void {
$join->on('interactions.song_id', '=', 'songs.id')->where('interactions.user_id', $user->id);
})
->join('albums', 'songs.album_id', '=', 'albums.id')
->join('artists', 'songs.artist_id', '=', 'artists.id')
->select(
'songs.*',
'albums.name',
'artists.name',
'interactions.liked',
'interactions.play_count'
);
}
public function hostedOnS3(): static
{
return $this->where('path', 'LIKE', 's3://%');
}
}

View file

@ -25,7 +25,9 @@ class ChangePasswordCommand extends Command
$email = $this->argument('email');
/** @var User|null $user */
$user = $email ? User::where('email', $email)->first() : User::where('is_admin', true)->first();
$user = $email
? User::query()->where('email', $email)->first()
: User::query()->where('is_admin', true)->first();
if (!$user) {
$this->error('The user account cannot be found.');

View file

@ -204,7 +204,7 @@ class InitCommand extends Command
private function setUpAdminAccount(): void
{
$this->components->task('Creating default admin account', function (): void {
User::create([
User::query()->create([
'name' => self::DEFAULT_ADMIN_NAME,
'email' => self::DEFAULT_ADMIN_EMAIL,
'password' => $this->hash->make(self::DEFAULT_ADMIN_PASSWORD),
@ -217,7 +217,7 @@ class InitCommand extends Command
private function maybeSeedDatabase(): void
{
if (!User::count()) {
if (!User::query()->count()) {
$this->setUpAdminAccount();
$this->components->task('Seeding data', function (): void {

View file

@ -12,22 +12,17 @@ use Illuminate\Contracts\Auth\Authenticatable;
class AlbumController extends Controller
{
/** @param User $user */
public function __construct(private AlbumRepository $albumRepository, private ?Authenticatable $user)
public function __construct(private AlbumRepository $repository, private ?Authenticatable $user)
{
}
public function index()
{
$pagination = Album::withMeta($this->user)
->isStandard()
->orderBy('albums.name')
->simplePaginate(21);
return AlbumResource::collection($pagination);
return AlbumResource::collection($this->repository->paginate($this->user));
}
public function show(Album $album)
{
return AlbumResource::make($this->albumRepository->getOne($album->id, $this->user));
return AlbumResource::make($this->repository->getOne($album->id, $this->user));
}
}

View file

@ -12,22 +12,17 @@ use Illuminate\Contracts\Auth\Authenticatable;
class ArtistController extends Controller
{
/** @param User $user */
public function __construct(private ArtistRepository $artistRepository, private ?Authenticatable $user)
public function __construct(private ArtistRepository $repository, private ?Authenticatable $user)
{
}
public function index()
{
$pagination = Artist::withMeta($this->user)
->isStandard()
->orderBy('artists.name')
->simplePaginate(21);
return ArtistResource::collection($pagination);
return ArtistResource::collection($this->repository->paginate($this->user));
}
public function show(Artist $artist)
{
return ArtistResource::make($this->artistRepository->getOne($artist->id, $this->user));
return ArtistResource::make($this->repository->getOne($artist->id, $this->user));
}
}

View file

@ -2,18 +2,15 @@
namespace App\Models;
use App\Builders\AlbumBuilder;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Query\Builder as BuilderContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Laravel\Scout\Searchable;
/**
@ -34,14 +31,6 @@ use Laravel\Scout\Searchable;
* @property float|string $length Total length of the album in seconds (dynamically calculated)
* @property int|string $play_count Total number of times the album's songs have been played (dynamically calculated)
* @property int|string $song_count Total number of songs on the album (dynamically calculated)
*
* @method static self firstOrCreate(array $where, array $params = [])
* @method static self|null find(int $id)
* @method static Builder where(...$params)
* @method static self first()
* @method static Builder whereArtistIdAndName(int $id, string $name)
* @method static orderBy(...$params)
* @method static Builder latest()
*/
class Album extends Model
{
@ -59,13 +48,23 @@ class Album extends Model
/** @deprecated */
protected $appends = ['is_compilation'];
public static function query(): AlbumBuilder
{
return parent::query();
}
public function newEloquentBuilder($query): AlbumBuilder
{
return new AlbumBuilder($query);
}
/**
* Get an album using some provided information.
* If such is not found, a new album will be created using the information.
*/
public static function getOrCreate(Artist $artist, ?string $name = null): self
public static function getOrCreate(Artist $artist, ?string $name = null): static
{
return static::firstOrCreate([
return static::query()->firstOrCreate([ // @phpstan-ignore-line
'artist_id' => $artist->id,
'name' => trim($name) ?: self::UNKNOWN_NAME,
]);
@ -143,29 +142,6 @@ class Album extends Model
return Attribute::get(fn () => $this->artist_id === Artist::VARIOUS_ID);
}
public function scopeIsStandard(Builder $query): Builder
{
return $query->whereNot('albums.id', self::UNKNOWN_ID);
}
public static function withMeta(User $scopedUser): BuilderContract
{
return static::query()
->with('artist')
->leftJoin('songs', 'albums.id', '=', 'songs.album_id')
->leftJoin('interactions', static function (JoinClause $join) use ($scopedUser): void {
$join->on('songs.id', '=', 'interactions.song_id')
->where('interactions.user_id', $scopedUser->id);
})
->groupBy('albums.id')
->select(
'albums.*',
DB::raw('CAST(SUM(interactions.play_count) AS UNSIGNED) AS play_count')
)
->withCount('songs AS song_count')
->withSum('songs AS length', 'length');
}
/** @return array<mixed> */
public function toSearchableArray(): array
{

View file

@ -2,18 +2,15 @@
namespace App\Models;
use App\Builders\ArtistBuilder;
use App\Facades\Util;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Query\Builder as BuilderContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Laravel\Scout\Searchable;
/**
@ -30,14 +27,6 @@ use Laravel\Scout\Searchable;
* @property string|int $song_count Total number of songs by the artist (dynamically calculated)
* @property string|int $album_count Total number of albums by the artist (dynamically calculated)
* @property Carbon $created_at
*
* @method static self find(int $id)
* @method static self firstOrCreate(array $where, array $params = [])
* @method static Builder where(...$params)
* @method static self first()
* @method static Builder whereName(string $name)
* @method static Builder orderBy(...$params)
* @method static Builder join(...$params)
*/
class Artist extends Model
{
@ -53,6 +42,16 @@ class Artist extends Model
protected $guarded = ['id'];
protected $hidden = ['created_at', 'updated_at'];
public static function query(): ArtistBuilder
{
return parent::query();
}
public function newEloquentBuilder($query): ArtistBuilder
{
return new ArtistBuilder($query);
}
/**
* Get an Artist object from their name.
* If such is not found, a new artist will be created.
@ -66,7 +65,7 @@ class Artist extends Model
$name = mb_convert_encoding($name, 'UTF-8', $encoding);
}
return static::firstOrCreate(['name' => trim($name) ?: self::UNKNOWN_NAME]);
return static::query()->firstOrCreate(['name' => trim($name) ?: self::UNKNOWN_NAME]);
}
public function albums(): HasMany
@ -120,29 +119,6 @@ class Artist extends Model
});
}
public function scopeIsStandard(Builder $query): Builder
{
return $query->whereNotIn('artists.id', [self::UNKNOWN_ID, self::VARIOUS_ID]);
}
public static function withMeta(User $scopedUser): BuilderContract
{
return static::query()
->leftJoin('songs', 'artists.id', '=', 'songs.artist_id')
->leftJoin('interactions', static function (JoinClause $join) use ($scopedUser): void {
$join->on('interactions.song_id', '=', 'songs.id')
->where('interactions.user_id', $scopedUser->id);
})
->groupBy('artists.id')
->select([
'artists.*',
DB::raw('CAST(SUM(interactions.play_count) AS UNSIGNED) AS play_count'),
DB::raw('COUNT(DISTINCT songs.album_id) AS album_count'),
])
->withCount('songs AS song_count')
->withSum('songs AS length', 'length');
}
/** @return array<mixed> */
public function toSearchableArray(): array
{

View file

@ -2,7 +2,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -14,12 +13,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property User $user
* @property int $id
* @property string $song_id
*
* @method static self firstOrCreate(array $where, array $params = [])
* @method static self find(int $id)
* @method static Builder whereSongIdAndUserId(string $songId, string $userId)
* @method static Builder whereIn(...$params)
* @method static Builder where(...$params)
*/
class Interaction extends Model
{

View file

@ -4,7 +4,6 @@ namespace App\Models;
use App\Casts\SmartPlaylistRulesCast;
use App\Values\SmartPlaylistRuleGroup;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -22,8 +21,6 @@ use Laravel\Scout\Searchable;
* @property bool $is_smart
* @property string $name
* @property user $user
*
* @method static Builder orderBy(string $field, string $order = 'asc')
*/
class Playlist extends Model
{

View file

@ -10,7 +10,6 @@ use Illuminate\Database\Eloquent\Model;
* @property mixed $value
*
* @method static self find(string $key)
* @method static self updateOrCreate(array $where, array $params)
*/
class Setting extends Model
{
@ -44,6 +43,6 @@ class Setting extends Model
return;
}
self::updateOrCreate(compact('key'), compact('value'));
self::query()->updateOrCreate(compact('key'), compact('value'));
}
}

View file

@ -2,16 +2,14 @@
namespace App\Models;
use App\Builders\SongBuilder;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Laravel\Scout\Searchable;
@ -31,26 +29,13 @@ use Laravel\Scout\Searchable;
* @property ?bool $liked Whether the song is liked by the current user (dynamically calculated)
* @property ?int $play_count The number of times the song has been played by the current user (dynamically calculated)
* @property Carbon $created_at
*
* @method static self updateOrCreate(array $where, array $params)
* @method static Builder select(string $string)
* @method static Builder inDirectory(string $path)
* @method static self first()
* @method static Builder orderBy(...$args)
* @method static int count()
* @method static self|Collection|null find($id)
* @method static Builder take(int $count)
* @method static float|int sum(string $column)
* @method static Builder latest(string $column = 'created_at')
* @method static Builder where(...$params)
* @method static Song findOrFail(string $id)
* @property array<mixed> $s3_params
*/
class Song extends Model
{
use HasFactory;
use Searchable;
use SupportsDeleteWhereValueNotIn;
use SupportsS3;
public const ID_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
@ -73,6 +58,16 @@ class Song extends Model
static::creating(static fn (self $song) => $song->id = Str::uuid()->toString());
}
public static function query(): SongBuilder
{
return parent::query();
}
public function newEloquentBuilder($query): SongBuilder
{
return new SongBuilder($query);
}
public function artist(): BelongsTo
{
return $this->belongsTo(Artist::class);
@ -93,17 +88,6 @@ class Song extends Model
return $this->hasMany(Interaction::class);
}
/**
* Scope a query to only include songs in a given directory.
*/
public function scopeInDirectory(Builder $query, string $path): Builder
{
// Make sure the path ends with a directory separator.
$path = rtrim(trim($path), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
return $query->where('path', 'LIKE', "$path%");
}
protected function title(): Attribute
{
return new Attribute(
@ -120,30 +104,22 @@ class Song extends Model
return new Attribute(get: $normalizer, set: $normalizer);
}
public static function withMeta(User $scopedUser, ?Builder $query = null): Builder
protected function s3Params(): Attribute
{
$query ??= static::query();
return $query
->with('artist', 'album', 'album.artist')
->leftJoin('interactions', static function (JoinClause $join) use ($scopedUser): void {
$join->on('interactions.song_id', '=', 'songs.id')
->where('interactions.user_id', $scopedUser->id);
})
->join('albums', 'songs.album_id', '=', 'albums.id')
->join('artists', 'songs.artist_id', '=', 'artists.id')
->select(
'songs.*',
'albums.name',
'artists.name',
'interactions.liked',
'interactions.play_count'
);
return Attribute::get(function (): ?array {
if (!preg_match('/^s3:\\/\\/(.*)/', $this->path, $matches)) {
return null;
}
public function scopeWithMeta(Builder $query, User $scopedUser): Builder
[$bucket, $key] = explode('/', $matches[1], 2);
return compact('bucket', 'key');
});
}
public static function getPathFromS3BucketAndKey(string $bucket, string $key): string
{
return static::withMeta($scopedUser, $query);
return "s3://$bucket/$key";
}
/** @return array<mixed> */

View file

@ -10,7 +10,6 @@ use ZipArchive;
class SongZipArchive
{
private ZipArchive $archive;
private string $path;
/**
@ -30,7 +29,7 @@ class SongZipArchive
}
}
public function addSongs(Collection $songs): self
public function addSongs(Collection $songs): static
{
$songs->each(function (Song $song): void {
$this->addSong($song);
@ -39,7 +38,7 @@ class SongZipArchive
return $this;
}
public function addSong(Song $song): self
public function addSong(Song $song): static
{
attempt(function () use ($song): void {
$path = Download::fromSong($song);
@ -49,7 +48,7 @@ class SongZipArchive
return $this;
}
public function finish(): self
public function finish(): static
{
$this->archive->close();

View file

@ -10,9 +10,7 @@ use Illuminate\Support\Facades\DB;
* MySQL and PostgresSQL seem to have a limit of 2^16-1 (65535) elements in an IN statement.
* This trait provides a method as a workaround to this limitation.
*
* @method static Builder whereIn($keys, array $values)
* @method static Builder whereNotIn($keys, array $values)
* @method static Builder select(string $string)
* @method static Builder query()
*/
trait SupportsDeleteWhereValueNotIn
{
@ -25,18 +23,18 @@ trait SupportsDeleteWhereValueNotIn
// If the number of entries is lower than, or equals to maxChunkSize, just go ahead.
if (count($values) <= $maxChunkSize) {
static::whereNotIn($field, $values)->delete();
static::query()->whereNotIn($field, $values)->delete();
return;
}
// Otherwise, we get the actual IDs that should be deleted…
$allIDs = static::select($field)->get()->pluck($field)->all();
$allIDs = static::query()->select($field)->get()->pluck($field)->all();
$whereInIDs = array_diff($allIDs, $values);
// …and see if we can delete them instead.
if (count($whereInIDs) < $maxChunkSize) {
static::whereIn($field, $whereInIDs)->delete();
static::query()->whereIn($field, $whereInIDs)->delete();
return;
}
@ -50,7 +48,7 @@ trait SupportsDeleteWhereValueNotIn
{
DB::transaction(static function () use ($values, $field, $chunkSize): void {
foreach (array_chunk($values, $chunkSize) as $chunk) {
static::whereIn($field, $chunk)->delete();
static::query()->whereIn($field, $chunk)->delete();
}
});
}

View file

@ -1,37 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* @property array<string>|null $s3_params The bucket and key name of an S3 object.
*
* @method static Builder hostedOnS3()
*/
trait SupportsS3
{
protected function s3Params(): Attribute
{
return Attribute::get(function (): ?array {
if (!preg_match('/^s3:\\/\\/(.*)/', $this->path, $matches)) {
return null;
}
[$bucket, $key] = explode('/', $matches[1], 2);
return compact('bucket', 'key');
});
}
public static function getPathFromS3BucketAndKey(string $bucket, string $key): string
{
return "s3://$bucket/$key";
}
public function scopeHostedOnS3(Builder $query): Builder
{
return $query->where('path', 'LIKE', 's3://%');
}
}

View file

@ -7,7 +7,6 @@ use App\Values\UserPreferences;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Query\Builder;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
@ -21,11 +20,6 @@ use Laravel\Sanctum\HasApiTokens;
* @property string $email
* @property string $password
* @property-read string $avatar
*
* @method static self create(array $params)
* @method static int count()
* @method static Builder where(...$params)
* @method static self|null firstWhere(...$params)
*/
class User extends Authenticatable
{

View file

@ -5,6 +5,7 @@ namespace App\Repositories;
use App\Models\Album;
use App\Models\User;
use App\Repositories\Traits\Searchable;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Support\Collection;
class AlbumRepository extends Repository
@ -13,7 +14,8 @@ class AlbumRepository extends Repository
public function getOne(int $id, ?User $scopedUser = null): Album
{
return Album::withMeta($scopedUser ?? $this->auth->user())
return Album::query()
->withMeta($scopedUser ?? $this->auth->user())
->where('albums.id', $id)
->first();
}
@ -21,7 +23,8 @@ class AlbumRepository extends Repository
/** @return Collection|array<array-key, Album> */
public function getRecentlyAdded(int $count = 6, ?User $scopedUser = null): Collection
{
return Album::withMeta($scopedUser ?? $this->auth->user())
return Album::query()
->withMeta($scopedUser ?? $this->auth->user())
->isStandard()
->latest('albums.created_at')
->limit($count)
@ -31,9 +34,8 @@ class AlbumRepository extends Repository
/** @return Collection|array<array-key, Album> */
public function getMostPlayed(int $count = 6, ?User $scopedUser = null): Collection
{
$scopedUser ??= $this->auth->user();
return Album::withMeta($scopedUser ?? $this->auth->user())
return Album::query()
->withMeta($scopedUser ?? $this->auth->user())
->isStandard()
->orderByDesc('play_count')
->limit($count)
@ -43,8 +45,18 @@ class AlbumRepository extends Repository
/** @return Collection|array<array-key, Album> */
public function getByIds(array $ids, ?User $scopedUser = null): Collection
{
return Album::withMeta($scopedUser ?? $this->auth->user())
return Album::query()
->withMeta($scopedUser ?? $this->auth->user())
->whereIn('albums.id', $ids)
->get();
}
public function paginate(?User $scopedUser = null): Paginator
{
return Album::query()
->withMeta($scopedUser ?? $this->auth->user())
->isStandard()
->orderBy('albums.name')
->simplePaginate(21);
}
}

View file

@ -5,6 +5,7 @@ namespace App\Repositories;
use App\Models\Artist;
use App\Models\User;
use App\Repositories\Traits\Searchable;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Collection;
class ArtistRepository extends Repository
@ -14,7 +15,8 @@ class ArtistRepository extends Repository
/** @return Collection|array<array-key, Artist> */
public function getMostPlayed(int $count = 6, ?User $scopedUser = null): Collection
{
return Artist::withMeta($scopedUser ?? $this->auth->user())
return Artist::query()
->withMeta($scopedUser ?? $this->auth->user())
->isStandard()
->orderByDesc('play_count')
->limit($count)
@ -23,7 +25,8 @@ class ArtistRepository extends Repository
public function getOne(int $id, ?User $scopedUser = null): Artist
{
return Artist::withMeta($scopedUser ?? $this->auth->user())
return Artist::query()
->withMeta($scopedUser ?? $this->auth->user())
->where('artists.id', $id)
->first();
}
@ -31,9 +34,19 @@ class ArtistRepository extends Repository
/** @return Collection|array<array-key, Artist> */
public function getByIds(array $ids, ?User $scopedUser = null): Collection
{
return Artist::withMeta($scopedUser ?? $this->auth->user())
return Artist::query()
->withMeta($scopedUser ?? $this->auth->user())
->isStandard()
->whereIn('artists.id', $ids)
->get();
}
public function paginate(?User $scopedUser = null): Paginator
{
return Artist::query()
->withMeta($scopedUser ?? $this->auth->user())
->isStandard()
->orderBy('artists.name')
->simplePaginate(21);
}
}

View file

@ -5,7 +5,6 @@ namespace App\Repositories;
use App\Models\Interaction;
use App\Models\User;
use App\Repositories\Traits\ByCurrentUser;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
class InteractionRepository extends Repository
@ -15,7 +14,9 @@ class InteractionRepository extends Repository
/** @return Collection|array<Interaction> */
public function getUserFavorites(User $user): Collection
{
return $this->model->where([
return $this->model
->newQuery()
->where([
'user_id' => $user->id,
'liked' => true,
])
@ -26,11 +27,11 @@ class InteractionRepository extends Repository
/** @return array<Interaction> */
public function getRecentlyPlayed(User $user, ?int $count = null): array
{
/** @var Builder $query */
$query = $this->model
->newQuery()
->where('user_id', $user->id)
->where('play_count', '>', 0)
->orderBy('updated_at', 'DESC');
->latest('updated_at');
if ($count) {
$query = $query->take($count);

View file

@ -31,27 +31,26 @@ class SongRepository extends Repository
public function getOneByPath(string $path): ?Song
{
return Song::where('path', $path)->first();
return Song::query()->where('path', $path)->first();
}
/** @return Collection|array<Song> */
public function getAllHostedOnS3(): Collection
{
return Song::hostedOnS3()->get();
return Song::query()->hostedOnS3()->get();
}
/** @return Collection|array<array-key, Song> */
public function getRecentlyAdded(int $count = 10, ?User $scopedUser = null): Collection
{
return Song::withMeta($scopedUser ?? $this->auth->user())->latest()->limit($count)->get();
return Song::query()->withMeta($scopedUser ?? $this->auth->user())->latest()->limit($count)->get();
}
/** @return Collection|array<array-key, Song> */
public function getMostPlayed(int $count = 7, ?User $scopedUser = null): Collection
{
$scopedUser ??= $this->auth->user();
return Song::withMeta($scopedUser)
return Song::query()
->withMeta($scopedUser ?? $this->auth->user())
->where('interactions.play_count', '>', 0)
->orderByDesc('interactions.play_count')
->limit($count)
@ -61,9 +60,8 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getRecentlyPlayed(int $count = 7, ?User $scopedUser = null): Collection
{
$scopedUser ??= $this->auth->user();
return Song::withMeta($scopedUser)
return Song::query()
->withMeta($scopedUser ?? $this->auth->user())
->where('interactions.play_count', '>', 0)
->orderByDesc('interactions.updated_at')
->limit($count)
@ -77,7 +75,7 @@ class SongRepository extends Repository
int $perPage = 50
): Paginator {
return self::applySort(
Song::withMeta($scopedUser ?? $this->auth->user()),
Song::query()->withMeta($scopedUser ?? $this->auth->user()),
$sortColumn,
$sortDirection
)
@ -92,7 +90,7 @@ class SongRepository extends Repository
?User $scopedUser = null,
): Collection {
return self::applySort(
Song::withMeta($scopedUser ?? $this->auth->user()),
Song::query()->withMeta($scopedUser ?? $this->auth->user()),
$sortColumn,
$sortDirection
)
@ -103,13 +101,14 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getFavorites(?User $scopedUser = null): Collection
{
return Song::withMeta($scopedUser ?? $this->auth->user())->where('interactions.liked', true)->get();
return Song::query()->withMeta($scopedUser ?? $this->auth->user())->where('interactions.liked', true)->get();
}
/** @return Collection|array<array-key, Song> */
public function getByAlbum(Album $album, ?User $scopedUser = null): Collection
{
return Song::withMeta($scopedUser ?? $this->auth->user())
return Song::query()
->withMeta($scopedUser ?? $this->auth->user())
->where('album_id', $album->id)
->orderBy('songs.track')
->orderBy('songs.disc')
@ -120,7 +119,8 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getByArtist(Artist $artist, ?User $scopedUser = null): Collection
{
return Song::withMeta($scopedUser ?? $this->auth->user())
return Song::query()
->withMeta($scopedUser ?? $this->auth->user())
->where('songs.artist_id', $artist->id)
->orderBy('albums.name')
->orderBy('songs.track')
@ -132,7 +132,8 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getByStandardPlaylist(Playlist $playlist, ?User $scopedUser = null): Collection
{
return Song::withMeta($scopedUser ?? $this->auth->user())
return Song::query()
->withMeta($scopedUser ?? $this->auth->user())
->leftJoin('playlist_song', 'songs.id', '=', 'playlist_song.song_id')
->leftJoin('playlists', 'playlists.id', '=', 'playlist_song.playlist_id')
->where('playlists.id', $playlist->id)
@ -143,28 +144,28 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getRandom(int $limit, ?User $scopedUser = null): Collection
{
return Song::withMeta($scopedUser ?? $this->auth->user())->inRandomOrder()->limit($limit)->get();
return Song::query()->withMeta($scopedUser ?? $this->auth->user())->inRandomOrder()->limit($limit)->get();
}
/** @return Collection|array<array-key, Song> */
public function getByIds(array $ids, ?User $scopedUser = null): Collection
{
return Song::withMeta($scopedUser ?? $this->auth->user())->whereIn('songs.id', $ids)->get();
return Song::query()->withMeta($scopedUser ?? $this->auth->user())->whereIn('songs.id', $ids)->get();
}
public function getOne($id, ?User $scopedUser = null): Song
{
return Song::withMeta($scopedUser ?? $this->auth->user())->findOrFail($id);
return Song::query()->withMeta($scopedUser ?? $this->auth->user())->findOrFail($id);
}
public function count(): int
{
return Song::count();
return Song::query()->count();
}
public function getTotalLength(): float
{
return Song::sum('length');
return Song::query()->sum('length');
}
private static function normalizeSortColumn(string $column): string

View file

@ -88,7 +88,7 @@ class FileSynchronizer
$data['album_id'] = $album->id;
$data['artist_id'] = $artist->id;
$this->song = Song::updateOrCreate(['path' => $this->filePath], $data);
$this->song = Song::query()->updateOrCreate(['path' => $this->filePath], $data); // @phpstan-ignore-line
return SyncResult::success($this->filePath);
}

View file

@ -19,7 +19,7 @@ class InteractionService
*/
public function increasePlayCount(string $songId, User $user): Interaction
{
return tap(Interaction::firstOrCreate([
return tap(Interaction::query()->firstOrCreate([
'song_id' => $songId,
'user_id' => $user->id,
]), static function (Interaction $interaction): void {
@ -39,7 +39,7 @@ class InteractionService
*/
public function toggleLike(string $songId, User $user): Interaction
{
return tap(Interaction::firstOrCreate([
return tap(Interaction::query()->firstOrCreate([
'song_id' => $songId,
'user_id' => $user->id,
]), static function (Interaction $interaction): void {
@ -59,21 +59,18 @@ class InteractionService
*/
public function batchLike(array $songIds, User $user): Collection
{
$interactions = collect($songIds)->map(static fn ($songId): Interaction => tap(Interaction::firstOrCreate([
$interactions = collect($songIds)->map(static function ($songId) use ($user): Interaction {
return tap(Interaction::query()->firstOrCreate([
'song_id' => $songId,
'user_id' => $user->id,
]), static function (Interaction $interaction): void {
if (!$interaction->exists) {
$interaction->play_count = 0;
}
$interaction->play_count ??= 0;
$interaction->liked = true;
$interaction->save();
}));
});
});
event(new SongsBatchLiked($interactions->map(static function (Interaction $interaction): Song {
return $interaction->song;
}), $user));
event(new SongsBatchLiked($interactions->map(static fn (Interaction $item) => $item->song), $user));
return $interactions;
}
@ -85,10 +82,11 @@ class InteractionService
*/
public function batchUnlike(array $songIds, User $user): void
{
Interaction::whereIn('song_id', $songIds)
Interaction::query()
->whereIn('song_id', $songIds)
->where('user_id', $user->id)
->update(['liked' => false]);
event(new SongsBatchUnliked(Song::find($songIds), $user));
event(new SongsBatchUnliked(Song::query()->find($songIds), $user));
}
}

View file

@ -5,7 +5,6 @@ namespace App\Services;
use App\Models\Album;
use App\Models\Artist;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
class LibraryManager
@ -19,13 +18,13 @@ class LibraryManager
public function prune(bool $dryRun = false): array
{
return DB::transaction(static function () use ($dryRun): array {
/** @var Builder $albumQuery */
$albumQuery = Album::leftJoin('songs', 'songs.album_id', '=', 'albums.id')
$albumQuery = Album::query()
->leftJoin('songs', 'songs.album_id', '=', 'albums.id')
->whereNull('songs.album_id')
->whereNotIn('albums.id', [Album::UNKNOWN_ID]);
/** @var Builder $artistQuery */
$artistQuery = Artist::leftJoin('songs', 'songs.artist_id', '=', 'artists.id')
$artistQuery = Artist::query()
->leftJoin('songs', 'songs.artist_id', '=', 'artists.id')
->leftJoin('albums', 'albums.artist_id', '=', 'artists.id')
->whereNull('songs.artist_id')
->whereNull('albums.artist_id')

View file

@ -38,8 +38,8 @@ class MediaCacheService
private function query(): array
{
return [
'albums' => Album::orderBy('name')->get(),
'artists' => Artist::orderBy('name')->get(),
'albums' => Album::query()->orderBy('name')->get(),
'artists' => Artist::query()->orderBy('name')->get(),
'songs' => Song::all(),
];
}

View file

@ -153,7 +153,7 @@ class MediaSyncService
private function handleDeletedDirectoryRecord(string $path): void
{
$count = Song::inDirectory($path)->delete();
$count = Song::query()->inDirectory($path)->delete();
if ($count) {
$this->logger->info("Deleted $count song(s) under $path");

View file

@ -66,7 +66,7 @@ class S3Service implements ObjectStorageInterface
);
}
$song = Song::updateOrCreate(['id' => Helper::getFileHash($path)], [
$song = Song::query()->updateOrCreate(['id' => Helper::getFileHash($path)], [
'path' => $path,
'album_id' => $album->id,
'artist_id' => $artist->id,

View file

@ -23,7 +23,7 @@ class SmartPlaylistService
{
throw_unless($playlist->is_smart, NonSmartPlaylistException::create($playlist));
$query = Song::withMeta($user ?? $this->auth->user());
$query = Song::query()->withMeta($user ?? $this->auth->user());
$playlist->rule_groups->each(static function (SmartPlaylistRuleGroup $group, int $index) use ($query): void {
$clause = $index === 0 ? 'where' : 'orWhere';

View file

@ -13,7 +13,7 @@ class UserService
public function createUser(string $name, string $email, string $plainTextPassword, bool $isAdmin): User
{
return User::create([
return User::query()->create([
'name' => $name,
'email' => $email,
'password' => $this->hash->make($plainTextPassword),

View file

@ -2,6 +2,7 @@
namespace App\Services\V6;
use App\Builders\SongBuilder;
use App\Models\Album;
use App\Models\Artist;
use App\Models\Song;
@ -10,7 +11,6 @@ use App\Repositories\AlbumRepository;
use App\Repositories\ArtistRepository;
use App\Repositories\SongRepository;
use App\Values\ExcerptSearchResult;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Support\Collection;
class SearchService
@ -55,7 +55,7 @@ class SearchService
int $limit = self::DEFAULT_MAX_SONG_RESULT_COUNT
): Collection {
return Song::search($keywords)
->query(static function (Builder $builder) use ($scopedUser, $limit): void {
->query(static function (SongBuilder $builder) use ($scopedUser, $limit): void {
$builder->withMeta($scopedUser ?? auth()->user())->limit($limit);
})
->get();

View file

@ -25,7 +25,8 @@ class CreateVariousArtists extends Migration
Artist::unguard();
$existingArtist = Artist::find(Artist::VARIOUS_ID);
/** @var Artist|null $existingArtist */
$existingArtist = Artist::query()->find(Artist::VARIOUS_ID);
if ($existingArtist) {
if ($existingArtist->name === Artist::VARIOUS_NAME) {
@ -34,7 +35,8 @@ class CreateVariousArtists extends Migration
// There's an existing artist with that special ID, but it's not our Various Artist
// We move it to the end of the table.
$latestArtist = Artist::orderBy('id', 'DESC')->first();
/** @var Artist $latestArtist */
$latestArtist = Artist::query()->orderByDesc('id')->first();
$existingArtist->id = $latestArtist->id + 1;
$existingArtist->save();
}

View file

@ -16,7 +16,7 @@ class FixArtistAutoindexValue extends Migration
}
/** @var Artist $latestArtist */
$latestArtist = Artist::orderBy('id', 'DESC')->first();
$latestArtist = Artist::query()->orderByDesc('id')->first();
DB::statement('ALTER TABLE artists AUTO_INCREMENT=' . ($latestArtist->id + 1));
}

View file

@ -8,6 +8,7 @@ class CopyArtistToContributingArtist extends Migration
public function up(): void
{
Song::with('album', 'album.artist')->get()->each(static function (Song $song): void {
// @phpstan-ignore-line
if (!$song->contributing_artist_id) {
$song->contributing_artist_id = $song->album->artist->id;
$song->save();

View file

@ -7,6 +7,6 @@ return new class extends Migration
{
public function up(): void
{
Album::where('cover', 'unknown-album.png')->update(['cover' => '']);
Album::query()->where('cover', 'unknown-album.png')->update(['cover' => '']);
}
};

View file

@ -11,9 +11,7 @@ class AlbumTableSeeder extends Seeder
{
public function run(): void
{
Album::firstOrCreate([
'id' => Album::UNKNOWN_ID,
], [
Album::query()->firstOrCreate(['id' => Album::UNKNOWN_ID], [
'artist_id' => Artist::UNKNOWN_ID,
'name' => Album::UNKNOWN_NAME,
]);

View file

@ -10,11 +10,7 @@ class ArtistTableSeeder extends Seeder
{
public function run(): void
{
Artist::firstOrCreate([
'id' => Artist::UNKNOWN_ID,
], [
'name' => Artist::UNKNOWN_NAME,
]);
Artist::query()->firstOrCreate(['id' => Artist::UNKNOWN_ID], ['name' => Artist::UNKNOWN_NAME]);
self::maybeResetPgsqlSerialValue();
}

View file

@ -21,13 +21,12 @@ parameters:
- '#expects .*, Mockery\\LegacyMockInterface given#'
- '#Call to an undefined method Illuminate\\Filesystem\\FilesystemAdapter::getAdapter\(\)#'
- '#Call to an undefined method Mockery\\ExpectationInterface|Mockery\\HigherOrderMessage::with\(\)#'
- '#Call to an undefined method Laravel\\Scout\\Builder::with\(\)#'
- '#Call to an undefined method Illuminate\\Contracts\\Database\\Query\\Builder::isStandard\(\)#'
- '#Call to an undefined method Illuminate\\Contracts\\Database\\Query\\Builder::withMeta\(\)#'
- '#should return App\\Models\\.*(\|null)? but returns Illuminate\\Database\\Eloquent\\Model(\|null)?#'
- '#Call to private method .*\(\) of parent class Illuminate\\Database\\Eloquent\\Builder<Illuminate\\Database\\Eloquent\\Model>#'
- '#should return App\\Models\\.*(\|null)? but returns .*Illuminate\\Database\\Eloquent\\Model(\|null)?#'
# Laravel factories allow declaration of dynamic methods as "states"
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Factories\\Factory::#'
- '#expects App\\Models\\User\|null, Illuminate\\Database\\Eloquent\\Collection\|Illuminate\\Database\\Eloquent\\Model given#'
- '#Method App\\Models\\.*::query\(\) should return App\\Builders\\.*Builder but returns Illuminate\\Database\\Eloquent\\Builder<Illuminate\\Database\\Eloquent\\Model>#'
excludePaths:
- ./routes/console.php

View file

@ -27,7 +27,8 @@ class DownloadTest extends TestCase
public function testNonLoggedInUserCannotDownload(): void
{
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
$this->downloadService
->shouldReceive('from')
@ -39,7 +40,8 @@ class DownloadTest extends TestCase
public function testDownloadOneSong(): void
{
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
/** @var User $user */
$user = User::factory()->create();
@ -62,7 +64,7 @@ class DownloadTest extends TestCase
$user = User::factory()->create();
/** @var array<Song>|Collection $songs */
$songs = Song::take(2)->orderBy('id')->get();
$songs = Song::query()->take(2)->orderBy('id')->get();
$this->downloadService
->shouldReceive('from')
@ -84,7 +86,8 @@ class DownloadTest extends TestCase
public function testDownloadAlbum(): void
{
$album = Album::first();
/** @var Album $album */
$album = Album::query()->first();
/** @var User $user */
$user = User::factory()->create();
@ -103,7 +106,8 @@ class DownloadTest extends TestCase
public function testDownloadArtist(): void
{
$artist = Artist::first();
/** @var Artist $artist */
$artist = Artist::query()->first();
/** @var User $user */
$user = User::factory()->create();

View file

@ -25,7 +25,7 @@ class InteractionTest extends TestCase
$user = User::factory()->create();
/** @var Song $song */
$song = Song::orderBy('id')->first();
$song = Song::query()->orderBy('id')->first();
$this->postAs('api/interaction/play', ['song' => $song->id], $user);
self::assertDatabaseHas('interactions', [
@ -52,7 +52,7 @@ class InteractionTest extends TestCase
$user = User::factory()->create();
/** @var Song $song */
$song = Song::orderBy('id')->first();
$song = Song::query()->orderBy('id')->first();
$this->postAs('api/interaction/like', ['song' => $song->id], $user);
self::assertDatabaseHas('interactions', [
@ -79,7 +79,7 @@ class InteractionTest extends TestCase
$user = User::factory()->create();
/** @var Collection|array<Song> $songs */
$songs = Song::orderBy('id')->take(2)->get();
$songs = Song::query()->orderBy('id')->take(2)->get();
$songIds = array_pluck($songs->toArray(), 'id');
$this->postAs('api/interaction/batch/like', ['songs' => $songIds], $user);

View file

@ -34,7 +34,7 @@ class S3Test extends TestCase
]);
/** @var Song $song */
$song = Song::where('path', 's3://koel/sample.mp3')->firstOrFail();
$song = Song::query()->where('path', 's3://koel/sample.mp3')->firstOrFail();
self::assertSame('A Koel Song', $song->title);
self::assertSame('Koel Testing Vol. 1', $song->album->name);

View file

@ -23,7 +23,7 @@ class PlaylistTest extends TestCase
$user = User::factory()->create();
/** @var array<Song>|Collection $songs */
$songs = Song::orderBy('id')->take(3)->get();
$songs = Song::query()->orderBy('id')->take(3)->get();
$response = $this->postAs('api/playlist', [
'name' => 'Foo Bar',
@ -34,7 +34,7 @@ class PlaylistTest extends TestCase
$response->assertOk();
/** @var Playlist $playlist */
$playlist = Playlist::orderBy('id', 'desc')->first();
$playlist = Playlist::query()->orderByDesc('id')->first();
self::assertSame('Foo Bar', $playlist->name);
self::assertTrue($playlist->user->is($user));
@ -63,7 +63,7 @@ class PlaylistTest extends TestCase
], $user);
/** @var Playlist $playlist */
$playlist = Playlist::orderBy('id', 'desc')->first();
$playlist = Playlist::query()->orderByDesc('id')->first();
self::assertSame('Smart Foo Bar', $playlist->name);
self::assertTrue($playlist->user->is($user));
@ -88,11 +88,11 @@ class PlaylistTest extends TestCase
],
],
],
'songs' => Song::orderBy('id')->take(3)->get()->pluck('id')->all(),
'songs' => Song::query()->orderBy('id')->take(3)->get()->pluck('id')->all(),
]);
/** @var Playlist $playlist */
$playlist = Playlist::orderBy('id', 'desc')->first();
$playlist = Playlist::query()->orderByDesc('id')->first();
self::assertSame('Smart Foo Bar', $playlist->name);
self::assertEmpty($playlist->songs);

View file

@ -21,7 +21,9 @@ class SongTest extends TestCase
{
/** @var User $user */
$user = User::factory()->admin()->create();
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
$this->putAs('/api/songs', [
'songs' => [$song->id],
@ -37,11 +39,11 @@ class SongTest extends TestCase
->assertOk();
/** @var Artist $artist */
$artist = Artist::where('name', 'John Cena')->first();
$artist = Artist::query()->where('name', 'John Cena')->first();
self::assertNotNull($artist);
/** @var Album $album */
$album = Album::where('name', 'One by One')->first();
$album = Album::query()->where('name', 'One by One')->first();
self::assertNotNull($album);
self::assertDatabaseHas(Song::class, [
@ -57,7 +59,10 @@ class SongTest extends TestCase
{
/** @var User $user */
$user = User::factory()->admin()->create();
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
$originalArtistId = $song->artist->id;
$this->putAs('/api/songs', [
@ -73,17 +78,17 @@ class SongTest extends TestCase
->assertOk();
// We don't expect the song's artist to change
self::assertEquals($originalArtistId, Song::find($song->id)->artist->id);
self::assertEquals($originalArtistId, $song->refresh()->artist->id);
// But we expect a new album to be created for this artist and contain this song
self::assertEquals('One by One', Song::find($song->id)->album->name);
self::assertEquals('One by One', $song->album->name);
}
public function testMultipleUpdateNoCompilation(): void
{
/** @var User $user */
$user = User::factory()->admin()->create();
$songIds = Song::latest()->take(3)->pluck('id')->toArray();
$songIds = Song::query()->latest()->take(3)->pluck('id')->toArray();
$this->putAs('/api/songs', [
'songs' => $songIds,
@ -97,7 +102,7 @@ class SongTest extends TestCase
], $user)
->assertOk();
$songs = Song::whereIn('id', $songIds)->get();
$songs = Song::query()->whereIn('id', $songIds)->get();
// All of these songs must now belong to a new album and artist set
self::assertEquals('One by One', $songs[0]->album->name);
@ -115,7 +120,7 @@ class SongTest extends TestCase
$user = User::factory()->admin()->create();
/** @var array<array-key, Song>|Collection $originalSongs */
$originalSongs = Song::latest()->take(3)->get();
$originalSongs = Song::query()->latest()->take(3)->get();
$songIds = $originalSongs->pluck('id')->toArray();
$this->putAs('/api/songs', [
@ -131,7 +136,7 @@ class SongTest extends TestCase
->assertOk();
/** @var array<Song>|Collection $songs */
$songs = Song::latest()->take(3)->get();
$songs = Song::query()->latest()->take(3)->get();
// Even though the album name doesn't change, a new artist should have been created
// and thus, a new album with the same name was created as well.
@ -152,7 +157,10 @@ class SongTest extends TestCase
{
/** @var User $user */
$user = User::factory()->admin()->create();
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
$this->putAs('/api/songs', [
'songs' => [$song->id],
@ -169,13 +177,13 @@ class SongTest extends TestCase
->assertOk();
/** @var Album $album */
$album = Album::where('name', 'One by One')->first();
$album = Album::query()->where('name', 'One by One')->first();
/** @var Artist $albumArtist */
$albumArtist = Artist::whereName('John Lennon')->first();
$albumArtist = Artist::query()->where('name', 'John Lennon')->first();
/** @var Artist $artist */
$artist = Artist::whereName('John Cena')->first();
$artist = Artist::query()->where('name', 'John Cena')->first();
self::assertDatabaseHas(Song::class, [
'id' => $song->id,
@ -191,11 +199,11 @@ class SongTest extends TestCase
public function testDeletingByChunk(): void
{
self::assertNotEquals(0, Song::count());
$ids = Song::select('id')->get()->pluck('id')->all();
self::assertNotEquals(0, Song::query()->count());
$ids = Song::query()->select('id')->get()->pluck('id')->all();
Song::deleteByChunk($ids, 'id', 1);
self::assertEquals(0, Song::count());
self::assertEquals(0, Song::query()->count());
}
}

View file

@ -30,7 +30,8 @@ class UserTest extends TestCase
], $admin)
->assertSuccessful();
$user = User::firstWhere('email', 'bar@baz.com');
/** @var User $user */
$user = User::query()->firstWhere('email', 'bar@baz.com');
self::assertTrue(Hash::check('secret', $user->password));
self::assertSame('Foo', $user->name);

View file

@ -44,6 +44,12 @@ class PlayCountTest extends TestCase
'play_count',
]);
self::assertEquals(1, Interaction::whereSongIdAndUserId($song->id, $user->id)->first()->play_count);
/** @var Interaction $interaction */
$interaction = Interaction::query()
->where('song_id', $song->id)
->where('user_id', $user->id)
->first();
self::assertEquals(1, $interaction->play_count);
}
}

View file

@ -21,13 +21,13 @@ class YouTubeTest extends TestCase
public function testSearchYouTubeVideos(): void
{
static::createSampleMediaSet();
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
$this->youTubeService
->shouldReceive('searchVideosRelatedToSong')
->with(Mockery::on(static function (Song $retrievedSong) use ($song) {
return $song->id === $retrievedSong->id;
}), 'foo')
->with(Mockery::on(static fn (Song $retrievedSong) => $song->is($retrievedSong)), 'foo')
->once();
$this->getAs("/api/youtube/search/song/{$song->id}?pageToken=foo")

View file

@ -27,11 +27,10 @@ class InteractionServiceTest extends TestCase
{
/** @var Interaction $interaction */
$interaction = Interaction::factory()->create();
$currentCount = $interaction->play_count;
$this->interactionService->increasePlayCount($interaction->song, $interaction->user);
$updatedInteraction = Interaction::find($interaction->id);
self::assertEquals($interaction->play_count + 1, $updatedInteraction->play_count);
self::assertEquals($currentCount + 1, $interaction->refresh()->play_count);
}
public function testToggleLike(): void
@ -40,11 +39,11 @@ class InteractionServiceTest extends TestCase
/** @var Interaction $interaction */
$interaction = Interaction::factory()->create();
$currentLiked = $interaction->liked;
$this->interactionService->toggleLike($interaction->song, $interaction->user);
$updatedInteraction = Interaction::find($interaction->id);
self::assertNotSame($interaction->liked, $updatedInteraction->liked);
self::assertNotSame($currentLiked, $interaction->refresh()->liked);
}
public function testLikeMultipleSongs(): void
@ -60,7 +59,13 @@ class InteractionServiceTest extends TestCase
$this->interactionService->batchLike($songs->pluck('id')->all(), $user);
$songs->each(static function (Song $song) use ($user): void {
self::assertTrue(Interaction::whereSongIdAndUserId($song->id, $user->id)->first()->liked);
/** @var Interaction $interaction */
$interaction = Interaction::query()
->where('song_id', $song->id)
->where('user_id', $user->id)
->first();
self::assertTrue($interaction->liked);
});
}
@ -80,7 +85,7 @@ class InteractionServiceTest extends TestCase
$this->interactionService->batchUnlike($interactions->pluck('song.id')->all(), $user);
$interactions->each(static function (Interaction $interaction): void {
self::assertFalse(Interaction::find($interaction->id)->liked);
self::assertFalse($interaction->refresh()->liked);
});
}
}

View file

@ -28,8 +28,8 @@ class MediaCacheServiceTest extends TestCase
Song::factory(5)->create();
$this->cache->shouldReceive('rememberForever')->andReturn([
'albums' => Album::orderBy('name')->get(),
'artists' => Artist::orderBy('name')->get(),
'albums' => Album::query()->orderBy('name')->get(),
'artists' => Artist::query()->orderBy('name')->get(),
'songs' => Song::all(),
]);

View file

@ -51,7 +51,7 @@ class MediaSyncServiceTest extends TestCase
// GitHub issue #380. folder.png should be copied and used as the cover for files
// under subdir/
/** @var Song $song */
$song = Song::where('path', $this->path('/subdir/back-in-black.ogg'))->first();
$song = Song::query()->where('path', $this->path('/subdir/back-in-black.ogg'))->first();
self::assertNotEmpty($song->album->cover);
// File search shouldn't be case-sensitive.
@ -72,12 +72,12 @@ class MediaSyncServiceTest extends TestCase
// Albums and artists should be correctly linked
/** @var Album $album */
$album = Album::where('name', 'Koel Testing Vol. 1')->first();
$album = Album::query()->where('name', 'Koel Testing Vol. 1')->first();
self::assertEquals('Koel', $album->artist->name);
// Compilation albums, artists and songs must be recognized
/** @var Song $song */
$song = Song::where('title', 'This song belongs to a compilation')->first();
$song = Song::query()->where('title', 'This song belongs to a compilation')->first();
self::assertFalse($song->album->artist->is($song->artist));
self::assertSame('Koel', $song->album->artist->name);
self::assertSame('Cuckoo', $song->artist->name);
@ -87,7 +87,8 @@ class MediaSyncServiceTest extends TestCase
{
$this->mediaService->sync();
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
touch($song->path, $time = time() + 1000);
$this->mediaService->sync();
@ -101,7 +102,8 @@ class MediaSyncServiceTest extends TestCase
$this->mediaService->sync();
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
$song->update([
'title' => "It's John Cena!",
@ -121,7 +123,8 @@ class MediaSyncServiceTest extends TestCase
$this->mediaService->sync();
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
$song->update([
'title' => "It's John Cena!",
@ -142,7 +145,8 @@ class MediaSyncServiceTest extends TestCase
$this->mediaService->sync();
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
$song->update([
'title' => "It's John Cena!",
@ -161,14 +165,16 @@ class MediaSyncServiceTest extends TestCase
{
$this->mediaService->sync();
$song = Song::first();
/** @var Song $song */
$song = Song::query()->first();
$song->delete();
$this->mediaService->sync(ignores: ['title', 'disc', 'track'], force: true);
// Song should be added back with all info
self::assertEquals(
Arr::except(Song::where('path', $song->path)->first()->toArray(), ['id', 'created_at']),
Arr::except(Song::query()->where('path', $song->path)->first()->toArray(), ['id', 'created_at']),
Arr::except($song->toArray(), ['id', 'created_at'])
);
}
@ -188,8 +194,9 @@ class MediaSyncServiceTest extends TestCase
$this->expectsEvents(LibraryChanged::class);
static::createSampleMediaSet();
$song = Song::first();
self::assertModelExists($song);
/** @var Song $song */
$song = Song::query()->first();
$this->mediaService->syncByWatchRecord(new InotifyWatchRecord("DELETE $song->path"));

View file

@ -30,7 +30,7 @@ trait CreatesApplication
{
$this->artisan->call('migrate');
if (!User::count()) {
if (!User::query()->count()) {
$this->artisan->call('db:seed');
}
}

View file

@ -22,7 +22,7 @@ class AlbumTest extends TestCase
$artist = Artist::factory()->create();
$name = 'Foo';
self::assertNull(Album::whereArtistIdAndName($artist->id, $name)->first());
self::assertNull(Album::query()->where('artist_id', $artist->id)->where('name', $name)->first());
$album = Album::getOrCreate($artist, $name);
self::assertSame('Foo', $album->name);

View file

@ -17,7 +17,7 @@ class ArtistTest extends TestCase
public function testNewArtistIsCreatedWithName(): void
{
self::assertNull(Artist::whereName('Foo')->first());
self::assertNull(Artist::query()->where('name', 'Foo')->first());
self::assertSame('Foo', Artist::getOrCreate('Foo')->name);
}

View file

@ -53,7 +53,7 @@ class MediaMetadataServiceTest extends TestCase
->with('/koel/public/img/album/foo.jpg', 'dummy-src');
$this->mediaMetadataService->writeAlbumCover($album, 'dummy-src', 'jpg', $coverPath);
self::assertEquals(album_cover_url('foo.jpg'), Album::find($album->id)->cover);
self::assertEquals(album_cover_url('foo.jpg'), $album->refresh()->cover);
}
public function testTryDownloadArtistImage(): void
@ -81,6 +81,7 @@ class MediaMetadataServiceTest extends TestCase
->with('/koel/public/img/artist/foo.jpg', 'dummy-src');
$this->mediaMetadataService->writeArtistImage($artist, 'dummy-src', 'jpg', $imagePath);
self::assertEquals(artist_image_url('foo.jpg'), Artist::find($artist->id)->image);
self::assertEquals(artist_image_url('foo.jpg'), $artist->refresh()->image);
}
}