wip - more fixes

- added user animelist/mangalist endpoints back
- fixed issues with the container image
- improved club model factory
- fixed ordering while searching when search engine is disabled (mongodb based search)
This commit is contained in:
pushrbx 2023-02-10 12:00:52 +00:00
parent 7ac4f8693e
commit fbc3b8277d
24 changed files with 853 additions and 32 deletions

View File

@ -0,0 +1,87 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Enums\AnimeListAiringStatusFilterEnum;
use App\Enums\AnimeListStatusEnum;
use App\Enums\UserAnimeListOrderByEnum;
use App\Enums\UserListTypeEnum;
use App\Rules\Attributes\EnumValidation;
use App\Services\JikanUserListRequestMapperService;
use Carbon\CarbonImmutable;
use Jikan\Request\User\UserAnimeListRequest;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\Validation\AfterOrEqual;
use Spatie\LaravelData\Attributes\Validation\BeforeOrEqual;
use Spatie\LaravelData\Attributes\Validation\DateFormat;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\Sometimes;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Attributes\WithTransformer;
use Spatie\LaravelData\Casts\DateTimeInterfaceCast;
use Spatie\LaravelData\Optional;
use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer;
final class QueryAnimeListOfUserCommand extends QueryListOfUserCommand
{
#[WithCast(EnumCast::class, AnimeListStatusEnum::class), EnumValidation(AnimeListStatusEnum::class)]
public AnimeListStatusEnum|Optional $status;
#[
WithCast(EnumCast::class, UserAnimeListOrderByEnum::class),
EnumValidation(UserAnimeListOrderByEnum::class),
MapInputName("order_by")
]
public UserAnimeListOrderByEnum|Optional $orderBy;
#[
WithCast(EnumCast::class, UserAnimeListOrderByEnum::class),
EnumValidation(UserAnimeListOrderByEnum::class),
MapInputName("order_by2")
]
public UserAnimeListOrderByEnum|Optional $orderBy2;
#[
WithCast(EnumCast::class, AnimeListAiringStatusFilterEnum::class),
EnumValidation(AnimeListAiringStatusFilterEnum::class),
MapInputName("airing_status")
]
public AnimeListAiringStatusFilterEnum|Optional $airingStatus;
#[Min(1500), Max(2999)]
public int|Optional $year;
#[Min(1)]
public int|Optional $producer;
#[
BeforeOrEqual("aired_to"),
DateFormat("Y-m-d"),
Sometimes,
Required,
WithCast(DateTimeInterfaceCast::class),
WithTransformer(DateTimeInterfaceTransformer::class),
MapInputName("aired_from")
]
public CarbonImmutable|Optional $airedFrom;
#[
AfterOrEqual("aired_from"),
DateFormat("Y-m-d"),
Sometimes,
Required,
WithCast(DateTimeInterfaceCast::class),
WithTransformer(DateTimeInterfaceTransformer::class),
MapInputName("aired_to")
]
public CarbonImmutable|Optional $airedTo;
public function toJikanParserRequest(): UserAnimeListRequest
{
$mapper = app(JikanUserListRequestMapperService::class);
return $mapper->map($this, UserListTypeEnum::anime());
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest;
use App\Dto\Concerns\HasPageParameter;
use App\Dto\Concerns\MapsRouteParameters;
use App\Enums\SortDirection;
use App\Rules\Attributes\EnumValidation;
use Illuminate\Http\JsonResponse;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional;
/**
* @implements DataRequest<JsonResponse>
*/
abstract class QueryListOfUserCommand extends Data implements DataRequest
{
use HasRequestFingerprint, HasPageParameter, MapsRouteParameters;
#[Min(3)]
public string $username;
#[Max(255), MapOutputName("title")]
public string|Optional $q;
#[WithCast(EnumCast::class, SortDirection::class), EnumValidation(SortDirection::class)]
public SortDirection|Optional $sort;
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Enums\MangaListStatusEnum;
use App\Enums\UserListTypeEnum;
use App\Enums\UserMangaListOrderByEnum;
use App\Enums\UserMangaListStatusFilterEnum;
use App\Rules\Attributes\EnumValidation;
use App\Services\JikanUserListRequestMapperService;
use Carbon\CarbonImmutable;
use Jikan\Request\User\UserMangaListRequest;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\Validation\AfterOrEqual;
use Spatie\LaravelData\Attributes\Validation\BeforeOrEqual;
use Spatie\LaravelData\Attributes\Validation\DateFormat;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\Sometimes;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Attributes\WithTransformer;
use Spatie\LaravelData\Casts\DateTimeInterfaceCast;
use Spatie\LaravelData\Optional;
use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer;
final class QueryMangaListOfUserCommand extends QueryListOfUserCommand
{
#[WithCast(EnumCast::class, MangaListStatusEnum::class), EnumValidation(MangaListStatusEnum::class)]
public MangaListStatusEnum|Optional $status;
#[
WithCast(EnumCast::class, UserMangaListOrderByEnum::class),
MapInputName("order_by"),
EnumValidation(UserMangaListOrderByEnum::class)
]
public UserMangaListOrderByEnum|Optional $orderBy;
#[
WithCast(EnumCast::class, UserMangaListOrderByEnum::class),
MapInputName("order_by2"),
EnumValidation(UserMangaListOrderByEnum::class)
]
public UserMangaListOrderByEnum|Optional $orderBy2;
#[Min(1)]
public int|Optional $magazine;
#[
BeforeOrEqual("published_to"),
DateFormat("Y-m-d"),
Sometimes,
Required,
WithCast(DateTimeInterfaceCast::class),
WithTransformer(DateTimeInterfaceTransformer::class),
MapInputName("published_from")
]
public CarbonImmutable|Optional $publishedFrom;
#[
AfterOrEqual("published_from"),
DateFormat("Y-m-d"),
Sometimes,
Required,
WithCast(DateTimeInterfaceCast::class),
WithTransformer(DateTimeInterfaceTransformer::class),
MapInputName("published_to")
]
public CarbonImmutable|Optional $publishedTo;
#[
WithCast(EnumCast::class, UserMangaListStatusFilterEnum::class),
EnumValidation(UserMangaListStatusFilterEnum::class),
MapInputName("publishing_status")
]
public UserMangaListStatusFilterEnum|Optional $publishingStatus;
public function toJikanParserRequest(): UserMangaListRequest
{
$mapper = app(JikanUserListRequestMapperService::class);
return $mapper->map($this, UserListTypeEnum::manga());
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
use Jikan\Helper\Constants as JikanConstants;
/**
* @method static self airing()
* @method static self finished()
* @method static self complete()
* @method static self to_be_aired()
* @method static self not_yet_aired()
* @method static self tba()
* @method static self nya()
*/
final class AnimeListAiringStatusFilterEnum extends Enum
{
protected static function labels()
{
return [
'airing' => JikanConstants::USER_ANIME_LIST_CURRENTLY_AIRING,
'finished' => JikanConstants::USER_ANIME_LIST_FINISHED_AIRING,
'complete' => JikanConstants::USER_ANIME_LIST_FINISHED_AIRING,
'to_be_aired' => JikanConstants::USER_ANIME_LIST_NOT_YET_AIRED,
'not_yet_aired' => JikanConstants::USER_ANIME_LIST_NOT_YET_AIRED,
'tba' => JikanConstants::USER_ANIME_LIST_NOT_YET_AIRED,
'nya' => JikanConstants::USER_ANIME_LIST_NOT_YET_AIRED,
];
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self all()
* @method static self watching()
* @method static self completed()
* @method static self onhold()
* @method static self dropped()
* @method static self plantowatch()
*
* @OA\Schema(
* schema="user_anime_list_status_filter",
* description="User's anime list status filter options",
* type="string",
* enum={"all", "watching", "completed", "onhold", "dropped", "plantowatch"}
* )
*/
final class AnimeListStatusEnum extends Enum
{
// labels will be the values used for mapping, meanwhile the values are the names of the enum elements,
// because these are getting passed in through the query string in requests, and we validate against them
protected static function labels(): array
{
return [
"all" => "7",
"watching" => "1",
"completed" => "2",
"onhold" => "3",
"dropped" => "4",
"plantowatch" => "6"
];
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self all()
* @method static self reading()
* @method static self completed()
* @method static self onhold()
* @method static self dropped()
* @method static self plantoread()
*
* @OA\Schema(
* schema="user_manga_list_status_filter",
* description="User's anime list status filter options",
* type="string",
* enum={"all", "reading", "completed", "onhold", "dropped", "plantoread"}
* )
*/
final class MangaListStatusEnum extends Enum
{
// labels will be the values used for mapping, meanwhile the values are the names of the enum elements,
// because these are getting passed in through the query string in requests, and we validate against them
protected static function labels(): array
{
return [
"all" => "7",
"reading" => "1",
"completed" => "2",
"onhold" => "3",
"dropped" => "4",
"plantoread" => "6"
];
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
use Jikan\Helper\Constants as JikanConstants;
/**
* @method static self title()
* @method static self started_date()
* @method static self score()
* @method static self last_updated()
* @method static self type()
* @method static self rated()
* @method static self rewatch_value()
* @method static self priority()
* @method static self episodes_watched()
* @method static self storage()
* @method static self air_start()
* @method static self air_end()
* @method static self status()
*/
final class UserAnimeListOrderByEnum extends Enum
{
// labels will be the values used for mapping, meanwhile the values are the names of the enum elements,
// because these are getting passed in through the query string in requests, and we validate against them
protected static function labels(): array
{
return [
'title' => JikanConstants::USER_ANIME_LIST_ORDER_BY_TITLE,
'finished_date' => JikanConstants::USER_ANIME_LIST_ORDER_BY_FINISHED_DATE,
'started_date' => JikanConstants::USER_ANIME_LIST_ORDER_BY_STARTED_DATE,
'score' => JikanConstants::USER_ANIME_LIST_ORDER_BY_SCORE,
'last_updated' => JikanConstants::USER_ANIME_LIST_ORDER_BY_LAST_UPDATED,
'type' => JikanConstants::USER_ANIME_LIST_ORDER_BY_TYPE,
'rated' => JikanConstants::USER_ANIME_LIST_ORDER_BY_RATED,
'rewatch_value' => JikanConstants::USER_ANIME_LIST_ORDER_BY_REWATCH_VALUE,
'priority' => JikanConstants::USER_ANIME_LIST_ORDER_BY_PRIORITY,
'episodes_watched' => JikanConstants::USER_ANIME_LIST_ORDER_BY_PROGRESS,
'storage' => JikanConstants::USER_ANIME_LIST_ORDER_BY_STORAGE,
'air_start' => JikanConstants::USER_ANIME_LIST_ORDER_BY_AIR_START,
'air_end' => JikanConstants::USER_ANIME_LIST_ORDER_BY_AIR_END,
'status' => JikanConstants::USER_ANIME_LIST_ORDER_BY_STATUS,
];
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self anime()
* @method static self manga()
*/
final class UserListTypeEnum extends Enum
{
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
use Jikan\Helper\Constants as JikanConstants;
/**
* @method static self title()
* @method static self started_date()
* @method static self score()
* @method static self last_updated()
* @method static self priority()
* @method static self progress()
* @method static self chapters_read()
* @method static self volumes_read()
* @method static self type()
* @method static self publish_start()
* @method static self publish_end()
* @method static self status()
*/
final class UserMangaListOrderByEnum extends Enum
{
// labels will be the values used for mapping, meanwhile the values are the names of the enum elements,
// because these are getting passed in through the query string in requests, and we validate against them
protected static function labels(): array
{
return [
'title' => JikanConstants::USER_MANGA_LIST_ORDER_BY_TITLE,
'finished_date' => JikanConstants::USER_MANGA_LIST_ORDER_BY_FINISHED_DATE,
'started_date' => JikanConstants::USER_MANGA_LIST_ORDER_BY_STARTED_DATE,
'score' => JikanConstants::USER_MANGA_LIST_ORDER_BY_SCORE,
'last_updated' => JikanConstants::USER_MANGA_LIST_ORDER_BY_LAST_UPDATED,
'priority' => JikanConstants::USER_MANGA_LIST_ORDER_BY_PRIORITY,
'progress' => JikanConstants::USER_MANGA_LIST_ORDER_BY_CHAPTERS,
'chapters_read' => JikanConstants::USER_MANGA_LIST_ORDER_BY_CHAPTERS,
'volumes_read' => JikanConstants::USER_MANGA_LIST_ORDER_BY_VOLUMES,
'type' => JikanConstants::USER_MANGA_LIST_ORDER_BY_TYPE,
'publish_start' => JikanConstants::USER_MANGA_LIST_ORDER_BY_PUBLISH_START,
'publish_end' => JikanConstants::USER_MANGA_LIST_ORDER_BY_PUBLISH_END,
'status' => JikanConstants::USER_MANGA_LIST_ORDER_BY_STATUS,
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
use Jikan\Helper\Constants as JikanConstants;
/**
* @method static self publishing()
* @method static self finished()
* @method static self complete()
* @method static self to_be_published()
* @method static self not_yet_published()
* @method static self tba()
* @method static self nya()
*/
final class UserMangaListStatusFilterEnum extends Enum
{
// labels will be the values used for mapping, meanwhile the values are the names of the enum elements,
// because these are getting passed in through the query string in requests, and we validate against them
protected static function labels(): array
{
return [
'airing' => JikanConstants::USER_ANIME_LIST_CURRENTLY_AIRING,
'finished' => JikanConstants::USER_ANIME_LIST_FINISHED_AIRING,
'complete' => JikanConstants::USER_ANIME_LIST_FINISHED_AIRING,
'to_be_aired' => JikanConstants::USER_ANIME_LIST_NOT_YET_AIRED,
'not_yet_aired' => JikanConstants::USER_ANIME_LIST_NOT_YET_AIRED,
'tba' => JikanConstants::USER_ANIME_LIST_NOT_YET_AIRED,
'nya' => JikanConstants::USER_ANIME_LIST_NOT_YET_AIRED,
];
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Features;
use App\Dto\QueryAnimeListOfUserCommand;
use App\Http\Resources\V4\UserProfileAnimeListCollection;
use App\Http\Resources\V4\UserProfileAnimeListResource;
use App\Support\CachedData;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\User\UserAnimeListRequest;
/**
* @extends RequestHandlerWithScraperCache<QueryAnimeListOfUserCommand, JsonResponse>
*/
final class QueryAnimeListOfUserHandler extends RequestHandlerWithScraperCache
{
/**
* @param QueryAnimeListOfUserCommand $request
* @return JsonResponse
*/
public function handle($request)
{
$requestParams = collect(["jikanParserRequest" => $request->toJikanParserRequest()]);
$requestFingerPrint = $request->getFingerPrint();
$results = $this->getScraperData($requestFingerPrint, $requestParams);
return $this->renderResponse($requestFingerPrint, $results);
}
public function resource(Collection $results): JsonResource
{
if ($results->isEmpty() || count($results->get("anime")) === 0) {
return new UserProfileAnimeListCollection([]);
}
$listResults = $results->get("anime");
foreach ($listResults as &$result) {
$result = (new UserProfileAnimeListResource($result));
}
return new UserProfileAnimeListCollection($listResults);
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryAnimeListOfUserCommand::class;
}
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
/**
* @var UserAnimeListRequest $jikanParserRequest
*/
$jikanParserRequest = $requestParams->get("jikanParserRequest");
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => ["anime" => $jikan->getUserAnimeList($jikanParserRequest)]
);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Features;
use App\Dto\QueryMangaListOfUserCommand;
use App\Http\Resources\V4\UserProfileAnimeListCollection;
use App\Http\Resources\V4\UserProfileMangaListCollection;
use App\Http\Resources\V4\UserProfileMangaListResource;
use App\Support\CachedData;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\User\UserMangaListRequest;
/**
* @extends RequestHandlerWithScraperCache<QueryMangaListOfUserCommand, JsonResponse>
*/
final class QueryMangaListOfUserHandler extends RequestHandlerWithScraperCache
{
/**
* @param QueryMangaListOfUserCommand $request
*/
public function handle($request)
{
$requestParams = collect(["jikanParserRequest" => $request->toJikanParserRequest()]);
$requestFingerPrint = $request->getFingerPrint();
$results = $this->getScraperData($requestFingerPrint, $requestParams);
return $this->renderResponse($requestFingerPrint, $results);
}
public function resource(Collection $results): JsonResource
{
if ($results->isEmpty()) {
return new UserProfileAnimeListCollection([]);
}
$listResults = $results->first()['manga'];
foreach ($listResults as &$result) {
$result = (new UserProfileMangaListResource($result));
}
return new UserProfileMangaListCollection($listResults);
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryMangaListOfUserCommand::class;
}
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
/**
* @var UserMangaListRequest $jikanParserRequest
*/
$jikanParserRequest = $requestParams->get("jikanParserRequest");
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => ["anime" => $jikan->getUserMangaList($jikanParserRequest)]
);
}
}

View File

@ -2,7 +2,9 @@
namespace App\Http\Controllers\V4DB;
use App\Dto\QueryMangaListOfUserCommand;
use App\Dto\QueryRecentlyOnlineUsersCommand;
use App\Dto\QueryAnimeListOfUserCommand;
use App\Dto\UserAboutLookupCommand;
use App\Dto\UserClubsLookupCommand;
use App\Dto\UserExternalLookupCommand;
@ -349,6 +351,12 @@ class UserController extends Controller
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="status",
* in="query",
* @OA\Schema(ref="#/components/schemas/user_anime_list_status_filter")
* ),
*
* @OA\Response(
* response="200",
* description="Returns user anime list",
@ -362,10 +370,9 @@ class UserController extends Controller
* ),
*
*/
public function animelist(Request $request, string $username, ?string $status = null)
public function animelist(QueryAnimeListOfUserCommand $command)
{
// noop, intentionally left blank
// todo: remove as this is obsolete
return $this->mediator->send($command);
}
/**
@ -383,6 +390,12 @@ class UserController extends Controller
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="status",
* in="query",
* @OA\Schema(ref="#/components/schemas/user_manga_list_status_filter")
* ),
*
* @OA\Response(
* response="200",
* description="Returns user manga list",
@ -396,10 +409,9 @@ class UserController extends Controller
* ),
*
*/
public function mangalist(Request $request, string $username, ?string $status = null)
public function mangalist(QueryMangaListOfUserCommand $command)
{
// noop, intentionally left blank
// todo: remove as this is obsolete
return $this->mediator->send($command);
}
/**

View File

@ -31,11 +31,13 @@ use App\Repositories\DefaultUserRepository;
use App\Repositories\MangaGenresRepository;
use App\Services\DefaultBuilderPaginatorService;
use App\Services\DefaultCachedScraperService;
use App\Services\DefaultPrivateFieldMapperService;
use App\Services\DefaultQueryBuilderService;
use App\Services\DefaultScoutSearchService;
use App\Services\ElasticScoutSearchService;
use App\Services\EloquentBuilderPaginatorService;
use App\Services\MongoSearchService;
use App\Services\PrivateFieldMapperService;
use App\Services\QueryBuilderPaginatorService;
use App\Services\ScoutBuilderPaginatorService;
use App\Services\ScoutSearchService;
@ -82,6 +84,7 @@ class AppServiceProvider extends ServiceProvider
// cache options class is used to share the request scope level cache settings
$this->app->singleton(CacheOptions::class);
$this->app->singleton(CachedScraperService::class, DefaultCachedScraperService::class);
$this->app->singleton(PrivateFieldMapperService::class, DefaultPrivateFieldMapperService::class);
$this->app->bind(QueryBuilderPaginatorService::class, DefaultBuilderPaginatorService::class);
$this->registerModelRepositories();
$this->registerRequestHandlers();
@ -276,7 +279,9 @@ class AppServiceProvider extends ServiceProvider
Features\QueryRecentlyAddedEpisodesHandler::class => $unitOfWorkInstance->documents("watch"),
Features\QueryPopularEpisodesHandler::class => $unitOfWorkInstance->documents("watch"),
Features\QueryRecentlyAddedPromoVideosHandler::class => $unitOfWorkInstance->documents("watch"),
Features\QueryPopularPromoVideosHandler::class => $unitOfWorkInstance->documents("watch")
Features\QueryPopularPromoVideosHandler::class => $unitOfWorkInstance->documents("watch"),
Features\QueryAnimeListOfUserHandler::class => $unitOfWorkInstance->documents("users_animelist"),
Features\QueryMangaListOfUserHandler::class => $unitOfWorkInstance->documents("users_mangalist")
];
foreach ($requestHandlersWithScraperService as $handlerClass => $repositoryInstance) {
@ -335,14 +340,6 @@ class AppServiceProvider extends ServiceProvider
];
}
private function getQueryBuilderFactory($queryBuilderClass): \Closure
{
return function($app) use($queryBuilderClass) {
$searchIndexesEnabled = $this->getSearchIndexesEnabledConfig($app);
return new $queryBuilderClass($searchIndexesEnabled, $app->make(ScoutSearchService::class));
};
}
private function getSearchIndexesEnabledConfig($app): bool
{
return $this->getSearchIndexDriver($app) != "null";

View File

@ -0,0 +1,34 @@
<?php
namespace App\Services;
use Spatie\Enum\Laravel\Enum;
use Spatie\LaravelData\Optional;
final class DefaultPrivateFieldMapperService implements PrivateFieldMapperService
{
public function map($instance, array $values): mixed
{
$cls = get_class($instance);
foreach ($values as $fieldName => $fieldValue) {
if ($fieldValue instanceof Optional) {
continue;
}
if ($fieldValue instanceof Enum) {
$fieldValue = $fieldValue->label;
}
if (!property_exists($cls, $fieldName)) {
continue;
}
$reflection = new \ReflectionProperty($cls, $fieldName);
// note: ->setAccessible call would be required under php version 8.1
$reflection->setValue($instance, $fieldValue);
}
return $instance;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Services;
use App\Dto\QueryListOfUserCommand;
use App\Enums\UserListTypeEnum;
use Carbon\CarbonImmutable;
use Jikan\Request\User\UserAnimeListRequest;
use Jikan\Request\User\UserMangaListRequest;
use Spatie\LaravelData\Optional;
final class JikanUserListRequestMapperService
{
public function __construct(private readonly PrivateFieldMapperService $fieldMapperService)
{
}
public function map(QueryListOfUserCommand $command, UserListTypeEnum $listType): UserAnimeListRequest|UserMangaListRequest
{
$values = collect($command->all())->except(["username", "page", "status"])->toArray();
$status = $command->status;
if ($status instanceof Optional) {
$status = 7;
}
if (array_key_exists("sort", $values)) {
$values["sort"] = $values["sort"] === "asc" ? -1 : 1;
}
if (!array_key_exists("page", $values)) {
$values["page"] = 1;
}
if ($listType->equals(UserListTypeEnum::anime())) {
$rangeFrom = "airedFrom";
$rangeTo = "airedTo";
$jikanUserListRequest = new UserAnimeListRequest($command->username, $command->page, $status);
}
else {
$rangeFrom = "publishedFrom";
$rangeTo = "publishedTo";
$jikanUserListRequest = new UserMangaListRequest($command->username, $command->page, $status);
}
foreach ([$rangeFrom, $rangeTo] as $rangeField) {
if (array_key_exists($rangeField, $values)) {
/**
* @var CarbonImmutable $c
*/
$c = $values[$rangeField];
$values[$rangeField] = [$c->year, $c->month, $c->day];
}
}
return $this->fieldMapperService->map($jikanUserListRequest, $values);
}
}

View File

@ -14,8 +14,7 @@ final class MongoSearchService extends SearchServiceBase
$query = $this->query();
/** @noinspection PhpParamsInspection */
/** @noinspection PhpIncompatibleReturnTypeInspection */
return $query->whereRaw([
$builder = $query->whereRaw([
'$text' => [
'$search' => $searchTerms
],
@ -24,5 +23,14 @@ final class MongoSearchService extends SearchServiceBase
'$meta' => 'textScore'
]
])->orderBy('textMatchScore', 'desc');
if ($orderByFields !== null) {
$order = explode(",", $orderByFields);
foreach ($order as $o) {
$builder = $builder->orderBy($o, $sortDirectionDescending ? 'desc' : 'asc');
}
}
return $builder;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Services;
interface PrivateFieldMapperService
{
public function map($instance, array $values): mixed;
}

View File

@ -3,8 +3,12 @@
namespace Database\Factories;
use App\Club;
use App\Enums\ClubCategoryEnum;
use App\Enums\ClubOrderByEnum;
use App\Enums\ClubTypeEnum;
use App\Testing\JikanDataGenerator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use MongoDB\BSON\UTCDateTime;
final class ClubFactory extends JikanModelFactory
@ -77,4 +81,98 @@ final class ClubFactory extends JikanModelFactory
"access" => $this->faker->randomElement(["public", "private"])
];
}
public function overrideFromQueryStringParameters(array $additionalParams, bool $doOpposite = false): self
{
$additionalParams = collect($additionalParams);
if ($doOpposite) {
$overrides = $this->getOppositeOverridesFromQueryStringParameters($additionalParams);
}
else {
$overrides = $this->getOverridesFromQueryStringParameters($additionalParams);
}
return $this->state($this->serializeStateDefinition($overrides));
}
protected function getOverridesFromQueryStringParameters(Collection $additionalParams): array
{
$overrides = [];
if ($additionalParams->has("type")) {
$typeOverride = collect(ClubTypeEnum::toArray())->get(strtolower($additionalParams["type"]));
if (!is_null($typeOverride)) {
$overrides["type"] = $typeOverride;
}
}
if ($additionalParams->has("letter")) {
$overrides["name"] = $additionalParams["letter"] . $this->createTitle();
}
if ($additionalParams->has("category")) {
$categoryOverride = collect(ClubCategoryEnum::toArray())->get(strtolower($additionalParams["category"]));
if (!is_null($categoryOverride)) {
$overrides["category"] = $categoryOverride;
}
}
return $overrides;
}
protected function getOppositeOverridesFromQueryStringParameters(Collection $additionalParams): array
{
$overrides = [];
if ($additionalParams->has("type")) {
// value => label key pairs
// we store labels in the database
$types = ClubTypeEnum::toArray();
$typeKey = $this->faker->randomElement(array_diff(array_keys($types), [$additionalParams["type"]]));
$overrides["type"] = $types[$typeKey];
}
if ($additionalParams->has("letter")) {
$alphabet = array_filter(range("a", "z"), fn ($elem) => $elem !== $additionalParams["letter"]);
$overrides["name"] = $this->faker->randomElement($alphabet) . $this->createTitle();
}
if ($additionalParams->has("category")) {
$categories = ClubCategoryEnum::toArray();
$categoryKey = $this->faker->randomElement(array_diff(array_keys($categories), [$additionalParams["category"]]));
$overrides["category"] = $categories[$categoryKey];
}
return $overrides;
}
public function createManyWithOrder(string $orderByField): Collection
{
$count = $this->count ?? 3;
$items = collect();
$fieldValueGenerator = match($orderByField) {
ClubOrderByEnum::name()->value => ((function() {
$alphabet = range("a", "z");
$alphabetCount = count($alphabet);
return fn($i) => $alphabet[$i % $alphabetCount];
})()),
ClubOrderByEnum::created()->value => ((function() {
$randomDate = $this->createRandomDateTime("-5 years");
return fn($i) => new UTCDateTime($randomDate->copy()->addDays($i)->getPreciseTimestamp(3));
})()),
default => fn($i) => $i,
};
for ($i = 1; $i <= $count; $i++) {
$createdItem = $this->createOne([
$orderByField => $fieldValueGenerator($i)
]);
$items->add($createdItem);
}
return $items;
}
}

View File

@ -133,6 +133,8 @@ abstract class JikanMediaModelFactory extends JikanModelFactory implements Media
$activityMarkerKeyName = $this->descriptor->activityMarkerKeyName();
// let's make all database items the same type
if ($additionalParams->has("type")) {
// value => label key pairs
// we store labels in the database
$typeOverride = collect($this->descriptor->typeParamMap())->get(strtolower($additionalParams["type"]));
if (!is_null($typeOverride)) {
$overrides["type"] = $typeOverride;
@ -256,8 +258,11 @@ abstract class JikanMediaModelFactory extends JikanModelFactory implements Media
$activityMarkerKeyName = $this->descriptor->activityMarkerKeyName();
if ($additionalParams->has("type")) {
// value => label key pairs
// we store labels in the database
$types = $this->descriptor->typeParamMap();
$overrides["type"] = $this->faker->randomElement(array_diff(array_keys($types), [$additionalParams["type"]]));
$typeKey = $this->faker->randomElement(array_diff(array_keys($types), [$additionalParams["type"]]));
$overrides["type"] = $types[$typeKey];
}
if ($additionalParams->has("letter")) {

View File

@ -7,7 +7,7 @@ require_once __DIR__.'/vendor/autoload.php';
$safe_defaults = [
// mongodb regex search by default
"SCOUT_DRIVER" => "null",
"SCOUT_DRIVER" => "none",
"SCOUT_QUEUE" => false,
"THROTTLE" => false,
"QUEUE_CONNECTION" => "database",

View File

@ -44,12 +44,22 @@ class AnimeSearchEndpointTest extends TestCase
public function limitParameterCombinationsProvider(): array
{
return [
[5, []],
[5, ["type" => "tv"]],
[5, ["type" => "tv", "min_score" => 7]],
[5, ["type" => "tv", "max_score" => 6]],
[5, ["type" => "tv", "status" => "complete", "max_score" => 8]],
[5, ["type" => "movie", "status" => "complete", "max_score" => 8]]
"query string = `?limit=5`" => [
[5, []]
],
"query string = `?limit=5&type=tv`" => [5, ["type" => "tv"]],
"query string = `?limit=5&type=tv&min_score=7`" => [
5, ["type" => "tv", "min_score" => 7]
],
"query string = `?limit=5&type=tv&max_score=6`" => [
5, ["type" => "tv", "max_score" => 6]
],
"query string = `?limit=5&type=tv&status=complete&max_score=8`" => [
5, ["type" => "tv", "status" => "complete", "max_score" => 8]
],
"query string = `?limit=5&type=movie&status=complete&max_score=8`" => [
5, ["type" => "movie", "status" => "complete", "max_score" => 8]
]
];
}

View File

@ -46,12 +46,16 @@ class MangaSearchEndpointTest extends TestCase
public function limitParameterCombinationsProvider(): array
{
return [
[5, []],
[5, ["type" => "manga"]],
[5, ["type" => "novel", "min_score" => 7]],
[5, ["type" => "manga", "max_score" => 6]],
[5, ["type" => "manga", "status" => "complete", "max_score" => 8]],
[5, ["type" => "oneshot", "status" => "complete", "max_score" => 8]]
"query string = `?limit=5`" => [5, []],
"query string = `?limit=5&type=manga`" =>[5, ["type" => "manga"]],
"query string = `?limit=5&type=novel&min_score=7`" => [5, ["type" => "novel", "min_score" => 7]],
"query string = `?limit=5&type=manga&max_score=6`" => [5, ["type" => "manga", "max_score" => 6]],
"query string = `?limit=5&type=manga&status=complete&max_score=8`" => [
5, ["type" => "manga", "status" => "complete", "max_score" => 8]
],
"query string = `?limit=5&type=oneshot&status=complete&max_score=8`" => [
5, ["type" => "oneshot", "status" => "complete", "max_score" => 8]
]
];
}

View File

@ -146,7 +146,7 @@ final class DefaultCachedScraperServiceTest extends TestCase
["dummy" => "dummy1"],
["dummy" => "dummy2"]
]
]]);;
]]);
[$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks(2);
$repositoryMock->expects()->insert(Mockery::capture($insertedData))->once()->andReturn(true);