mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat(smart-playlist): use proper Eloquent cast for rules (#1363)
This commit is contained in:
parent
230ec454dd
commit
b29000bf8d
17 changed files with 304 additions and 167 deletions
23
app/Casts/SmartPlaylistRulesCast.php
Normal file
23
app/Casts/SmartPlaylistRulesCast.php
Normal 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 ?: []);
|
||||
}
|
||||
}
|
20
app/Exceptions/NonSmartPlaylistException.php
Normal file
20
app/Exceptions/NonSmartPlaylistException.php
Normal 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.');
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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> */
|
||||
|
|
24
app/Services/PlaylistService.php
Normal file
24
app/Services/PlaylistService.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
41
app/Values/SmartPlaylistRuleGroup.php
Normal file
41
app/Values/SmartPlaylistRuleGroup.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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
19
composer.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue