wip - mediator refactor

- anime schedules - validation and corrections
- refactorings around "augmentResponse" - macro usage instead
This commit is contained in:
pushrbx 2023-01-21 01:53:37 +00:00
parent c564de979d
commit 49ebc8f581
76 changed files with 1318 additions and 842 deletions

View File

@ -4,6 +4,7 @@ namespace App\Concerns;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
use Spatie\LaravelData\Resolvers\DataFromSomethingResolver;
/**
* Helper trait for data transfer objects
@ -16,7 +17,8 @@ trait HasRequestFingerprint
public static function fromRequest(Request $request): ?static
{
$result = new self();
$result = app(DataFromSomethingResolver::class)
->withoutMagicalCreation()->execute(self::class, $request);
$result->fingerprint = HttpHelper::resolveRequestFingerprint($request);
return $result;
}

View File

@ -2,10 +2,12 @@
namespace App\Concerns;
use Illuminate\Support\Env;
trait ScraperCacheTtl
{
protected function cacheTtl(): int
protected static function cacheTtl(): int
{
return (int) env('CACHE_DEFAULT_EXPIRE');
return (int) Env::get('CACHE_DEFAULT_EXPIRE');
}
}

View File

@ -3,7 +3,8 @@
namespace App\Contracts;
use App\Anime;
use \Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use App\Enums\AnimeScheduleFilterEnum;
use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder;
use \Laravel\Scout\Builder as ScoutBuilder;
/**
@ -22,4 +23,10 @@ interface AnimeRepository extends Repository
public function orderByFavoriteCount(): EloquentBuilder|ScoutBuilder;
public function orderByRank(): EloquentBuilder|ScoutBuilder;
public function getCurrentlyAiring(
?AnimeScheduleFilterEnum $filter = null,
bool $kids = false,
bool $sfw = false
): EloquentBuilder;
}

View File

@ -33,6 +33,4 @@ interface CachedScraperService
public function findByKey(string $key, mixed $val, string $cacheKey): CachedData;
public function get(string $cacheKey): CachedData;
public function augmentResponse(JsonResponse|Response $response, string $cacheKey, CachedData $scraperResults): JsonResponse|Response;
}

View File

@ -3,7 +3,7 @@
namespace App\Contracts;
use App\Manga;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder;
use Laravel\Scout\Builder as ScoutBuilder;
/**
@ -20,4 +20,6 @@ interface MangaRepository extends Repository
public function orderByFavoriteCount(): EloquentBuilder|ScoutBuilder;
public function orderByRank(): EloquentBuilder|ScoutBuilder;
public function exceptItemsWithAdultRating(): EloquentBuilder|ScoutBuilder;
}

View File

@ -33,4 +33,6 @@ interface Repository extends RepositoryQuery
public function scrape(int|string $id): array;
public function insert(array $attributes): bool;
public function random(int $numberOfRandomItems = 1): Collection;
}

View File

@ -3,6 +3,7 @@
namespace App\Dto;
use Illuminate\Http\JsonResponse;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Optional;
@ -11,6 +12,6 @@ use Spatie\LaravelData\Optional;
*/
final class AnimeEpisodesLookupCommand extends LookupDataCommand
{
#[Numeric]
#[Numeric, Min(1)]
public int|Optional $page;
}

View File

@ -3,6 +3,7 @@
namespace App\Dto;
use Illuminate\Http\JsonResponse;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Optional;
@ -11,6 +12,6 @@ use Spatie\LaravelData\Optional;
*/
final class AnimeNewsLookupCommand extends LookupDataCommand
{
#[Numeric]
#[Numeric, Min(1)]
public int|Optional $page;
}

View File

