feat(smart-playlist): use proper Eloquent cast for rules (#1363)

This commit is contained in:
Phan An 2021-10-08 18:23:45 +02:00 committed by GitHub
parent 230ec454dd
commit b29000bf8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 304 additions and 167 deletions

View file

@ -0,0 +1,23 @@
<?php
namespace App\Casts;
use App\Values\SmartPlaylistRuleGroup;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
class SmartPlaylistRulesCast implements CastsAttributes
{
/** @return Collection|array<SmartPlaylistRuleGroup> */
public function get($model, string $key, $value, array $attributes): Collection
{
return collect(json_decode($value, true) ?: [])->map(static function (array $group): ?SmartPlaylistRuleGroup {
return SmartPlaylistRuleGroup::create($group);
});
}
public function set($model, string $key, $value, array $attributes): ?string
{
return json_encode($value ?: []);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Exceptions;
use App\Models\Playlist;
use Exception;
use Throwable;
class NonSmartPlaylistException extends Exception
{
private function __construct(string $message = '', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
public static function create(Playlist $playlist): self
{
return new static($playlist->name . ' is not a smart playlist.');
}
}

View file

@ -2,7 +2,7 @@
namespace App\Factories;
use App\Models\Rule;
use App\Values\SmartPlaylistRule;
use Webmozart\Assert\Assert;
class SmartPlaylistRuleParameterFactory
@ -15,17 +15,17 @@ class SmartPlaylistRuleParameterFactory
public function createParameters(string $model, string $operator, array $value): array
{
$ruleParameterMap = [
Rule::OPERATOR_BEGINS_WITH => [$model, 'LIKE', "$value[0]%"],
Rule::OPERATOR_ENDS_WITH => [$model, 'LIKE', "%$value[0]"],
Rule::OPERATOR_IS => [$model, '=', $value[0]],
Rule::OPERATOR_IS_NOT => [$model, '<>', $value[0]],
Rule::OPERATOR_CONTAINS => [$model, 'LIKE', "%$value[0]%"],
Rule::OPERATOR_NOT_CONTAIN => [$model, 'NOT LIKE', "%$value[0]%"],
Rule::OPERATOR_IS_LESS_THAN => [$model, '<', $value[0]],
Rule::OPERATOR_IS_GREATER_THAN => [$model, '>', $value[0]],
Rule::OPERATOR_IS_BETWEEN => [$model, $value],
Rule::OPERATOR_NOT_IN_LAST => static fn (): array => [$model, '<', now()->subDays($value[0])],
Rule::OPERATOR_IN_LAST => static fn (): array => [$model, '>=', now()->subDays($value[0])],
SmartPlaylistRule::OPERATOR_BEGINS_WITH => [$model, 'LIKE', "$value[0]%"],
SmartPlaylistRule::OPERATOR_ENDS_WITH => [$model, 'LIKE', "%$value[0]"],
SmartPlaylistRule::OPERATOR_IS => [$model, '=', $value[0]],
SmartPlaylistRule::OPERATOR_IS_NOT => [$model, '<>', $value[0]],
SmartPlaylistRule::OPERATOR_CONTAINS => [$model, 'LIKE', "%$value[0]%"],
SmartPlaylistRule::OPERATOR_NOT_CONTAIN => [$model, 'NOT LIKE', "%$value[0]%"],
SmartPlaylistRule::OPERATOR_IS_LESS_THAN => [$model, '<', $value[0]],
SmartPlaylistRule::OPERATOR_IS_GREATER_THAN => [$model, '>', $value[0]],
SmartPlaylistRule::OPERATOR_IS_BETWEEN => [$model, $value],
SmartPlaylistRule::OPERATOR_NOT_IN_LAST => static fn (): array => [$model, '<', now()->subDays($value[0])],
SmartPlaylistRule::OPERATOR_IN_LAST => static fn (): array => [$model, '>=', now()->subDays($value[0])],
];
Assert::keyExists($ruleParameterMap, $operator);

View file

@ -20,7 +20,7 @@ class AuthController extends Controller
private TokenManager $tokenManager;
/** @var User */
private ?Authenticatable $currentUser = null;
private ?Authenticatable $currentUser;
public function __construct(
UserRepository $userRepository,
@ -30,8 +30,8 @@ class AuthController extends Controller
) {
$this->userRepository = $userRepository;
$this->hash = $hash;
$this->currentUser = $currentUser;
$this->tokenManager = $tokenManager;
$this->currentUser = $currentUser;
}
public function login(UserLoginRequest $request)

View file

@ -7,6 +7,7 @@ use App\Http\Requests\API\PlaylistSyncRequest;
use App\Models\Playlist;
use App\Models\User;
use App\Repositories\PlaylistRepository;
use App\Services\PlaylistService;
use App\Services\SmartPlaylistService;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
@ -14,6 +15,7 @@ use Illuminate\Http\Request;
class PlaylistController extends Controller
{
private PlaylistRepository $playlistRepository;
private PlaylistService $playlistService;
private SmartPlaylistService $smartPlaylistService;
/** @var User */
@ -21,10 +23,12 @@ class PlaylistController extends Controller
public function __construct(
PlaylistRepository $playlistRepository,
PlaylistService $playlistService,
SmartPlaylistService $smartPlaylistService,
?Authenticatable $currentUser
) {
$this->playlistRepository = $playlistRepository;
$this->playlistService = $playlistService;
$this->smartPlaylistService = $smartPlaylistService;
$this->currentUser = $currentUser;
}
@ -36,24 +40,16 @@ class PlaylistController extends Controller
public function store(PlaylistStoreRequest $request)
{
/** @var Playlist $playlist */
$playlist = $this->currentUser->playlists()->create([
'name' => $request->name,
'rules' => $request->rules,
]);
$playlist = $this->playlistService->createPlaylist(
$request->name,
$this->currentUser,
(array) $request->songs,
$request->rules
);
if (!$playlist->is_smart) {
$songs = (array) $request->songs;
$playlist->songs = $playlist->songs->pluck('id')->toArray();
if ($songs) {
$playlist->songs()->sync($songs);
}
}
$playlistAsArray = $playlist->toArray();
$playlistAsArray['songs'] = $playlist->songs->pluck('id');
return response()->json($playlistAsArray);
return response()->json($playlist);
}
public function update(Request $request, Playlist $playlist)

View file

@ -2,20 +2,22 @@
namespace App\Models;
use App\Casts\SmartPlaylistRulesCast;
use App\Traits\CanFilterByUser;
use App\Values\SmartPlaylistRuleGroup;
use Illuminate\Database\Eloquent\Builder;
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\BelongsToMany;
use Illuminate\Support\Collection;
use Laravel\Scout\Searchable;
/**
* @property int $user_id
* @property Collection $songs
* @property Collection|array $songs
* @property int $id
* @property array $rules
* @property Collection|array<SmartPlaylistRuleGroup> $rule_groups
* @property bool $is_smart
* @property string $name
* @property user $user
@ -30,10 +32,12 @@ class Playlist extends Model
protected $hidden = ['user_id', 'created_at', 'updated_at'];
protected $guarded = ['id'];
protected $casts = [
'user_id' => 'int',
'rules' => 'array',
'rules' => SmartPlaylistRulesCast::class,
];
protected $appends = ['is_smart'];
public function songs(): BelongsToMany
@ -48,7 +52,13 @@ class Playlist extends Model
public function getIsSmartAttribute(): bool
{
return (bool) $this->rules;
return $this->rule_groups->isNotEmpty();
}
/** @return Collection|array<SmartPlaylistRuleGroup> */
public function getRuleGroupsAttribute(): Collection
{
return $this->rules;
}
/** @return array<mixed> */

View file

@ -0,0 +1,24 @@
<?php
namespace App\Services;
use App\Models\Playlist;
use App\Models\User;
class PlaylistService
{
public function createPlaylist(string $name, User $user, array $songs, array $ruleGroups): Playlist
{
/** @var Playlist $playlist */
$playlist = $user->playlists()->create([
'name' => $name,
'rules' => $ruleGroups,
]);
if (!$playlist->is_smart && $songs) {
$playlist->songs()->sync($songs);
}
return $playlist;
}
}

View file

@ -2,39 +2,46 @@
namespace App\Services;
use App\Exceptions\NonSmartPlaylistException;
use App\Factories\SmartPlaylistRuleParameterFactory;
use App\Models\Playlist;
use App\Models\Rule;
use App\Models\Song;
use App\Models\User;
use App\Values\SmartPlaylistRule;
use App\Values\SmartPlaylistRuleGroup;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use RuntimeException;
use Illuminate\Support\Collection;
class SmartPlaylistService
{
private const RULE_REQUIRES_USER_PREFIXES = ['interactions.'];
private const USER_REQUIRING_RULE_PREFIXES = ['interactions.'];
private SmartPlaylistRuleParameterFactory $parameterFactory;
public function __construct(SmartPlaylistRuleParameterFactory $parameterFactory)
{
$this->parameterFactory = $parameterFactory;
}
/** @return Collection|array<Song> */
public function getSongs(Playlist $playlist): Collection
{
if (!$playlist->is_smart) {
throw new RuntimeException($playlist->name . ' is not a smart playlist.');
}
throw_unless($playlist->is_smart, NonSmartPlaylistException::create($playlist));
$rules = $this->addRequiresUserRules($playlist->rules, $playlist->user);
$ruleGroups = $this->addRequiresUserRules($playlist->rule_groups, $playlist->user);
return $this->buildQueryFromRules($rules)->get();
return $this->buildQueryFromRules($ruleGroups)->get();
}
public function buildQueryFromRules(array $rules): Builder
public function buildQueryFromRules(Collection $ruleGroups): Builder
{
$query = Song::query();
collect($rules)->each(static function (array $ruleGroup) use ($query): void {
$query->orWhere(static function (Builder $subQuery) use ($ruleGroup): void {
foreach ($ruleGroup['rules'] as $config) {
Rule::create($config)->build($subQuery);
}
$ruleGroups->each(function (SmartPlaylistRuleGroup $group) use ($query): void {
$query->orWhere(function (Builder $subQuery) use ($group): void {
$group->rules->each(function (SmartPlaylistRule $rule) use ($subQuery): void {
$this->buildQueryForRule($subQuery, $rule);
});
});
});
@ -46,35 +53,67 @@ class SmartPlaylistService
* (basically everything related to interactions).
* For those, we create an additional "user_id" rule.
*
* @return array<mixed>
* @return Collection|array<SmartPlaylistRuleGroup>
*/
public function addRequiresUserRules(array $rules, User $user): array
public function addRequiresUserRules(Collection $ruleGroups, User $user): Collection
{
foreach ($rules as &$ruleGroup) {
$additionalRules = [];
return $ruleGroups->map(function (SmartPlaylistRuleGroup $group) use ($user): SmartPlaylistRuleGroup {
$clonedGroup = clone $group;
$additionalRules = collect();
foreach ($ruleGroup['rules'] as $config) {
foreach (self::RULE_REQUIRES_USER_PREFIXES as $modelPrefix) {
if (starts_with($config['model'], $modelPrefix)) {
$additionalRules[] = $this->createRequireUserRule($user, $modelPrefix);
$group->rules->each(function (SmartPlaylistRule $rule) use ($additionalRules, $user): void {
foreach (self::USER_REQUIRING_RULE_PREFIXES as $modelPrefix) {
if (starts_with($rule->model, $modelPrefix)) {
$additionalRules->add($this->createRequiresUserRule($user, $modelPrefix));
}
}
}
});
// make sure all those additional rules are unique.
$ruleGroup['rules'] = array_merge($ruleGroup['rules'], collect($additionalRules)->unique('model')->all());
}
// Make sure all those additional rules are unique.
$clonedGroup->rules = $clonedGroup->rules->merge($additionalRules->unique('model')->collect());
return $rules;
return $clonedGroup;
});
}
/** @return array<mixed> */
private function createRequireUserRule(User $user, string $modelPrefix): array
private function createRequiresUserRule(User $user, string $modelPrefix): SmartPlaylistRule
{
return [
return SmartPlaylistRule::create([
'model' => $modelPrefix . 'user_id',
'operator' => 'is',
'value' => [$user->id],
];
]);
}
public function buildQueryForRule(Builder $query, SmartPlaylistRule $rule, ?string $model = null): Builder
{
if (!$model) {
$model = $rule->model;
}
$fragments = explode('.', $model, 2);
if (count($fragments) === 1) {
return $query->{$this->resolveWhereLogic($rule)}(
...$this->parameterFactory->createParameters($model, $rule->operator, $rule->value)
);
}
// If the model is something like 'artist.name' or 'interactions.play_count', we have a subquery to deal with.
// We handle such a case with a recursive call which, in theory, should work with an unlimited level of nesting,
// though in practice we only have one level max.
return $query->whereHas(
$fragments[0],
fn (Builder $subQuery) => $this->buildQueryForRule($subQuery, $rule, $fragments[1])
);
}
/**
* Resolve the logic of a (sub)query base on the configured operator.
* Basically, if the operator is "between," we use "whereBetween". Otherwise, it's "where". Simple.
*/
private function resolveWhereLogic(SmartPlaylistRule $rule): string
{
return $rule->operator === SmartPlaylistRule::OPERATOR_IS_BETWEEN ? 'whereBetween' : 'where';
}
}

View file

@ -15,7 +15,7 @@ final class LastfmLoveTrackParameters
public static function make(string $trackName, string $artistName): self
{
return new static($trackName, $artistName);
return new self($trackName, $artistName);
}
public function getTrackName(): string

View file

@ -1,13 +1,11 @@
<?php
namespace App\Models;
namespace App\Values;
use App\Factories\SmartPlaylistRuleParameterFactory;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Builder;
use Webmozart\Assert\Assert;
class Rule implements Arrayable
final class SmartPlaylistRule implements Arrayable
{
public const OPERATOR_IS = 'is';
public const OPERATOR_IS_NOT = 'isNot';
@ -35,20 +33,19 @@ class Rule implements Arrayable
self::OPERATOR_NOT_IN_LAST,
];
private $operator;
private $value;
private $model;
private SmartPlaylistRuleParameterFactory $parameterFactory;
public ?int $id;
public string $operator;
public array $value;
public string $model;
private function __construct(array $config)
{
Assert::oneOf($config['operator'], self::VALID_OPERATORS);
$this->id = $config['id'] ?? null;
$this->value = $config['value'];
$this->model = $config['model'];
$this->operator = $config['operator'];
$this->parameterFactory = new SmartPlaylistRuleParameterFactory();
}
public static function create(array $config): self
@ -56,49 +53,26 @@ class Rule implements Arrayable
return new static($config);
}
public function build(Builder $query, ?string $model = null): Builder
{
if (!$model) {
$model = $this->model;
}
$fragments = explode('.', $model, 2);
if (count($fragments) === 1) {
return $query->{$this->resolveLogic()}(
...$this->parameterFactory->createParameters($model, $this->operator, $this->value)
);
}
// If the model is something like 'artist.name' or 'interactions.play_count', we have a subquery to deal with.
// We handle such a case with a recursive call which, in theory, should work with an unlimited level of nesting,
// though in practice we only have one level max.
return $query->whereHas($fragments[0], fn (Builder $subQuery) => $this->build($subQuery, $fragments[1]));
}
/**
* Resolve the logic of a (sub)query base on the configured operator.
* Basically, if the operator is "between," we use "whereBetween." Otherwise, it's "where." Simple.
*/
private function resolveLogic(): string
{
return $this->operator === self::OPERATOR_IS_BETWEEN ? 'whereBetween' : 'where';
}
/** @return array<mixed> */
public function toArray(): array
{
return [
'id' => $this->id,
'model' => $this->model,
'operator' => $this->operator,
'value' => $this->value,
];
}
public function equals(Rule $rule): bool
/** @param array|self $rule */
public function equals($rule): bool
{
if (is_array($rule)) {
$rule = self::create($rule);
}
return $this->operator === $rule->operator
&& $this->value === $rule->value
&& !array_diff($this->value, $rule->value)
&& $this->model === $rule->model;
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Values;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
use Throwable;
final class SmartPlaylistRuleGroup implements Arrayable
{
public ?int $id;
/** @var Collection|array<SmartPlaylistRule> */
public Collection $rules;
public static function create(array $jsonArray): ?self
{
$group = new self();
try {
$group->id = $jsonArray['id'] ?? null;
$group->rules = collect(array_map(static function (array $rawRuleConfig) {
return SmartPlaylistRule::create($rawRuleConfig);
}, $jsonArray['rules']));
return $group;
} catch (Throwable $exception) {
return null;
}
}
/** @return array<mixed> */
public function toArray(): array
{
return [
'id' => $this->id,
'rules' => $this->rules->toArray(),
];
}
}

View file

@ -24,13 +24,13 @@
"daverandom/resume": "^0.0.3",
"laravel/helpers": "^1.0",
"intervention/image": "^2.5",
"laravel/sanctum": "^2.6",
"doctrine/dbal": "^2.10",
"lstrojny/functional-php": "^1.14",
"teamtnt/laravel-scout-tntsearch-driver": "^11.1",
"algolia/algoliasearch-client-php": "^2.7",
"laravel/ui": "^3.2",
"webmozart/assert": "^1.10"
"webmozart/assert": "^1.10",
"laravel/sanctum": "^2.11"
},
"require-dev": {
"facade/ignition": "^2.5",

19
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "87c0801fdd9eb89bb8e45869c9607cbf",
"content-hash": "a3a41a7eccc0c230272c4319a31a11a4",
"packages": [
{
"name": "algolia/algoliasearch-client-php",
@ -1578,16 +1578,16 @@
},
{
"name": "laravel/framework",
"version": "v8.51.0",
"version": "v8.61.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "208d9c0043b4c192a9bb9b15782cc4ec37f28bb0"
"reference": "3d528d3d3c8ecb444b50a266c212a52973a6669b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/208d9c0043b4c192a9bb9b15782cc4ec37f28bb0",
"reference": "208d9c0043b4c192a9bb9b15782cc4ec37f28bb0",
"url": "https://api.github.com/repos/laravel/framework/zipball/3d528d3d3c8ecb444b50a266c212a52973a6669b",
"reference": "3d528d3d3c8ecb444b50a266c212a52973a6669b",
"shasum": ""
},
"require": {
@ -1624,7 +1624,8 @@
"tightenco/collect": "<5.5.33"
},
"provide": {
"psr/container-implementation": "1.0"
"psr/container-implementation": "1.0",
"psr/simple-cache-implementation": "1.0"
},
"replace": {
"illuminate/auth": "self.version",
@ -1660,7 +1661,7 @@
"illuminate/view": "self.version"
},
"require-dev": {
"aws/aws-sdk-php": "^3.155",
"aws/aws-sdk-php": "^3.189.0",
"doctrine/dbal": "^2.6|^3.0",
"filp/whoops": "^2.8",
"guzzlehttp/guzzle": "^6.5.5|^7.0.1",
@ -1673,7 +1674,7 @@
"symfony/cache": "^5.1.4"
},
"suggest": {
"aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).",
"aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.189.0).",
"brianium/paratest": "Required to run tests in parallel (^6.0).",
"doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6|^3.0).",
"ext-ftp": "Required to use the Flysystem FTP driver.",
@ -1742,7 +1743,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2021-07-20T14:38:36+00:00"
"time": "2021-09-14T13:31:32+00:00"
},
{
"name": "laravel/helpers",

View file

@ -1,6 +1,7 @@
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::group(['namespace' => 'API'], static function (): void {
Route::post('me', 'AuthController@login')->name('auth.login');

View file

@ -3,9 +3,9 @@
namespace Tests\Feature;
use App\Models\Playlist;
use App\Models\Rule;
use App\Models\Song;
use App\Models\User;
use App\Values\SmartPlaylistRule;
use Illuminate\Support\Collection;
class PlaylistTest extends TestCase
@ -25,26 +25,20 @@ class PlaylistTest extends TestCase
/** @var array<Song>|Collection $songs */
$songs = Song::orderBy('id')->take(3)->get();
$this->postAsUser('api/playlist', [
$response = $this->postAsUser('api/playlist', [
'name' => 'Foo Bar',
'songs' => $songs->pluck('id')->toArray(),
'rules' => [],
], $user);
self::assertDatabaseHas('playlists', [
'user_id' => $user->id,
'name' => 'Foo Bar',
]);
$response->assertOk();
/** @var Playlist $playlist */
$playlist = Playlist::orderBy('id', 'desc')->first();
foreach ($songs as $song) {
self::assertDatabaseHas('playlist_song', [
'playlist_id' => $playlist->id,
'song_id' => $song->id,
]);
}
self::assertSame('Foo Bar', $playlist->name);
self::assertTrue($playlist->user->is($user));
self::assertEqualsCanonicalizing($songs->pluck('id')->all(), $playlist->songs->pluck('id')->all());
}
public function testCreatingSmartPlaylist(): void
@ -52,15 +46,20 @@ class PlaylistTest extends TestCase
/** @var User $user */
$user = User::factory()->create();
$rule = Rule::create([
$rule = SmartPlaylistRule::create([
'model' => 'artist.name',
'operator' => Rule::OPERATOR_IS,
'value' => 'Bob Dylan',
'operator' => SmartPlaylistRule::OPERATOR_IS,
'value' => ['Bob Dylan'],
]);
$this->postAsUser('api/playlist', [
'name' => 'Smart Foo Bar',
'rules' => [$rule->toArray()],
'rules' => [
[
'id' => 12345,
'rules' => [$rule->toArray()],
],
],
], $user);
/** @var Playlist $playlist */
@ -69,8 +68,8 @@ class PlaylistTest extends TestCase
self::assertSame('Smart Foo Bar', $playlist->name);
self::assertTrue($playlist->user->is($user));
self::assertTrue($playlist->is_smart);
self::assertCount(1, $playlist->rules);
self::assertTrue(Rule::create($playlist->rules[0])->equals($rule));
self::assertCount(1, $playlist->rule_groups);
self::assertTrue($rule->equals($playlist->rule_groups[0]->rules[0]));
}
public function testCreatingSmartPlaylistIgnoresSongs(): void
@ -81,13 +80,13 @@ class PlaylistTest extends TestCase
$this->postAsUser('api/playlist', [
'name' => 'Smart Foo Bar',
'rules' => [
Rule::create([
SmartPlaylistRule::create([
'model' => 'artist.name',
'operator' => Rule::OPERATOR_IS,
'value' => 'Bob Dylan',
'operator' => SmartPlaylistRule::OPERATOR_IS,
'value' => ['Bob Dylan'],
])->toArray(),
],
'songs' => Song::orderBy('id')->take(3)->get()->pluck('id')->toArray(),
'songs' => Song::orderBy('id')->take(3)->get()->pluck('id')->all(),
], $user);
/** @var Playlist $playlist */
@ -108,7 +107,7 @@ class PlaylistTest extends TestCase
'name' => 'Foo',
]);
$this->putAsUser("api/playlist/{$playlist->id}", ['name' => 'Bar'], $user);
$this->putAsUser("api/playlist/$playlist->id", ['name' => 'Bar'], $user);
self::assertSame('Bar', $playlist->refresh()->name);
}
@ -120,7 +119,7 @@ class PlaylistTest extends TestCase
'name' => 'Foo',
]);
$response = $this->putAsUser("api/playlist/{$playlist->id}", ['name' => 'Qux']);
$response = $this->putAsUser("api/playlist/$playlist->id", ['name' => 'Qux']);
$response->assertStatus(403);
}
@ -136,13 +135,13 @@ class PlaylistTest extends TestCase
/** @var array<Song>|Collection $songs */
$songs = Song::orderBy('id')->take(4)->get();
$playlist->songs()->attach($songs->pluck('id')->toArray());
$playlist->songs()->attach($songs->pluck('id')->all());
/** @var Song $removedSong */
$removedSong = $songs->pop();
$this->putAsUser("api/playlist/{$playlist->id}/sync", [
'songs' => $songs->pluck('id')->toArray(),
$this->putAsUser("api/playlist/$playlist->id/sync", [
'songs' => $songs->pluck('id')->all(),
], $user);
// We should still see the first 3 songs, but not the removed one
@ -169,7 +168,7 @@ class PlaylistTest extends TestCase
'user_id' => $user->id,
]);
$this->deleteAsUser("api/playlist/{$playlist->id}", [], $user);
$this->deleteAsUser("api/playlist/$playlist->id", [], $user);
self::assertDatabaseMissing('playlists', ['id' => $playlist->id]);
}
@ -178,7 +177,7 @@ class PlaylistTest extends TestCase
/** @var Playlist $playlist */
$playlist = Playlist::factory()->create();
$this->deleteAsUser("api/playlist/{$playlist->id}")
$this->deleteAsUser("api/playlist/$playlist->id")
->assertStatus(403);
}
@ -195,7 +194,7 @@ class PlaylistTest extends TestCase
$songs = Song::factory(2)->create();
$playlist->songs()->saveMany($songs);
$this->getAsUser("api/playlist/{$playlist->id}/songs", $user)
$this->getAsUser("api/playlist/$playlist->id/songs", $user)
->assertJson($songs->pluck('id')->all());
}
}

View file

@ -11,7 +11,7 @@ use App\Values\LastfmLoveTrackParameters;
use Mockery;
use Tests\Feature\TestCase;
class LoveTrackOnLastfmTest extends TestCase
class LoveTrackOnLastFmTest extends TestCase
{
public function testHandle(): void
{

View file

@ -2,10 +2,12 @@
namespace Tests\Integration\Services;
use App\Models\Rule;
use App\Models\User;
use App\Services\SmartPlaylistService;
use App\Values\SmartPlaylistRule;
use App\Values\SmartPlaylistRuleGroup;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Tests\TestCase;
class SmartPlaylistServiceTest extends TestCase
@ -27,7 +29,7 @@ class SmartPlaylistServiceTest extends TestCase
}
/** @return array<array<mixed>> */
public function provideRules(): array
public function provideRuleConfigs(): array
{
return [
[
@ -98,15 +100,14 @@ class SmartPlaylistServiceTest extends TestCase
];
}
/**
* @dataProvider provideRules
*
* @param array<string> $rules
* @param array<mixed> $bindings
*/
public function testBuildQueryForRules(array $rules, string $sql, array $bindings): void
/** @dataProvider provideRuleConfigs */
public function testBuildQueryForRules(array $rawRules, string $sql, array $bindings): void
{
$query = $this->service->buildQueryFromRules($rules);
$ruleGroups = collect($rawRules)->map(static function (array $group): SmartPlaylistRuleGroup {
return SmartPlaylistRuleGroup::create($group);
});
$query = $this->service->buildQueryFromRules($ruleGroups);
self::assertSame($sql, $query->toSql());
$queryBinding = $query->getBindings();
@ -120,22 +121,30 @@ class SmartPlaylistServiceTest extends TestCase
public function testAddRequiresUserRules(): void
{
$rules = $this->readFixtureFile('requiresUser.json');
$ruleGroups = collect($this->readFixtureFile('requiresUser.json'))->map(
static function (array $group): SmartPlaylistRuleGroup {
return SmartPlaylistRuleGroup::create($group);
}
);
/** @var User $user */
$user = User::factory()->create();
self::assertEquals([
/** @var Collection|array<SmartPlaylistRule> $finalRules */
$finalRules = $this->service->addRequiresUserRules($ruleGroups, $user)->first()->rules;
self::assertCount(2, $finalRules);
self::assertTrue($finalRules[1]->equals([
'model' => 'interactions.user_id',
'operator' => 'is',
'value' => [$user->id],
], $this->service->addRequiresUserRules($rules, $user)[0]['rules'][1]);
]));
}
public function testAllOperatorsAreCovered(): void
{
$rules = collect($this->provideRules())->map(static function (array $providedRule): array {
return $providedRule[0];
$rules = collect($this->provideRuleConfigs())->map(static function (array $config): array {
return $config[0];
});
$operators = [];
@ -148,6 +157,6 @@ class SmartPlaylistServiceTest extends TestCase
}
}
self::assertSame(count(Rule::VALID_OPERATORS), count(array_unique($operators)));
self::assertSame(count(SmartPlaylistRule::VALID_OPERATORS), count(array_unique($operators)));
}
}