refactored user endpoints and cache ttl config

This commit is contained in:
pushrbx 2023-01-24 23:30:47 +00:00
parent 525ac030f3
commit 4a25c30d7d
73 changed files with 1023 additions and 862 deletions

View File

@ -12,8 +12,8 @@ use Jikan\Model\Common\DateProp;
*/ */
class CarbonDateRange class CarbonDateRange
{ {
private ?Carbon $fromObj; private ?Carbon $fromObj = null;
private ?Carbon $untilObj; private ?Carbon $untilObj = null;
public function __construct(?Carbon $from, ?Carbon $to) public function __construct(?Carbon $from, ?Carbon $to)
{ {

View File

@ -13,7 +13,7 @@ use Spatie\LaravelData\Resolvers\DataFromSomethingResolver;
*/ */
trait HasRequestFingerprint trait HasRequestFingerprint
{ {
protected ?string $fingerprint; protected ?string $fingerprint = null;
public static function fromRequest(Request $request): ?static public static function fromRequest(Request $request): ?static
{ {

View File

@ -7,7 +7,7 @@ use App\Enums\AnimeScheduleFilterEnum;
use App\Enums\AnimeTypeEnum; use App\Enums\AnimeTypeEnum;
use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder; use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use \Laravel\Scout\Builder as ScoutBuilder; use Laravel\Scout\Builder as ScoutBuilder;
/** /**
* @implements Repository<Anime> * @implements Repository<Anime>

View File

@ -3,8 +3,8 @@
namespace App\Contracts; namespace App\Contracts;
use App\Character; use App\Character;
use \Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder;
use \Laravel\Scout\Builder as ScoutBuilder; use Laravel\Scout\Builder as ScoutBuilder;
/** /**
* @implements Repository<Character> * @implements Repository<Character>

View File

@ -3,8 +3,8 @@
namespace App\Contracts; namespace App\Contracts;
use App\Person; use App\Person;
use \Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder;
use \Laravel\Scout\Builder as ScoutBuilder; use Laravel\Scout\Builder as ScoutBuilder;
/** /**
* @implements Repository<Person> * @implements Repository<Person>

View File

@ -5,9 +5,9 @@ namespace App\Dto;
use App\Casts\EnumCast; use App\Casts\EnumCast;
use App\Enums\AnimeForumFilterEnum; use App\Enums\AnimeForumFilterEnum;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/** /**
* @extends LookupDataCommand<JsonResponse> * @extends LookupDataCommand<JsonResponse>

View File

@ -3,9 +3,9 @@
namespace App\Dto; namespace App\Dto;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Optional;
use Spatie\LaravelData\Attributes\Validation\Min; use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric; use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Optional;
/** /**
* @extends LookupDataCommand<JsonResponse> * @extends LookupDataCommand<JsonResponse>

View File

@ -6,11 +6,11 @@ use App\Casts\EnumCast;
use App\Contracts\DataRequest; use App\Contracts\DataRequest;
use App\Enums\CharacterOrderByEnum; use App\Enums\CharacterOrderByEnum;
use App\Http\Resources\V4\CharacterCollection; use App\Http\Resources\V4\CharacterCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/** /**
* @implements DataRequest<CharacterCollection> * @implements DataRequest<CharacterCollection>

View File

@ -8,11 +8,11 @@ use App\Enums\ClubCategoryEnum;
use App\Enums\ClubOrderByEnum; use App\Enums\ClubOrderByEnum;
use App\Enums\ClubTypeEnum; use App\Enums\ClubTypeEnum;
use App\Http\Resources\V4\ClubCollection; use App\Http\Resources\V4\ClubCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/** /**
* @implements DataRequest<ClubCollection> * @implements DataRequest<ClubCollection>

View File

@ -0,0 +1,16 @@
<?php
namespace App\Dto\Concerns;
use App\Rules\Attributes\MaxLimitWithFallback;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Optional;
trait HasLimitParameter
{
use MapsDefaultLimitParameter;
#[IntegerType, Min(1), MaxLimitWithFallback]
public int|Optional $limit;
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto\Concerns;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Optional;
trait HasPageParameter
{
#[Numeric, Min(1)]
public int|Optional $page = 1;
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Dto;
use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest;
use App\Dto\Concerns\MapsRouteParameters;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Response;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Data;
/**
* Base class for all requests/commands which are for looking up things by username.
* @template T of ResourceCollection|JsonResource|Response
* @implements DataRequest<T>
*/
abstract class LookupByUsernameCommand extends Data implements DataRequest
{
use MapsRouteParameters, HasRequestFingerprint;
#[StringType, Max(255), Min(3)]
public string $username;
}

View File

@ -6,11 +6,11 @@ use App\Casts\EnumCast;
use App\Contracts\DataRequest; use App\Contracts\DataRequest;
use App\Enums\MagazineOrderByEnum; use App\Enums\MagazineOrderByEnum;
use App\Http\Resources\V4\MagazineCollection; use App\Http\Resources\V4\MagazineCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/** /**
* @implements DataRequest<MagazineCollection> * @implements DataRequest<MagazineCollection>

View File

@ -6,9 +6,9 @@ namespace App\Dto;
use App\Casts\EnumCast; use App\Casts\EnumCast;
use App\Enums\MangaForumFilterEnum; use App\Enums\MangaForumFilterEnum;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/** /**
* @extends LookupDataCommand<JsonResponse> * @extends LookupDataCommand<JsonResponse>

View File

@ -6,11 +6,11 @@ use App\Casts\EnumCast;
use App\Contracts\DataRequest; use App\Contracts\DataRequest;
use App\Enums\PeopleOrderByEnum; use App\Enums\PeopleOrderByEnum;
use App\Http\Resources\V4\PersonCollection; use App\Http\Resources\V4\PersonCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/** /**
* @implements DataRequest<PersonCollection> * @implements DataRequest<PersonCollection>

View File

@ -6,11 +6,11 @@ use App\Casts\EnumCast;
use App\Contracts\DataRequest; use App\Contracts\DataRequest;
use App\Enums\ProducerOrderByEnum; use App\Enums\ProducerOrderByEnum;
use App\Http\Resources\V4\ProducerCollection; use App\Http\Resources\V4\ProducerCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/** /**
* @implements DataRequest<ProducerCollection> * @implements DataRequest<ProducerCollection>

View File

@ -6,18 +6,14 @@ namespace App\Dto;
use App\Casts\EnumCast; use App\Casts\EnumCast;
use App\Concerns\HasRequestFingerprint; use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest; use App\Contracts\DataRequest;
use App\Dto\Concerns\MapsDefaultLimitParameter; use App\Dto\Concerns\HasLimitParameter;
use App\Dto\Concerns\HasPageParameter;
use App\Enums\AnimeScheduleFilterEnum; use App\Enums\AnimeScheduleFilterEnum;
use App\Rules\Attributes\MaxLimitWithFallback;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Env;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\BooleanType; 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\Nullable;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional; use Spatie\LaravelData\Optional;
@ -27,13 +23,7 @@ use Spatie\LaravelData\Optional;
*/ */
final class QueryAnimeSchedulesCommand extends Data implements DataRequest final class QueryAnimeSchedulesCommand extends Data implements DataRequest
{ {
use MapsDefaultLimitParameter, HasRequestFingerprint; use HasLimitParameter, HasRequestFingerprint, HasPageParameter;
#[Numeric, Min(1)]
public int|Optional $page = 1;
#[IntegerType, Min(1), MaxLimitWithFallback]
public int|Optional $limit;
#[BooleanType] #[BooleanType]
public bool|Optional $kids = false; public bool|Optional $kids = false;

View File

@ -6,12 +6,10 @@ namespace App\Dto;
use App\Casts\EnumCast; use App\Casts\EnumCast;
use App\Concerns\HasRequestFingerprint; use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest; use App\Contracts\DataRequest;
use App\Dto\Concerns\MapsDefaultLimitParameter; use App\Dto\Concerns\HasLimitParameter;
use App\Dto\Concerns\HasPageParameter;
use App\Enums\AnimeTypeEnum; use App\Enums\AnimeTypeEnum;
use App\Rules\Attributes\MaxLimitWithFallback;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional; use Spatie\LaravelData\Optional;
@ -19,18 +17,11 @@ use Spatie\LaravelData\Optional;
abstract class QueryAnimeSeasonCommand extends Data implements DataRequest abstract class QueryAnimeSeasonCommand extends Data implements DataRequest
{ {
use MapsDefaultLimitParameter, HasRequestFingerprint; use HasLimitParameter, HasRequestFingerprint, HasPageParameter;
#[WithCast(EnumCast::class, AnimeTypeEnum::class)] #[WithCast(EnumCast::class, AnimeTypeEnum::class)]
public AnimeTypeEnum|Optional $filter; public AnimeTypeEnum|Optional $filter;
#[Numeric, Min(1)]
public int|Optional $page = 1;
#[Numeric, Min(1), MaxLimitWithFallback]
public int|Optional $limit;
public static function rules(...$args): array public static function rules(...$args): array
{ {
return [ return [

View File

@ -4,9 +4,9 @@ namespace App\Dto;
use App\Contracts\DataRequest; use App\Contracts\DataRequest;
use App\Http\Resources\V4\AnimeResource; use App\Http\Resources\V4\AnimeResource;
use Illuminate\Support\Optional;
use Spatie\LaravelData\Attributes\Validation\BooleanType; use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional;
/** /**
* @implements DataRequest<AnimeResource> * @implements DataRequest<AnimeResource>

View File

@ -4,9 +4,9 @@ namespace App\Dto;
use App\Contracts\DataRequest; use App\Contracts\DataRequest;
use App\Http\Resources\V4\MangaResource; use App\Http\Resources\V4\MangaResource;
use Illuminate\Support\Optional;
use Spatie\LaravelData\Attributes\Validation\BooleanType; use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional;
/** /**
* @implements DataRequest<MangaResource> * @implements DataRequest<MangaResource>

View File

@ -0,0 +1,15 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use Illuminate\Http\JsonResponse;
use Spatie\LaravelData\Data;
/**
* @extends DataRequest<JsonResponse>
*/
final class QueryRecentlyOnlineUsersCommand extends Data implements DataRequest
{
}

View File

@ -6,6 +6,7 @@ namespace App\Dto;
use App\Casts\EnumCast; use App\Casts\EnumCast;
use App\Concerns\HasRequestFingerprint; use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest; use App\Contracts\DataRequest;
use App\Dto\Concerns\HasPageParameter;
use App\Enums\MediaReviewsSortEnum; use App\Enums\MediaReviewsSortEnum;
use App\Http\Resources\V4\ResultsResource; use App\Http\Resources\V4\ResultsResource;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
@ -21,10 +22,7 @@ use Spatie\LaravelData\Optional;
*/ */
abstract class QueryReviewsCommand extends Data implements DataRequest abstract class QueryReviewsCommand extends Data implements DataRequest
{ {
use HasRequestFingerprint; use HasRequestFingerprint, HasPageParameter;
#[Numeric, Min(1)]
public int|Optional $page = 1;
#[WithCast(EnumCast::class, MediaReviewsSortEnum::class)] #[WithCast(EnumCast::class, MediaReviewsSortEnum::class)]
public MediaReviewsSortEnum|Optional $sort; public MediaReviewsSortEnum|Optional $sort;

View File

@ -2,20 +2,11 @@
namespace App\Dto; namespace App\Dto;
use App\Dto\Concerns\MapsDefaultLimitParameter; use App\Dto\Concerns\HasLimitParameter;
use App\Rules\Attributes\MaxLimitWithFallback; use App\Dto\Concerns\HasPageParameter;
use Illuminate\Support\Optional;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
abstract class QueryTopItemsCommand extends Data abstract class QueryTopItemsCommand extends Data
{ {
use MapsDefaultLimitParameter; use HasLimitParameter, HasPageParameter;
#[Numeric, Min(1)]
public int|Optional $page = 1;
#[Numeric, Min(1), MaxLimitWithFallback]
public int|Optional $limit;
} }

View File

@ -7,10 +7,9 @@ use App\Contracts\DataRequest;
use App\Enums\MangaTypeEnum; use App\Enums\MangaTypeEnum;
use App\Enums\TopMangaFilterEnum; use App\Enums\TopMangaFilterEnum;
use App\Http\Resources\V4\MangaCollection; use App\Http\Resources\V4\MangaCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\Rule;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/** /**
* @implements DataRequest<MangaCollection> * @implements DataRequest<MangaCollection>

View File

@ -3,17 +3,12 @@
namespace App\Dto; namespace App\Dto;
use App\Casts\EnumCast; use App\Casts\EnumCast;
use App\Dto\Concerns\MapsDefaultLimitParameter; use App\Dto\Concerns\HasLimitParameter;
use App\Dto\Concerns\HasPageParameter;
use App\Enums\SortDirection; use App\Enums\SortDirection;
use App\Rules\Attributes\MaxLimitWithFallback;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\Validation\Alpha; use Spatie\LaravelData\Attributes\Validation\Alpha;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\Max; use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Attributes\Validation\Size; use Spatie\LaravelData\Attributes\Validation\Size;
use Spatie\LaravelData\Attributes\Validation\StringType; use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
@ -22,7 +17,7 @@ use Spatie\LaravelData\Optional;
class SearchCommand extends Data class SearchCommand extends Data
{ {
use MapsDefaultLimitParameter; use HasLimitParameter, HasPageParameter;
/** /**
* The search keywords * The search keywords
@ -31,12 +26,6 @@ class SearchCommand extends Data
#[Max(255), StringType] #[Max(255), StringType]
public string|Optional $q; public string|Optional $q;
#[Numeric, Min(1)]
public int|Optional $page = 1;
#[IntegerType, Min(1), MaxLimitWithFallback]
public int|Optional $limit;
#[WithCast(EnumCast::class, SortDirection::class)] #[WithCast(EnumCast::class, SortDirection::class)]
public SortDirection|Optional $sort; public SortDirection|Optional $sort;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
<?php
namespace App\Dto;
use App\Dto\Concerns\HasPageParameter;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class UserFriendsLookupCommand extends LookupByUsernameCommand
{
use HasPageParameter;
}

View File

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

View File

@ -0,0 +1,27 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Enums\UserHistoryTypeEnum;
use Illuminate\Http\JsonResponse;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\Nullable;
use Spatie\LaravelData\Attributes\WithCast;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class UserHistoryLookupCommand extends LookupByUsernameCommand
{
#[WithCast(EnumCast::class, UserHistoryTypeEnum::class)]
public ?UserHistoryTypeEnum $type;
public static function rules(...$args): array
{
return [
"type" => [new EnumRule(UserHistoryTypeEnum::class), new Nullable()]
];
}
}

View File

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

View File

@ -0,0 +1,15 @@
<?php
namespace App\Dto;
use App\Dto\Concerns\HasPageParameter;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class UserRecommendationsLookupCommand extends LookupByUsernameCommand
{
use HasPageParameter;
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Dto;
use App\Dto\Concerns\HasPageParameter;
use Illuminate\Http\JsonResponse;
/**
* @extends LookupDataCommand<JsonResponse>
*/
final class UserReviewsLookupCommand extends LookupByUsernameCommand
{
use HasPageParameter;
}

View File

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

View File

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

View File

@ -7,10 +7,10 @@ use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest; use App\Contracts\DataRequest;
use App\Enums\GenderEnum; use App\Enums\GenderEnum;
use App\Http\Resources\V4\UserCollection; use App\Http\Resources\V4\UserCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\StringType; use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/** /**
* @implements DataRequest<UserCollection> * @implements DataRequest<UserCollection>

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

View File

@ -0,0 +1,29 @@
<?php
namespace App\Features;
use App\Dto\QueryRecentlyOnlineUsersCommand;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use App\Support\CachedData;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\User\RecentlyOnlineUsersRequest;
/**
* @extends RequestHandlerWithScraperCache<QueryRecentlyOnlineUsersCommand, JsonResponse>
*/
final class QueryRecentlyOnlineUsersHandler extends RequestHandlerWithScraperCache
{
public function requestClass(): string
{
return QueryRecentlyOnlineUsersCommand::class;
}
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => ["results" => $jikan->getRecentOnlineUsers(new RecentlyOnlineUsersRequest())]
);
}
}

View File

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

View File

@ -0,0 +1,30 @@
<?php
namespace App\Features;
use App\Dto\UserClubsLookupCommand;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use App\Support\CachedData;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\User\UserClubsRequest;
/**
* @extends RequestHandlerWithScraperCache<UserClubsLookupCommand, JsonResponse>
*/
final class UserClubsLookupHandler extends RequestHandlerWithScraperCache
{
public function requestClass(): string
{
return UserClubsLookupCommand::class;
}
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
$username = $requestParams->get("username");
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => ["results" => $jikan->getUserClubs(new UserClubsRequest($username))]
);
}
}

View File

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

View File

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

View File

@ -0,0 +1,31 @@
<?php
namespace App\Features;
use App\Dto\UserFriendsLookupCommand;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use App\Support\CachedData;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\User\UserFriendsRequest;
/**
* @extends RequestHandlerWithScraperCache<UserFriendsLookupCommand, JsonResponse>
*/
final class UserFriendsLookupHandler extends RequestHandlerWithScraperCache
{
public function requestClass(): string
{
return UserFriendsLookupCommand::class;
}
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
$username = $requestParams->get("username");
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => $jikan->getUserFriends(new UserFriendsRequest($username, $page)),
$requestParams->get("page", 1)
);
}
}

View File

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

View File

@ -0,0 +1,39 @@
<?php
namespace App\Features;
use App\Dto\UserHistoryLookupCommand;
use App\Http\Resources\V4\ProfileHistoryResource;
use App\Support\CachedData;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\User\UserHistoryRequest;
/**
* @extends RequestHandlerWithScraperCache<UserHistoryLookupCommand>
*/
final class UserHistoryLookupHandler extends RequestHandlerWithScraperCache
{
public function requestClass(): string
{
return UserHistoryLookupCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new ProfileHistoryResource($results->first());
}
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
$type = $requestParams->get("type");
$username = $requestParams->get("username");
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => ["history" => $jikan->getUserHistory(new UserHistoryRequest(
$username, $type
))]
);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Features;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
use App\Support\CachedData;
use Jikan\MyAnimeList\MalClient;
/**
* @template TRequest
* @extends RequestHandlerWithScraperCache<TRequest, JsonResponse>
*/
abstract class UserLookupHandler extends RequestHandlerWithScraperCache
{
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
$username = $requestParams->get("username");
return $this->scraperService->findByKey(
"username",
$username,
$requestFingerPrint,
);
}
}

View File

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

View File

@ -0,0 +1,31 @@
<?php
namespace App\Features;
use App\Dto\UserRecommendationsLookupCommand;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use App\Support\CachedData;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\User\UserRecommendationsRequest;
/**
* @extends RequestHandlerWithScraperCache<UserRecommendationsLookupCommand, JsonResponse>
*/
final class UserRecommendationsLookupHandler extends RequestHandlerWithScraperCache
{
public function requestClass(): string
{
return UserRecommendationsLookupCommand::class;
}
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
$username = $requestParams->get("username");
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => $jikan->getUserRecommendations(new UserRecommendationsRequest($username, $page)),
$requestParams->get("page", 1)
);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Features;
use App\Dto\UserReviewsLookupCommand;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use App\Support\CachedData;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\User\UserReviewsRequest;
/**
* @extends RequestHandlerWithScraperCache<UserReviewsLookupCommand, JsonResponse>
*/
final class UserReviewsLookupHandler extends RequestHandlerWithScraperCache
{
public function requestClass(): string
{
return UserReviewsLookupCommand::class;
}
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
{
$username = $requestParams->get("username");
return $this->scraperService->findList(
$requestFingerPrint,
fn(MalClient $jikan, ?int $page = null) => $jikan->getUserReviews(
new UserReviewsRequest(
$username,
$page,
)
),
$requestParams->get("page", 1)
);
}
}

View File

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

View File

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

View File

@ -1,37 +0,0 @@
<?php
namespace App\Http\Controllers\V4DB\Traits;
use App\Providers\SearchQueryBuilderProvider;
use Illuminate\Http\Request;
trait JikanApiQueryBuilder
{
private SearchQueryBuilderProvider $searchQueryBuilderProvider;
protected function getQueryBuilder(string $name, Request $request): \Illuminate\Database\Eloquent\Builder|\Laravel\Scout\Builder
{
$queryBuilder = $this->searchQueryBuilderProvider->getQueryBuilder($name);
return $queryBuilder->query($request);
}
protected function getPaginator(string $name, Request $request, \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $results): \Illuminate\Contracts\Pagination\LengthAwarePaginator
{
$queryBuilder = $this->searchQueryBuilderProvider->getQueryBuilder($name);
return $queryBuilder->paginateBuilder($request, $results);
}
/**
* @template T
* @param class-string<T> $resourceCollectionClass
* @param string $resourceTypeName
* @param \Illuminate\Http\Request $request
* @return T
*/
protected function preparePaginatedResponse(string $resourceCollectionClass, string $resourceTypeName, Request $request)
{
$results = $this->getQueryBuilder($resourceTypeName, $request);
$paginator = $this->getPaginator($resourceTypeName, $request, $results);
return new $resourceCollectionClass($paginator);
}
}

View File

@ -2,29 +2,20 @@
namespace App\Http\Controllers\V4DB; namespace App\Http\Controllers\V4DB;
use App\Http\HttpResponse; use App\Dto\QueryRecentlyOnlineUsersCommand;
use App\Http\QueryBuilder\UserListQueryBuilder; use App\Dto\UserAboutLookupCommand;
use App\Http\Resources\V4\ExternalLinksResource; use App\Dto\UserClubsLookupCommand;
use App\Http\Resources\V4\ProfileHistoryResource; use App\Dto\UserExternalLookupCommand;
use App\Http\Resources\V4\ResultsResource; use App\Dto\UserFavoritesLookupCommand;
use App\Http\Resources\V4\UserProfileAnimeListCollection; use App\Dto\UserFriendsLookupCommand;
use App\Http\Resources\V4\UserProfileAnimeListResource; use App\Dto\UserFullLookupCommand;
use App\Http\Resources\V4\UserProfileMangaListCollection; use App\Dto\UserHistoryLookupCommand;
use App\Http\Resources\V4\UserProfileMangaListResource; use App\Dto\UserProfileLookupCommand;
use App\Profile; use App\Dto\UserRecommendationsLookupCommand;
use App\Dto\UserReviewsLookupCommand;
use App\Dto\UserStatisticsLookupCommand;
use App\Dto\UserUpdatesLookupCommand;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Helper\Constants;
use Jikan\Request\User\RecentlyOnlineUsersRequest;
use Jikan\Request\User\UserAnimeListRequest;
use Jikan\Request\User\UserClubsRequest;
use Jikan\Request\User\UserFriendsRequest;
use Jikan\Request\User\UserHistoryRequest;
use Jikan\Request\User\UserMangaListRequest;
use Jikan\Request\User\UserRecommendationsRequest;
use Jikan\Request\User\UserReviewsRequest;
use MongoDB\BSON\UTCDateTime;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
/** /**
* Class Controller * Class Controller
@ -62,61 +53,9 @@ class UserController extends Controller
* ), * ),
* ), * ),
*/ */
public function full(Request $request, string $username) public function full(UserFullLookupCommand $command)
{ {
$username = strtolower($username); return $this->mediator->send($command);
$results = Profile::query()
->where('internal_username', $username)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Profile::scrape($username);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint,
'internal_username' => $username
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Profile::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Profile::query()
->where('internal_username', $username)
->update($response);
}
$results = Profile::query()
->where('internal_username', $username)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ProfileFullResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -148,61 +87,9 @@ class UserController extends Controller
* ), * ),
* ), * ),
*/ */
public function profile(Request $request, string $username) public function profile(UserProfileLookupCommand $command)
{ {
$username = strtolower($username); return $this->mediator->send($command);
$results = Profile::query()
->where('internal_username', $username)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Profile::scrape($username);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint,
'internal_username' => $username
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Profile::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Profile::query()
->where('internal_username', $username)
->update($response);
}
$results = Profile::query()
->where('internal_username', $username)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ProfileResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -231,62 +118,9 @@ class UserController extends Controller
* ), * ),
* ), * ),
*/ */
public function statistics(Request $request, string $username) public function statistics(UserStatisticsLookupCommand $command)
{ {
return $this->mediator->send($command);
$username = strtolower($username);
$results = Profile::query()
->where('internal_username', $username)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Profile::scrape($username);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint,
'internal_username' => $username
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Profile::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Profile::query()
->where('internal_username', $username)
->update($response);
}
$results = Profile::query()
->where('internal_username', $username)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ProfileStatisticsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
@ -319,62 +153,9 @@ class UserController extends Controller
* ), * ),
* ), * ),
*/ */
public function favorites(Request $request, string $username) public function favorites(UserFavoritesLookupCommand $command)
{ {
return $this->mediator->send($command);
$username = strtolower($username);
$results = Profile::query()
->where('internal_username', $username)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Profile::scrape($username);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint,
'internal_username' => $username
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Profile::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Profile::query()
->where('internal_username', $username)
->update($response);
}
$results = Profile::query()
->where('internal_username', $username)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ProfileFavoritesResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -403,62 +184,9 @@ class UserController extends Controller
* ), * ),
* ), * ),
*/ */
public function userupdates(Request $request, string $username) public function userupdates(UserUpdatesLookupCommand $command)
{ {
return $this->mediator->send($command);
$username = strtolower($username);
$results = Profile::query()
->where('internal_username', $username)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Profile::scrape($username);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint,
'internal_username' => $username
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Profile::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Profile::query()
->where('internal_username', $username)
->update($response);
}
$results = Profile::query()
->where('internal_username', $username)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ProfileLastUpdatesResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -487,62 +215,9 @@ class UserController extends Controller
* ), * ),
* ), * ),
*/ */
public function about(Request $request, string $username) public function about(UserAboutLookupCommand $command)
{ {
return $this->mediator->send($command);
$username = strtolower($username);
$results = Profile::query()
->where('internal_username', $username)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Profile::scrape($username);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint,
'internal_username' => $username
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Profile::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Profile::query()
->where('internal_username', $username)
->update($response);
}
$results = Profile::query()
->where('internal_username', $username)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ProfileAboutResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -578,45 +253,9 @@ class UserController extends Controller
* ), * ),
* ), * ),
*/ */
public function history(Request $request, string $username, ?string $type = null) public function history(UserHistoryLookupCommand $command)
{ {
$filter = $request->get('filter') ?? null; return $this->mediator->send($command);
if (!is_null($type)) {
$type = strtolower($type);
}
if (!is_null($filter) && is_null($type)) {
$type = strtolower($filter);
}
if (!is_null($type) && !\in_array($type, ['anime', 'manga'])) {
return HttpResponse::badRequest($request);
}
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$data = ['history'=>$this->jikan->getUserHistory(new UserHistoryRequest($username, $type))];
$response = \json_decode($this->serializer->serialize($data, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ProfileHistoryResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -690,32 +329,9 @@ class UserController extends Controller
* } * }
* ), * ),
*/ */
public function friends(Request $request, string $username) public function friends(UserFriendsLookupCommand $command)
{ {
$results = DB::table($this->getRouteTable($request)) return $this->mediator->send($command);
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$data = $this->jikan->getUserFriends(new UserFriendsRequest($username, $page));
$response = \json_decode($this->serializer->serialize($data, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -748,51 +364,8 @@ class UserController extends Controller
*/ */
public function animelist(Request $request, string $username, ?string $status = null) public function animelist(Request $request, string $username, ?string $status = null)
{ {
if (!is_null($status)) { // noop, intentionally left blank
$status = strtolower($status); // todo: remove as this is obsolete
if (!\in_array($status, ['all', 'watching', 'completed', 'onhold', 'dropped', 'plantowatch'])) {
return HttpResponse::badRequest($request);
}
}
$status = $this->listStatusToId($status);
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$data = $this->jikan->getUserAnimeList(
UserListQueryBuilder::create(
$request,
new UserAnimeListRequest($username, $page, $status)
)
);
$response = ['anime' => \json_decode($this->serializer->serialize($data, 'json'), true)];
$results = $this->updateCache($request, $results, $response);
}
$listResults = $results->first()['anime'];
foreach ($listResults as &$result) {
$result = (new UserProfileAnimeListResource($result));
}
$response = (new UserProfileAnimeListCollection(
$listResults
))->response($request);
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -825,52 +398,8 @@ class UserController extends Controller
*/ */
public function mangalist(Request $request, string $username, ?string $status = null) public function mangalist(Request $request, string $username, ?string $status = null)
{ {
if (!is_null($status)) { // noop, intentionally left blank
$status = strtolower($status); // todo: remove as this is obsolete
if (!\in_array($status, ['all', 'reading', 'completed', 'onhold', 'dropped', 'plantoread', 'ptr'])) {
return response()->json([
'error' => 'Bad Request'
])->setStatusCode(400);
}
}
$status = $this->listStatusToId($status);
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$data = $this->jikan->getUserMangaList(
UserListQueryBuilder::create(
$request,
new UserMangaListRequest($username, $page, $status)
)
);
$response = ['manga' => \json_decode($this->serializer->serialize($data, 'json'), true)];
$results = $this->updateCache($request, $results, $response);
}
$listResults = $results->first()['manga'];
foreach ($listResults as &$result) {
$result = (new UserProfileMangaListResource($result));
}
$response = (new UserProfileMangaListCollection(
$listResults
))->response($request);
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -955,39 +484,9 @@ class UserController extends Controller
* ), * ),
* ), * ),
*/ */
public function reviews(Request $request, string $username) public function reviews(UserReviewsLookupCommand $command)
{ {
$results = DB::table($this->getRouteTable($request)) return $this->mediator->send($command);
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$data = $this->jikan
->getUserReviews(
new UserReviewsRequest(
$username,
$page,
)
);
$response = \json_decode($this->serializer->serialize($data, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -1017,32 +516,9 @@ class UserController extends Controller
* ), * ),
* *
*/ */
public function recommendations(Request $request, string $username) public function recommendations(UserRecommendationsLookupCommand $command)
{ {
$results = DB::table($this->getRouteTable($request)) return $this->mediator->send($command);
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$data = $this->jikan->getUserRecommendations(new UserRecommendationsRequest($username, $page));
$response = \json_decode($this->serializer->serialize($data, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -1106,31 +582,9 @@ class UserController extends Controller
* } * }
* ), * ),
*/ */
public function clubs(Request $request, string $username) public function clubs(UserClubsLookupCommand $command)
{ {
$results = DB::table($this->getRouteTable($request)) return $this->mediator->send($command);
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$data = ['results' => $this->jikan->getUserClubs(new UserClubsRequest($username))];
$response = \json_decode($this->serializer->serialize($data, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** /**
@ -1160,125 +614,13 @@ class UserController extends Controller
* ), * ),
* ), * ),
*/ */
public function external(Request $request, string $username) public function external(UserExternalLookupCommand $command)
{ {
$username = strtolower($username); return $this->mediator->send($command);
$results = Profile::query()
->where('internal_username', $username)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Profile::scrape($username);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint,
'internal_username' => $username
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Profile::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Profile::query()
->where('internal_username', $username)
->update($response);
}
$results = Profile::query()
->where('internal_username', $username)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new ExternalLinksResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
} }
/** public function recentlyOnline(QueryRecentlyOnlineUsersCommand $command)
* @param Request $request
* @return mixed
* @throws \Jikan\Exception\BadResponseException
* @throws \Jikan\Exception\ParserException
*/
public function recentlyOnline(Request $request)
{ {
$results = DB::table($this->getRouteTable($request)) return $this->mediator->send($command);
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$data = ['results'=>$this->jikan->getRecentOnlineUsers(new RecentlyOnlineUsersRequest())];
$response = \json_decode($this->serializer->serialize($data, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @param string|null $status
* @return int
*/
private function listStatusToId(?string $status) : int
{
if (is_null($status)) {
return 7;
}
switch ($status) {
case 'all':
return 7;
case 'watching':
case 'reading':
return 1;
case 'completed':
return 2;
case 'onhold':
return 3;
case 'dropped':
return 4;
case 'plantowatch':
case 'ptw':
case 'plantoread':
case 'ptr':
return 6;
default:
return 7;
}
} }
} }

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Middleware;
use App\Http\HttpHelper;
use App\Support\CacheOptions;
use App\Support\JikanConfig;
use Closure;
use Illuminate\Http\Request;
/**
* Middleware which sets the cache ttl globally based on the endpoint's name
*/
final class EndpointCacheTtlMiddleware
{
// CacheOptions instance is singleton, so we set the ttl globally
public function __construct(private readonly CacheOptions $cacheOptions,
private readonly JikanConfig $jikanConfig)
{
}
public function handle(Request $request, Closure $next)
{
$routeName = HttpHelper::getRouteName($request);
$ttl = $this->jikanConfig->cacheTtlForEndpoint($routeName);
$this->cacheOptions->setTtl($ttl);
$response = $next($request);
$this->cacheOptions->setTtl(null);
return $response;
}
}

View File

@ -2,8 +2,8 @@
namespace App\Macros; namespace App\Macros;
use App\Concerns\ScraperCacheTtl;
use App\Support\CachedData; use App\Support\CachedData;
use App\Support\CacheOptions;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@ -12,8 +12,6 @@ use Illuminate\Support\Carbon;
*/ */
final class ResponseJikanCacheFlags final class ResponseJikanCacheFlags
{ {
use ScraperCacheTtl;
public function __invoke(): \Closure public function __invoke(): \Closure
{ {
return function (string $cacheKey, CachedData $scraperResults) { return function (string $cacheKey, CachedData $scraperResults) {
@ -22,7 +20,7 @@ final class ResponseJikanCacheFlags
*/ */
return $this return $this
->header("X-Request-Fingerprint", $cacheKey) ->header("X-Request-Fingerprint", $cacheKey)
->setTtl(ResponseJikanCacheFlags::cacheTtl()) ->setTtl(app(CacheOptions::class)->ttl())
->setExpires(Carbon::createFromTimestamp($scraperResults->expiry())) ->setExpires(Carbon::createFromTimestamp($scraperResults->expiry()))
->setLastModified(Carbon::createFromTimestamp($scraperResults->lastModified())); ->setLastModified(Carbon::createFromTimestamp($scraperResults->lastModified()));
}; };

View File

@ -15,22 +15,11 @@ use App\Contracts\Repository;
use App\Contracts\RequestHandler; use App\Contracts\RequestHandler;
use App\Contracts\UnitOfWork; use App\Contracts\UnitOfWork;
use App\Contracts\UserRepository; use App\Contracts\UserRepository;
use App\GenreAnime; use App\Http\Middleware\EndpointCacheTtlMiddleware;
use App\GenreManga;
use App\Http\QueryBuilder\AnimeSearchQueryBuilder;
use App\Http\QueryBuilder\CharacterSearchQueryBuilder;
use App\Http\QueryBuilder\ClubSearchQueryBuilder;
use App\Http\QueryBuilder\PeopleSearchQueryBuilder;
use App\Http\QueryBuilder\SimpleSearchQueryBuilder;
use App\Http\QueryBuilder\MangaSearchQueryBuilder;
use App\Http\QueryBuilder\TopAnimeQueryBuilder;
use App\Http\QueryBuilder\TopMangaQueryBuilder;
use App\Macros\CollectionOffsetGetFirst; use App\Macros\CollectionOffsetGetFirst;
use App\Macros\ResponseJikanCacheFlags; use App\Macros\ResponseJikanCacheFlags;
use App\Macros\To2dArrayWithDottedKeys; use App\Macros\To2dArrayWithDottedKeys;
use App\Magazine;
use App\Mixins\ScoutBuilderMixin; use App\Mixins\ScoutBuilderMixin;
use App\Producers;
use App\Repositories\AnimeGenresRepository; use App\Repositories\AnimeGenresRepository;
use App\Repositories\DefaultAnimeRepository; use App\Repositories\DefaultAnimeRepository;
use App\Repositories\DefaultCharacterRepository; use App\Repositories\DefaultCharacterRepository;
@ -53,7 +42,9 @@ use App\Services\ScoutSearchService;
use App\Services\SearchEngineSearchService; use App\Services\SearchEngineSearchService;
use App\Services\SearchService; use App\Services\SearchService;
use App\Services\TypeSenseScoutSearchService; use App\Services\TypeSenseScoutSearchService;
use App\Support\CacheOptions;
use App\Support\DefaultMediator; use App\Support\DefaultMediator;
use App\Support\JikanConfig;
use App\Support\JikanUnitOfWork; use App\Support\JikanUnitOfWork;
use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@ -87,6 +78,9 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
$this->app->singleton(JikanConfig::class, fn() => new JikanConfig(config("jikan")));
// cache options class is used to share the request scope level cache settings
$this->app->singleton(CacheOptions::class);
$this->app->singleton(CachedScraperService::class, DefaultCachedScraperService::class); $this->app->singleton(CachedScraperService::class, DefaultCachedScraperService::class);
if ($this->getSearchIndexesEnabledConfig($this->app)) { if ($this->getSearchIndexesEnabledConfig($this->app)) {
$this->app->bind(QueryBuilderPaginatorService::class, ScoutBuilderPaginatorService::class); $this->app->bind(QueryBuilderPaginatorService::class, ScoutBuilderPaginatorService::class);
@ -268,7 +262,20 @@ class AppServiceProvider extends ServiceProvider
Features\QueryMangaRecommendationsHandler::class => $unitOfWorkInstance->documents("recommendations"), Features\QueryMangaRecommendationsHandler::class => $unitOfWorkInstance->documents("recommendations"),
Features\QueryAnimeReviewsHandler::class => $unitOfWorkInstance->documents("reviews"), 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") Features\QueryAnimeSeasonListHandler::class => $unitOfWorkInstance->documents("season_archive"),
Features\UserFullLookupHandler::class => $unitOfWorkInstance->users(),
Features\UserProfileLookupHandler::class => $unitOfWorkInstance->users(),
Features\UserStatisticsLookupHandler::class => $unitOfWorkInstance->users(),
Features\UserFavoritesLookupHandler::class => $unitOfWorkInstance->users(),
Features\UserUpdatesLookupHandler::class => $unitOfWorkInstance->users(),
Features\UserAboutLookupHandler::class => $unitOfWorkInstance->users(),
Features\UserHistoryLookupHandler::class => $unitOfWorkInstance->documents("users_history"),
Features\UserFriendsLookupHandler::class => $unitOfWorkInstance->documents("users_friends"),
Features\UserReviewsLookupHandler::class => $unitOfWorkInstance->documents("users_reviews"),
Features\UserRecommendationsLookupHandler::class => $unitOfWorkInstance->documents("users_recommendations"),
Features\UserClubsLookupHandler::class => $unitOfWorkInstance->documents("users_clubs"),
Features\UserExternalLookupHandler::class => $unitOfWorkInstance->users(),
Features\QueryRecentlyOnlineUsersHandler::class => $unitOfWorkInstance->documents("users_recently_online")
]; ];
foreach ($requestHandlersWithScraperService as $handlerClass => $repositoryInstance) { foreach ($requestHandlersWithScraperService as $handlerClass => $repositoryInstance) {
@ -347,15 +354,11 @@ class AppServiceProvider extends ServiceProvider
public static function servicesToWarm(): array public static function servicesToWarm(): array
{ {
// todo: test again with roadrunner -- specific issue: typesense driver not loaded in time
$services = [ $services = [
ScoutSearchService::class, ScoutSearchService::class,
AnimeSearchQueryBuilder::class, UnitOfWork::class,
MangaSearchQueryBuilder::class, CachedScraperService::class
ClubSearchQueryBuilder::class,
CharacterSearchQueryBuilder::class,
PeopleSearchQueryBuilder::class,
TopAnimeQueryBuilder::class,
TopMangaQueryBuilder::class
]; ];
if (Env::get("SCOUT_DRIVER") === "typesense") { if (Env::get("SCOUT_DRIVER") === "typesense") {
@ -366,6 +369,12 @@ class AppServiceProvider extends ServiceProvider
$services[] = \Elastic\Elasticsearch\Client::class; $services[] = \Elastic\Elasticsearch\Client::class;
} }
if (Env::get("SCOUT_DRIVER") !== "none" && Env::get("SCOUT_DRIVER")) {
$services[] = ScoutBuilderPaginatorService::class;
} else {
$services[] = EloquentBuilderPaginatorService::class;
}
return $services; return $services;
} }
} }

View File

@ -5,13 +5,13 @@ namespace App\Repositories;
use App\Character; use App\Character;
use App\Contracts\CharacterRepository; use App\Contracts\CharacterRepository;
use App\Contracts\Repository; use App\Contracts\Repository;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder;
use Laravel\Scout\Builder as ScoutBuilder; use Laravel\Scout\Builder as ScoutBuilder;
/** /**
* @implements Repository<Character> * @implements Repository<Character>
*/ */
class DefaultCharacterRepository extends DatabaseRepository implements CharacterRepository final class DefaultCharacterRepository extends DatabaseRepository implements CharacterRepository
{ {
public function __construct() public function __construct()
{ {

View File

@ -9,7 +9,7 @@ use App\Contracts\Repository;
/** /**
* @implements Repository<Club> * @implements Repository<Club>
*/ */
class DefaultClubRepository extends DatabaseRepository implements ClubRepository final class DefaultClubRepository extends DatabaseRepository implements ClubRepository
{ {
public function __construct() public function __construct()
{ {

View File

@ -9,7 +9,7 @@ use App\Magazine;
/** /**
* @implements Repository<Magazine> * @implements Repository<Magazine>
*/ */
class DefaultMagazineRepository extends DatabaseRepository implements MagazineRepository final class DefaultMagazineRepository extends DatabaseRepository implements MagazineRepository
{ {
public function __construct() public function __construct()
{ {

View File

@ -9,7 +9,7 @@ use App\Manga;
use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder; use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder;
use Laravel\Scout\Builder as ScoutBuilder; use Laravel\Scout\Builder as ScoutBuilder;
class DefaultMangaRepository extends DatabaseRepository implements MangaRepository final class DefaultMangaRepository extends DatabaseRepository implements MangaRepository
{ {
public function __construct() public function __construct()
{ {

View File

@ -5,7 +5,7 @@ namespace App\Repositories;
use App\Contracts\PeopleRepository; use App\Contracts\PeopleRepository;
use App\Contracts\Repository; use App\Contracts\Repository;
use App\Person; use App\Person;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder;
use Laravel\Scout\Builder as ScoutBuilder; use Laravel\Scout\Builder as ScoutBuilder;
/** /**

View File

@ -9,7 +9,7 @@ use App\Producers;
/** /**
* @implements Repository<Producers> * @implements Repository<Producers>
*/ */
class DefaultProducerRepository extends DatabaseRepository implements ProducerRepository final class DefaultProducerRepository extends DatabaseRepository implements ProducerRepository
{ {
public function __construct() public function __construct()
{ {

View File

@ -9,7 +9,7 @@ use App\Profile;
/** /**
* @implements Repository<Profile> * @implements Repository<Profile>
*/ */
class DefaultUserRepository extends DatabaseRepository implements UserRepository final class DefaultUserRepository extends DatabaseRepository implements UserRepository
{ {
public function __construct() public function __construct()
{ {

View File

@ -2,7 +2,6 @@
namespace App\Services; namespace App\Services;
use App\Concerns\ScraperCacheTtl;
use App\Contracts\CachedScraperService; use App\Contracts\CachedScraperService;
use App\Contracts\Repository; use App\Contracts\Repository;
use App\Http\HttpHelper; use App\Http\HttpHelper;
@ -19,8 +18,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
*/ */
final class DefaultCachedScraperService implements CachedScraperService final class DefaultCachedScraperService implements CachedScraperService
{ {
use ScraperCacheTtl;
public function __construct( public function __construct(
private readonly Repository $repository, private readonly Repository $repository,
private readonly MalClient $jikan, private readonly MalClient $jikan,
@ -133,7 +130,7 @@ final class DefaultCachedScraperService implements CachedScraperService
$this->repository->queryByMalId($id)->update($response->toArray()); $this->repository->queryByMalId($id)->update($response->toArray());
} }
return new CachedData(collect($this->repository->getAllByMalId($id))); return CachedData::from(collect($this->repository->getAllByMalId($id)));
} }
private function updateCacheByKey(string $cacheKey, CachedData $results, array $scraperResponse): CachedData private function updateCacheByKey(string $cacheKey, CachedData $results, array $scraperResponse): CachedData
@ -149,7 +146,7 @@ final class DefaultCachedScraperService implements CachedScraperService
$this->getQueryableByCacheKey($cacheKey)->update($response->toArray()); $this->getQueryableByCacheKey($cacheKey)->update($response->toArray());
} }
return new CachedData($this->getByCacheKey($cacheKey)); return CachedData::from($this->getByCacheKey($cacheKey));
} }
private function prepareScraperResponse(string $cacheKey, bool $resultsEmpty, array $scraperResponse): CachedData private function prepareScraperResponse(string $cacheKey, bool $resultsEmpty, array $scraperResponse): CachedData
@ -166,7 +163,7 @@ final class DefaultCachedScraperService implements CachedScraperService
$meta['modifiedAt'] = new UTCDateTime(); $meta['modifiedAt'] = new UTCDateTime();
// join meta data with response // join meta data with response
return new CachedData(collect($meta + $scraperResponse)); return CachedData::from(collect($meta + $scraperResponse));
} }
private function getByCacheKey(string $cacheKey): Collection private function getByCacheKey(string $cacheKey): Collection
@ -179,7 +176,7 @@ final class DefaultCachedScraperService implements CachedScraperService
return $this->repository->where("request_hash", $cacheKey); return $this->repository->where("request_hash", $cacheKey);
} }
private function serializeScraperResult(array $data): array private function serializeScraperResult(mixed $data): array
{ {
return $this->serializer->toArray($data); return $this->serializer->toArray($data);
} }

View File

@ -0,0 +1,22 @@
<?php
namespace App\Support;
final class CacheOptions
{
private ?int $ttl = null;
public function __construct(private readonly JikanConfig $jikanConfig)
{
}
public function ttl(): int
{
return $this->ttl ?? $this->jikanConfig->defaultCacheExpire();
}
public function setTtl(?int $ttl): void
{
$this->ttl = $ttl;
}
}

View File

@ -5,20 +5,23 @@ namespace App\Support;
use App\Concerns\ScraperCacheTtl; use App\Concerns\ScraperCacheTtl;
use App\JikanApiModel; use App\JikanApiModel;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Env;
final class CachedData final class CachedData
{ {
use ScraperCacheTtl; private int $cacheTimeToLive;
public function __construct( private function __construct(
private readonly Collection $scraperResult private readonly Collection $scraperResult,
int $cacheTtl
) )
{ {
$this->cacheTimeToLive = $cacheTtl;
} }
public static function from(Collection $scraperResult): self public static function from(Collection $scraperResult): self
{ {
return new self($scraperResult); return new self($scraperResult, app(CacheOptions::class)->ttl());
} }
public function collect(): Collection public function collect(): Collection
@ -62,10 +65,15 @@ final class CachedData
public function expiry(): int public function expiry(): int
{ {
$modifiedAt = $this->lastModified(); $modifiedAt = $this->lastModified();
$ttl = $this->cacheTtl(); $ttl = $this->cacheTimeToLive;
return $modifiedAt !== null ? $ttl + $modifiedAt : $ttl; return $modifiedAt !== null ? $ttl + $modifiedAt : $ttl;
} }
public function cacheTtl(): int
{
return $this->cacheTimeToLive;
}
public function lastModified(): ?int public function lastModified(): ?int
{ {
if ($this->scraperResult->isEmpty()) { if ($this->scraperResult->isEmpty()) {

View File

@ -0,0 +1,35 @@
<?php
namespace App\Support;
use Illuminate\Support\Collection;
/**
* Jikan behavior config
*/
final class JikanConfig
{
/**
* @var array<string, int> $perEndpointCacheTtl
*/
private array $perEndpointCacheTtl;
private int $defaultCacheExpire;
public function __construct(array $config)
{
$config = collect($config);
$this->perEndpointCacheTtl = $config->get("per_endpoint_cache_ttl", []);
$this->defaultCacheExpire = $config->get("default_cache_expire", 0);
}
public function cacheTtlForEndpoint(string $endpoint): ?int
{
return collect($this->perEndpointCacheTtl)->get($endpoint);
}
public function defaultCacheExpire(): int
{
return $this->defaultCacheExpire;
}
}

View File

@ -6,8 +6,8 @@ use Illuminate\Contracts\Database\Query\Builder;
class RepositoryQueryBase class RepositoryQueryBase
{ {
private ?Builder $queryableBuilder; private ?Builder $queryableBuilder = null;
private ?ScoutBuilder $searchableBuilder; private ?ScoutBuilder $searchableBuilder = null;
public function __construct( public function __construct(
private readonly \Closure $getQueryable, private readonly \Closure $getQueryable,

View File

@ -41,7 +41,6 @@ $app->instance('path.config', app()->basePath() . DIRECTORY_SEPARATOR . 'config'
$app->instance('path.storage', app()->basePath() . DIRECTORY_SEPARATOR . 'storage'); $app->instance('path.storage', app()->basePath() . DIRECTORY_SEPARATOR . 'storage');
$app->withEloquent(); $app->withEloquent();
$app->configure('swagger-lume'); $app->configure('swagger-lume');
$app->configure('scout'); $app->configure('scout');
@ -88,6 +87,7 @@ $app->middleware($globalMiddleware);
$app->routeMiddleware([ $app->routeMiddleware([
'microcaching' => \App\Http\Middleware\MicroCaching::class, 'microcaching' => \App\Http\Middleware\MicroCaching::class,
'source-health-monitor' => SourceHeartbeatMonitor::class, 'source-health-monitor' => SourceHeartbeatMonitor::class,
'cache-ttl' => \App\Http\Middleware\EndpointCacheTtlMiddleware::class
]); ]);
/* /*
@ -109,6 +109,7 @@ $app->configure('controller-to-table-mapping');
$app->configure('controller'); $app->configure('controller');
$app->configure('roadrunner'); $app->configure('roadrunner');
$app->configure('data'); $app->configure('data');
$app->configure('jikan');
$app->register(\pushrbx\LumenRoadRunner\ServiceProvider::class); $app->register(\pushrbx\LumenRoadRunner\ServiceProvider::class);
$app->register(\SwaggerLume\ServiceProvider::class); $app->register(\SwaggerLume\ServiceProvider::class);
@ -169,6 +170,7 @@ if (env("SCOUT_DRIVER") === "Matchish\ScoutElasticSearch\Engines\ElasticSearchEn
$commonMiddleware = [ $commonMiddleware = [
'source-health-monitor', 'source-health-monitor',
'microcaching', 'microcaching',
'cache-ttl'
]; ];

143
config/jikan.php Normal file
View File

@ -0,0 +1,143 @@
<?php
return [
'default_cache_expire' => env('CACHE_DEFAULT_EXPIRE', 86400),
'per_endpoint_cache_ttl' => [
/**
* Anime
*/
'AnimeController@main' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@characters_staff' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@characters' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@staff' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@episodes' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@episode' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@news' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@forum' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@videos' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@videosEpisodes' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@pictures' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@stats' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@moreInfo' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@recommendations' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@userupdates' => env('CACHE_DEFAULT_EXPIRE'),
'AnimeController@reviews' => env('CACHE_DEFAULT_EXPIRE'),
/**
* Manga
*/
'MangaController@main' => env('CACHE_DEFAULT_EXPIRE'),
'MangaController@characters' => env('CACHE_DEFAULT_EXPIRE'),
'MangaController@news' => env('CACHE_DEFAULT_EXPIRE'),
'MangaController@forum' => env('CACHE_DEFAULT_EXPIRE'),
'MangaController@pictures' => env('CACHE_DEFAULT_EXPIRE'),
'MangaController@stats' => env('CACHE_DEFAULT_EXPIRE'),
'MangaController@moreInfo' => env('CACHE_DEFAULT_EXPIRE'),
'MangaController@recommendations' => env('CACHE_DEFAULT_EXPIRE'),
'MangaController@userupdates' => env('CACHE_DEFAULT_EXPIRE'),
'MangaController@reviews' => env('CACHE_DEFAULT_EXPIRE'),
/**
* Characters
*/
'CharacterController@main' => env('CACHE_DEFAULT_EXPIRE'),
'CharacterController@anime' => env('CACHE_DEFAULT_EXPIRE'),
'CharacterController@manga' => env('CACHE_DEFAULT_EXPIRE'),
'CharacterController@voices' => env('CACHE_DEFAULT_EXPIRE'),
'CharacterController@pictures' => env('CACHE_DEFAULT_EXPIRE'),
/**
* Person
*/
'PersonController@main' => env('CACHE_DEFAULT_EXPIRE'),
'PersonController@anime' => env('CACHE_DEFAULT_EXPIRE'),
'PersonController@manga' => env('CACHE_DEFAULT_EXPIRE'),
'PersonController@seiyuu' => env('CACHE_DEFAULT_EXPIRE'),
'PersonController@pictures' => env('CACHE_DEFAULT_EXPIRE'),
/**
* Season
*/
'SeasonController@archive' => env('CACHE_DEFAULT_EXPIRE'),
'SeasonController@later' => env('CACHE_DEFAULT_EXPIRE'),
'SeasonController@main' => env('CACHE_DEFAULT_EXPIRE'),
/**
* Schedule
*/
'ScheduleController@main' => env('CACHE_DEFAULT_EXPIRE'),
/**
* Producers
*/
'ProducerController@main' => env('CACHE_DEFAULT_EXPIRE'),
/**
* Magazines
*/
'MagazineController@main' => env('CACHE_MAGAZINE_EXPIRE'),
'MagazineController@resource' => env('CACHE_DEFAULT_EXPIRE'),
/**
* Users
*/
'UserController@recentlyOnline' => env('CACHE_DEFAULT_EXPIRE'),
'UserController@profile' => env('CACHE_USER_EXPIRE'),
'UserController@statistics' => env('CACHE_USER_EXPIRE'),
'UserController@favorites' => env('CACHE_USER_EXPIRE'),
'UserController@about' => env('CACHE_USER_EXPIRE'),
'UserController@history' => env('CACHE_USER_EXPIRE'),
'UserController@friends' => env('CACHE_USER_EXPIRE'),
'UserController@recommendations' => env('CACHE_USER_EXPIRE'),
'UserController@reviews' => env('CACHE_USER_EXPIRE'),
'UserController@clubs' => env('CACHE_USER_EXPIRE'),
/**
* User Lists
*/
'UserController@animelist' => env('CACHE_USERLIST_EXPIRE'),
'UserController@mangalist' => env('CACHE_USERLIST_EXPIRE'),
/**
* Genre
*/
'GenreController@mainAnime' => env('CACHE_GENRE_EXPIRE'),
'GenreController@mainManga' => env('CACHE_GENRE_EXPIRE'),
'GenreController@anime' => env('CACHE_DEFAULT_EXPIRE'),
'GenreController@manga' => env('CACHE_DEFAULT_EXPIRE'),
/**
* Top
*/
'TopController@anime' => env('CACHE_DEFAULT_EXPIRE'),
'TopController@manga' => env('CACHE_DEFAULT_EXPIRE'),
'TopController@characters' => env('CACHE_DEFAULT_EXPIRE'),
'TopController@people' => env('CACHE_DEFAULT_EXPIRE'),
'TopController@reviews' => env('CACHE_DEFAULT_EXPIRE'),
/**
* Search
*/
'SearchController@anime' => env('CACHE_SEARCH_EXPIRE'),
'SearchController@manga' => env('CACHE_SEARCH_EXPIRE'),
'SearchController@character' => env('CACHE_SEARCH_EXPIRE'),
'SearchController@people' => env('CACHE_SEARCH_EXPIRE'),
'SearchController@users' => env('CACHE_SEARCH_EXPIRE'),
'SearchController@userById' => env('CACHE_SEARCH_EXPIRE'),
'SearchController@producers' => env('CACHE_SEARCH_EXPIRE'),
'ClubController@main' => env('CACHE_DEFAULT_EXPIRE'),
'ClubController@members' => env('CACHE_DEFAULT_EXPIRE'),
'ReviewsController@anime' => env('CACHE_DEFAULT_EXPIRE'),
'ReviewsController@manga' => env('CACHE_DEFAULT_EXPIRE'),
'RecommendationsController@anime' => env('CACHE_DEFAULT_EXPIRE'),
'RecommendationsController@manga' => env('CACHE_DEFAULT_EXPIRE'),
'WatchController@recentEpisodes' => env('CACHE_DEFAULT_EXPIRE'),
'WatchController@popularEpisodes' => env('CACHE_DEFAULT_EXPIRE'),
'WatchController@recentPromos' => env('CACHE_DEFAULT_EXPIRE'),
'WatchController@popularPromos' => env('CACHE_DEFAULT_EXPIRE'),
]
];

View File

@ -15,7 +15,7 @@ abstract class JikanMediaModelFactory extends JikanModelFactory implements Media
{ {
use JikanDataGenerator; use JikanDataGenerator;
protected ?MediaModelFactoryDescriptor $descriptor; protected ?MediaModelFactoryDescriptor $descriptor = null;
public function __construct( public function __construct(
MediaModelFactoryDescriptor $descriptor, MediaModelFactoryDescriptor $descriptor,