@ -7,6 +7,7 @@ use App\Enums\MediaReviewsSortEnum;
use Illuminate\Http\JsonResponse;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
@ -16,7 +17,7 @@ use Spatie\LaravelData\Optional;
*/
final class AnimeReviewsLookupCommand extends LookupDataCommand
{
#[Numeric]
#[Numeric, Min(1)]
public int|Optional $page;
#[WithCast(EnumCast::class, MediaReviewsSortEnum::class)]

View File

@ -3,6 +3,7 @@
namespace App\Dto;
use Illuminate\Http\JsonResponse;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Optional;
@ -11,6 +12,6 @@ use Spatie\LaravelData\Optional;
*/
final class AnimeUserUpdatesLookupCommand extends LookupDataCommand
{
#[Numeric]
#[Numeric, Min(1)]
public int|Optional $page;
}

View File

@ -4,6 +4,7 @@ namespace App\Dto;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Optional;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
/**
@ -11,6 +12,6 @@ use Spatie\LaravelData\Attributes\Validation\Numeric;
*/
final class AnimeVideosEpisodesLookupCommand extends LookupDataCommand
{
#[Numeric]
#[Numeric, Min(1)]
public int|Optional $page;
}

View File

@ -3,6 +3,7 @@
namespace App\Dto;
use Illuminate\Http\JsonResponse;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Optional;
@ -11,6 +12,6 @@ use Spatie\LaravelData\Optional;
*/
final class ClubMembersLookupCommand extends LookupDataCommand
{
#[Numeric]
#[Numeric, Min(1)]
public int|Optional $page;
}

View File

@ -4,6 +4,7 @@ namespace App\Dto;
use Illuminate\Http\JsonResponse;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Optional;
@ -12,6 +13,6 @@ use Spatie\LaravelData\Optional;
*/
final class MangaNewsLookupCommand extends LookupDataCommand
{
#[Numeric]
#[Numeric, Min(1)]
public int|Optional $page;
}

View File

@ -8,6 +8,7 @@ use App\Enums\MediaReviewsSortEnum;
use Illuminate\Http\JsonResponse;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
@ -17,7 +18,7 @@ use Spatie\LaravelData\Optional;
*/
final class MangaReviewsLookupCommand extends LookupDataCommand
{
#[Numeric]
#[Numeric, Min(1)]
public int|Optional $page;
#[WithCast(EnumCast::class, MediaReviewsSortEnum::class)]

View File

@ -4,6 +4,7 @@ namespace App\Dto;
use Illuminate\Http\JsonResponse;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Optional;
@ -12,6 +13,6 @@ use Spatie\LaravelData\Optional;
*/
final class MangaUserUpdatesLookupCommand extends LookupDataCommand
{
#[Numeric]
#[Numeric, Min(1)]
public int|Optional $page;
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class PersonAnimeLookupCommand extends LookupDataCommand
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class PersonFullLookupCommand extends LookupDataCommand
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class PersonLookupCommand extends LookupDataCommand
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class PersonMangaLookupCommand extends LookupDataCommand
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class PersonPicturesLookupCommand extends LookupDataCommand
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class PersonVoicesLookupCommand extends LookupDataCommand
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class ProducerExternalLookupCommand extends LookupDataCommand
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class ProducerFullLookupCommand extends LookupDataCommand
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class ProducerLookupCommand extends LookupDataCommand
{
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Dto;
use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\ResultsResource;
use Spatie\LaravelData\Data;
/**
* @implements DataRequest<ResultsResource>
*/
final class QueryAnimeRecommendationsCommand extends Data implements DataRequest
{
use HasRequestFingerprint;
}

View File

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

View File

@ -0,0 +1,64 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest;
use App\Enums\AnimeScheduleFilterEnum;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Nullable;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Data;
/**
* @implements DataRequest<JsonResponse>
*/
final class QueryAnimeSchedulesCommand extends Data implements DataRequest
{
use HasRequestFingerprint;
#[Numeric, Min(1)]
public int $page = 1;
#[IntegerType, Min(1), Nullable]
public ?int $limit;
#[BooleanType]
public bool $kids = false;
#[BooleanType]
public bool $sfw = false;
#[WithCast(EnumCast::class, AnimeScheduleFilterEnum::class)]
public ?AnimeScheduleFilterEnum $filter;
public static function rules(...$args): array
{
return [
"filter" => [new EnumRule(AnimeScheduleFilterEnum::class), new Nullable()]
];
}
/** @noinspection PhpUnused */
public static function fromRequestAndDay(Request $request, ?string $day): self
{
/**
* @var QueryAnimeSchedulesCommand $data
*/
$data = self::fromRequest($request);
if (!is_null($day)) {
$data->filter = AnimeScheduleFilterEnum::from($day);
}
return $data;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Dto;
use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\ResultsResource;
use Spatie\LaravelData\Data;
/**
* @implements DataRequest<ResultsResource>
*/
final class QueryMangaRecommendationsCommand extends Data implements DataRequest
{
use HasRequestFingerprint;
}

View File

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

View File

@ -0,0 +1,18 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\AnimeResource;
use Illuminate\Support\Optional;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Data;
/**
* @implements DataRequest<AnimeResource>
*/
final class QueryRandomAnimeCommand extends Data implements DataRequest
{
#[BooleanType]
public bool|Optional $sfw;
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\CharacterResource;
use Spatie\LaravelData\Data;
/**
* @implements DataRequest<CharacterResource>
*/
final class QueryRandomCharacterCommand extends Data implements DataRequest
{
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\MangaResource;
use Illuminate\Support\Optional;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Data;
/**
* @implements DataRequest<MangaResource>
*/
final class QueryRandomMangaCommand extends Data implements DataRequest
{
#[BooleanType]
public bool|Optional $sfw;
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\PersonResource;
use Spatie\LaravelData\Data;
/**
* @implements DataRequest<PersonResource>
*/
final class QueryRandomPersonCommand extends Data implements DataRequest
{
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\ProfileResource;
use Spatie\LaravelData\Data;
/**
* @implements DataRequest<ProfileResource>
*/
final class QueryRandomUserCommand extends Data implements DataRequest
{
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest;
use App\Enums\MediaReviewsSortEnum;
use App\Http\Resources\V4\ResultsResource;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional;
/**
* @implements DataRequest<ResultsResource>
*/
abstract class QueryReviewsCommand extends Data implements DataRequest
{
use HasRequestFingerprint;
#[Numeric, Min(1)]
public int|Optional $page;
#[WithCast(EnumCast::class, MediaReviewsSortEnum::class)]
public MediaReviewsSortEnum|Optional $sort;
#[BooleanType]
public bool|Optional $spoilers;
#[BooleanType]
public bool|Optional $preliminary;
public static function rules(): array
{
return [
"sort" => [new EnumRule(MediaReviewsSortEnum::class)]
];
}
}

View File

@ -3,11 +3,15 @@
namespace App\Dto;
use Illuminate\Support\Optional;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Data;
abstract class QueryTopItemsCommand extends Data
{
#[Numeric, Min(1)]
public int|Optional $page;
#[Numeric, Min(1)]
public int|Optional $limit;
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self monday()
* @method static self tuesday()
* @method static self wednesday()
* @method static self thursday()
* @method static self friday()
* @method static self saturday()
* @method static self sunday()
* @method static self other()
* @method static self unknown()
*/
final class AnimeScheduleFilterEnum extends Enum
{
public function isWeekDay(): bool
{
return $this->value !== self::other()->value && $this->value !== self::unknown()->value;
}
protected static function labels(): array
{
return [
...collect(self::values())->map(fn ($x) => ucfirst($x))->toArray(),
"other" => "Not scheduled once per week",
"unknown" => "Unknown"
];
}
}

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 ReviewTypeEnum extends Enum
{
}

View File

@ -34,7 +34,7 @@ abstract class ItemLookupHandler extends Data implements RequestHandler
$results = $this->scraperService->find($request->id, $requestFingerprint);
$resource = $this->resource($results->collect());
return $this->scraperService->augmentResponse($resource->response(), $requestFingerprint, $results);
return $resource->response()->addJikanCacheFlags($requestFingerprint, $results);
}
protected abstract function resource(Collection $results): JsonResource;

View File

@ -0,0 +1,24 @@
<?php
namespace App\Features;
use App\Dto\PersonAnimeLookupCommand;
use App\Http\Resources\V4\PersonAnimeCollection;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
/**
* @extends ItemLookupHandler<PersonAnimeLookupCommand, JsonResponse>
*/
final class PersonAnimeLookupHandler extends ItemLookupHandler
{
public function requestClass(): string
{
return PersonAnimeLookupCommand::class;
}
protected function resource(Collection $results): PersonAnimeCollection
{
return new PersonAnimeCollection($results->offsetGetFirst("anime_staff_positions"));
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Features;
use App\Dto\PersonFullLookupCommand;
use App\Http\Resources\V4\PersonFullResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends ItemLookupHandler<PersonFullLookupCommand, JsonResponse>
*/
final class PersonFullLookupHandler extends ItemLookupHandler
{
public function requestClass(): string
{
return PersonFullLookupCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new PersonFullResource($results->first());
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Features;
use App\Dto\PersonLookupCommand;
use App\Http\Resources\V4\PersonResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends ItemLookupHandler<PersonLookupCommand, JsonResponse>
*/
final class PersonLookupHandler extends ItemLookupHandler
{
public function requestClass(): string
{
return PersonLookupCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new PersonResource($results->first());
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Features;
use App\Dto\PersonMangaLookupCommand;
use App\Http\Resources\V4\PersonMangaCollection;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends ItemLookupHandler<PersonMangaLookupCommand, JsonResponse>
*/
final class PersonMangaLookupHandler extends ItemLookupHandler
{
public function requestClass(): string
{
return PersonMangaLookupCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new PersonMangaCollection($results->offsetGetFirst("published_manga"));
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Features;
use App\Dto\PersonPicturesLookupCommand;
use App\Http\Resources\V4\PicturesResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
use App\Support\CachedData;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\Person\PersonPicturesRequest;
/**
* @extends RequestHandlerWithScraperCache<PersonPicturesLookupCommand, JsonResponse>
*/
final class PersonPicturesLookupHandler extends RequestHandlerWithScraperCache
{
public function requestClass(): string
{
return PersonPicturesLookupCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new PicturesResource($results->first());
}
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
$id = $requestParams->get("id");
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => collect(
["pictures" => $jikan->getPersonPictures(new PersonPicturesRequest($id))]
)
);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Features;
use App\Dto\PersonVoicesLookupCommand;
use App\Http\Resources\V4\PersonVoicesCollection;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends ItemLookupHandler<PersonVoicesLookupCommand, JsonResponse>
*/
final class PersonVoicesLookupHandler extends ItemLookupHandler
{
public function requestClass(): string
{
return PersonVoicesLookupCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new PersonVoicesCollection($results->offsetGetFirst("voice_acting_roles"));
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Features;
use App\Dto\ProducerExternalLookupCommand;
use App\Http\Resources\V4\ExternalLinksResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends ItemLookupHandler<ProducerExternalLookupCommand, JsonResponse>
*/
final class ProducerExternalLookupHandler extends ItemLookupHandler
{
public function requestClass(): string
{
return ProducerExternalLookupCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new ExternalLinksResource($results->first());
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Features;
use App\Dto\ProducerFullLookupCommand;
use App\Http\Resources\V4\ProducerFullResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends ItemLookupHandler<ProducerFullLookupCommand, JsonResponse>
*/
final class ProducerFullLookupHandler extends ItemLookupHandler
{
public function requestClass(): string
{
return ProducerFullLookupCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new ProducerFullResource($results->first());
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Features;
use App\Dto\ProducerLookupCommand;
use App\Http\Resources\V4\ProducerResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends ItemLookupHandler<ProducerLookupCommand, JsonResponse>
*/
final class ProducerLookupHandler extends ItemLookupHandler
{
public function requestClass(): string
{
return ProducerLookupCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new ProducerResource($results->first());
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Features;
use App\Dto\QueryAnimeRecommendationsCommand;
use Jikan\Helper\Constants;
/**
* @extends QueryRecommendationsHandler<QueryAnimeRecommendationsCommand>
*/
final class QueryAnimeRecommendationsHandler extends QueryRecommendationsHandler
{
public function requestClass(): string
{
return QueryAnimeRecommendationsCommand::class;
}
protected function recommendationType(): string
{
return Constants::RECENT_RECOMMENDATION_ANIME;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Features;
use App\Dto\QueryAnimeReviewsCommand;
use App\Enums\ReviewTypeEnum;
/**
* @extends QueryReviewsHandler<QueryAnimeReviewsCommand>
*/
final class QueryAnimeReviewsHandler extends QueryReviewsHandler
{
protected function reviewType(): ReviewTypeEnum
{
return ReviewTypeEnum::anime();
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryAnimeReviewsCommand::class;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Features;
use App\Contracts\AnimeRepository;
use App\Contracts\RequestHandler;
use App\Dto\QueryAnimeSchedulesCommand;
use App\Http\Resources\V4\AnimeCollection;
use App\Support\CachedData;
use Illuminate\Support\Env;
/**
* @implements RequestHandler<QueryAnimeSchedulesCommand, AnimeCollection>
*/
final class QueryAnimeSchedulesHandler implements RequestHandler
{
public function __construct(private readonly AnimeRepository $repository)
{
}
/**
* @inheritDoc
*/
public function handle($request)
{
$limit = intval($request->limit ?? Env::get("MAX_RESULTS_PER_PAGE", 25));
$results = $this->repository->getCurrentlyAiring($request->filter, $request->kids, $request->sfw);
$results = $results->paginate(
$limit,
["*"],
null,
$request->page
);
$animeCollection = new AnimeCollection(
$results
);
$response = $animeCollection->response();
return $response->addJikanCacheFlags($request->getFingerPrint(), CachedData::from($animeCollection->collection));
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryAnimeSchedulesCommand::class;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Features;
use App\Dto\QueryMangaRecommendationsCommand;
use Jikan\Helper\Constants;
/**
* @extends QueryRecommendationsHandler<QueryMangaRecommendationsCommand>
*/
final class QueryMangaRecommendationsHandler extends QueryRecommendationsHandler
{
protected function recommendationType(): string
{
return Constants::RECENT_RECOMMENDATION_MANGA;
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryMangaRecommendationsCommand::class;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Features;
use App\Dto\QueryMangaReviewsCommand;
use App\Enums\ReviewTypeEnum;
/**
* @extends QueryReviewsHandler<QueryMangaReviewsCommand>
*/
final class QueryMangaReviewsHandler extends QueryReviewsHandler
{
protected function reviewType(): ReviewTypeEnum
{
return ReviewTypeEnum::manga();
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryMangaReviewsCommand::class;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Features;
use App\Contracts\AnimeRepository;
use App\Contracts\RequestHandler;
use App\Dto\QueryRandomAnimeCommand;
use App\Http\Resources\V4\AnimeResource;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Optional;
/**
* @implements RequestHandler<QueryRandomAnimeCommand, AnimeResource>
*/
final class QueryRandomAnimeHandler implements RequestHandler
{
public function __construct(
private readonly AnimeRepository $repository
)
{
}
/**
* @inheritDoc
*/
public function handle($request): AnimeResource
{
$sfw = Optional::create() !== $request->sfw ? $request->sfw : null;
/**
* @var Collection $results;
*/
if ($sfw) {
$results = $this->repository->exceptItemsWithAdultRating()->random();
} else {
$results = $this->repository->random();
}
return new AnimeResource(
$results->first()
);
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryRandomAnimeCommand::class;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Features;
use App\Contracts\CharacterRepository;
use App\Dto\QueryRandomCharacterCommand;
use App\Http\Resources\V4\CharacterResource;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends QueryRandomItemHandler<QueryRandomCharacterCommand, CharacterResource>
*/
final class QueryRandomCharacterHandler extends QueryRandomItemHandler
{
public function __construct(CharacterRepository $repository)
{
parent::__construct($repository);
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryRandomCharacterCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new CharacterResource($results->first());
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Features;
use App\Contracts\Repository;
use App\Contracts\RequestHandler;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
/**
* @template TRequest of Data
* @template TResponse of ResourceCollection|JsonResource|Response
* @implements RequestHandler<TRequest, TResponse>
*/
abstract class QueryRandomItemHandler implements RequestHandler
{
protected function __construct(protected readonly Repository $repository)
{
}
/**
* @inheritDoc
*/
public function handle($request)
{
$results = $this->repository->random();
return $this->resource($results);
}
protected abstract function resource(Collection $results): JsonResource;
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Features;
use App\Contracts\MangaRepository;
use App\Contracts\RequestHandler;
use App\Dto\QueryRandomMangaCommand;
use App\Http\Resources\V4\MangaResource;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Optional;
/**
* @implements RequestHandler<QueryRandomMangaCommand, MangaResource>
*/
final class QueryRandomMangaHandler implements RequestHandler
{
public function __construct(
private readonly MangaRepository $repository
)
{
}
/**
* @inheritDoc
*/
public function handle($request)
{
$sfw = Optional::create() !== $request->sfw ? $request->sfw : null;
/**
* @var Collection $results;
*/
if ($sfw) {
$results = $this->repository->exceptItemsWithAdultRating()->random();
} else {
$results = $this->repository->random();
}
return new MangaResource(
$results->first()
);
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryRandomMangaCommand::class;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Features;
use App\Contracts\PeopleRepository;
use App\Dto\QueryRandomPersonCommand;
use App\Http\Resources\V4\PersonResource;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends QueryRandomItemHandler<QueryRandomPersonCommand, PersonResource>
*/
final class QueryRandomPersonHandler extends QueryRandomItemHandler
{
public function __construct(PeopleRepository $repository)
{
parent::__construct($repository);
}
protected function resource(Collection $results): JsonResource
{
return new PersonResource(
$results->first()
);
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryRandomPersonCommand::class;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Features;
use App\Contracts\UserRepository;
use App\Dto\QueryRandomUserCommand;
use App\Http\Resources\V4\ProfileResource;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends QueryRandomItemHandler<QueryRandomUserCommand, ProfileResource>
*/
final class QueryRandomUserHandler extends QueryRandomItemHandler
{
public function __construct(UserRepository $repository)
{
parent::__construct($repository);
}
protected function resource(Collection $results): JsonResource
{
return new ProfileResource(
$results->first()
);
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryRandomUserCommand::class;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Features;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Support\Collection;
use App\Support\CachedData;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\Recommendations\RecentRecommendationsRequest;
/**
* @template TRequest of DataRequest<ResultsResource>
* @extends RequestHandlerWithScraperCache<TRequest, ResultsResource>
*/
abstract class QueryRecommendationsHandler extends RequestHandlerWithScraperCache
{
protected abstract function recommendationType(): string;
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => $jikan->getRecentRecommendations(new RecentRecommendationsRequest(
$this->recommendationType(), $page
)),
$requestParams->get("page", 1)
);
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Features;
use App\Contracts\DataRequest;
use App\Enums\ReviewTypeEnum;
use App\Features\Concerns\ResolvesMediaReviewParams;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Support\Collection;
use App\Support\CachedData;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\Reviews\ReviewsRequest;
/**
* @template TRequest of DataRequest<ResultsResource>
* @extends RequestHandlerWithScraperCache<TRequest, ResultsResource>
*/
abstract class QueryReviewsHandler extends RequestHandlerWithScraperCache
{
use ResolvesMediaReviewParams;
protected abstract function reviewType(): ReviewTypeEnum;
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
$reviewRequestParams = $this->getReviewRequestParams($requestParams);
extract($reviewRequestParams);
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => $jikan->getReviews(
new ReviewsRequest(
$this->reviewType()->value,
$page,
$sort,
$spoilers,
$preliminary
)
)
);
}
}

View File

@ -53,6 +53,6 @@ abstract class RequestHandlerWithScraperCache implements RequestHandler
{
$finalResults = $results->collect();
$response = $this->resource($finalResults)->response();
return $this->scraperService->augmentResponse($response, $requestFingerPrint, $results);
return $response->addJikanCacheFlags($requestFingerPrint, $results);
}
}

View File

@ -2,22 +2,13 @@
namespace App\Http\Controllers\V4DB;
use App\Character;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\PersonAnimeCollection;
use App\Http\Resources\V4\PersonAnimeResource;
use App\Http\Resources\V4\PersonMangaCollection;
use App\Http\Resources\V4\PersonVoiceResource;
use App\Http\Resources\V4\PersonVoicesCollection;
use App\Http\Resources\V4\PicturesResource;
use App\Person;
use App\Dto\PersonAnimeLookupCommand;
use App\Dto\PersonFullLookupCommand;
use App\Dto\PersonLookupCommand;
use App\Dto\PersonMangaLookupCommand;
use App\Dto\PersonPicturesLookupCommand;
use App\Dto\PersonVoicesLookupCommand;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Character\CharacterPicturesRequest;
use Jikan\Request\Person\PersonRequest;
use Jikan\Request\Person\PersonPicturesRequest;
use MongoDB\BSON\UTCDateTime;
class PersonController extends Controller
{
@ -53,59 +44,8 @@ class PersonController extends Controller
*/
public function full(Request $request, int $id)
{
$results = Person::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Person::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Person::create($response);
}
if ($this->isExpired($request, $results)) {
Person::query()
->where('mal_id', $id)
->update($response);
}
$results = Person::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\PersonFullResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
$command = PersonFullLookupCommand::from($request, $id);
return $this->mediator->send($command);
}
/**
@ -140,59 +80,8 @@ class PersonController extends Controller
*/
public function main(Request $request, int $id)
{
$results = Person::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Person::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Person::create($response);
}
if ($this->isExpired($request, $results)) {
Person::query()
->where('mal_id', $id)
->update($response);
}
$results = Person::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\PersonResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
$command = PersonLookupCommand::from($request, $id);
return $this->mediator->send($command);
}
/**
@ -221,59 +110,8 @@ class PersonController extends Controller
*/
public function anime(Request $request, int $id)
{
$results = Person::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Person::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Person::create($response);
}
if ($this->isExpired($request, $results)) {
Person::query()
->where('mal_id', $id)
->update($response);
}
$results = Person::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new PersonAnimeCollection(
$results->first()['anime_staff_positions']
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
$command = PersonAnimeLookupCommand::from($request, $id);
return $this->mediator->send($command);
}
/**
@ -302,59 +140,8 @@ class PersonController extends Controller
*/
public function voices(Request $request, int $id)
{
$results = Person::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Person::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Person::create($response);
}
if ($this->isExpired($request, $results)) {
Person::query()
->where('mal_id', $id)
->update($response);
}
$results = Person::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new PersonVoicesCollection(
$results->first()['voice_acting_roles']
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
$command = PersonVoicesLookupCommand::from($request, $id);
return $this->mediator->send($command);
}
/**
@ -383,59 +170,8 @@ class PersonController extends Controller
*/
public function manga(Request $request, int $id)
{
$results = Person::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Person::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Person::create($response);
}
if ($this->isExpired($request, $results)) {
Person::query()
->where('mal_id', $id)
->update($response);
}
$results = Person::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new PersonMangaCollection(
$results->first()['published_manga']
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
$command = PersonMangaLookupCommand::from($request, $id);
return $this->mediator->send($command);
}
/**
@ -480,28 +216,7 @@ class PersonController extends Controller
*/
public function pictures(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$person = ['pictures' => $this->jikan->getPersonPictures(new PersonPicturesRequest($id))];
$response = \json_decode($this->serializer->serialize($person, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new PicturesResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
$command = PersonPicturesLookupCommand::from($request, $id);
return $this->mediator->send($command);
}
}

View File

@ -2,19 +2,12 @@
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Http\Resources\V4\ExternalLinksResource;
use App\Producers;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\QueryBuilder\SearchQueryBuilderProducer;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\ProducerCollection;
use App\Dto\ProducerExternalLookupCommand;
use App\Dto\ProducerFullLookupCommand;
use App\Dto\ProducerLookupCommand;
use Illuminate\Http\Request;
use Jikan\Model\Producer\Producer;
use MongoDB\BSON\UTCDateTime;
class ProducerController extends ControllerWithQueryBuilderProvider
class ProducerController extends Controller
{
/**
* @OA\Get(
@ -47,60 +40,8 @@ class ProducerController extends ControllerWithQueryBuilderProvider
*/
public function main(Request $request, int $id)
{
$results = Producers::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Producers::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Producers::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Producers::query()
->where('mal_id', $id)
->update($response);
}
$results = Producers::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ProducerResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
$command = ProducerLookupCommand::from($request, $id);
return $this->mediator->send($command);
}
/**
@ -134,60 +75,8 @@ class ProducerController extends ControllerWithQueryBuilderProvider
*/
public function full(Request $request, int $id)
{
$results = Producers::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Producers::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Producers::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Producers::query()
->where('mal_id', $id)
->update($response);
}
$results = Producers::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ProducerFullResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
$command = ProducerFullLookupCommand::from($request, $id);
return $this->mediator->send($command);
}
/**
@ -218,59 +107,7 @@ class ProducerController extends ControllerWithQueryBuilderProvider
*/
public function external(Request $request, int $id)
{
$results = Producers::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Producers::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Producers::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Producers::query()
->where('mal_id', $id)
->update($response);
}
$results = Producers::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new ExternalLinksResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
$command = ProducerExternalLookupCommand::from($request, $id);
return $this->mediator->send($command);
}
}

View File

@ -4,6 +4,11 @@ namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Character;
use App\Dto\QueryRandomAnimeCommand;
use App\Dto\QueryRandomCharacterCommand;
use App\Dto\QueryRandomMangaCommand;
use App\Dto\QueryRandomPersonCommand;
use App\Dto\QueryRandomUserCommand;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\AnimeCollection;
@ -72,23 +77,9 @@ class RandomController extends Controller
* ),
* ),
*/
public function anime(Request $request)
public function anime(QueryRandomAnimeCommand $command)
{
$sfw = $request->get('sfw');
$results = Anime::query();
if (!is_null($sfw)) {
$results = $results
->where('rating', '!=', 'Rx - Hentai');
}
$results = $results
->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ]));
return new AnimeResource(
$results->first()
);
return $this->mediator->send($command);
}
/**
@ -113,24 +104,9 @@ class RandomController extends Controller
* ),
* ),
*/
public function manga(Request $request)
public function manga(QueryRandomMangaCommand $command)
{
$sfw = $request->get('sfw');
$results = Manga::query();
if (!is_null($sfw)) {
$results = $results
->where('type', '!=', 'Doujinshi');
}
$results = $results
->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ]));
return new MangaResource(
$results->first()
);
return $this->mediator->send($command);
}
/**
@ -155,14 +131,9 @@ class RandomController extends Controller
* ),
* ),
*/
public function characters(Request $request)
public function characters(QueryRandomCharacterCommand $command)
{
$results = Character::query()
->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ]));
return new CharacterResource(
$results->first()
);
return $this->mediator->send($command);
}
/**
@ -187,14 +158,9 @@ class RandomController extends Controller
* ),
* ),
*/
public function people(Request $request)
public function people(QueryRandomPersonCommand $command)
{
$results = Person::query()
->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ]));
return new PersonResource(
$results->first()
);
return $this->mediator->send($command);
}
/**
@ -219,13 +185,8 @@ class RandomController extends Controller
* ),
* ),
*/
public function users(Request $request)
public function users(QueryRandomUserCommand $command)
{
$results = Profile::query()
->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ]));
return new ProfileResource(
$results->first()
);
return $this->mediator->send($command);
}
}

View File

@ -2,15 +2,8 @@
namespace App\Http\Controllers\V4DB;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Helper\Constants;
use Jikan\Request\Recommendations\RecentRecommendationsRequest;
use Jikan\Request\Reviews\RecentReviewsRequest;
use MongoDB\BSON\UTCDateTime;
use App\Dto\QueryAnimeRecommendationsCommand;
use App\Dto\QueryMangaRecommendationsCommand;
class RecommendationsController extends Controller
{
@ -37,33 +30,9 @@ class RecommendationsController extends Controller
* ),
*
*/
public function anime(Request $request)
public function anime(QueryAnimeRecommendationsCommand $command)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$anime = $this->jikan->getRecentRecommendations(new RecentRecommendationsRequest(Constants::RECENT_RECOMMENDATION_ANIME, $page));
$response = \json_decode($this->serializer->serialize($anime, '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);
}
/**
@ -88,31 +57,8 @@ class RecommendationsController extends Controller
* ),
*
*/
public function manga(Request $request)
public function manga(QueryMangaRecommendationsCommand $command)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$anime = $this->jikan->getRecentRecommendations(new RecentRecommendationsRequest(Constants::RECENT_RECOMMENDATION_MANGA, $page));
$response = \json_decode($this->serializer->serialize($anime, '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);
}
}

View File

@ -2,16 +2,8 @@
namespace App\Http\Controllers\V4DB;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Helper\Constants;
use Jikan\Request\Reviews\RecentReviewsRequest;
use Jikan\Request\Reviews\ReviewsRequest;
use MongoDB\BSON\UTCDateTime;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use App\Dto\QueryAnimeReviewsCommand;
use App\Dto\QueryMangaReviewsCommand;
class ReviewsController extends Controller
{
@ -63,52 +55,9 @@ class ReviewsController extends Controller
* ),
* ),
*/
public function anime(Request $request)
public function anime(QueryAnimeReviewsCommand $command)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$sort = $request->get('sort') ?? Constants::REVIEWS_SORT_MOST_VOTED;
if (!in_array($sort, [Constants::REVIEWS_SORT_MOST_VOTED, Constants::REVIEWS_SORT_NEWEST, Constants::REVIEWS_SORT_OLDEST])) {
throw new BadRequestException('Invalid sort for reviews. Please refer to the documentation: https://docs.api.jikan.moe/');
}
$spoilers = $request->get('spoilers') ?? false;
$preliminary = $request->get('preliminary') ?? false;
$anime = $this->jikan
->getReviews(
new ReviewsRequest(
Constants::ANIME,
$page,
$sort,
$spoilers,
$preliminary
)
);
$response = \json_decode($this->serializer->serialize($anime, '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);
}
/**
@ -159,49 +108,8 @@ class ReviewsController extends Controller
* ),
* ),
*/
public function manga(Request $request)
public function manga(QueryMangaReviewsCommand $command)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$sort = $request->get('sort') ?? Constants::REVIEWS_SORT_MOST_VOTED;
if (!in_array($sort, [Constants::REVIEWS_SORT_MOST_VOTED, Constants::REVIEWS_SORT_NEWEST, Constants::REVIEWS_SORT_OLDEST])) {
throw new BadRequestException('Invalid sort for reviews. Please refer to the documentation: https://docs.api.jikan.moe/');
}
$spoilers = $request->get('spoilers') ?? false;
$preliminary = $request->get('preliminary') ?? false;
$anime = $this->jikan
->getReviews(
new ReviewsRequest(
Constants::MANGA,
$page,
$sort,
$spoilers,
$preliminary
)
);
$response = \json_decode($this->serializer->serialize($anime, '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);
}
}

View File

@ -2,44 +2,11 @@
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Http\HttpResponse;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\AnimeResource;
use App\Http\Resources\V4\CommonResource;
use App\Http\Resources\V4\ScheduleResource;
use App\Dto\QueryAnimeSchedulesCommand;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Helper\Constants;
use Jikan\Request\Schedule\ScheduleRequest;
class ScheduleController extends Controller
{
private const VALID_FILTERS = [
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday',
'other',
'unknown',
];
private const VALID_DAYS = [
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday',
];
private $request;
private $day;
/**
* @OA\Get(
* path="/schedules",
@ -107,92 +74,9 @@ class ScheduleController extends Controller
* }
* )
*/
/*
* all have status as currently airing
* all have premiered but they're not necesarily the current season or year
* all have aired date but they're not necessarily the current date/season
*
*/
public function main(Request $request, ?string $day = null)
{
$this->request = $request;
$page = $this->request->get('page') ?? 1;
$limit = $this->request->get('limit') ?? env('MAX_RESULTS_PER_PAGE', 25);
$filter = $this->request->get('filter') ?? null;
$kids = $this->request->get('kids') ?? false;
$sfw = $this->request->get('sfw') ?? false;
if (!is_null($day)) {
$this->day = strtolower($day);
}
if (!is_null($filter) && is_null($day)) {
$this->day = strtolower($filter);
}
if (null !== $this->day
&& !\in_array($this->day, self::VALID_FILTERS, true)) {
return HttpResponse::badRequest($this->request);
}
$results = Anime::query()
->orderBy('members')
->where('type', 'TV')
->where('status', 'Currently Airing')
;
if ($kids) {
$results = $results
->orWhere('demographics.mal_id', '!=', Constants::GENRE_ANIME_KIDS);
}
if ($sfw) {
$results = $results
->orWhere('demographics.mal_id', '!=', Constants::GENRE_ANIME_HENTAI);
}
// if (is_null($sfw)) {
// $results = $results
// ->orWhere('genres.mal_id', '!=', Constants::GENRE_ANIME_HENTAI)
// ->orWhere('genres.mal_id', '!=', Constants::GENRE_ANIME_BOYS_LOVE)
// ->orWhere('genres.mal_id', '!=', Constants::GENRE_ANIME_GIRLS_LOVE)
// ;
// }
if (\in_array($this->day, self::VALID_DAYS)) {
$this->day = ucfirst($this->day);
$results
->where('broadcast', 'like', "{$this->day}%");
}
if ($this->day === 'unknown') {
$results
->where('broadcast', 'Unknown');
}
if ($this->day === 'other') {
$results
->where('broadcast', 'Not scheduled once per week');
}
$results = $results
->paginate(
intval($limit),
['*'],
null,
$page
);
$response = (new AnimeCollection(
$results
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
$command = QueryAnimeSchedulesCommand::from($request, $day);
return $this->mediator->send($command);
}
}

View File

@ -3,6 +3,8 @@
namespace App;
use App\Filters\FilterQueryString;
use Illuminate\Support\Collection;
use Jenssegers\Mongodb\Eloquent\Builder;
class JikanApiModel extends \Jenssegers\Mongodb\Eloquent\Model
{
@ -15,4 +17,12 @@ class JikanApiModel extends \Jenssegers\Mongodb\Eloquent\Model
* @var string[]
*/
protected array $filters = [];
/** @noinspection PhpUnused */
public function scopeRandom(Builder $query, int $numberOfRandomItems = 1): Collection
{
return $query->raw(fn(\MongoDB\Collection $collection) => $collection->aggregate([
['$sample' => ['size' => $numberOfRandomItems]]
]));
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Macros;
use App\Concerns\ScraperCacheTtl;
use App\Support\CachedData;
use Illuminate\Http\Response;
use Illuminate\Support\Carbon;
/**
* @mixin Response
*/
final class ResponseJikanCacheFlags
{
use ScraperCacheTtl;
public function __invoke(): \Closure
{
return function (string $cacheKey, CachedData $scraperResults) {
/**
* @var Response $this
*/
return $this
->header("X-Request-Fingerprint", $cacheKey)
->setTtl(self::cacheTtl())
->setExpires(Carbon::createFromTimestamp($scraperResults->expiry()))
->setLastModified(Carbon::createFromTimestamp($scraperResults->lastModified()));
};
}
}

View File

@ -26,6 +26,7 @@ use App\Http\QueryBuilder\MangaSearchQueryBuilder;
use App\Http\QueryBuilder\TopAnimeQueryBuilder;
use App\Http\QueryBuilder\TopMangaQueryBuilder;
use App\Macros\CollectionOffsetGetFirst;
use App\Macros\ResponseJikanCacheFlags;
use App\Macros\To2dArrayWithDottedKeys;
use App\Magazine;
use App\Mixins\ScoutBuilderMixin;
@ -54,6 +55,7 @@ use App\Support\DefaultMediator;
use App\Support\JikanUnitOfWork;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Response;
use Illuminate\Support\Env;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Collection;
@ -307,7 +309,20 @@ class AppServiceProvider extends ServiceProvider
Features\MangaLookupHandler::class => $unitOfWorkInstance->manga(),
Features\MangaFullLookupHandler::class => $unitOfWorkInstance->manga(),
Features\MangaRelationsLookupHandler::class => $unitOfWorkInstance->manga(),
Features\MangaExternalLookupHandler::class => $unitOfWorkInstance->manga()
Features\MangaExternalLookupHandler::class => $unitOfWorkInstance->manga(),
Features\PersonLookupHandler::class => $unitOfWorkInstance->people(),
Features\PersonAnimeLookupHandler::class => $unitOfWorkInstance->people(),
Features\PersonFullLookupHandler::class => $unitOfWorkInstance->people(),
Features\PersonMangaLookupHandler::class => $unitOfWorkInstance->people(),
Features\PersonVoicesLookupHandler::class => $unitOfWorkInstance->people(),
Features\PersonPicturesLookupHandler::class => $unitOfWorkInstance->documents("people_pictures"),
Features\ProducerLookupHandler::class => $unitOfWorkInstance->producers(),
Features\ProducerFullLookupHandler::class => $unitOfWorkInstance->producers(),
Features\ProducerExternalLookupHandler::class => $unitOfWorkInstance->producers(),
Features\QueryAnimeRecommendationsHandler::class => $unitOfWorkInstance->documents("recommendations"),
Features\QueryMangaRecommendationsHandler::class => $unitOfWorkInstance->documents("recommendations"),
Features\QueryAnimeReviewsHandler::class => $unitOfWorkInstance->documents("reviews"),
Features\QueryMangaReviewsHandler::class => $unitOfWorkInstance->documents("reviews")
];
foreach ($requestHandlersWithScraperService as $handlerClass => $repositoryInstance) {
@ -319,7 +334,14 @@ class AppServiceProvider extends ServiceProvider
]);
}
// automatically resolvable dependencies or no dependencies at all
$requestHandlersWithNoDependencies = [
Features\QueryRandomAnimeHandler::class,
Features\QueryRandomMangaHandler::class,
Features\QueryRandomCharacterHandler::class,
Features\QueryRandomPersonHandler::class,
Features\QueryRandomUserHandler::class,
Features\QueryAnimeSchedulesHandler::class
];
foreach ($requestHandlersWithNoDependencies as $handlerClass) {
@ -372,6 +394,8 @@ class AppServiceProvider extends ServiceProvider
->reject(fn ($class, $macro) => Collection::hasMacro($macro))
->each(fn ($class, $macro) => Collection::macro($macro, app($class)()));
Response::macro("addJikanCacheFlags", app(ResponseJikanCacheFlags::class)());
ScoutBuilder::mixin(new ScoutBuilderMixin());
}

View File

@ -55,4 +55,9 @@ class DatabaseRepository extends RepositoryQuery implements Repository
{
return $this->queryable(true)->insert($attributes);
}
public function random(int $numberOfRandomItems = 1): Collection
{
return $this->queryable(true)->random($numberOfRandomItems);
}
}

View File

@ -6,8 +6,11 @@ use App\Anime;
use App\Contracts\AnimeRepository;
use App\Contracts\Repository;
use App\Enums\AnimeRatingEnum;
use App\Enums\AnimeScheduleFilterEnum;
use App\Enums\AnimeStatusEnum;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use App\Enums\AnimeTypeEnum;
use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder;
use Jikan\Helper\Constants;
use Laravel\Scout\Builder as ScoutBuilder;
/**
@ -60,4 +63,42 @@ final class DefaultAnimeRepository extends DatabaseRepository implements AnimeRe
->where("rank", ">", 0)
->orderBy("rank");
}
public function getCurrentlyAiring(
?AnimeScheduleFilterEnum $filter = null,
bool $kids = false,
bool $sfw = false): EloquentBuilder
{
/*
* all have status as currently airing
* all have premiered, but they're not necessarily the current season or year
* all have aired date, but they're not necessarily the current date/season
*/
$queryable = $this->queryable(true)
->orderBy("members")
->where("type", AnimeTypeEnum::tv()->label)
->where("status", AnimeStatusEnum::airing()->label);
if ($kids) {
$queryable = $queryable->where("demographics.mal_id", Constants::GENRE_ANIME_KIDS);
}
else {
$queryable = $queryable->where("demographics.mal_id", "!=", Constants::GENRE_ANIME_KIDS);
}
if ($sfw) {
$queryable = $queryable->where("demographics.mal_id", "!=", Constants::GENRE_ANIME_HENTAI);
}
if (!is_null($filter)) {
if ($filter->isWeekDay()) {
$queryable = $queryable->where("broadcast", "like", "{$filter->label}%");
}
else {
$queryable = $queryable->where("broadcast", $filter->label);
}
}
return $queryable;
}
}

View File

@ -4,8 +4,9 @@ namespace App\Repositories;
use App\Contracts\MangaRepository;
use App\Enums\MangaStatusEnum;
use App\Enums\MangaTypeEnum;
use App\Manga;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder;
use Laravel\Scout\Builder as ScoutBuilder;
class DefaultMangaRepository extends DatabaseRepository implements MangaRepository
@ -44,4 +45,9 @@ class DefaultMangaRepository extends DatabaseRepository implements MangaReposito
->where("rank", ">", 0)
->orderBy("rank");
}
public function exceptItemsWithAdultRating(): EloquentBuilder|ScoutBuilder
{
return $this->queryable()->where("type", "!=", MangaTypeEnum::doujin()->label);
}
}

View File

@ -8,9 +8,6 @@ use App\Contracts\Repository;
use App\Http\HttpHelper;
use App\Support\CachedData;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Jikan\MyAnimeList\MalClient;
use MongoDB\BSON\UTCDateTime;
@ -110,15 +107,6 @@ final class DefaultCachedScraperService implements CachedScraperService
return CachedData::from($this->getByCacheKey($cacheKey));
}
public function augmentResponse(JsonResponse|Response $response, string $cacheKey, CachedData $scraperResults): JsonResponse|Response
{
return $response
->header("X-Request-Fingerprint", $cacheKey)
->setTtl($this->cacheTtl())
->setExpires(Carbon::createFromTimestamp($scraperResults->expiry()))
->setLastModified(Carbon::createFromTimestamp($scraperResults->lastModified()));
}
private function raiseNotFoundIfEmpty(CachedData $results)
{
if ($results->isEmpty()) {

View File

@ -73,11 +73,11 @@ final class CachedData
$result = $this->scraperResult->first();
if (is_array($result)) {
if (is_array($result) && array_key_exists("modifiedAt", $result)) {
return (int) $result["modifiedAt"]->toDateTime()->format("U");
}
if (is_object($result)) {
if (is_object($result) && property_exists($result, "modifiedAt")) {
return $result->modifiedAt->toDateTime()->format("U");
}