feat: convert smart playlist options to enums

This commit is contained in:
Phan An 2024-04-18 16:11:47 +02:00
parent 4ecf947cc9
commit f5677a5b2f
6 changed files with 109 additions and 116 deletions

View file

@ -0,0 +1,34 @@
<?php
namespace App\Enums;
enum SmartPlaylistModel: string
{
case TITLE = 'title';
case ALBUM_NAME = 'album.name';
case ARTIST_NAME = 'artist.name';
case PLAY_COUNT = 'interactions.play_count';
case LAST_PLAYED = 'interactions.last_played_at';
case USER_ID = 'interactions.user_id';
case LENGTH = 'length';
case DATE_ADDED = 'created_at';
case DATE_MODIFIED = 'updated_at';
case GENRE = 'genre';
case YEAR = 'year';
public function toColumnName(): string
{
return match ($this) {
self::ALBUM_NAME => 'albums.name',
self::ARTIST_NAME => 'artists.name',
self::DATE_ADDED => 'songs.created_at',
self::DATE_MODIFIED => 'songs.updated_at',
default => $this->value,
};
}
public function isDate(): bool
{
return in_array($this, [self::LAST_PLAYED, self::DATE_ADDED, self::DATE_MODIFIED], true);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Enums;
enum SmartPlaylistOperator: string
{
case IS = 'is';
case IS_NOT = 'isNot';
case CONTAINS = 'contains';
case NOT_CONTAIN = 'notContain';
case IS_BETWEEN = 'isBetween';
case IS_GREATER_THAN = 'isGreaterThan';
case IS_LESS_THAN = 'isLessThan';
case BEGINS_WITH = 'beginsWith';
case ENDS_WITH = 'endsWith';
case IN_LAST = 'inLast';
case NOT_IN_LAST = 'notInLast';
case IS_NOT_BETWEEN = 'isNotBetween';
public function toEloquentClause(): string
{
return match ($this) {
self::IS_BETWEEN => 'whereBetween',
self::IS_NOT_BETWEEN => 'whereNotBetween',
default => 'where',
};
}
}

View file

@ -2,69 +2,18 @@
namespace App\Values;
use App\Enums\SmartPlaylistModel;
use App\Enums\SmartPlaylistOperator;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Str;
use Webmozart\Assert\Assert;
final class SmartPlaylistRule implements Arrayable
{
public const OPERATOR_IS = 'is';
public const OPERATOR_IS_NOT = 'isNot';
public const OPERATOR_CONTAINS = 'contains';
public const OPERATOR_NOT_CONTAIN = 'notContain';
public const OPERATOR_IS_BETWEEN = 'isBetween';
public const OPERATOR_IS_GREATER_THAN = 'isGreaterThan';
public const OPERATOR_IS_LESS_THAN = 'isLessThan';
public const OPERATOR_BEGINS_WITH = 'beginsWith';
public const OPERATOR_ENDS_WITH = 'endsWith';
public const OPERATOR_IN_LAST = 'inLast';
public const OPERATOR_NOT_IN_LAST = 'notInLast';
public const OPERATOR_IS_NOT_BETWEEN = 'isNotBetween';
public const VALID_OPERATORS = [
self::OPERATOR_BEGINS_WITH,
self::OPERATOR_CONTAINS,
self::OPERATOR_ENDS_WITH,
self::OPERATOR_IN_LAST,
self::OPERATOR_IS,
self::OPERATOR_IS_BETWEEN,
self::OPERATOR_IS_NOT_BETWEEN,
self::OPERATOR_IS_GREATER_THAN,
self::OPERATOR_IS_LESS_THAN,
self::OPERATOR_IS_NOT,
self::OPERATOR_NOT_CONTAIN,
self::OPERATOR_NOT_IN_LAST,
];
private const MODEL_TITLE = 'title';
public const MODEL_ALBUM_NAME = 'album.name';
public const MODEL_ARTIST_NAME = 'artist.name';
private const MODEL_PLAY_COUNT = 'interactions.play_count';
public const MODEL_LAST_PLAYED = 'interactions.last_played_at';
private const MODEL_USER_ID = 'interactions.user_id';
private const MODEL_LENGTH = 'length';
public const MODEL_DATE_ADDED = 'created_at';
public const MODEL_DATE_MODIFIED = 'updated_at';
private const MODEL_GENRE = 'genre';
private const MODEL_YEAR = 'year';
private const VALID_MODELS = [
self::MODEL_TITLE,
self::MODEL_ALBUM_NAME,
self::MODEL_ARTIST_NAME,
self::MODEL_PLAY_COUNT,
self::MODEL_LAST_PLAYED,
self::MODEL_LENGTH,
self::MODEL_DATE_ADDED,
self::MODEL_DATE_MODIFIED,
self::MODEL_GENRE,
self::MODEL_YEAR,
];
public string $id;
public string $operator;
public SmartPlaylistModel $model;
public SmartPlaylistOperator $operator;
public array $value;
public string $model;
private function __construct(array $config)
{
@ -72,21 +21,25 @@ final class SmartPlaylistRule implements Arrayable
$this->id = $config['id'] ?? Str::uuid()->toString();
$this->value = $config['value'];
$this->model = $config['model'];
$this->operator = $config['operator'];
$this->model = SmartPlaylistModel::from($config['model']);
$this->operator = SmartPlaylistOperator::from($config['operator']);
}
/** @noinspection PhpExpressionResultUnusedInspection */
public static function assertConfig(array $config, bool $allowUserIdModel = true): void
{
if ($config['id'] ?? null) {
Assert::uuid($config['id']);
}
Assert::oneOf($config['operator'], self::VALID_OPERATORS);
Assert::oneOf(
$config['model'],
$allowUserIdModel ? array_prepend(self::VALID_MODELS, self::MODEL_USER_ID) : self::VALID_MODELS
);
SmartPlaylistOperator::from($config['operator']);
if (!$allowUserIdModel) {
Assert::false($config['model'] === SmartPlaylistModel::USER_ID);
}
SmartPlaylistModel::from($config['model']);
Assert::isArray($config['value']);
Assert::countBetween($config['value'], 1, 2);
}
@ -101,8 +54,8 @@ final class SmartPlaylistRule implements Arrayable
{
return [
'id' => $this->id,
'model' => $this->model,
'operator' => $this->operator,
'model' => $this->model->value,
'operator' => $this->operator->value,
'value' => $this->value,
];
}

View file

@ -2,30 +2,14 @@
namespace App\Values;
use App\Enums\SmartPlaylistModel as Model;
use App\Enums\SmartPlaylistOperator as Operator;
use App\Values\SmartPlaylistRule as Rule;
use Carbon\Carbon;
use Closure;
use Webmozart\Assert\Assert;
final class SmartPlaylistSqlElements
{
private const DATE_MODELS = [
Rule::MODEL_LAST_PLAYED,
Rule::MODEL_DATE_ADDED,
Rule::MODEL_DATE_MODIFIED,
];
private const MODEL_COLUMN_REMAP = [
Rule::MODEL_ALBUM_NAME => 'albums.name',
Rule::MODEL_ARTIST_NAME => 'artists.name',
Rule::MODEL_DATE_ADDED => 'songs.created_at',
Rule::MODEL_DATE_MODIFIED => 'songs.updated_at',
];
private const CLAUSE_WHERE = 'where';
private const CLAUSE_WHERE_BETWEEN = 'whereBetween';
private const CLAUSE_WHERE_NOT_BETWEEN = 'whereNotBetween';
public string $clause;
public array $parameters;
@ -43,44 +27,38 @@ final class SmartPlaylistSqlElements
// If the rule is a date rule and the operator is "is" or "is not", we need to
// convert the date to a range of dates and use the "between" or "not between" operator instead,
// as we store dates as timestamps in the database.
if (
in_array($rule->model, self::DATE_MODELS, true) &&
in_array($operator, [Rule::OPERATOR_IS, Rule::OPERATOR_IS_NOT], true)
) {
$operator = $operator === Rule::OPERATOR_IS ? Rule::OPERATOR_IS_BETWEEN : Rule::OPERATOR_IS_NOT_BETWEEN;
if ($rule->model->isDate() && in_array($operator, [Operator::IS, Operator::IS_NOT], true)) {
$operator = $operator === Operator::IS ? Operator::IS_BETWEEN : Operator::IS_NOT_BETWEEN;
$nextDay = Carbon::createFromFormat('Y-m-d', $value[0])->addDay()->format('Y-m-d');
$value = [$value[0], $nextDay];
}
$column = array_key_exists($rule->model, self::MODEL_COLUMN_REMAP)
? self::MODEL_COLUMN_REMAP[$rule->model]
: $rule->model;
return new self(
$operator->toEloquentClause(),
...self::generateParameters($rule->model, $operator, $value)
);
}
$resolvers = [
Rule::OPERATOR_BEGINS_WITH => [$column, 'LIKE', "$value[0]%"],
Rule::OPERATOR_ENDS_WITH => [$column, 'LIKE', "%$value[0]"],
Rule::OPERATOR_IS => [$column, '=', $value[0]],
Rule::OPERATOR_IS_NOT => [$column, '<>', $value[0]],
Rule::OPERATOR_CONTAINS => [$column, 'LIKE', "%$value[0]%"],
Rule::OPERATOR_NOT_CONTAIN => [$column, 'NOT LIKE', "%$value[0]%"],
Rule::OPERATOR_IS_LESS_THAN => [$column, '<', $value[0]],
Rule::OPERATOR_IS_GREATER_THAN => [$column, '>', $value[0]],
Rule::OPERATOR_IS_BETWEEN => [$column, $value],
Rule::OPERATOR_IS_NOT_BETWEEN => [$column, $value],
Rule::OPERATOR_NOT_IN_LAST => static fn () => [$column, '<', now()->subDays($value[0])],
Rule::OPERATOR_IN_LAST => static fn () => [$column, '>=', now()->subDays($value[0])],
];
/** @return array<mixed> */
private static function generateParameters(Model $model, Operator $operator, array $value): array
{
$column = $model->toColumnName();
Assert::keyExists($resolvers, $operator);
$clause = match ($operator) {
Rule::OPERATOR_IS_BETWEEN => self::CLAUSE_WHERE_BETWEEN,
Rule::OPERATOR_IS_NOT_BETWEEN => self::CLAUSE_WHERE_NOT_BETWEEN,
default => self::CLAUSE_WHERE,
$resolver = match ($operator) {
Operator::BEGINS_WITH => [$column, 'LIKE', "$value[0]%"],
Operator::ENDS_WITH => [$column, 'LIKE', "%$value[0]"],
Operator::IS => [$column, '=', $value[0]],
Operator::IS_NOT => [$column, '<>', $value[0]],
Operator::CONTAINS => [$column, 'LIKE', "%$value[0]%"],
Operator::NOT_CONTAIN => [$column, 'NOT LIKE', "%$value[0]%"],
Operator::IS_LESS_THAN => [$column, '<', $value[0]],
Operator::IS_GREATER_THAN => [$column, '>', $value[0]],
Operator::IS_BETWEEN, Operator::IS_NOT_BETWEEN => [$column, $value],
Operator::NOT_IN_LAST => static fn () => [$column, '<', now()->subDays($value[0])],
Operator::IN_LAST => static fn () => [$column, '>=', now()->subDays($value[0])],
};
$parameters = $resolvers[$operator] instanceof Closure ? $resolvers[$operator]() : $resolvers[$operator];
return new self($clause, ...$parameters);
return $resolver instanceof Closure ? $resolver() : $resolver;
}
}

View file

@ -17,7 +17,7 @@ class PlaylistTest extends PlusTestCase
$rule = SmartPlaylistRule::make([
'model' => 'artist.name',
'operator' => SmartPlaylistRule::OPERATOR_IS,
'operator' => 'is',
'value' => ['Bob Dylan'],
]);

View file

@ -52,7 +52,7 @@ class PlaylistTest extends TestCase
$rule = SmartPlaylistRule::make([
'model' => 'artist.name',
'operator' => SmartPlaylistRule::OPERATOR_IS,
'operator' => 'is',
'value' => ['Bob Dylan'],
]);
@ -87,7 +87,7 @@ class PlaylistTest extends TestCase
'rules' => [
SmartPlaylistRule::make([
'model' => 'artist.name',
'operator' => SmartPlaylistRule::OPERATOR_IS,
'operator' => 'is',
'value' => ['Bob Dylan'],
])->toArray(),
],