wip - anime season controller refactor, central limit parameter validation

- fixed cache validation
This commit is contained in:
pushrbx 2023-01-22 19:47:21 +00:00
parent 79b7a0f657
commit 37c273fbab
34 changed files with 494 additions and 294 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View 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;
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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;
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Dto;
final class QueryCurrentAnimeSeasonCommand extends QueryAnimeSeasonCommand
{
}

View File

@ -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;

View 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."
];
}
}

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Dto;
final class QueryUpcomingAnimeSeasonCommand extends QueryAnimeSeasonCommand
{
}

View File

@ -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)]

View 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)
];
}
}

View 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())
);
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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");
}
}

View 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());
}
}

View 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);
}
}

View File

@ -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");
}

View File

@ -256,7 +256,7 @@ $router->group(
]);
$router->get('/now', [
'uses' => 'SeasonController@main'
'uses' => 'SeasonController@now'
]);
$router->get('/upcoming', [