mirror of
https://github.com/jikan-me/jikan-rest.git
synced 2025-02-20 11:23:35 +08:00
added scraper service and rewritten anime controller
This commit is contained in:
parent
a145f18bbd
commit
69d66378be
11
app/Concerns/ScraperCacheTtl.php
Normal file
11
app/Concerns/ScraperCacheTtl.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
trait ScraperCacheTtl
|
||||
{
|
||||
protected function cacheTtl(): int
|
||||
{
|
||||
return (int) env('CACHE_DEFAULT_EXPIRE');
|
||||
}
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
use App\Contracts\Repository;
|
||||
use App\Http\HttpHelper;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use MongoDB\BSON\UTCDateTime;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
trait ScraperResultCache
|
||||
{
|
||||
protected function queryFromScraperCacheByFingerPrint(string $cacheTableName, string $requestFingerPrint, \Closure $getMalDataCallback, ?int $page = null): Collection
|
||||
{
|
||||
$queryable = DB::table($cacheTableName);
|
||||
$results = $this->getScraperCacheByFingerPrint($queryable, $requestFingerPrint);
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($cacheTableName, $results)
|
||||
) {
|
||||
$page = $page ?? 1;
|
||||
$data = App::call(function (MalClient $jikan) use ($getMalDataCallback, $page) {
|
||||
return $getMalDataCallback($jikan, $page);
|
||||
});
|
||||
$response = $this->serializeScraperResult($data);
|
||||
$results = $this->updateCacheByFingerPrint($queryable, $requestFingerPrint, $results, $response);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Repository $repository
|
||||
* @param int $id
|
||||
* @param string $requestFingerPrint
|
||||
* @return Collection
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
protected function queryFromScraperCacheById(Repository $repository, int $id, string $requestFingerPrint): Collection
|
||||
{
|
||||
$results = $repository->getAllByMalId($id);
|
||||
$tableName = $repository->tableName();
|
||||
|
||||
if ($results->isEmpty() || $this->isExpired($tableName, $results)) {
|
||||
$response = $repository->scrape($id);
|
||||
|
||||
if (HttpHelper::hasError($response)) {
|
||||
abort(404, "Resource not found.");
|
||||
}
|
||||
|
||||
$results = $this->updateCacheById($repository, $id, $requestFingerPrint, $results, $response);
|
||||
}
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
abort(404, "Resource not found.");
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function serializeScraperResult($data): array
|
||||
{
|
||||
$serializer = app("SerializerV4");
|
||||
return $serializer->toArray($data);
|
||||
}
|
||||
|
||||
private function prepareScraperResponse(string $requestFingerPrint, Collection $results, array $response): array
|
||||
{
|
||||
// If resource doesn't exist, prepare meta
|
||||
if ($results->isEmpty()) {
|
||||
$meta = [
|
||||
'createdAt' => new UTCDateTime(),
|
||||
'request_hash' => $requestFingerPrint
|
||||
];
|
||||
}
|
||||
|
||||
// Update `modifiedAt` meta
|
||||
$meta['modifiedAt'] = new UTCDateTime();
|
||||
// join meta data with response
|
||||
return $meta + $response;
|
||||
}
|
||||
|
||||
private function updateCacheById(Repository $repository, int $id, string $requestFingerPrint, Collection $results, array $response): Collection
|
||||
{
|
||||
$response = $this->prepareScraperResponse($requestFingerPrint, $results, $response);
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
$repository->insert($response);
|
||||
}
|
||||
|
||||
if ($this->isExpired($repository->tableName(), $results)) {
|
||||
$repository->queryByMalId($id)->update($response);
|
||||
}
|
||||
|
||||
return $repository->getAllByMalId($id);
|
||||
}
|
||||
|
||||
private function updateCacheByFingerPrint(QueryBuilder $queryable, string $requestFingerPrint, Collection $results, array $response): Collection
|
||||
{
|
||||
$response = $this->prepareScraperResponse($requestFingerPrint, $results, $response);
|
||||
|
||||
// insert cache if resource doesn't exist
|
||||
if ($results->isEmpty()) {
|
||||
$queryable->insert($response);
|
||||
}
|
||||
|
||||
// update cache if resource exists
|
||||
if ($this->isExpired($queryable->from, $results)) {
|
||||
$this->getQueryableByFingerPrint($queryable, $requestFingerPrint)->update($response);
|
||||
}
|
||||
|
||||
return $this->getScraperCacheByFingerPrint($queryable, $requestFingerPrint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of Response
|
||||
* @param string $requestFingerPrint
|
||||
* @param Collection $results
|
||||
* @param T $response
|
||||
* @return T
|
||||
*/
|
||||
protected function prepareResponse(string $requestFingerPrint, Collection $results, $response)
|
||||
{
|
||||
return $response
|
||||
->header("X-Request-Fingerprint", $requestFingerPrint)
|
||||
->setTtl($this->getTtl())
|
||||
->setExpires(Carbon::createFromTimestamp($this->getExpiry($results)))
|
||||
->setLastModified(Carbon::createFromTimestamp($this->getLastModified($results)));
|
||||
}
|
||||
|
||||
protected function getQueryableByFingerPrint(QueryBuilder|EloquentBuilder $queryable, string $requestFingerPrint): EloquentBuilder
|
||||
{
|
||||
return $queryable->where("request_hash", $requestFingerPrint);
|
||||
}
|
||||
|
||||
protected function getScraperCacheByFingerPrint(QueryBuilder|EloquentBuilder $queryable, string $requestFingerPrint): Collection
|
||||
{
|
||||
return $this->getQueryableByFingerPrint($queryable, $requestFingerPrint)->get();
|
||||
}
|
||||
|
||||
private function getLastModified(Collection $results) : ?int
|
||||
{
|
||||
if (is_array($results->first())) {
|
||||
return (int) $results->first()['modifiedAt']->toDateTime()->format('U');
|
||||
}
|
||||
|
||||
if (is_object($results->first())) {
|
||||
return (int) $results->first()->modifiedAt->toDateTime()->format('U');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getTtl(): int
|
||||
{
|
||||
return (int) env('CACHE_DEFAULT_EXPIRE');
|
||||
}
|
||||
|
||||
private function getExpiry(Collection $results): int
|
||||
{
|
||||
$modifiedAt = $this->getLastModified($results);
|
||||
$ttl = $this->getTtl();
|
||||
return $modifiedAt !== null ? $ttl + $modifiedAt : $ttl;
|
||||
}
|
||||
|
||||
private function isExpired(string $cacheTableName, Collection $results): bool
|
||||
{
|
||||
$lastModified = $this->getLastModified($results);
|
||||
|
||||
if ($lastModified === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$expiry = $this->getExpiry($results);
|
||||
|
||||
return time() > $expiry;
|
||||
}
|
||||
}
|
38
app/Contracts/CachedScraperService.php
Normal file
38
app/Contracts/CachedScraperService.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Representation of a service which knows about cached MAL scraper results.
|
||||
*/
|
||||
interface CachedScraperService
|
||||
{
|
||||
/**
|
||||
* Finds cached scraper results by cacheKey, if not found scrapes them from MAL via the provided callback.
|
||||
* @param string $cacheKey
|
||||
* @param \Closure $getMalDataCallback
|
||||
* @param int|null $page
|
||||
* @return CachedData
|
||||
*/
|
||||
public function findList(string $cacheKey, \Closure $getMalDataCallback, ?int $page = null): CachedData;
|
||||
|
||||
/**
|
||||
* Finds cached scraper results by id in the database, if not found scrapes them from MAL.
|
||||
* @param int $id
|
||||
* @param string $cacheKey
|
||||
* @return CachedData
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function find(int $id, string $cacheKey): CachedData;
|
||||
|
||||
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;
|
||||
}
|
@ -2,11 +2,6 @@
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Http\Response;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
interface Mediator
|
||||
{
|
||||
/**
|
||||
|
@ -3,7 +3,7 @@
|
||||
namespace App\Contracts;
|
||||
|
||||
use App\JikanApiModel;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Contracts\Database\Query\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
@ -24,13 +24,13 @@ interface Repository extends RepositoryQuery
|
||||
|
||||
public function getAllByMalId(int $id): Collection;
|
||||
|
||||
public function queryByMalId(int $id): EloquentBuilder;
|
||||
public function queryByMalId(int $id): Builder;
|
||||
|
||||
public function tableName(): string;
|
||||
|
||||
// fixme: this should not be here.
|
||||
// this is here because we have the "scrape" static method on models
|
||||
public function scrape(int $id): array;
|
||||
public function scrape(int|string $id): array;
|
||||
|
||||
public function insert(array $attributes): bool;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
namespace App\Contracts;
|
||||
|
||||
use App\JikanApiModel;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Contracts\Database\Query\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Scout\Builder as ScoutBuilder;
|
||||
|
||||
@ -14,9 +14,9 @@ interface RepositoryQuery
|
||||
{
|
||||
/**
|
||||
* @param Collection $params
|
||||
* @return EloquentBuilder<T>|ScoutBuilder<T>
|
||||
* @return Builder<T>|ScoutBuilder<T>
|
||||
*/
|
||||
public function filter(Collection $params): EloquentBuilder|ScoutBuilder;
|
||||
public function filter(Collection $params): Builder|ScoutBuilder;
|
||||
|
||||
/**
|
||||
* @param string $keywords
|
||||
@ -24,4 +24,12 @@ interface RepositoryQuery
|
||||
* @return ScoutBuilder<T>
|
||||
*/
|
||||
public function search(string $keywords, ?\Closure $callback = null): ScoutBuilder;
|
||||
|
||||
/**
|
||||
* Get a where filter query
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return Builder
|
||||
*/
|
||||
public function where(string $key, mixed $value): Builder;
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use App\Repositories\DocumentRepository;
|
||||
|
||||
interface UnitOfWork
|
||||
{
|
||||
public function anime(): AnimeRepository;
|
||||
@ -23,4 +25,11 @@ interface UnitOfWork
|
||||
public function animeGenres(): GenreRepository;
|
||||
|
||||
public function mangaGenres(): GenreRepository;
|
||||
|
||||
/**
|
||||
* Returns the repository instance for a document collection which doesn't have a model representation.
|
||||
* @param string $tableName
|
||||
* @return DocumentRepository
|
||||
*/
|
||||
public function documents(string $tableName): DocumentRepository;
|
||||
}
|
||||
|
12
app/Dto/AnimeCharactersLookupCommand.php
Normal file
12
app/Dto/AnimeCharactersLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeCharactersLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
29
app/Dto/AnimeEpisodeLookupCommand.php
Normal file
29
app/Dto/AnimeEpisodeLookupCommand.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeEpisodeLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric, Required]
|
||||
public int $episodeId;
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
public static function fromMultiple(Request $request, int $id, int $episodeId): ?self
|
||||
{
|
||||
/**
|
||||
* @var AnimeEpisodeLookupCommand $data
|
||||
*/
|
||||
$data = self::fromRequestAndKey($request, $id);
|
||||
$data->episodeId = $episodeId;
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
16
app/Dto/AnimeEpisodesLookupCommand.php
Normal file
16
app/Dto/AnimeEpisodesLookupCommand.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
use Spatie\LaravelData\Optional;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeEpisodesLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric]
|
||||
public int|Optional $page;
|
||||
}
|
12
app/Dto/AnimeExternalLookupCommand.php
Normal file
12
app/Dto/AnimeExternalLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeExternalLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
26
app/Dto/AnimeForumLookupCommand.php
Normal file
26
app/Dto/AnimeForumLookupCommand.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Casts\EnumCast;
|
||||
use App\Enums\AnimeForumFilterEnum;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Optional;
|
||||
use Spatie\Enum\Laravel\Rules\EnumRule;
|
||||
use Spatie\LaravelData\Attributes\WithCast;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeForumLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[WithCast(EnumCast::class, AnimeForumFilterEnum::class)]
|
||||
public AnimeForumFilterEnum|Optional $filter;
|
||||
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
"filter" => [new EnumRule(AnimeForumFilterEnum::class)]
|
||||
];
|
||||
}
|
||||
}
|
12
app/Dto/AnimeFullLookupCommand.php
Normal file
12
app/Dto/AnimeFullLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeFullLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
12
app/Dto/AnimeLookupCommand.php
Normal file
12
app/Dto/AnimeLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
12
app/Dto/AnimeMoreInfoLookupCommand.php
Normal file
12
app/Dto/AnimeMoreInfoLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeMoreInfoLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
16
app/Dto/AnimeNewsLookupCommand.php
Normal file
16
app/Dto/AnimeNewsLookupCommand.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
use Spatie\LaravelData\Optional;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeNewsLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric]
|
||||
public int|Optional $page;
|
||||
}
|
12
app/Dto/AnimePicturesLookupCommand.php
Normal file
12
app/Dto/AnimePicturesLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimePicturesLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
12
app/Dto/AnimeRecommendationsLookupCommand.php
Normal file
12
app/Dto/AnimeRecommendationsLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeRecommendationsLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
12
app/Dto/AnimeRelationsLookupCommand.php
Normal file
12
app/Dto/AnimeRelationsLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeRelationsLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
37
app/Dto/AnimeReviewsLookupCommand.php
Normal file
37
app/Dto/AnimeReviewsLookupCommand.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Casts\EnumCast;
|
||||
use App\Enums\AnimeReviewsSortEnum;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Spatie\Enum\Laravel\Rules\EnumRule;
|
||||
use Spatie\LaravelData\Attributes\Validation\BooleanType;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
use Spatie\LaravelData\Attributes\WithCast;
|
||||
use Spatie\LaravelData\Optional;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeReviewsLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric]
|
||||
public int|Optional $page;
|
||||
|
||||
#[WithCast(EnumCast::class, AnimeReviewsSortEnum::class)]
|
||||
public AnimeReviewsSortEnum|Optional $sort;
|
||||
|
||||
#[BooleanType]
|
||||
public bool|Optional $spoilers;
|
||||
|
||||
#[BooleanType]
|
||||
public bool|Optional $preliminary;
|
||||
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
"sort" => [new EnumRule(AnimeReviewsSortEnum::class)]
|
||||
];
|
||||
}
|
||||
}
|
12
app/Dto/AnimeStaffLookupCommand.php
Normal file
12
app/Dto/AnimeStaffLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeStaffLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
12
app/Dto/AnimeStatsLookupCommand.php
Normal file
12
app/Dto/AnimeStatsLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeStatsLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
12
app/Dto/AnimeStreamingLookupCommand.php
Normal file
12
app/Dto/AnimeStreamingLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeStreamingLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
12
app/Dto/AnimeThemesLookupCommand.php
Normal file
12
app/Dto/AnimeThemesLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeThemesLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
16
app/Dto/AnimeUserUpdatesLookupCommand.php
Normal file
16
app/Dto/AnimeUserUpdatesLookupCommand.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
use Spatie\LaravelData\Optional;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeUserUpdatesLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric]
|
||||
public int|Optional $page;
|
||||
}
|
16
app/Dto/AnimeVideosEpisodesLookupCommand.php
Normal file
16
app/Dto/AnimeVideosEpisodesLookupCommand.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Optional;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeVideosEpisodesLookupCommand extends LookupDataCommand
|
||||
{
|
||||
#[Numeric]
|
||||
public int|Optional $page;
|
||||
}
|
12
app/Dto/AnimeVideosLookupCommand.php
Normal file
12
app/Dto/AnimeVideosLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class AnimeVideosLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
34
app/Dto/LookupDataCommand.php
Normal file
34
app/Dto/LookupDataCommand.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Concerns\HasRequestFingerprint;
|
||||
use App\Contracts\DataRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Http\Response;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
/**
|
||||
* Base class for all requests/commands which are for looking up things by id.
|
||||
* @template T of ResourceCollection|JsonResource|Response
|
||||
* @implements DataRequest<T>
|
||||
*/
|
||||
abstract class LookupDataCommand extends Data implements DataRequest
|
||||
{
|
||||
use HasRequestFingerprint;
|
||||
|
||||
#[Numeric, Required]
|
||||
public int $id;
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
public static function fromRequestAndKey(Request $request, int $id): self
|
||||
{
|
||||
$data = static::fromRequest($request);
|
||||
$data->id = $id;
|
||||
return $data;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Concerns\HasRequestFingerprint;
|
||||
use App\Contracts\DataRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Spatie\LaravelData\Attributes\Validation\Numeric;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
/**
|
||||
* @implements DataRequest<JsonResponse>
|
||||
*/
|
||||
class QueryFullAnimeCommand extends Data implements DataRequest
|
||||
{
|
||||
use HasRequestFingerprint;
|
||||
|
||||
#[Numeric, Required]
|
||||
public int $id;
|
||||
}
|
@ -11,6 +11,7 @@ use Spatie\LaravelData\Attributes\Validation\Alpha;
|
||||
use Spatie\LaravelData\Attributes\Validation\IntegerType;
|
||||
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\StringType;
|
||||
use Spatie\LaravelData\Attributes\WithCast;
|
||||
@ -26,6 +27,9 @@ class SearchCommand extends Data
|
||||
#[Max(255), StringType]
|
||||
public string|Optional $q;
|
||||
|
||||
#[Numeric, Min(1)]
|
||||
public int|Optional $page;
|
||||
|
||||
#[IntegerType, Min(1)]
|
||||
public int|Optional $limit;
|
||||
|
||||
|
12
app/Dto/UserByIdLookupCommand.php
Normal file
12
app/Dto/UserByIdLookupCommand.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @extends LookupDataCommand<JsonResponse>
|
||||
*/
|
||||
final class UserByIdLookupCommand extends LookupDataCommand
|
||||
{
|
||||
}
|
14
app/Enums/AnimeForumFilterEnum.php
Normal file
14
app/Enums/AnimeForumFilterEnum.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Spatie\Enum\Laravel\Enum;
|
||||
|
||||
/**
|
||||
* @method static self all()
|
||||
* @method static self episode()
|
||||
* @method static self other()
|
||||
*/
|
||||
final class AnimeForumFilterEnum extends Enum
|
||||
{
|
||||
}
|
23
app/Enums/AnimeReviewsSortEnum.php
Normal file
23
app/Enums/AnimeReviewsSortEnum.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Jikan\Helper\Constants;
|
||||
use Spatie\Enum\Laravel\Enum;
|
||||
|
||||
/**
|
||||
* @method static self mostVoted()
|
||||
* @method static self newest()
|
||||
* @method static self oldest()
|
||||
*/
|
||||
final class AnimeReviewsSortEnum extends Enum
|
||||
{
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
"mostVoted" => Constants::REVIEWS_SORT_MOST_VOTED,
|
||||
"newest" => Constants::REVIEWS_SORT_NEWEST,
|
||||
"oldest" => Constants::REVIEWS_SORT_OLDEST
|
||||
];
|
||||
}
|
||||
}
|
40
app/Features/AnimeCharactersLookupHandler.php
Normal file
40
app/Features/AnimeCharactersLookupHandler.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeCharactersLookupCommand;
|
||||
use App\Http\Resources\V4\AnimeCharactersResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeCharactersAndStaffRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeCharactersLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeCharactersLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new AnimeCharactersResource($results->first());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeCharactersLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getAnimeCharactersAndStaff(new AnimeCharactersAndStaffRequest($id))
|
||||
);
|
||||
}
|
||||
}
|
44
app/Features/AnimeEpisodeLookupHandler.php
Normal file
44
app/Features/AnimeEpisodeLookupHandler.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeEpisodeLookupCommand;
|
||||
use App\Http\Resources\V4\AnimeEpisodeResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeEpisodeRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeEpisodeLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeEpisodeLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new AnimeEpisodeResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeEpisodeLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
$episodeId = $requestParams->get("episodeId");
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getAnimeEpisode(new AnimeEpisodeRequest($id, $episodeId)),
|
||||
);
|
||||
}
|
||||
}
|
31
app/Features/AnimeEpisodesLookupHandler.php
Normal file
31
app/Features/AnimeEpisodesLookupHandler.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeEpisodesLookupCommand;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeEpisodesRequest;
|
||||
|
||||
final class AnimeEpisodesLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeEpisodesLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getAnimeEpisodes(new AnimeEpisodesRequest($id, $page)),
|
||||
$requestParams->get("page", 1)
|
||||
);
|
||||
}
|
||||
}
|
27
app/Features/AnimeExternalLookupHandler.php
Normal file
27
app/Features/AnimeExternalLookupHandler.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeExternalLookupCommand;
|
||||
use App\Http\Resources\V4\ExternalLinksResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends ItemLookupHandler<AnimeExternalLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeExternalLookupHandler extends ItemLookupHandler
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new ExternalLinksResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeExternalLookupCommand::class;
|
||||
}
|
||||
}
|
47
app/Features/AnimeForumLookupHandler.php
Normal file
47
app/Features/AnimeForumLookupHandler.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeForumLookupCommand;
|
||||
use App\Enums\AnimeForumFilterEnum;
|
||||
use App\Http\Resources\V4\ForumResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeForumRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeForumLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeForumLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new ForumResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeForumLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
$topic = $requestParams->get("filter", AnimeForumFilterEnum::all()->value);
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => collect(
|
||||
["topics" => $jikan->getAnimeForum(new AnimeForumRequest($id, $topic))]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
45
app/Features/AnimeMoreInfoLookupHandler.php
Normal file
45
app/Features/AnimeMoreInfoLookupHandler.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeStatsLookupCommand;
|
||||
use App\Http\Resources\V4\MoreInfoResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeMoreInfoRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeStatsLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeMoreInfoLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new MoreInfoResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeStatsLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => collect(
|
||||
["moreinfo" => $jikan->getAnimeMoreInfo(new AnimeMoreInfoRequest($id))]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
35
app/Features/AnimeNewsLookupHandler.php
Normal file
35
app/Features/AnimeNewsLookupHandler.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeNewsLookupCommand;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeNewsRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeNewsLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeNewsLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeNewsLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getNewsList(new AnimeNewsRequest($id, $page)),
|
||||
$requestParams->get("page", 1)
|
||||
);
|
||||
}
|
||||
}
|
46
app/Features/AnimePicturesLookupHandler.php
Normal file
46
app/Features/AnimePicturesLookupHandler.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimePicturesLookupCommand;
|
||||
use App\Http\Resources\V4\PicturesResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimePicturesRequest;
|
||||
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimePicturesLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimePicturesLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new PicturesResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimePicturesLookupCommand::class;
|
||||
}
|
||||
|
||||
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->getAnimePictures(new AnimePicturesRequest($id))]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
45
app/Features/AnimeRecommendationsLookupHandler.php
Normal file
45
app/Features/AnimeRecommendationsLookupHandler.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeRecommendationsLookupCommand;
|
||||
use App\Http\Resources\V4\RecommendationsResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeRecommendationsRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeRecommendationsLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeRecommendationsLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new RecommendationsResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeRecommendationsLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => collect(
|
||||
["recommendations" => $jikan->getAnimeRecommendations(new AnimeRecommendationsRequest($id))]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
30
app/Features/AnimeRelationsLookupHandler.php
Normal file
30
app/Features/AnimeRelationsLookupHandler.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeRelationsLookupCommand;
|
||||
use App\Http\Resources\V4\AnimeRelationsResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends ItemLookupHandler<AnimeRelationsLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeRelationsLookupHandler extends ItemLookupHandler
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new AnimeRelationsResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeRelationsLookupCommand::class;
|
||||
}
|
||||
}
|
41
app/Features/AnimeReviewsLookupHandler.php
Normal file
41
app/Features/AnimeReviewsLookupHandler.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeReviewsLookupCommand;
|
||||
use App\Enums\AnimeReviewsSortEnum;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeReviewsRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeReviewsLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeReviewsLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeReviewsLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
$sort = $requestParams->get("sort", AnimeReviewsSortEnum::mostVoted()->value);
|
||||
$spoilers = $requestParams->get("spoilers", false);
|
||||
$preliminary = $requestParams->get("preliminary", false);
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getAnimeReviews(new AnimeReviewsRequest(
|
||||
$id, $page, $sort, $spoilers, $preliminary
|
||||
)),
|
||||
$requestParams->get("page", 1)
|
||||
);
|
||||
}
|
||||
}
|
40
app/Features/AnimeStaffLookupHandler.php
Normal file
40
app/Features/AnimeStaffLookupHandler.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeStaffLookupCommand;
|
||||
use App\Http\Resources\V4\AnimeStaffResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeCharactersAndStaffRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeStaffLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeStaffLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new AnimeStaffResource($results->first());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeStaffLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getAnimeCharactersAndStaff(new AnimeCharactersAndStaffRequest($id))
|
||||
);
|
||||
}
|
||||
}
|
42
app/Features/AnimeStatsLookupHandler.php
Normal file
42
app/Features/AnimeStatsLookupHandler.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeStatsLookupCommand;
|
||||
use App\Http\Resources\V4\AnimeStatisticsResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeStatsLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeStatsLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new AnimeStatisticsResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeStatsLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getAnimeStats($id)
|
||||
);
|
||||
}
|
||||
}
|
30
app/Features/AnimeStreamingLookupHandler.php
Normal file
30
app/Features/AnimeStreamingLookupHandler.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeStreamingLookupCommand;
|
||||
use App\Http\Resources\V4\StreamingLinksResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends ItemLookupHandler<AnimeStreamingLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeStreamingLookupHandler extends ItemLookupHandler
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new StreamingLinksResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeStreamingLookupCommand::class;
|
||||
}
|
||||
}
|
31
app/Features/AnimeThemesLookupHandler.php
Normal file
31
app/Features/AnimeThemesLookupHandler.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeThemesLookupCommand;
|
||||
use App\Http\Resources\V4\StreamingLinksResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends ItemLookupHandler<AnimeThemesLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeThemesLookupHandler extends ItemLookupHandler
|
||||
{
|
||||
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new StreamingLinksResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeThemesLookupCommand::class;
|
||||
}
|
||||
}
|
37
app/Features/AnimeUserUpdatesLookupHandler.php
Normal file
37
app/Features/AnimeUserUpdatesLookupHandler.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeUserUpdatesLookupCommand;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeRecentlyUpdatedByUsersRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeUserUpdatesLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeUserUpdatesLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeUserUpdatesLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getAnimeRecentlyUpdatedByUsers(
|
||||
new AnimeRecentlyUpdatedByUsersRequest($id, $page)
|
||||
),
|
||||
$requestParams->get("page", 1)
|
||||
);
|
||||
}
|
||||
}
|
45
app/Features/AnimeVideosEpisodesLookupHandler.php
Normal file
45
app/Features/AnimeVideosEpisodesLookupHandler.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeVideosEpisodesLookupCommand;
|
||||
use App\Http\Resources\V4\AnimeEpisodesResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeVideosEpisodesRequest;
|
||||
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeVideosEpisodesLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeVideosEpisodesLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new AnimeEpisodesResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeVideosEpisodesLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getAnimeVideosEpisodes(new AnimeVideosEpisodesRequest($id, $page)),
|
||||
$requestParams->get("page", 1)
|
||||
);
|
||||
}
|
||||
}
|
43
app/Features/AnimeVideosLookupHandler.php
Normal file
43
app/Features/AnimeVideosLookupHandler.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeVideosLookupCommand;
|
||||
use App\Http\Resources\V4\AnimeVideosResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeVideosRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<AnimeVideosLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class AnimeVideosLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new AnimeVideosResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeVideosLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$id = $requestParams->get("id");
|
||||
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getAnimeVideos(new AnimeVideosRequest($id))
|
||||
);
|
||||
}
|
||||
}
|
@ -2,9 +2,8 @@
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Concerns\ScraperResultCache;
|
||||
use App\Contracts\DataRequest;
|
||||
use App\Contracts\Repository;
|
||||
use App\Contracts\CachedScraperService;
|
||||
use App\Dto\LookupDataCommand;
|
||||
use App\Contracts\RequestHandler;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
@ -14,34 +13,28 @@ use Spatie\LaravelData\Data;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* @template TRequest of DataRequest<TResponse>
|
||||
* @template TRequest of LookupDataCommand<TResponse>
|
||||
* @template TResponse of ResourceCollection|JsonResource|Response
|
||||
* @implements RequestHandler<TRequest, TResponse>
|
||||
*/
|
||||
abstract class ItemLookupHandler extends Data implements RequestHandler
|
||||
{
|
||||
use ScraperResultCache;
|
||||
|
||||
public function __construct(protected readonly Repository $repository)
|
||||
public function __construct(protected readonly CachedScraperService $scraperService)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TRequest $request
|
||||
* @param TRequest|LookupDataCommand<TResponse> $request
|
||||
* @return TResponse
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function handle($request)
|
||||
{
|
||||
$requestFingerprint = $request->getFingerPrint();
|
||||
$results = $this->queryFromScraperCacheById(
|
||||
$this->repository,
|
||||
$request->id,
|
||||
$request->getFingerPrint(),
|
||||
);
|
||||
$results = $this->scraperService->find($request->id, $requestFingerprint);
|
||||
|
||||
$resource = $this->resource($results);
|
||||
return $this->prepareResponse($requestFingerprint, $results, $resource->response());
|
||||
$resource = $this->resource($results->collect());
|
||||
return $this->scraperService->augmentResponse($resource->response(), $requestFingerprint, $results);
|
||||
}
|
||||
|
||||
protected abstract function resource(Collection $results): JsonResource;
|
||||
|
28
app/Features/QueryAnimeHandler.php
Normal file
28
app/Features/QueryAnimeHandler.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\AnimeLookupCommand;
|
||||
use App\Http\Resources\V4\AnimeResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends ItemLookupHandler<AnimeLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class QueryAnimeHandler extends ItemLookupHandler
|
||||
{
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new AnimeResource($results->first());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function requestClass(): string
|
||||
{
|
||||
return AnimeLookupCommand::class;
|
||||
}
|
||||
}
|
@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Concerns\ScraperResultCache;
|
||||
use App\Contracts\AnimeRepository;
|
||||
use App\Dto\QueryFullAnimeCommand;
|
||||
use App\Dto\AnimeFullLookupCommand;
|
||||
use App\Http\Resources\V4\AnimeFullResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
@ -12,18 +10,13 @@ use Illuminate\Support\Collection;
|
||||
|
||||
|
||||
/**
|
||||
* @extends ItemLookupHandler<QueryFullAnimeCommand, JsonResponse>
|
||||
* @extends ItemLookupHandler<AnimeFullLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class QueryFullAnimeHandler extends ItemLookupHandler
|
||||
{
|
||||
public function __construct(AnimeRepository $repository)
|
||||
{
|
||||
parent::__construct($repository);
|
||||
}
|
||||
|
||||
public function requestClass(): string
|
||||
{
|
||||
return QueryFullAnimeCommand::class;
|
||||
return AnimeFullLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function resource(Collection $results): JsonResource
|
||||
|
@ -2,42 +2,19 @@
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Concerns\ScraperResultCache;
|
||||
use App\Contracts\RequestHandler;
|
||||
use App\Dto\QueryTopReviewsCommand;
|
||||
use App\Http\Resources\V4\ResultsResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\Helper\Constants;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Reviews\RecentReviewsRequest;
|
||||
|
||||
/**
|
||||
* @implements RequestHandler<QueryTopReviewsCommand, JsonResponse>
|
||||
* @extends RequestHandlerWithScraperCache<QueryTopReviewsCommand, JsonResponse>
|
||||
*/
|
||||
class QueryTopReviewsHandler implements RequestHandler
|
||||
final class QueryTopReviewsHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
use ScraperResultCache;
|
||||
|
||||
/**
|
||||
* @param QueryTopReviewsCommand $request
|
||||
* @returns JsonResponse
|
||||
*/
|
||||
public function handle($request): JsonResponse
|
||||
{
|
||||
$requestParams = collect($request->all());
|
||||
$requestFingerPrint = $request->getFingerPrint();
|
||||
$results = $this->queryFromScraperCacheByFingerPrint(
|
||||
"reviews",
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, int $page) => $jikan->getRecentReviews(
|
||||
new RecentReviewsRequest(Constants::RECENT_REVIEW_BEST_VOTED, $page)
|
||||
), $requestParams->get("page"));
|
||||
|
||||
return $this->prepareResponse($requestFingerPrint, $results, (new ResultsResource(
|
||||
$results->first()
|
||||
))->response());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
@ -45,4 +22,12 @@ class QueryTopReviewsHandler implements RequestHandler
|
||||
{
|
||||
return QueryTopReviewsCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => $jikan->getRecentReviews(new RecentReviewsRequest(Constants::RECENT_REVIEW_BEST_VOTED, $page)),
|
||||
$requestParams->get("page"));
|
||||
}
|
||||
}
|
||||
|
58
app/Features/RequestHandlerWithScraperCache.php
Normal file
58
app/Features/RequestHandlerWithScraperCache.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Contracts\CachedScraperService;
|
||||
use App\Contracts\RequestHandler;
|
||||
use App\Contracts\DataRequest;
|
||||
use App\Http\Resources\V4\ResultsResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @template TRequest of DataRequest<TResponse>
|
||||
* @template TResponse of ResourceCollection|JsonResource|Response
|
||||
* @implements RequestHandler<TRequest, TResponse>
|
||||
*/
|
||||
abstract class RequestHandlerWithScraperCache implements RequestHandler
|
||||
{
|
||||
public function __construct(protected readonly CachedScraperService $scraperService)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function handle($request)
|
||||
{
|
||||
$requestParams = collect($request->all());
|
||||
$requestFingerPrint = $request->getFingerPrint();
|
||||
$results = $this->getScraperData($requestFingerPrint, $requestParams);
|
||||
|
||||
return $this->renderResponse($requestFingerPrint, $results);
|
||||
}
|
||||
|
||||
protected abstract function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData;
|
||||
|
||||
protected function resource(Collection $results): JsonResource
|
||||
{
|
||||
return new ResultsResource(
|
||||
$results->first()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $requestFingerPrint
|
||||
* @param CachedData $results
|
||||
* @return TResponse
|
||||
*/
|
||||
protected function renderResponse(string $requestFingerPrint, CachedData $results)
|
||||
{
|
||||
$finalResults = $results->collect();
|
||||
$response = $this->resource($finalResults)->response();
|
||||
return $this->scraperService->augmentResponse($response, $requestFingerPrint, $results);
|
||||
}
|
||||
}
|
29
app/Features/UserByIdLookupHandler.php
Normal file
29
app/Features/UserByIdLookupHandler.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Dto\UserByIdLookupCommand;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\User\UsernameByIdRequest;
|
||||
|
||||
/**
|
||||
* @extends RequestHandlerWithScraperCache<UserByIdLookupCommand, JsonResponse>
|
||||
*/
|
||||
final class UserByIdLookupHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
public function requestClass(): string
|
||||
{
|
||||
return UserByIdLookupCommand::class;
|
||||
}
|
||||
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, ?int $page = null) => collect(['results' => $jikan->getUsernameById(new UsernameByIdRequest($requestParams->get("id")))])
|
||||
);
|
||||
}
|
||||
}
|
@ -2,21 +2,18 @@
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Concerns\ScraperResultCache;
|
||||
use App\Contracts\RequestHandler;
|
||||
use App\Dto\UsersSearchCommand;
|
||||
use App\Http\Resources\V4\ResultsResource;
|
||||
use App\Support\CachedData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Search\UserSearchRequest;
|
||||
|
||||
/**
|
||||
* @implements RequestHandler<UsersSearchCommand, JsonResponse>
|
||||
* @implements RequestHandlerWithScraperCache<UsersSearchCommand, JsonResponse>
|
||||
*/
|
||||
final class UserSearchHandler implements RequestHandler
|
||||
final class UserSearchHandler extends RequestHandlerWithScraperCache
|
||||
{
|
||||
use ScraperResultCache;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
@ -25,16 +22,9 @@ final class UserSearchHandler implements RequestHandler
|
||||
return UsersSearchCommand::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UsersSearchCommand $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function handle($request): JsonResponse
|
||||
protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData
|
||||
{
|
||||
$requestParams = collect($request->all());
|
||||
$requestFingerPrint = $request->getFingerPrint();
|
||||
$results = $this->queryFromScraperCacheByFingerPrint(
|
||||
"users",
|
||||
return $this->scraperService->findList(
|
||||
$requestFingerPrint,
|
||||
fn (MalClient $jikan, int $page) => $jikan->getUserSearch((new UserSearchRequest())
|
||||
->setQuery($requestParams->get("q"))
|
||||
@ -45,9 +35,5 @@ final class UserSearchHandler implements RequestHandler
|
||||
->setPage($page)),
|
||||
$requestParams->get("page")
|
||||
);
|
||||
|
||||
return $this->prepareResponse($requestFingerPrint, $results, (new ResultsResource(
|
||||
$results->first()
|
||||
))->response());
|
||||
}
|
||||
}
|
||||
|
@ -2,45 +2,27 @@
|
||||
|
||||
namespace App\Http\Controllers\V4DB;
|
||||
|
||||
use App\Anime;
|
||||
use App\Http\HttpHelper;
|
||||
use App\Http\HttpResponse;
|
||||
use App\Http\Resources\V4\AnimeCharactersResource;
|
||||
use App\Http\Resources\V4\AnimeEpisodeResource;
|
||||
use App\Http\Resources\V4\ExternalLinksResource;
|
||||
use App\Http\Resources\V4\AnimeRelationsResource;
|
||||
use App\Http\Resources\V4\AnimeThemesResource;
|
||||
use App\Http\Resources\V4\MoreInfoResource;
|
||||
use App\Http\Resources\V4\PicturesResource;
|
||||
use App\Http\Resources\V4\RecommendationsResource;
|
||||
use App\Http\Resources\V4\ResultsResource;
|
||||
use App\Http\Resources\V4\AnimeStaffResource;
|
||||
use App\Http\Resources\V4\AnimeStatisticsResource;
|
||||
use App\Http\Resources\V4\StreamingLinksResource;
|
||||
use App\Http\Resources\V4\UserUpdatesResource;
|
||||
use App\Http\Resources\V4\AnimeVideosResource;
|
||||
use App\Http\Resources\V4\ForumResource;
|
||||
use App\Dto\AnimeCharactersLookupCommand;
|
||||
use App\Dto\AnimeEpisodeLookupCommand;
|
||||
use App\Dto\AnimeEpisodesLookupCommand;
|
||||
use App\Dto\AnimeExternalLookupCommand;
|
||||
use App\Dto\AnimeForumLookupCommand;
|
||||
use App\Dto\AnimeFullLookupCommand;
|
||||
use App\Dto\AnimeLookupCommand;
|
||||
use App\Dto\AnimeMoreInfoLookupCommand;
|
||||
use App\Dto\AnimeNewsLookupCommand;
|
||||
use App\Dto\AnimePicturesLookupCommand;
|
||||
use App\Dto\AnimeRecommendationsLookupCommand;
|
||||
use App\Dto\AnimeRelationsLookupCommand;
|
||||
use App\Dto\AnimeReviewsLookupCommand;
|
||||
use App\Dto\AnimeStaffLookupCommand;
|
||||
use App\Dto\AnimeStatsLookupCommand;
|
||||
use App\Dto\AnimeStreamingLookupCommand;
|
||||
use App\Dto\AnimeThemesLookupCommand;
|
||||
use App\Dto\AnimeUserUpdatesLookupCommand;
|
||||
use App\Dto\AnimeVideosEpisodesLookupCommand;
|
||||
use App\Dto\AnimeVideosLookupCommand;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Jikan\Helper\Constants;
|
||||
use Jikan\Request\Anime\AnimeCharactersAndStaffRequest;
|
||||
use Jikan\Request\Anime\AnimeEpisodeRequest;
|
||||
use Jikan\Request\Anime\AnimeEpisodesRequest;
|
||||
use Jikan\Request\Anime\AnimeForumRequest;
|
||||
use Jikan\Request\Anime\AnimeMoreInfoRequest;
|
||||
use Jikan\Request\Anime\AnimeNewsRequest;
|
||||
use Jikan\Request\Anime\AnimePicturesRequest;
|
||||
use Jikan\Request\Anime\AnimeRecentlyUpdatedByUsersRequest;
|
||||
use Jikan\Request\Anime\AnimeRecommendationsRequest;
|
||||
use Jikan\Request\Anime\AnimeRequest;
|
||||
use Jikan\Request\Anime\AnimeReviewsRequest;
|
||||
use Jikan\Request\Anime\AnimeStatsRequest;
|
||||
use Jikan\Request\Anime\AnimeVideosEpisodesRequest;
|
||||
use Jikan\Request\Anime\AnimeVideosRequest;
|
||||
use Laravel\Lumen\Http\ResponseFactory;
|
||||
use MongoDB\BSON\UTCDateTime;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class AnimeController extends Controller
|
||||
{
|
||||
@ -75,59 +57,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function full(Request $request, int $id)
|
||||
{
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$response = Anime::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()) {
|
||||
Anime::create($response);
|
||||
}
|
||||
|
||||
if ($this->isExpired($request, $results)) {
|
||||
Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->update($response);
|
||||
}
|
||||
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
}
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
return HttpResponse::notFound($request);
|
||||
}
|
||||
|
||||
$response = (new \App\Http\Resources\V4\AnimeFullResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeFullLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -161,59 +92,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function main(Request $request, int $id)
|
||||
{
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$response = Anime::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()) {
|
||||
Anime::create($response);
|
||||
}
|
||||
|
||||
if ($this->isExpired($request, $results)) {
|
||||
Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->update($response);
|
||||
}
|
||||
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
}
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
return HttpResponse::notFound($request);
|
||||
}
|
||||
|
||||
$response = (new \App\Http\Resources\V4\AnimeResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -244,29 +124,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function characters(Request $request, int $id)
|
||||
{
|
||||
$results = DB::table($this->getRouteTable($request))
|
||||
->where('request_hash', $this->fingerprint)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$anime = $this->jikan->getAnimeCharactersAndStaff(new AnimeCharactersAndStaffRequest($id));
|
||||
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new AnimeCharactersResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeCharactersLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -297,30 +156,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function staff(Request $request, int $id)
|
||||
{
|
||||
$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->getAnimeCharactersAndStaff(new AnimeCharactersAndStaffRequest($id));
|
||||
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new AnimeStaffResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeStaffLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -426,30 +263,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function episodes(Request $request, int $id)
|
||||
{
|
||||
$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->getAnimeEpisodes(new AnimeEpisodesRequest($id, $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
|
||||
);
|
||||
$command = AnimeEpisodesLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -490,30 +305,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function episode(Request $request, int $id, int $episodeId)
|
||||
{
|
||||
$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->getAnimeEpisode(new AnimeEpisodeRequest($id, $episodeId));
|
||||
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new AnimeEpisodeResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeEpisodeLookupCommand::from($request, $id, $episodeId);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -558,30 +351,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function news(Request $request, int $id)
|
||||
{
|
||||
$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->getNewsList(new AnimeNewsRequest($id, $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
|
||||
);
|
||||
$command = AnimeNewsLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -620,35 +391,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function forum(Request $request, int $id)
|
||||
{
|
||||
$results = DB::table($this->getRouteTable($request))
|
||||
->where('request_hash', $this->fingerprint)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$topic = $request->get('topic');
|
||||
|
||||
if ($request->get('filter') != null) {
|
||||
$topic = $request->get('filter');
|
||||
}
|
||||
|
||||
$anime = ['topics' => $this->jikan->getAnimeForum(new AnimeForumRequest($id, $topic))];
|
||||
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new ForumResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeForumLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -679,29 +423,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function videos(Request $request, int $id)
|
||||
{
|
||||
$results = DB::table($this->getRouteTable($request))
|
||||
->where('request_hash', $this->fingerprint)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$anime = $this->jikan->getAnimeVideos(new AnimeVideosRequest($id));
|
||||
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new AnimeVideosResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeVideosLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -777,30 +500,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function videosEpisodes(Request $request, int $id)
|
||||
{
|
||||
$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->getAnimeVideosEpisodes(new AnimeVideosEpisodesRequest($id, $page));
|
||||
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new AnimeEpisodesResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeVideosEpisodesLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -833,29 +534,8 @@ class AnimeController 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)
|
||||
) {
|
||||
$anime = ['pictures' => $this->jikan->getAnimePictures(new AnimePicturesRequest($id))];
|
||||
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new PicturesResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimePicturesLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -886,29 +566,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function stats(Request $request, int $id)
|
||||
{
|
||||
$results = DB::table($this->getRouteTable($request))
|
||||
->where('request_hash', $this->fingerprint)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$anime = $this->jikan->getAnimeStats(new AnimeStatsRequest($id));
|
||||
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new AnimeStatisticsResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeStatsLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -939,29 +598,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function moreInfo(Request $request, int $id)
|
||||
{
|
||||
$results = DB::table($this->getRouteTable($request))
|
||||
->where('request_hash', $this->fingerprint)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$anime = ['moreinfo' => $this->jikan->getAnimeMoreInfo(new AnimeMoreInfoRequest($id))];
|
||||
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new MoreInfoResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeMoreInfoLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -992,29 +630,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function recommendations(Request $request, int $id)
|
||||
{
|
||||
$results = DB::table($this->getRouteTable($request))
|
||||
->where('request_hash', $this->fingerprint)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$anime = ['recommendations' => $this->jikan->getAnimeRecommendations(new AnimeRecommendationsRequest($id))];
|
||||
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
|
||||
|
||||
$results = $this->updateCache($request, $results, $response);
|
||||
}
|
||||
|
||||
$response = (new RecommendationsResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeRecommendationsLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1047,30 +664,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function userupdates(Request $request, int $id)
|
||||
{
|
||||
$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->getAnimeRecentlyUpdatedByUsers(new AnimeRecentlyUpdatedByUsersRequest($id, $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
|
||||
);
|
||||
$command = AnimeUserUpdatesLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1103,48 +698,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function reviews(Request $request, int $id)
|
||||
{
|
||||
$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
|
||||
->getAnimeReviews(
|
||||
new AnimeReviewsRequest(
|
||||
$id,
|
||||
$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
|
||||
);
|
||||
$command = AnimeReviewsLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
|
||||
@ -1184,55 +739,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function relations(Request $request, int $id)
|
||||
{
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$response = Anime::scrape($id);
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
$meta = [
|
||||
'createdAt' => new UTCDateTime(),
|
||||
'modifiedAt' => new UTCDateTime(),
|
||||
'request_hash' => $this->fingerprint
|
||||
];
|
||||
}
|
||||
$meta['modifiedAt'] = new UTCDateTime();
|
||||
|
||||
$response = $meta + $response;
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
Anime::create($response);
|
||||
}
|
||||
|
||||
if ($this->isExpired($request, $results)) {
|
||||
Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->update($response);
|
||||
}
|
||||
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
}
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
return HttpResponse::notFound($request);
|
||||
}
|
||||
|
||||
$response = (new AnimeRelationsResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeRelationsLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1263,56 +771,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function themes(Request $request, int $id)
|
||||
{
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$response = Anime::scrape($id);
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
$meta = [
|
||||
'createdAt' => new UTCDateTime(),
|
||||
'modifiedAt' => new UTCDateTime(),
|
||||
'request_hash' => $this->fingerprint
|
||||
];
|
||||
}
|
||||
$meta['modifiedAt'] = new UTCDateTime();
|
||||
|
||||
$response = $meta + $response;
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
Anime::create($response);
|
||||
}
|
||||
|
||||
if ($this->isExpired($request, $results)) {
|
||||
Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->update($response);
|
||||
}
|
||||
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
}
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
return HttpResponse::notFound($request);
|
||||
}
|
||||
|
||||
|
||||
$response = (new AnimeThemesResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeThemesLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1343,56 +803,8 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function external(Request $request, int $id)
|
||||
{
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$response = Anime::scrape($id);
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
$meta = [
|
||||
'createdAt' => new UTCDateTime(),
|
||||
'modifiedAt' => new UTCDateTime(),
|
||||
'request_hash' => $this->fingerprint
|
||||
];
|
||||
}
|
||||
$meta['modifiedAt'] = new UTCDateTime();
|
||||
|
||||
$response = $meta + $response;
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
Anime::create($response);
|
||||
}
|
||||
|
||||
if ($this->isExpired($request, $results)) {
|
||||
Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->update($response);
|
||||
}
|
||||
|
||||
$results = Anime::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 = AnimeExternalLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1423,56 +835,7 @@ class AnimeController extends Controller
|
||||
*/
|
||||
public function streaming(Request $request, int $id)
|
||||
{
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$response = Anime::scrape($id);
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
$meta = [
|
||||
'createdAt' => new UTCDateTime(),
|
||||
'modifiedAt' => new UTCDateTime(),
|
||||
'request_hash' => $this->fingerprint
|
||||
];
|
||||
}
|
||||
$meta['modifiedAt'] = new UTCDateTime();
|
||||
|
||||
$response = $meta + $response;
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
Anime::query()
|
||||
->insert($response);
|
||||
}
|
||||
|
||||
if ($this->isExpired($request, $results)) {
|
||||
Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->update($response);
|
||||
}
|
||||
|
||||
$results = Anime::query()
|
||||
->where('mal_id', $id)
|
||||
->get();
|
||||
}
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
return HttpResponse::notFound($request);
|
||||
}
|
||||
|
||||
|
||||
$response = (new StreamingLinksResource(
|
||||
$results->first()
|
||||
))->response();
|
||||
|
||||
return $this->prepareResponse(
|
||||
$response,
|
||||
$results,
|
||||
$request
|
||||
);
|
||||
$command = AnimeStreamingLookupCommand::from($request, $id);
|
||||
return $this->mediator->send($command);
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ use App\Dto\ClubSearchCommand;
|
||||
use App\Dto\MangaSearchCommand;
|
||||
use App\Dto\PeopleSearchCommand;
|
||||
use App\Dto\ProducersSearchCommand;
|
||||
use App\Dto\UserByIdLookupCommand;
|
||||
use App\Dto\UsersSearchCommand;
|
||||
use App\Http\Resources\V4\ResultsResource;
|
||||
use Illuminate\Http\Request;
|
||||
@ -518,29 +519,7 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function userById(Request $request, int $id)
|
||||
{
|
||||
$results = DB::table($this->getRouteTable($request))
|
||||
->where('request_hash', $this->fingerprint)
|
||||
->get();
|
||||
|
||||
if (
|
||||
$results->isEmpty()
|
||||
|| $this->isExpired($request, $results)
|
||||
) {
|
||||
$anime = ['results'=>$this->jikan->getUsernameById(new UsernameByIdRequest($id))];
|
||||
$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(UserByIdLookupCommand::from($request, $id));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Contracts\AnimeRepository;
|
||||
use App\Contracts\CachedScraperService;
|
||||
use App\Contracts\CharacterRepository;
|
||||
use App\Contracts\ClubRepository;
|
||||
use App\Contracts\MagazineRepository;
|
||||
@ -15,18 +16,39 @@ use App\Contracts\RequestHandler;
|
||||
use App\Contracts\UnitOfWork;
|
||||
use App\Contracts\UserRepository;
|
||||
use App\Dto\QueryTopPeopleCommand;
|
||||
use App\Features\AnimeEpisodeLookupHandler;
|
||||
use App\Features\AnimeEpisodesLookupHandler;
|
||||
use App\Features\AnimeExternalLookupHandler;
|
||||
use App\Features\AnimeForumLookupHandler;
|
||||
use App\Features\AnimeGenreListHandler;
|
||||
use App\Features\AnimeMoreInfoLookupHandler;
|
||||
use App\Features\AnimeNewsLookupHandler;
|
||||
use App\Features\AnimePicturesLookupHandler;
|
||||
use App\Features\AnimeRecommendationsLookupHandler;
|
||||
use App\Features\AnimeRelationsLookupHandler;
|
||||
use App\Features\AnimeReviewsLookupHandler;
|
||||
use App\Features\AnimeSearchHandler;
|
||||
use App\Features\AnimeCharactersLookupHandler;
|
||||
use App\Features\AnimeStaffLookupHandler;
|
||||
use App\Features\AnimeStatsLookupHandler;
|
||||
use App\Features\AnimeStreamingLookupHandler;
|
||||
use App\Features\AnimeThemesLookupHandler;
|
||||
use App\Features\AnimeVideosEpisodesLookupHandler;
|
||||
use App\Features\AnimeVideosLookupHandler;
|
||||
use App\Features\CharacterSearchHandler;
|
||||
use App\Features\ClubSearchHandler;
|
||||
use App\Features\MagazineSearchHandler;
|
||||
use App\Features\MangaGenreListHandler;
|
||||
use App\Features\MangaSearchHandler;
|
||||
use App\Features\PeopleSearchHandler;
|
||||
use App\Features\ProducerSearchHandler;
|
||||
use App\Features\QueryAnimeHandler;
|
||||
use App\Features\QueryFullAnimeHandler;
|
||||
use App\Features\QueryTopAnimeItemsHandler;
|
||||
use App\Features\QueryTopCharactersHandler;
|
||||
use App\Features\QueryTopMangaItemsHandler;
|
||||
use App\Features\QueryTopReviewsHandler;
|
||||
use App\Features\UserByIdLookupHandler;
|
||||
use App\Features\UserSearchHandler;
|
||||
use App\GenreAnime;
|
||||
use App\GenreManga;
|
||||
@ -52,6 +74,7 @@ use App\Repositories\DefaultPeopleRepository;
|
||||
use App\Repositories\DefaultProducerRepository;
|
||||
use App\Repositories\DefaultUserRepository;
|
||||
use App\Repositories\MangaGenresRepository;
|
||||
use App\Services\DefaultCachedScraperService;
|
||||
use App\Services\DefaultQueryBuilderService;
|
||||
use App\Services\DefaultScoutSearchService;
|
||||
use App\Services\ElasticScoutSearchService;
|
||||
@ -67,6 +90,7 @@ use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Collection;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Laravel\Scout\Builder as ScoutBuilder;
|
||||
use Typesense\LaravelTypesense\Typesense;
|
||||
|
||||
@ -195,6 +219,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
$this->app->singleton(MangaGenresRepository::class);
|
||||
|
||||
$this->app->singleton(UnitOfWork::class, JikanUnitOfWork::class);
|
||||
$this->app->singleton(CachedScraperService::class, DefaultCachedScraperService::class);
|
||||
}
|
||||
|
||||
private function registerRequestHandlers()
|
||||
@ -209,9 +234,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
* Validation for requests is specified in the DTOs.
|
||||
* Querying/Filtering entirely happens on the model side.
|
||||
* The lines below explicitly define the mapping between request handlers and repositories.
|
||||
* Repositories are just a bit of abstraction over models. (A step away from magic class strings)
|
||||
* Every request handler gets a dedicated instance of QueryBuilderService which will be configured with a
|
||||
* specific repository instance.
|
||||
* Repositories are just a bit of abstraction over models.
|
||||
*/
|
||||
$this->app->when(DefaultMediator::class)
|
||||
->needs(RequestHandler::class)
|
||||
@ -229,11 +252,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
ClubSearchHandler::class => $unitOfWorkInstance->clubs(),
|
||||
MagazineSearchHandler::class => $unitOfWorkInstance->magazines(),
|
||||
ProducerSearchHandler::class => $unitOfWorkInstance->producers(),
|
||||
UserSearchHandler::class => $unitOfWorkInstance->users()
|
||||
];
|
||||
$requestHandlers = [];
|
||||
foreach ($searchRequestHandlersDescriptors as $handlerClass => $repositoryInstance) {
|
||||
$requestHandlers[] = $this->app->make($handlerClass, [
|
||||
$requestHandlers[] = $app->make($handlerClass, [
|
||||
$app->make(DefaultQueryBuilderService::class, [
|
||||
static::makeSearchService($app, $searchIndexesEnabled, $repositoryInstance),
|
||||
$app->make($searchIndexesEnabled ? ScoutBuilderPaginatorService::class : EloquentBuilderPaginatorService::class)
|
||||
@ -249,23 +271,64 @@ class AppServiceProvider extends ServiceProvider
|
||||
];
|
||||
|
||||
foreach ($queryTopItemsDescriptors as $handlerClass => $repositoryInstance) {
|
||||
$requestHandlers[] = $this->app->make($handlerClass, [
|
||||
$requestHandlers[] = $app->make($handlerClass, [
|
||||
$repositoryInstance,
|
||||
// top queries don't use the search engine, so it's enough for them to use eloquent paginator
|
||||
$this->app->make(EloquentBuilderPaginatorService::class)
|
||||
$app->make(EloquentBuilderPaginatorService::class)
|
||||
]);
|
||||
}
|
||||
|
||||
$genreRequestHandlerDescriptors = [
|
||||
// request handlers which only depend on a repository instance
|
||||
$requestHandlersWithOnlyRepositoryDependency = [
|
||||
AnimeGenreListHandler::class => $unitOfWorkInstance->animeGenres(),
|
||||
MangaGenresRepository::class => $unitOfWorkInstance->mangaGenres()
|
||||
MangaGenreListHandler::class => $unitOfWorkInstance->mangaGenres(),
|
||||
];
|
||||
|
||||
foreach ($genreRequestHandlerDescriptors as $handlerClass => $repositoryInstance) {
|
||||
$requestHandlers[] = $this->app->make($handlerClass, [$repositoryInstance]);
|
||||
foreach ($requestHandlersWithOnlyRepositoryDependency as $handlerClass => $repositoryInstance) {
|
||||
$requestHandlers[] = $app->make($handlerClass, [$repositoryInstance]);
|
||||
}
|
||||
|
||||
$requestHandlers[] = $this->app->make(QueryTopReviewsHandler::class);
|
||||
// request handlers which are fetching data through the jikan library from MAL, and caching the result.
|
||||
$requestHandlersWithScraperService = [
|
||||
QueryFullAnimeHandler::class => $unitOfWorkInstance->anime(),
|
||||
QueryAnimeHandler::class => $unitOfWorkInstance->anime(),
|
||||
UserSearchHandler::class => $unitOfWorkInstance->documents("common"),
|
||||
QueryTopReviewsHandler::class => $unitOfWorkInstance->documents("common"),
|
||||
UserByIdLookupHandler::class => $unitOfWorkInstance->documents("common"),
|
||||
AnimeCharactersLookupHandler::class => $unitOfWorkInstance->documents("anime_characters_staff"),
|
||||
AnimeStaffLookupHandler::class => $unitOfWorkInstance->documents("anime_characters_staff"),
|
||||
AnimeEpisodesLookupHandler::class => $unitOfWorkInstance->documents("anime_episodes"),
|
||||
AnimeEpisodeLookupHandler::class => $unitOfWorkInstance->documents("anime_episode"),
|
||||
AnimeNewsLookupHandler::class => $unitOfWorkInstance->documents("anime_news"),
|
||||
AnimeForumLookupHandler::class => $unitOfWorkInstance->documents("anime_forum"),
|
||||
AnimeVideosLookupHandler::class => $unitOfWorkInstance->documents("anime_videos"),
|
||||
AnimeVideosEpisodesLookupHandler::class => $unitOfWorkInstance->documents("anime_videos_episodes"),
|
||||
AnimePicturesLookupHandler::class => $unitOfWorkInstance->documents("anime_pictures"),
|
||||
AnimeStatsLookupHandler::class => $unitOfWorkInstance->documents("anime_stats"),
|
||||
AnimeMoreInfoLookupHandler::class => $unitOfWorkInstance->documents("anime_moreinfo"),
|
||||
AnimeRecommendationsLookupHandler::class => $unitOfWorkInstance->documents("anime_recommendations"),
|
||||
AnimeReviewsLookupHandler::class => $unitOfWorkInstance->documents("anime_reviews"),
|
||||
AnimeRelationsLookupHandler::class => $unitOfWorkInstance->anime(),
|
||||
AnimeExternalLookupHandler::class => $unitOfWorkInstance->anime(),
|
||||
AnimeStreamingLookupHandler::class => $unitOfWorkInstance->anime(),
|
||||
AnimeThemesLookupHandler::class => $unitOfWorkInstance->anime(),
|
||||
];
|
||||
|
||||
foreach ($requestHandlersWithScraperService as $handlerClass => $repositoryInstance) {
|
||||
$jikan = $app->make(MalClient::class);
|
||||
$serializer = $app->make("SerializerV4");
|
||||
$requestHandlers[] = $app->make($handlerClass, [
|
||||
$app->make(DefaultCachedScraperService::class,
|
||||
[$repositoryInstance, $jikan, $serializer])
|
||||
]);
|
||||
}
|
||||
|
||||
$requestHandlersWithNoDependencies = [
|
||||
];
|
||||
|
||||
foreach ($requestHandlersWithNoDependencies as $handlerClass) {
|
||||
$requestHandlers[] = $this->app->make($handlerClass);
|
||||
}
|
||||
|
||||
return $requestHandlers;
|
||||
});
|
||||
|
@ -4,7 +4,7 @@ namespace App\Repositories;
|
||||
|
||||
use App\Contracts\Repository;
|
||||
use App\Support\RepositoryQuery;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Contracts\Database\Query\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class DatabaseRepository extends RepositoryQuery implements Repository
|
||||
@ -30,7 +30,7 @@ class DatabaseRepository extends RepositoryQuery implements Repository
|
||||
->get();
|
||||
}
|
||||
|
||||
public function queryByMalId(int $id): EloquentBuilder
|
||||
public function queryByMalId(int $id): Builder
|
||||
{
|
||||
return $this->queryable(true)
|
||||
->where('mal_id', $id);
|
||||
@ -43,7 +43,7 @@ class DatabaseRepository extends RepositoryQuery implements Repository
|
||||
|
||||
// fixme: this should not be here.
|
||||
// this is here because we have the "scrape" static method on models
|
||||
public function scrape(int $id): array
|
||||
public function scrape(int|string $id): array
|
||||
{
|
||||
$modelClass = get_class($this->queryable(true)->newModelInstance());
|
||||
|
||||
|
34
app/Repositories/DocumentRepository.php
Normal file
34
app/Repositories/DocumentRepository.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Represents a table in the document database which doesn't have a model representation in the code base.
|
||||
*/
|
||||
final class DocumentRepository extends DatabaseRepository
|
||||
{
|
||||
private string $tableName;
|
||||
|
||||
public function __construct(string $tableName)
|
||||
{
|
||||
parent::__construct(fn() => DB::table($tableName), fn($x, $y) => throw new \Exception("Not supported"));
|
||||
$this->tableName = $tableName;
|
||||
}
|
||||
|
||||
public function scrape(int|string $id): array
|
||||
{
|
||||
throw new \Exception("Not supported");
|
||||
}
|
||||
|
||||
public function tableName(): string
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
public function createEntity()
|
||||
{
|
||||
throw new \Exception("Not supported");
|
||||
}
|
||||
}
|
198
app/Services/DefaultCachedScraperService.php
Normal file
198
app/Services/DefaultCachedScraperService.php
Normal file
@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Concerns\ScraperCacheTtl;
|
||||
use App\Contracts\CachedScraperService;
|
||||
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;
|
||||
use JMS\Serializer\Serializer;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* A service which scrapes data from MAL if cache is expired or empty
|
||||
*/
|
||||
final class DefaultCachedScraperService implements CachedScraperService
|
||||
{
|
||||
use ScraperCacheTtl;
|
||||
|
||||
public function __construct(
|
||||
private readonly Repository $repository,
|
||||
private readonly MalClient $jikan,
|
||||
private readonly Serializer $serializer,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds cached scraper results by cacheKey, if not found scrapes them from MAL via the provided callback.
|
||||
* @param string $cacheKey
|
||||
* @param \Closure $getMalDataCallback
|
||||
* @param int|null $page
|
||||
* @return CachedData
|
||||
*/
|
||||
public function findList(string $cacheKey, \Closure $getMalDataCallback, ?int $page = null): CachedData
|
||||
{
|
||||
$results = $this->get($cacheKey);
|
||||
|
||||
if ($results->isEmpty() || $results->isExpired()) {
|
||||
$page = $page ?? 1;
|
||||
$data = $getMalDataCallback($this->jikan, $page);
|
||||
$scraperResponse = $this->serializeScraperResult($data);
|
||||
$results = $this->updateCacheByKey($cacheKey, $results, $scraperResponse);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds cached scraper results by id in the database, if not found scrapes them from MAL.
|
||||
* @param int $id
|
||||
* @param string $cacheKey
|
||||
* @return CachedData
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function find(int $id, string $cacheKey): CachedData
|
||||
{
|
||||
$results = CachedData::from($this->repository->getAllByMalId($id));
|
||||
|
||||
if ($results->isEmpty() || $results->isExpired()) {
|
||||
$response = $this->repository->scrape($id);
|
||||
|
||||
$this->raiseNotFoundIfErrors($response);
|
||||
|
||||
$results = $this->updateCacheById($id, $cacheKey, $results, $response);
|
||||
}
|
||||
|
||||
$this->raiseNotFoundIfEmpty($results);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function findByKey(string $key, mixed $val, string $cacheKey): CachedData
|
||||
{
|
||||
$results = CachedData::from($this->repository->where($key, $val)->get());
|
||||
|
||||
if ($results->isEmpty() || $results->isExpired()) {
|
||||
$scraperResponse = $this->repository->scrape($key);
|
||||
|
||||
$this->raiseNotFoundIfErrors($scraperResponse);
|
||||
|
||||
$response = $this->prepareScraperResponse($cacheKey, $results->isEmpty(), $scraperResponse);
|
||||
$response->offsetSet($key, $val);
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
$this->repository->insert($response->toArray());
|
||||
}
|
||||
|
||||
if ($results->isExpired()) {
|
||||
$this->repository->where($key, $val)->update($response->toArray());
|
||||
}
|
||||
|
||||
$results = CachedData::from($this->repository->where($key, $val)->get());
|
||||
}
|
||||
|
||||
$this->raiseNotFoundIfEmpty($results);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function get(string $cacheKey): CachedData
|
||||
{
|
||||
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()) {
|
||||
abort(404, "Resource not found.");
|
||||
}
|
||||
}
|
||||
|
||||
private function raiseNotFoundIfErrors(mixed $response)
|
||||
{
|
||||
if (HttpHelper::hasError($response)) {
|
||||
abort(404, "Resource not found.");
|
||||
}
|
||||
}
|
||||
|
||||
private function updateCacheById(int $id, string $cacheKey, CachedData $results, array $scraperResponse): CachedData
|
||||
{
|
||||
$response = $this->prepareScraperResponse($cacheKey, $results->isEmpty(), $scraperResponse);
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
$this->repository->insert($response->toArray());
|
||||
}
|
||||
|
||||
if ($results->isExpired()) {
|
||||
$this->repository->queryByMalId($id)->update($response->toArray());
|
||||
}
|
||||
|
||||
return new CachedData(collect($this->repository->getAllByMalId($id)));
|
||||
}
|
||||
|
||||
private function updateCacheByKey(string $cacheKey, CachedData $results, array $scraperResponse): CachedData
|
||||
{
|
||||
$response = $this->prepareScraperResponse($cacheKey, $results->isEmpty(), $scraperResponse);
|
||||
|
||||
// insert cache if resource doesn't exist
|
||||
if ($results->isEmpty()) {
|
||||
$this->repository->insert($response->toArray());
|
||||
}
|
||||
|
||||
if ($results->isExpired()) {
|
||||
$this->getQueryableByCacheKey($cacheKey)->update($response->toArray());
|
||||
}
|
||||
|
||||
return new CachedData($this->getByCacheKey($cacheKey));
|
||||
}
|
||||
|
||||
private function prepareScraperResponse(string $cacheKey, bool $resultsEmpty, array $scraperResponse): CachedData
|
||||
{
|
||||
$meta = [];
|
||||
if ($resultsEmpty) {
|
||||
$meta = [
|
||||
'createdAt' => new UTCDateTime(),
|
||||
'request_hash' => $cacheKey
|
||||
];
|
||||
}
|
||||
|
||||
// Update `modifiedAt` meta
|
||||
$meta['modifiedAt'] = new UTCDateTime();
|
||||
|
||||
// join meta data with response
|
||||
return new CachedData(collect($meta + $scraperResponse));
|
||||
}
|
||||
|
||||
private function getByCacheKey(string $cacheKey): Collection
|
||||
{
|
||||
return $this->getQueryableByCacheKey($cacheKey)->get();
|
||||
}
|
||||
|
||||
private function getQueryableByCacheKey(string $cacheKey): Builder
|
||||
{
|
||||
return $this->repository->where("request_hash", $cacheKey);
|
||||
}
|
||||
|
||||
private function serializeScraperResult($data): array
|
||||
{
|
||||
return $this->serializer->toArray($data);
|
||||
}
|
||||
}
|
86
app/Support/CachedData.php
Normal file
86
app/Support/CachedData.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Concerns\ScraperCacheTtl;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class CachedData
|
||||
{
|
||||
use ScraperCacheTtl;
|
||||
|
||||
public function __construct(
|
||||
private readonly Collection $scraperResult
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public static function from(Collection $scraperResult): self
|
||||
{
|
||||
return new self($scraperResult);
|
||||
}
|
||||
|
||||
public function collect(): Collection
|
||||
{
|
||||
return $this->scraperResult;
|
||||
}
|
||||
|
||||
public function offsetGet(string $key): mixed
|
||||
{
|
||||
return $this->scraperResult->offsetGet($key);
|
||||
}
|
||||
|
||||
public function offsetSet(string $key, mixed $value): void
|
||||
{
|
||||
$this->scraperResult->offsetSet($key, $value);
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->scraperResult->isEmpty();
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
$lastModified = $this->lastModified();
|
||||
|
||||
if ($lastModified === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$expiry = $this->expiry();
|
||||
|
||||
return time() > $expiry;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->scraperResult->toArray();
|
||||
}
|
||||
|
||||
public function expiry(): int
|
||||
{
|
||||
$modifiedAt = $this->lastModified();
|
||||
$ttl = $this->cacheTtl();
|
||||
return $modifiedAt !== null ? $ttl + $modifiedAt : $ttl;
|
||||
}
|
||||
|
||||
public function lastModified(): ?int
|
||||
{
|
||||
if ($this->scraperResult->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = $this->scraperResult->first();
|
||||
|
||||
if (is_array($result)) {
|
||||
return (int) $result["modifiedAt"]->toDateTime()->format("U");
|
||||
}
|
||||
|
||||
if (is_object($result)) {
|
||||
return $result->modifiedAt->toDateTime()->format("U");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ use App\Contracts\UnitOfWork;
|
||||
use App\Contracts\UserRepository;
|
||||
use App\Repositories\AnimeGenresRepository;
|
||||
use App\Repositories\DatabaseRepository;
|
||||
use App\Repositories\DocumentRepository;
|
||||
use App\Repositories\MangaGenresRepository;
|
||||
|
||||
final class JikanUnitOfWork implements UnitOfWork
|
||||
@ -93,4 +94,9 @@ final class JikanUnitOfWork implements UnitOfWork
|
||||
{
|
||||
return new DatabaseRepository(fn () => $modelClass::query(), fn ($x, $y) => $modelClass::search($x, $y));
|
||||
}
|
||||
|
||||
public function documents(string $tableName): DocumentRepository
|
||||
{
|
||||
return new DocumentRepository($tableName);
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,13 @@
|
||||
namespace App\Support;
|
||||
|
||||
use App\Contracts\RepositoryQuery as RepositoryQueryContract;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Contracts\Database\Query\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Scout\Builder as ScoutBuilder;
|
||||
|
||||
class RepositoryQuery extends RepositoryQueryBase implements RepositoryQueryContract
|
||||
{
|
||||
public function filter(Collection $params): EloquentBuilder|ScoutBuilder
|
||||
public function filter(Collection $params): Builder|ScoutBuilder
|
||||
{
|
||||
return $this->queryable()->filter($params);
|
||||
}
|
||||
@ -18,4 +18,9 @@ class RepositoryQuery extends RepositoryQueryBase implements RepositoryQueryCont
|
||||
{
|
||||
return $this->searchable($keywords, $callback);
|
||||
}
|
||||
|
||||
public function where(string $key, mixed $value): Builder
|
||||
{
|
||||
return $this->queryable()->where($key, $value);
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
namespace App\Support;
|
||||
use Laravel\Scout\Builder as ScoutBuilder;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Contracts\Database\Query\Builder;
|
||||
|
||||
class RepositoryQueryBase
|
||||
{
|
||||
private ?EloquentBuilder $queryableBuilder;
|
||||
private ?Builder $queryableBuilder;
|
||||
private ?ScoutBuilder $searchableBuilder;
|
||||
|
||||
public function __construct(
|
||||
@ -15,7 +15,7 @@ class RepositoryQueryBase
|
||||
{
|
||||
}
|
||||
|
||||
protected function queryable(bool $createNew = false): EloquentBuilder
|
||||
protected function queryable(bool $createNew = false): Builder
|
||||
{
|
||||
if ($createNew) {
|
||||
$callback = $this->getQueryable;
|
||||
|
Loading…
x
Reference in New Issue
Block a user