mirror of
https://github.com/jikan-me/jikan-rest.git
synced 2025-02-20 11:23:35 +08:00
wip - anime season controller refactor, central limit parameter validation
- fixed cache validation
This commit is contained in:
parent
79b7a0f657
commit
37c273fbab
@ -33,4 +33,6 @@ interface AnimeRepository extends Repository
|
||||
): EloquentBuilder;
|
||||
|
||||
public function getAiredBetween(Carbon $from, Carbon $to, ?AnimeTypeEnum $type = null): EloquentBuilder;
|
||||
|
||||
public function getUpcomingSeasonItems(?AnimeTypeEnum $type = null): EloquentBuilder;
|
||||
}
|
||||
|
@ -13,5 +13,5 @@ use Spatie\LaravelData\Optional;
|
||||
final class AnimeEpisodesLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
}
|
||||
|
@ -13,5 +13,5 @@ use Spatie\LaravelData\Optional;
|
||||
final class AnimeNewsLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ use Spatie\LaravelData\Optional;
|
||||
final class AnimeReviewsLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
|
||||
#[WithCast(EnumCast::class, MediaReviewsSortEnum::class)]
|
||||
public MediaReviewsSortEnum|Optional $sort;
|
||||
|
@ -13,5 +13,5 @@ use Spatie\LaravelData\Optional;
|
||||
final class AnimeUserUpdatesLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
}
|
||||
|
@ -13,5 +13,5 @@ use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
final class AnimeVideosEpisodesLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
}
|
||||
|
@ -13,5 +13,5 @@ use Spatie\LaravelData\Optional;
|
||||
final class ClubMembersLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
}
|
||||
|
21
app/Dto/Concerns/MapsDefaultLimitParameter.php
Normal file
21
app/Dto/Concerns/MapsDefaultLimitParameter.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto\Concerns;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Env;
|
||||
|
||||
trait MapsDefaultLimitParameter
|
||||
{
|
||||
public static function prepareForPipeline(Collection $properties): Collection
|
||||
{
|
||||
if (!$properties->has("limit"))
|
||||
{
|
||||
/** @noinspection PhpUndefinedFieldInspection */
|
||||
$properties->put("limit", Env::get("MAX_RESULTS_PER_PAGE",
|
||||
property_exists(static::class, "defaultLimit") ? static::$defaultLimit : 25));
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
}
|
26
app/Dto/Concerns/MapsRouteParameters.php
Normal file
26
app/Dto/Concerns/MapsRouteParameters.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto\Concerns;
|
||||
|
||||
use App\DataPipes\MapRouteParametersDataPipe;
|
||||
use Spatie\LaravelData\DataPipeline;
|
||||
use Spatie\LaravelData\DataPipes\AuthorizedDataPipe;
|
||||
use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe;
|
||||
use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe;
|
||||
use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe;
|
||||
use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe;
|
||||
|
||||
trait MapsRouteParameters
|
||||
{
|
||||
public static function pipeline(): DataPipeline
|
||||
{
|
||||
return DataPipeline::create()
|
||||
->into(static::class)
|
||||
->through(AuthorizedDataPipe::class)
|
||||
->through(MapPropertiesDataPipe::class)
|
||||
->through(MapRouteParametersDataPipe::class) // if a payload is a request object, we map route params
|
||||
->through(ValidatePropertiesDataPipe::class)
|
||||
->through(DefaultValuesDataPipe::class)
|
||||
->through(CastPropertiesDataPipe::class);
|
||||
}
|
||||
}
|
@ -5,11 +5,10 @@ namespace App\Dto;
|
||||
use App\Concerns\HasRequestFingerprint;
|
||||
use App\Contracts\DataRequest;
|
||||
use App\DataPipes\MapRouteParametersDataPipe;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Dto\Concerns\MapsRouteParameters;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\LaravelData\Attributes\Validation\Min;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
@ -28,20 +27,8 @@ use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe;
|
||||
*/
|
||||
abstract class LookupDataCommand extends Data implements DataRequest
|
||||
{
|
||||
use HasRequestFingerprint;
|
||||
use MapsRouteParameters, HasRequestFingerprint;
|
||||
|
||||
#[Numeric, Required, Min(1)]
|
||||
public int $id;
|
||||
|
||||
public static function pipeline(): DataPipeline
|
||||
{
|
||||
return DataPipeline::create()
|
||||
->into(static::class)
|
||||
->through(AuthorizedDataPipe::class)
|
||||
->through(MapPropertiesDataPipe::class)
|
||||
->through(MapRouteParametersDataPipe::class) // if a payload is a request object, we map route params
|
||||
->through(ValidatePropertiesDataPipe::class)
|
||||
->through(DefaultValuesDataPipe::class)
|
||||
->through(CastPropertiesDataPipe::class);
|
||||
}
|
||||
}
|
||||
|
@ -14,5 +14,5 @@ use Spatie\LaravelData\Optional;
|
||||
final class MangaNewsLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ use Spatie\LaravelData\Optional;
|
||||
final class MangaReviewsLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
|
||||
#[WithCast(EnumCast::class, MediaReviewsSortEnum::class)]
|
||||
public MediaReviewsSortEnum|Optional $sort;
|
||||
|
@ -14,5 +14,5 @@ use Spatie\LaravelData\Optional;
|
||||
final class MangaUserUpdatesLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ namespace App\Dto;
|
||||
use App\Casts\EnumCast;
|
||||
use App\Concerns\HasRequestFingerprint;
|
||||
use App\Contracts\DataRequest;
|
||||
use App\Dto\Concerns\MapsDefaultLimitParameter;
|
||||
use App\Enums\AnimeScheduleFilterEnum;
|
||||
use App\Rules\Attributes\MaxLimitWithFallback;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Env;
|
||||
@ -25,12 +27,12 @@ use Spatie\LaravelData\Optional;
|
||||
*/
|
||||
final class QueryAnimeSchedulesCommand extends Data implements DataRequest
|
||||
{
|
||||
use HasRequestFingerprint;
|
||||
use MapsDefaultLimitParameter, HasRequestFingerprint;
|
||||
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page = 1;
|
||||
|
||||
#[IntegerType, Min(1)]
|
||||
#[IntegerType, Min(1), MaxLimitWithFallback]
|
||||
public int|Optional $limit;
|
||||
|
||||
#[BooleanType]
|
||||
@ -61,10 +63,6 @@ final class QueryAnimeSchedulesCommand extends Data implements DataRequest
|
||||
$data->filter = AnimeScheduleFilterEnum::from($day);
|
||||
}
|
||||
|
||||
if ($data->limit == Optional::create()) {
|
||||
$data->limit = Env::get("MAX_RESULTS_PER_PAGE", 25);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@ -6,34 +6,20 @@ namespace App\Dto;
|
||||
use App\Casts\EnumCast;
|
||||
use App\Concerns\HasRequestFingerprint;
|
||||
use App\Contracts\DataRequest;
|
||||
use App\Enums\AnimeSeasonEnum;
|
||||
use App\Dto\Concerns\MapsDefaultLimitParameter;
|
||||
use App\Enums\AnimeTypeEnum;
|
||||
use App\Http\HttpHelper;
|
||||
use App\Http\Resources\V4\AnimeCollection;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Env;
|
||||
use App\Rules\Attributes\MaxLimitWithFallback;
|
||||
use Spatie\Enum\Laravel\Rules\EnumRule;
|
||||
use Spatie\LaravelData\Attributes\Validation\Between;
|
||||
use Spatie\LaravelData\Attributes\Validation\Min;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Attributes\WithCast;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Optional;
|
||||
use Spatie\LaravelData\Resolvers\DataFromSomethingResolver;
|
||||
|
||||
/**
|
||||
* @implements DataRequest<AnimeCollection>
|
||||
*/
|
||||
final class QueryAnimeSeasonCommand extends Data implements DataRequest
|
||||
|
||||
abstract class QueryAnimeSeasonCommand extends Data implements DataRequest
|
||||
{
|
||||
use HasRequestFingerprint;
|
||||
|
||||
#[Required, Between(1000, 2999)]
|
||||
public int $year;
|
||||
|
||||
#[WithCast(EnumCast::class, AnimeSeasonEnum::class)]
|
||||
public AnimeSeasonEnum $season;
|
||||
use MapsDefaultLimitParameter, HasRequestFingerprint;
|
||||
|
||||
#[WithCast(EnumCast::class, AnimeTypeEnum::class)]
|
||||
public AnimeTypeEnum|Optional $filter;
|
||||
@ -41,37 +27,14 @@ final class QueryAnimeSeasonCommand extends Data implements DataRequest
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page = 1;
|
||||
|
||||
#[Numeric, Min(1)]
|
||||
#[Numeric, Min(1), MaxLimitWithFallback]
|
||||
public int|Optional $limit;
|
||||
|
||||
|
||||
public static function rules(...$args): array
|
||||
{
|
||||
return [
|
||||
"season" => [new EnumRule(AnimeSeasonEnum::class), new Required()],
|
||||
"filter" => [new EnumRule(AnimeTypeEnum::class)]
|
||||
];
|
||||
}
|
||||
|
||||
public static function messages(...$args): array
|
||||
{
|
||||
return [
|
||||
"season.enum" => "Invalid season supplied."
|
||||
];
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
public static function fromRequestAndRequired(Request $request, int $year, AnimeSeasonEnum $season): self
|
||||
{
|
||||
/**
|
||||
* @var QueryAnimeSeasonCommand $data
|
||||
*/
|
||||
$data = app(DataFromSomethingResolver::class)
|
||||
->withoutMagicalCreation()->execute(self::class, ["request" => $request, "year" => $year, "season" => $season->value]);
|
||||
$data->fingerprint = HttpHelper::resolveRequestFingerprint($request);
|
||||
if ($data->limit == Optional::create()) {
|
||||
$data->limit = Env::get("MAX_RESULTS_PER_PAGE", 30);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
17
app/Dto/QueryAnimeSeasonListCommand.php
Normal file
17
app/Dto/QueryAnimeSeasonListCommand.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
|
||||
use App\Concerns\HasRequestFingerprint;
|
||||
use App\Contracts\DataRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
/**
|
||||
* @implements DataRequest<JsonResponse>
|
||||
*/
|
||||
final class QueryAnimeSeasonListCommand extends Data implements DataRequest
|
||||
{
|
||||
use HasRequestFingerprint;
|
||||
}
|
8
app/Dto/QueryCurrentAnimeSeasonCommand.php
Normal file
8
app/Dto/QueryCurrentAnimeSeasonCommand.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
|
||||
final class QueryCurrentAnimeSeasonCommand extends QueryAnimeSeasonCommand
|
||||
{
|
||||
}
|
@ -24,7 +24,7 @@ abstract class QueryReviewsCommand extends Data implements DataRequest
|
||||
use HasRequestFingerprint;
|
||||
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
|
||||
#[WithCast(EnumCast::class, MediaReviewsSortEnum::class)]
|
||||
public MediaReviewsSortEnum|Optional $sort;
|
||||
|
40
app/Dto/QuerySpecificAnimeSeasonCommand.php
Normal file
40
app/Dto/QuerySpecificAnimeSeasonCommand.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Casts\EnumCast;
|
||||
use App\Dto\Concerns\MapsRouteParameters;
|
||||
use App\Enums\AnimeSeasonEnum;
|
||||
use Spatie\Enum\Laravel\Rules\EnumRule;
|
||||
use Spatie\LaravelData\Attributes\Validation\Between;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Attributes\WithCast;
|
||||
|
||||
|
||||
final class QuerySpecificAnimeSeasonCommand extends QueryAnimeSeasonCommand
|
||||
{
|
||||
use MapsRouteParameters;
|
||||
|
||||
#[Required, Between(1000, 2999)]
|
||||
public int $year;
|
||||
|
||||
#[WithCast(EnumCast::class, AnimeSeasonEnum::class)]
|
||||
public AnimeSeasonEnum $season;
|
||||
|
||||
private static int $defaultLimit = 30;
|
||||
|
||||
public static function rules(...$args): array
|
||||
{
|
||||
return [
|
||||
...parent::rules(...$args),
|
||||
"season" => [new EnumRule(AnimeSeasonEnum::class), new Required()]
|
||||
];
|
||||
}
|
||||
|
||||
public static function messages(...$args): array
|
||||
{
|
||||
return [
|
||||
"season.enum" => "Invalid season supplied."
|
||||
];
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Dto\Concerns\MapsDefaultLimitParameter;
|
||||
use App\Rules\Attributes\MaxLimitWithFallback;
|
||||
use Illuminate\Support\Optional;
|
||||
use Spatie\LaravelData\Attributes\Validation\Min;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
@ -9,9 +11,11 @@ use Spatie\LaravelData\Data;
|
||||
|
||||
abstract class QueryTopItemsCommand extends Data
|
||||
{
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
use MapsDefaultLimitParameter;
|
||||
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page = 1;
|
||||
|
||||
#[Numeric, Min(1), MaxLimitWithFallback]
|
||||
public int|Optional $limit;
|
||||
}
|
||||
|
7
app/Dto/QueryUpcomingAnimeSeasonCommand.php
Normal file
7
app/Dto/QueryUpcomingAnimeSeasonCommand.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
final class QueryUpcomingAnimeSeasonCommand extends QueryAnimeSeasonCommand
|
||||
{
|
||||
}
|
@ -3,7 +3,9 @@
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Casts\EnumCast;
|
||||
use App\Dto\Concerns\MapsDefaultLimitParameter;
|
||||
use App\Enums\SortDirection;
|
||||
use App\Rules\Attributes\MaxLimitWithFallback;
|
||||
use Spatie\Enum\Laravel\Rules\EnumRule;
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
@ -20,6 +22,8 @@ use Spatie\LaravelData\Optional;
|
||||
|
||||
class SearchCommand extends Data
|
||||
{
|
||||
use MapsDefaultLimitParameter;
|
||||
|
||||
/**
|
||||
* The search keywords
|
||||
* @var string|Optional
|
||||
@ -28,9 +32,9 @@ class SearchCommand extends Data
|
||||
public string|Optional $q;
|
||||
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
public int|Optional $page = 1;
|
||||
|
||||
#[IntegerType, Min(1)]
|
||||
#[IntegerType, Min(1), MaxLimitWithFallback]
|
||||
public int|Optional $limit;
|
||||
|
||||
#[WithCast(EnumCast::class, SortDirection::class)]
|
||||
|
67
app/Features/QueryAnimeSeasonHandlerBase.php
Normal file
67
app/Features/QueryAnimeSeasonHandlerBase.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Contracts\AnimeRepository;
|
||||
use App\Contracts\RequestHandler;
|
||||
use App\Dto\QueryAnimeSeasonCommand;
|
||||
use App\Enums\AnimeSeasonEnum;
|
||||
use App\Http\Resources\V4\AnimeCollection;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
|
||||
/**
|
||||
* @template TRequest of QueryAnimeSeasonCommand
|
||||
* @implements RequestHandler<TRequest, JsonResponse>
|
||||
*/
|
||||
abstract class QueryAnimeSeasonHandlerBase implements RequestHandler
|
||||
{
|
||||
public function __construct(protected readonly AnimeRepository $repository)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryAnimeSeasonCommand $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function handle($request): JsonResponse
|
||||
{
|
||||
/**
|
||||
* @var Carbon $from
|
||||
* @var Carbon $to
|
||||
*/
|
||||
[$from, $to] = $this->getSeasonRangeFrom($request);
|
||||
$type = collect($request->all())->has("filter") ? $request->filter : null;
|
||||
$results = $this->repository->getAiredBetween($from, $to, $type);
|
||||
$results = $results->paginate($request->limit, ["*"], null, $request->page);
|
||||
|
||||
$animeCollection = new AnimeCollection($results);
|
||||
$response = $animeCollection->response();
|
||||
|
||||
return $response->addJikanCacheFlags($request->getFingerPrint(), CachedData::from($animeCollection->collection));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TRequest $request
|
||||
* @return array
|
||||
*/
|
||||
protected abstract function getSeasonRangeFrom($request): array;
|
||||
|
||||
protected function getSeasonRange(int $year, AnimeSeasonEnum $season): array
|
||||
{
|
||||
[$monthStart, $monthEnd] = match ($season->value) {
|
||||
AnimeSeasonEnum::winter()->value => [1, 3],
|
||||
AnimeSeasonEnum::spring()->value => [4, 6],
|
||||
AnimeSeasonEnum::summer()->value => [7, 9],
|
||||
AnimeSeasonEnum::fall()->value => [10, 12],
|
||||
default => throw new BadRequestException('Invalid season supplied'),
|
||||
};
|
||||
|
||||
return [
|
||||
Carbon::createFromDate($year, $monthStart, 1),
|
||||
Carbon::createFromDate($year, $monthEnd, 1)
|
||||
];
|
||||
}
|
||||
}
|
29
app/Features/QueryAnimeSeasonListHandler.php
Normal file
29
app/Features/QueryAnimeSeasonListHandler.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\QueryAnimeSeasonListCommand;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Support\CachedData;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\SeasonList\SeasonListRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<QueryAnimeSeasonListCommand, JsonResponse>
|
||||
*/
|
||||
final class QueryAnimeSeasonListHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
public function requestClass(): string
|
||||
{
|
||||
return QueryAnimeSeasonListCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn(MalClient $jikan, ?int $page = null) => $jikan->getSeasonList(new SeasonListRequest())
|
||||
);
|
||||
}
|
||||
}
|
54
app/Features/QueryCurrentAnimeSeasonHandler.php
Normal file
54
app/Features/QueryCurrentAnimeSeasonHandler.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Contracts\RequestHandler;
|
||||
use App\Dto\QueryCurrentAnimeSeasonCommand;
|
||||
use App\Enums\AnimeSeasonEnum;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @implements RequestHandler<QueryCurrentAnimeSeasonCommand, JsonResponse>
|
||||
*/
|
||||
final class QueryCurrentAnimeSeasonHandler extends QueryAnimeSeasonHandlerBase
|
||||
{
|
||||
public function requestClass(): string
|
||||
{
|
||||
return QueryCurrentAnimeSeasonCommand::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function getCurrentSeason() : array
|
||||
{
|
||||
$date = new \DateTime(null, new \DateTimeZone('Asia/Tokyo'));
|
||||
|
||||
$year = (int) $date->format('Y');
|
||||
$month = (int) $date->format('n');
|
||||
|
||||
return match ($month) {
|
||||
in_array($month, range(1, 3)) => [AnimeSeasonEnum::winter(), $year],
|
||||
in_array($month, range(4, 6)) => [AnimeSeasonEnum::spring(), $year],
|
||||
in_array($month, range(7, 9)) => [AnimeSeasonEnum::summer(), $year],
|
||||
in_array($month, range(10, 12)) => [AnimeSeasonEnum::fall(), $year],
|
||||
default => throw new Exception('Could not generate seasonal string'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getSeasonRangeFrom($request): array
|
||||
{
|
||||
/**
|
||||
* @var AnimeSeasonEnum $season
|
||||
* @var int $year
|
||||
*/
|
||||
[$season, $year] = $this->getCurrentSeason();
|
||||
|
||||
return $this->getSeasonRange($year, $season);
|
||||
}
|
||||
}
|
24
app/Features/QuerySpecificAnimeSeasonHandler.php
Normal file
24
app/Features/QuerySpecificAnimeSeasonHandler.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\QuerySpecificAnimeSeasonCommand;
|
||||
use App\Enums\AnimeSeasonEnum;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
|
||||
/**
|
||||
* @extends QueryAnimeSeasonHandlerBase<QuerySpecificAnimeSeasonCommand>
|
||||
*/
|
||||
final class QuerySpecificAnimeSeasonHandler extends QueryAnimeSeasonHandlerBase
|
||||
{
|
||||
public function requestClass(): string
|
||||
{
|
||||
return QuerySpecificAnimeSeasonCommand::class;
|
||||
}
|
||||
|
||||
protected function getSeasonRangeFrom($request): array
|
||||
{
|
||||
return $this->getSeasonRange($request->year, $request->season);
|
||||
}
|
||||
}
|
37
app/Features/QueryUpcomingAnimeSeasonHandler.php
Normal file
37
app/Features/QueryUpcomingAnimeSeasonHandler.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Contracts\AnimeRepository;
|
||||
use App\Contracts\RequestHandler;
|
||||
use App\Dto\QueryUpcomingAnimeSeasonCommand;
|
||||
use App\Http\Resources\V4\AnimeCollection;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @implements RequestHandler<QueryUpcomingAnimeSeasonCommand, JsonResponse>
|
||||
*/
|
||||
final class QueryUpcomingAnimeSeasonHandler implements RequestHandler
|
||||
{
|
||||
public function __construct(protected readonly AnimeRepository $repository)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle($request): JsonResponse
|
||||
{
|
||||
$type = collect($request->all())->has("filter") ? $request->filter : null;
|
||||
$results = $this->repository->getUpcomingSeasonItems($type);
|
||||
$results = $results->paginate($request->limit, ["*"], null, $request->page);
|
||||
|
||||
$animeCollection = new AnimeCollection($results);
|
||||
$response = $animeCollection->response();
|
||||
|
||||
return $response->addJikanCacheFlags($request->getFingerPrint(), CachedData::from($animeCollection->collection));
|
||||
}
|
||||
|
||||
public function requestClass(): string
|
||||
{
|
||||
return QueryUpcomingAnimeSeasonCommand::class;
|
||||
}
|
||||
}
|
@ -3,6 +3,10 @@
|
||||
namespace App\Http\Controllers\V4DB;
|
||||
|
||||
use App\Anime;
|
||||
use App\Dto\QueryAnimeSeasonListCommand;
|
||||
use App\Dto\QueryCurrentAnimeSeasonCommand;
|
||||
use App\Dto\QuerySpecificAnimeSeasonCommand;
|
||||
use App\Dto\QueryUpcomingAnimeSeasonCommand;
|
||||
use App\Http\HttpResponse;
|
||||
use App\Http\QueryBuilder\AnimeSearchQueryBuilder;
|
||||
use App\Http\Resources\V4\AnimeCollection;
|
||||
@ -11,8 +15,6 @@ use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Jikan\Helper\Constants;
|
||||
use Jikan\Model\Common\DateRange;
|
||||
use Jikan\Request\SeasonList\SeasonListRequest;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
|
||||
@ -22,14 +24,38 @@ use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
class SeasonController extends Controller
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/seasons/now",
|
||||
* operationId="getSeasonNow",
|
||||
* tags={"seasons"},
|
||||
*
|
||||
* @OA\Parameter(ref="#/components/parameters/page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/limit"),
|
||||
* @OA\Parameter(
|
||||
* name="filter",
|
||||
* description="Entry types",
|
||||
* in="query",
|
||||
* @OA\Schema(type="string",enum={"tv","movie","ova","special","ona","music"})
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response="200",
|
||||
* description="Returns current seasonal anime",
|
||||
* @OA\JsonContent(
|
||||
* ref="#/components/schemas/anime_search"
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="400",
|
||||
* description="Error: Bad request. When required parameters were not supplied.",
|
||||
* ),
|
||||
* ),
|
||||
* @throws Exception
|
||||
*/
|
||||
private const VALID_SEASONS = [
|
||||
'Summer',
|
||||
'Spring',
|
||||
'Winter',
|
||||
'Fall'
|
||||
];
|
||||
public function now(QueryCurrentAnimeSeasonCommand $command)
|
||||
{
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
@ -59,6 +85,7 @@ class SeasonController extends Controller
|
||||
* ),
|
||||
*
|
||||
* @OA\Parameter(ref="#/components/parameters/page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/limit"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response="200",
|
||||
@ -72,94 +99,11 @@ class SeasonController extends Controller
|
||||
* description="Error: Bad request. When required parameters were not supplied.",
|
||||
* ),
|
||||
* ),
|
||||
*
|
||||
* @OA\Get(
|
||||
* path="/seasons/now",
|
||||
* operationId="getSeasonNow",
|
||||
* tags={"seasons"},
|
||||
*
|
||||
* @OA\Parameter(ref="#/components/parameters/page"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response="200",
|
||||
* description="Returns current seasonal anime",
|
||||
* @OA\JsonContent(
|
||||
* ref="#/components/schemas/anime_search"
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="400",
|
||||
* description="Error: Bad request. When required parameters were not supplied.",
|
||||
* ),
|
||||
* ),
|
||||
* @throws Exception
|
||||
*/
|
||||
public function main(Request $request, ?int $year = null, ?string $season = null)
|
||||
public function main(QuerySpecificAnimeSeasonCommand $command)
|
||||
{
|
||||
$maxResultsPerPage = env('MAX_RESULTS_PER_PAGE', 30);
|
||||
$page = $request->get('page') ?? 1;
|
||||
$limit = $request->get('limit') ?? $maxResultsPerPage;
|
||||
$type = $request->get('filter');
|
||||
|
||||
if (!empty($limit)) {
|
||||
$limit = (int) $limit;
|
||||
|
||||
if ($limit <= 0) {
|
||||
$limit = 1;
|
||||
}
|
||||
|
||||
if ($limit > $maxResultsPerPage) {
|
||||
$limit = $maxResultsPerPage;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_null($season)) {
|
||||
$season = ucfirst(
|
||||
strtolower($season)
|
||||
);
|
||||
}
|
||||
|
||||
if (!is_null($year)) {
|
||||
$year = (int) $year;
|
||||
}
|
||||
|
||||
if (!is_null($season)
|
||||
&& !\in_array($season, self::VALID_SEASONS)) {
|
||||
return HttpResponse::badRequest($request);
|
||||
}
|
||||
|
||||
if (is_null($season) && is_null($year)) {
|
||||
list($season, $year) = $this->getSeasonStr();
|
||||
}
|
||||
|
||||
$range = $this->getSeasonRange($year, $season);
|
||||
|
||||
$results = Anime::query()
|
||||
->whereBetween('aired.from', [$range['from'], $range['to']]);
|
||||
|
||||
if (array_key_exists(strtolower($type), AnimeSearchQueryBuilder::MAP_TYPES)) {
|
||||
$results = $results
|
||||
->where('type', AnimeSearchQueryBuilder::MAP_TYPES[$type]);
|
||||
}
|
||||
|
||||
$results = $results
|
||||
->orderBy('members', 'desc')
|
||||
->paginate(
|
||||
$limit,
|
||||
['*'],
|
||||
null,
|
||||
$page
|
||||
);
|
||||
|
||||
$response = (new AnimeCollection(
|
||||
$results
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -208,31 +152,9 @@ class SeasonController extends Controller
|
||||
* ),
|
||||
* ),
|
||||
*/
|
||||
public function archive(Request $request)
|
||||
public function archive(QueryAnimeSeasonListCommand $command)
|
||||
{
|
||||
$results = DB::table($this->getRouteTable($request))
|
||||
->where('request_hash', $this->fingerprint)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$items = $this->jikan->getSeasonList(new SeasonListRequest());
|
||||
$response = \json_decode($this->serializer->serialize($items, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new ResultsResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -249,6 +171,7 @@ class SeasonController extends Controller
|
||||
* ),
|
||||
*
|
||||
* @OA\Parameter(ref="#/components/parameters/page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/limit"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response="200",
|
||||
@ -264,98 +187,8 @@ class SeasonController extends Controller
|
||||
* ),
|
||||
* @throws Exception
|
||||
*/
|
||||
public function later(Request $request)
|
||||
public function later(QueryUpcomingAnimeSeasonCommand $command)
|
||||
{
|
||||
$maxResultsPerPage = env('MAX_RESULTS_PER_PAGE', 30);
|
||||
$page = $request->get('page') ?? 1;
|
||||
$limit = $request->get('limit') ?? $maxResultsPerPage;
|
||||
$type = $request->get('filter');
|
||||
|
||||
|
||||
if (!empty($limit)) {
|
||||
$limit = (int) $limit;
|
||||
|
||||
if ($limit <= 0) {
|
||||
$limit = 1;
|
||||
}
|
||||
|
||||
if ($limit > $maxResultsPerPage) {
|
||||
$limit = $maxResultsPerPage;
|
||||
}
|
||||
}
|
||||
|
||||
$results = Anime::query()
|
||||
->where('status', 'Not yet aired');
|
||||
|
||||
if (array_key_exists(strtolower($type), AnimeSearchQueryBuilder::MAP_TYPES)) {
|
||||
$results = $results
|
||||
->where('type', AnimeSearchQueryBuilder::MAP_TYPES[$type]);
|
||||
}
|
||||
|
||||
$season = 'Later';
|
||||
$results = $results
|
||||
->orderBy('members', 'desc')
|
||||
->paginate(
|
||||
$limit,
|
||||
['*'],
|
||||
null,
|
||||
$page
|
||||
);
|
||||
|
||||
$response = (new AnimeCollection(
|
||||
$results
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function getSeasonStr() : array
|
||||
{
|
||||
$date = new \DateTime(null, new \DateTimeZone('Asia/Tokyo'));
|
||||
|
||||
$year = (int) $date->format('Y');
|
||||
$month = (int) $date->format('n');
|
||||
|
||||
switch ($month) {
|
||||
case \in_array($month, range(1, 3)):
|
||||
return ['Winter', $year];
|
||||
case \in_array($month, range(4, 6)):
|
||||
return ['Spring', $year];
|
||||
case \in_array($month, range(7, 9)):
|
||||
return ['Summer', $year];
|
||||
case \in_array($month, range(10, 12)):
|
||||
return ['Fall', $year];
|
||||
default: throw new Exception('Could not generate seasonal string');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $year
|
||||
* @param string $season
|
||||
* @return string[]
|
||||
*/
|
||||
private function getSeasonRange(int $year, string $season) : array
|
||||
{
|
||||
[$monthStart, $monthEnd] = match ($season) {
|
||||
'Winter' => [1, 3],
|
||||
'Spring' => [4, 6],
|
||||
'Summer' => [7, 9],
|
||||
'Fall' => [10, 12],
|
||||
default => throw new BadRequestException('Invalid season supplied'),
|
||||
};
|
||||
|
||||
return [
|
||||
'from' => Carbon::createFromDate($year, $monthStart, 1)->toAtomString(),
|
||||
'to' => Carbon::createFromDate($year, $monthEnd, 1)->modify('last day of this month')->toAtomString()
|
||||
];
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
}
|
||||
|
@ -69,8 +69,6 @@ use App\Features;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
private \ReflectionClass $simpleSearchQueryBuilderClassReflection;
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
* @throws \Illuminate\Contracts\Container\BindingResolutionException
|
||||
@ -78,7 +76,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerMacros();
|
||||
$this->simpleSearchQueryBuilderClassReflection = new \ReflectionClass(SimpleSearchQueryBuilder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -270,7 +267,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
Features\QueryAnimeRecommendationsHandler::class => $unitOfWorkInstance->documents("recommendations"),
|
||||
Features\QueryMangaRecommendationsHandler::class => $unitOfWorkInstance->documents("recommendations"),
|
||||
Features\QueryAnimeReviewsHandler::class => $unitOfWorkInstance->documents("reviews"),
|
||||
Features\QueryMangaReviewsHandler::class => $unitOfWorkInstance->documents("reviews")
|
||||
Features\QueryMangaReviewsHandler::class => $unitOfWorkInstance->documents("reviews"),
|
||||
Features\QueryAnimeSeasonListHandler::class => $unitOfWorkInstance->documents("season_archive")
|
||||
];
|
||||
|
||||
foreach ($requestHandlersWithScraperService as $handlerClass => $repositoryInstance) {
|
||||
@ -290,7 +288,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
Features\QueryRandomCharacterHandler::class,
|
||||
Features\QueryRandomPersonHandler::class,
|
||||
Features\QueryRandomUserHandler::class,
|
||||
Features\QueryAnimeSchedulesHandler::class
|
||||
Features\QueryAnimeSchedulesHandler::class,
|
||||
Features\QueryCurrentAnimeSeasonHandler::class,
|
||||
Features\QuerySpecificAnimeSeasonHandler::class,
|
||||
Features\QueryUpcomingAnimeSeasonHandler::class
|
||||
];
|
||||
|
||||
foreach ($requestHandlersWithNoDependencies as $handlerClass) {
|
||||
|
@ -116,4 +116,15 @@ final class DefaultAnimeRepository extends DatabaseRepository implements AnimeRe
|
||||
|
||||
return $queryable->orderBy("members", "desc");
|
||||
}
|
||||
|
||||
public function getUpcomingSeasonItems(?AnimeTypeEnum $type = null): EloquentBuilder
|
||||
{
|
||||
$queryable = $this->queryable(true)->where("status", AnimeStatusEnum::upcoming()->label);
|
||||
|
||||
if (!is_null($type)) {
|
||||
$queryable = $queryable->where("type", $type->label);
|
||||
}
|
||||
|
||||
return $queryable->orderBy("members", "desc");
|
||||
}
|
||||
}
|
||||
|
16
app/Rules/Attributes/MaxLimitWithFallback.php
Normal file
16
app/Rules/Attributes/MaxLimitWithFallback.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules\Attributes;
|
||||
|
||||
use App\Rules\MaxResultsPerPageRule;
|
||||
use Attribute;
|
||||
use Spatie\LaravelData\Attributes\Validation\Rule;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY)]
|
||||
final class MaxLimitWithFallback extends Rule
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new MaxResultsPerPageRule());
|
||||
}
|
||||
}
|
46
app/Rules/MaxResultsPerPageRule.php
Normal file
46
app/Rules/MaxResultsPerPageRule.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Support\Env;
|
||||
|
||||
final class MaxResultsPerPageRule implements Rule
|
||||
{
|
||||
private mixed $value;
|
||||
private int $fallbackLimit;
|
||||
|
||||
public function __construct($fallbackLimit = 25)
|
||||
{
|
||||
$this->fallbackLimit = $fallbackLimit;
|
||||
}
|
||||
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_int($value)) {
|
||||
$value = intval($value);
|
||||
}
|
||||
|
||||
if ($value > $this->maxResultsPerPage()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function message(): array|string
|
||||
{
|
||||
return "Value {$this->value} is higher than the configured '{$this->maxResultsPerPage()}' max value.";
|
||||
}
|
||||
|
||||
private function maxResultsPerPage(): int
|
||||
{
|
||||
return (int) Env::get("MAX_RESULTS_PER_PAGE", $this->fallbackLimit);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace App\Support;
|
||||
|
||||
use App\Concerns\ScraperCacheTtl;
|
||||
use App\JikanApiModel;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class CachedData
|
||||
@ -73,6 +74,10 @@ final class CachedData
|
||||
|
||||
$result = $this->scraperResult->first();
|
||||
|
||||
if ($result instanceof JikanApiModel && !is_null($result->getAttributeValue("modifiedAt"))) {
|
||||
return (int) $result["modifiedAt"]->toDateTime()->format("U");
|
||||
}
|
||||
|
||||
if (is_array($result) && array_key_exists("modifiedAt", $result)) {
|
||||
return (int) $result["modifiedAt"]->toDateTime()->format("U");
|
||||
}
|
||||
|
@ -256,7 +256,7 @@ $router->group(
|
||||
]);
|
||||
|
||||
$router->get('/now', [
|
||||
'uses' => 'SeasonController@main'
|
||||
'uses' => 'SeasonController@now'
|
||||
]);
|
||||
|
||||
$router->get('/upcoming', [
|
||||
|
Loading…
x
Reference in New Issue
Block a user