wip - major refactor

- AppServiceProvider is needs more work to wire in new services
- todo: more dtos
- todo: add unit tests
- todo: add more integration tests
This commit is contained in:
pushrbx 2023-01-02 16:29:05 +00:00
parent 30fc65c6c4
commit a145f18bbd
139 changed files with 4360 additions and 198 deletions

View File

@ -2,7 +2,12 @@
namespace App;
use App\Concerns\FilteredByLetter;
use App\Concerns\MediaFilters;
use App\Enums\AnimeRatingEnum;
use App\Enums\AnimeTypeEnum;
use App\Http\HttpHelper;
use Carbon\CarbonImmutable;
use Database\Factories\AnimeFactory;
use Illuminate\Support\Facades\App;
use Jikan\Jikan;
@ -11,11 +16,9 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
class Anime extends JikanApiSearchableModel
{
use HasFactory;
use HasFactory, MediaFilters, FilteredByLetter;
// note that here we skip "score", "min_score", "max_score", "rating" and others because they need special logic
// to set the correct filtering on the ORM.
protected array $filters = ["order_by", "status", "type", "sort"];
protected array $filters = ["order_by", "status", "type", "sort", "max_score", "min_score", "score", "rating", "start_date", "end_date", "producer", "producers", "letter"];
/**
* The attributes that are mass assignable.
*
@ -25,6 +28,8 @@ class Anime extends JikanApiSearchableModel
'mal_id','url','title','title_english','title_japanese','title_synonyms', 'titles', 'images', 'type','source','episodes','status','airing','aired','duration','rating','score','scored_by','rank','popularity','members','favorites','synopsis','background','premiered','broadcast','related','producers','licensors','studios','genres', 'explicit_genres', 'themes', 'demographics', 'opening_themes','ending_themes'
];
protected ?string $displayNameFieldName = "title";
/**
* The accessors to append to the model's array form.
*
@ -124,6 +129,68 @@ class Anime extends JikanApiSearchableModel
];
}
/** @noinspection PhpUnused */
public function filterByLetter(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, string $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("title", "like", "{$value}%");
}
/** @noinspection PhpUnused */
public function filterByType(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, AnimeTypeEnum $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("type", $value->label);
}
/** @noinspection PhpUnused */
public function filterByRating(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, AnimeRatingEnum $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("rating", $value->label);
}
/** @noinspection PhpUnused */
public function filterByStartDate(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, CarbonImmutable $date): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("aired.from", $date->setTime(0, 0)->toAtomString());
}
/** @noinspection PhpUnused */
public function filterByEndDate(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, CarbonImmutable $date): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("aired.to", $date->setTime(0, 0)->toAtomString());
}
public function filterByProducer(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, string $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
if (empty($value)) {
return $query;
}
$producer = (int)$value;
return $query
->orWhere('producers.mal_id', $producer)
->orWhere('licensors.mal_id', $producer)
->orWhere('studios.mal_id', $producer);
}
/** @noinspection PhpUnused */
public function filterByProducers(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, string $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
if (empty($value)) {
return $query;
}
$producers = explode(',', $value);
foreach ($producers as $producer) {
if (empty($producer)) {
continue;
}
$query = $this->filterByProducer($query, $value);
}
return $query;
}
public static function scrape(int $id)
{
$data = app('JikanParser')->getAnime(new AnimeRequest($id));

37
app/Casts/EnumCast.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace App\Casts;
use BackedEnum;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Casts\Uncastable;
use Spatie\LaravelData\Exceptions\CannotCastEnum;
use Spatie\LaravelData\Support\DataProperty;
use Throwable;
class EnumCast implements Cast
{
public function __construct(
protected ?string $type = null
) {
}
/**
* @throws CannotCastEnum
*/
public function cast(DataProperty $property, mixed $value, array $context): mixed
{
$type = $this->type ?? $property->type->findAcceptedTypeForBaseType(BackedEnum::class);
if ($type === null) {
return Uncastable::create();
}
try {
/** @noinspection PhpUndefinedMethodInspection */
return $type::from($value);
} catch (Throwable $e) {
throw CannotCastEnum::create($type, $value);
}
}
}

View File

@ -2,15 +2,16 @@
namespace App;
use App\Concerns\FilteredByLetter;
use Jikan\Jikan;
use Jikan\Request\Character\CharacterRequest;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Character extends JikanApiSearchableModel
{
use HasFactory;
use HasFactory, FilteredByLetter;
protected array $filters = ["order_by", "sort"];
protected array $filters = ["order_by", "sort", "letter"];
/**
* The attributes that are mass assignable.
@ -28,6 +29,8 @@ class Character extends JikanApiSearchableModel
*/
protected $appends = ['images', 'favorites'];
protected ?string $displayNameFieldName = "name";
/**
* The table associated with the model.
*
@ -44,6 +47,7 @@ class Character extends JikanApiSearchableModel
'_id', 'trailer_url', 'premiered', 'opening_themes', 'ending_themes', 'images', 'member_favorites'
];
/** @noinspection PhpUnused */
public function getFavoritesAttribute()
{
return $this->attributes['member_favorites'];

View File

@ -2,11 +2,16 @@
namespace App;
use App\Concerns\FilteredByLetter;
use App\Enums\ClubCategoryEnum;
use App\Enums\ClubTypeEnum;
use Jikan\Request\Club\ClubRequest;
class Club extends JikanApiSearchableModel
{
protected array $filters = ["order_by", "sort"];
use FilteredByLetter;
protected array $filters = ["order_by", "sort", "letter", "category", "type"];
/**
* The attributes that are mass assignable.
@ -24,6 +29,8 @@ class Club extends JikanApiSearchableModel
*/
protected $appends = ['images'];
protected ?string $displayNameFieldName = "title";
/**
* The table associated with the model.
*
@ -40,6 +47,18 @@ class Club extends JikanApiSearchableModel
'_id', 'request_hash', 'expiresAt', 'images'
];
/** @noinspection PhpUnused */
public function filterByCategory(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, ClubCategoryEnum $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("category", $value->label);
}
/** @noinspection PhpUnused */
public function filterByType(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, ClubTypeEnum $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("access", $value->label);
}
public static function scrape(int $id)
{
$data = app('JikanParser')->getClub(new ClubRequest($id));

View File

@ -0,0 +1,33 @@
<?php
namespace App\Concerns;
trait FilteredByLetter
{
/**
* The name of the field which contains the display name of the record.
* @var ?string
*/
protected ?string $displayNameFieldName;
/** @noinspection PhpUnused */
public function filterByLetter(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, string $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
if (empty($this->displayNameFieldName)) {
return $query;
}
return $query->where($this->displayNameFieldName, "like", "{$value}%");
}
public function getDisplayNameFieldName(): string
{
return $this->displayNameFieldName;
}
public function displayNameFieldName(string $name): self
{
$this->displayNameFieldName = $name;
return $this;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Concerns;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
/**
* Helper trait for data transfer objects
*
* Ref: https://spatie.be/docs/laravel-data/v2/as-a-data-transfer-object/request-to-data-object#content-mapping-a-request-onto-a-data-object
*/
trait HasRequestFingerprint
{
protected ?string $fingerprint;
public static function fromRequest(Request $request): ?static
{
$result = new self();
$result->fingerprint = HttpHelper::resolveRequestFingerprint($request);
return $result;
}
public function getFingerPrint(): string
{
return $this->fingerprint;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Concerns;
use Spatie\Enum\Enum;
trait MediaFilters
{
public function filterByMaxScore(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, mixed $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("score", "<=", floatval($value));
}
public function filterByMinScore(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, mixed $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("score", ">=", floatval($value));
}
public function filterByScore(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, mixed $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("score", floatval($value));
}
public function filterByStatus(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, Enum $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("status", $value->label);
}
public function filterByGenres(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, mixed $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
if (!is_string($value) || empty($value)) {
return $query;
}
$genres = explode(',', $value);
foreach ($genres as $genreItem) {
$genre = (int) $genreItem;
$query = $query->orWhere('genres.mal_id', $genre)
->orWhere('demographics.mal_id', $genre)
->orWhere('themes.mal_id', $genre)
->orWhere('explicit_genres.mal_id', $genre);
}
return $query;
}
public function filterByGenresExclude(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, mixed $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
if (!is_string($value) || empty($value)) {
return $query;
}
$genres = explode(',', $value);
foreach ($genres as $genreItem) {
$genre = (int) $genreItem;
$query = $query
->where('genres.mal_id', '!=', $genre)
->where('demographics.mal_id', '!=', $genre)
->where('themes.mal_id', '!=', $genre)
->where('explicit_genres.mal_id', '!=', $genre);
}
return $query;
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Concerns;
trait ResolvesPaginatorParams
{
private function getPaginatorParams(?int $limit = null, ?int $page = null): array
{
$default_max_results_per_page = env('MAX_RESULTS_PER_PAGE', 25);
$limit = $limit ?? $default_max_results_per_page;
$page = $page ?? 1;
return compact($limit, $page);
}
}

View File

@ -0,0 +1,186 @@
<?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;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Contracts;
use App\Anime;
use \Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use \Laravel\Scout\Builder as ScoutBuilder;
/**
* @implements Repository<Anime>
*/
interface AnimeRepository extends Repository
{
public function getTopAiringItems(): EloquentBuilder|ScoutBuilder;
public function getTopUpcomingItems(): EloquentBuilder|ScoutBuilder;
public function exceptItemsWithAdultRating(): EloquentBuilder|ScoutBuilder;
public function orderByPopularity(): EloquentBuilder|ScoutBuilder;
public function orderByFavoriteCount(): EloquentBuilder|ScoutBuilder;
public function orderByRank(): EloquentBuilder|ScoutBuilder;
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Contracts;
use App\Character;
use \Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use \Laravel\Scout\Builder as ScoutBuilder;
/**
* @implements Repository<Character>
*/
interface CharacterRepository extends Repository
{
public function topCharacters(): EloquentBuilder|ScoutBuilder;
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Contracts;
use App\Club;
/**
* @implements Repository<Club>
*/
interface ClubRepository extends Repository
{
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Contracts;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Response;
/**
* Marker interface to represent a request with a response
* @template T of ResourceCollection|JsonResource|Response
*/
interface DataRequest
{
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Contracts;
use Illuminate\Support\Collection;
interface GenreRepository extends Repository
{
public function genres(): Collection;
public function getExplicitItems(): Collection;
public function getThemes(): Collection;
public function getDemographics(): Collection;
public function all(): Collection;
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Contracts;
use App\Magazine;
/**
* @implements Repository<Magazine>
*/
interface MagazineRepository extends Repository
{
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Contracts;
use App\Manga;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Laravel\Scout\Builder as ScoutBuilder;
/**
* @implements Repository<Manga>
*/
interface MangaRepository extends Repository
{
public function getTopPublishingItems(): EloquentBuilder|ScoutBuilder;
public function getTopUpcomingItems(): EloquentBuilder|ScoutBuilder;
public function orderByPopularity(): EloquentBuilder|ScoutBuilder;
public function orderByFavoriteCount(): EloquentBuilder|ScoutBuilder;
public function orderByRank(): EloquentBuilder|ScoutBuilder;
}

View File

@ -0,0 +1,19 @@
<?php
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
{
/**
* Send a request to a single handler
* @template T
* @param DataRequest<T> $requestData
* @return T
*/
public function send(DataRequest $requestData);
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Contracts;
use App\Person;
use \Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use \Laravel\Scout\Builder as ScoutBuilder;
/**
* @implements Repository<Person>
*/
interface PeopleRepository extends Repository
{
public function topPeople(): EloquentBuilder|ScoutBuilder;
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Contracts;
use App\Producers;
/**
* @implements Repository<Producers>
*/
interface ProducerRepository extends Repository
{
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Contracts;
use App\JikanApiModel;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Support\Collection;
/**
* @template T of JikanApiModel
* @implements RepositoryQuery<T>
*/
interface Repository extends RepositoryQuery
{
/**
* @return T
*/
public function createEntity();
/**
* @return ?T
*/
public function getByMalId(int $id);
public function getAllByMalId(int $id): Collection;
public function queryByMalId(int $id): EloquentBuilder;
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 insert(array $attributes): bool;
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Contracts;
use App\JikanApiModel;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Support\Collection;
use Laravel\Scout\Builder as ScoutBuilder;
/**
* @template T of JikanApiModel
*/
interface RepositoryQuery
{
/**
* @param Collection $params
* @return EloquentBuilder<T>|ScoutBuilder<T>
*/
public function filter(Collection $params): EloquentBuilder|ScoutBuilder;
/**
* @param string $keywords
* @param \Closure|null $callback
* @return ScoutBuilder<T>
*/
public function search(string $keywords, ?\Closure $callback = null): ScoutBuilder;
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Contracts;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Response;
/**
* @template TRequest of DataRequest<TResponse>
* @template TResponse of ResourceCollection|JsonResource|Response
*/
interface RequestHandler
{
/**
* @param TRequest $request
* @return TResponse
*/
public function handle($request);
/**
* @return class-string<TRequest>
*/
public function requestClass(): string;
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Contracts;
interface UnitOfWork
{
public function anime(): AnimeRepository;
public function manga(): MangaRepository;
public function characters(): CharacterRepository;
public function people(): PeopleRepository;
public function clubs(): ClubRepository;
public function producers(): ProducerRepository;
public function magazines(): MagazineRepository;
public function users(): UserRepository;
public function animeGenres(): GenreRepository;
public function mangaGenres(): GenreRepository;
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Contracts;
use App\Profile;
/**
* @implements Repository<Profile>
*/
interface UserRepository extends Repository
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\GenreCollection;
/**
* @implements DataRequest<GenreCollection>
*/
final class AnimeGenreListCommand extends GenreListCommand implements DataRequest
{
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use App\Enums\AnimeOrderByEnum;
use App\Enums\AnimeRatingEnum;
use App\Enums\AnimeStatusEnum;
use App\Http\Resources\V4\AnimeCollection;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Prohibits;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\WithCast;
use App\Casts\EnumCast;
use Spatie\LaravelData\Optional;
/**
* @implements DataRequest<AnimeCollection>
*/
final class AnimeSearchCommand extends MediaSearchCommand implements DataRequest
{
#[WithCast(EnumCast::class, AnimeStatusEnum::class)]
public AnimeStatusEnum|Optional $status;
#[WithCast(EnumCast::class, AnimeRatingEnum::class)]
public AnimeRatingEnum|Optional $rating;
#[IntegerType, Min(1)]
public int|Optional $producer;
#[Prohibits("producer"), StringType]
public string|Optional $producers;
#[MapInputName("order_by"), MapOutputName("order_by"), WithCast(EnumCast::class, AnimeOrderByEnum::class)]
public AnimeOrderByEnum|Optional $orderBy;
public static function rules(): array
{
return [
...parent::rules(),
"status" => [new EnumRule(AnimeStatusEnum::class)],
"rating" => [new EnumRule(AnimeRatingEnum::class)],
"order_by" => [new EnumRule(AnimeOrderByEnum::class)]
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Contracts\DataRequest;
use App\Enums\CharacterOrderByEnum;
use App\Http\Resources\V4\CharacterCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithCast;
/**
* @implements DataRequest<CharacterCollection>
*/
final class CharactersSearchCommand extends SearchCommand implements DataRequest
{
#[MapInputName("order_by"), MapOutputName("order_by"), WithCast(EnumCast::class, CharacterOrderByEnum::class)]
public CharacterOrderByEnum|Optional $orderBy;
public static function rules(): array
{
return [
"order_by" => [new EnumRule(CharacterOrderByEnum::class)]
];
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Contracts\DataRequest;
use App\Enums\ClubCategoryEnum;
use App\Enums\ClubOrderByEnum;
use App\Enums\ClubTypeEnum;
use App\Http\Resources\V4\ClubCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithCast;
/**
* @implements DataRequest<ClubCollection>
*/
final class ClubSearchCommand extends SearchCommand implements DataRequest
{
#[WithCast(EnumCast::class, ClubCategoryEnum::class)]
public ClubCategoryEnum|Optional $category;
#[WithCast(EnumCast::class, ClubTypeEnum::class)]
public ClubTypeEnum|Optional $type;
#[MapInputName("order_by"), MapOutputName("order_by"), WithCast(EnumCast::class, ClubOrderByEnum::class)]
public ClubOrderByEnum|Optional $orderBy;
public static function rules(): array
{
return [
...parent::rules(),
"category" => [new EnumRule(ClubCategoryEnum::class)],
"type" => [new EnumRule(ClubTypeEnum::class)],
"order_by" => [new EnumRule(ClubOrderByEnum::class)]
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Dto;
use App\Enums\GenreFilterEnum;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\WithCast;
use App\Casts\EnumCast;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional;
abstract class GenreListCommand extends Data
{
#[WithCast(EnumCast::class, GenreFilterEnum::class)]
public GenreFilterEnum|Optional $filter;
public static function rules(): array
{
return [
"filter" => [new EnumRule(GenreFilterEnum::class)]
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Contracts\DataRequest;
use App\Enums\MagazineOrderByEnum;
use App\Http\Resources\V4\MagazineCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithCast;
/**
* @implements DataRequest<MagazineCollection>
*/
final class MagazineSearchCommand extends SearchCommand implements DataRequest
{
#[MapInputName("order_by"), MapOutputName("order_by"), WithCast(EnumCast::class, MagazineOrderByEnum::class)]
public MagazineOrderByEnum|Optional $orderBy;
public static function rules(): array
{
return [
"order_by" => [new EnumRule(MagazineOrderByEnum::class)]
];
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\GenreCollection;
/**
* @implements DataRequest<GenreCollection>
*/
final class MangaGenreListCommand extends GenreListCommand implements DataRequest
{
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Contracts\DataRequest;
use App\Enums\MangaOrderByEnum;
use App\Enums\MangaStatusEnum;
use App\Http\Resources\V4\MangaCollection;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/**
* @implements DataRequest<MangaCollection>
*/
final class MangaSearchCommand extends MediaSearchCommand implements DataRequest
{
#[WithCast(EnumCast::class, MangaStatusEnum::class)]
public MangaStatusEnum|Optional $status;
#[StringType]
public string|Optional $magazines;
#[MapInputName("order_by"), MapOutputName("order_by"), WithCast(EnumCast::class, MangaOrderByEnum::class)]
public MangaOrderByEnum|Optional $orderBy;
public static function rules(): array
{
return [
...parent::rules(),
"status" => new EnumRule(MangaStatusEnum::class),
"order_by" => new EnumRule(MangaOrderByEnum::class)
];
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Dto;
use Carbon\CarbonImmutable;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\Validation\AfterOrEqual;
use Spatie\LaravelData\Attributes\Validation\BeforeOrEqual;
use Spatie\LaravelData\Attributes\Validation\Between;
use Spatie\LaravelData\Attributes\Validation\DateFormat;
use Spatie\LaravelData\Attributes\Validation\GreaterThanOrEqualTo;
use Spatie\LaravelData\Attributes\Validation\LessThanOrEqualTo;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Attributes\Validation\Prohibits;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Attributes\WithTransformer;
use Spatie\LaravelData\Casts\DateTimeInterfaceCast;
use Spatie\LaravelData\Optional;
use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer;
class MediaSearchCommand extends SearchCommand
{
#[MapInputName("min_score"), MapOutputName("min_score"), Between(1.00, 9.99), Numeric]
public float|Optional $minScore;
#[MapInputName("max_score"), MapOutputName("max_score"), Between(1.00, 9.99), Numeric]
public float|Optional $maxScore;
#[Between(1.00, 9.99), Numeric, Prohibits(["min_score", "max_score"])]
public float|Optional $score;
public bool|Optional $sfw;
public string|Optional $genres;
#[MapInputName("genres_exclude"), MapOutputName("genres_exclude")]
public string|Optional $genresExclude;
#[WithCast(DateTimeInterfaceCast::class), WithTransformer(DateTimeInterfaceTransformer::class)]
#[BeforeOrEqual("end_date"), DateFormat("Y-m-d")]
public CarbonImmutable|Optional $start_date;
#[WithCast(DateTimeInterfaceCast::class), WithTransformer(DateTimeInterfaceTransformer::class)]
#[AfterOrEqual("start_date"), DateFormat("Y-m-d")]
public CarbonImmutable|Optional $end_date;
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Contracts\DataRequest;
use App\Enums\PeopleOrderByEnum;
use App\Http\Resources\V4\PersonCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithCast;
/**
* @implements DataRequest<PersonCollection>
*/
final class PeopleSearchCommand extends SearchCommand implements DataRequest
{
#[MapInputName("order_by"), MapOutputName("order_by"), WithCast(EnumCast::class, PeopleOrderByEnum::class)]
public PeopleOrderByEnum|Optional $orderBy;
public static function rules(): array
{
return [
"order_by" => [new EnumRule(PeopleOrderByEnum::class)]
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Contracts\DataRequest;
use App\Enums\ProducerOrderByEnum;
use App\Http\Resources\V4\ProducerCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithCast;
/**
* @implements DataRequest<ProducerCollection>
*/
final class ProducersSearchCommand extends SearchCommand implements DataRequest
{
#[MapInputName("order_by"), MapOutputName("order_by"), WithCast(EnumCast::class, ProducerOrderByEnum::class)]
public ProducerOrderByEnum|Optional $orderBy;
public static function rules(): array
{
return [
"order_by" => [new EnumRule(ProducerOrderByEnum::class)]
];
}
}

View File

@ -0,0 +1,21 @@
<?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;
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Contracts\DataRequest;
use App\Enums\AnimeTypeEnum;
use App\Enums\TopAnimeFilterEnum;
use App\Http\Resources\V4\AnimeCollection;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Optional;
/**
* @implements DataRequest<AnimeCollection>
*/
final class QueryTopAnimeItemsCommand extends QueryTopItemsCommand implements DataRequest
{
#[WithCast(EnumCast::class, AnimeTypeEnum::class)]
public AnimeTypeEnum|Optional $type;
#[WithCast(EnumCast::class, TopAnimeFilterEnum::class)]
public TopAnimeFilterEnum|Optional $filter;
public static function rules(): array
{
return [
"type" => [new EnumRule(AnimeTypeEnum::class)],
"filter" => [new EnumRule(TopAnimeFilterEnum::class)]
];
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\CharacterCollection;
/**
* @implements DataRequest<CharacterCollection>
*/
final class QueryTopCharactersCommand extends QueryTopItemsCommand implements DataRequest
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use Illuminate\Support\Optional;
use Spatie\LaravelData\Data;
abstract class QueryTopItemsCommand extends Data
{
public int|Optional $page;
public int|Optional $limit;
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Contracts\DataRequest;
use App\Enums\MangaTypeEnum;
use App\Enums\TopMangaFilterEnum;
use App\Http\Resources\V4\MangaCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\Rule;
use Spatie\LaravelData\Attributes\WithCast;
/**
* @implements DataRequest<MangaCollection>
*/
final class QueryTopMangaItemsCommand extends QueryTopItemsCommand implements DataRequest
{
#[WithCast(EnumCast::class, MangaTypeEnum::class)]
public MangaTypeEnum|Optional $type;
#[WithCast(EnumCast::class, TopMangaFilterEnum::class)]
public TopMangaFilterEnum|Optional $filter;
public static function rules(): array
{
return [
"type" => [new EnumRule(MangaTypeEnum::class)],
"filter" => [new EnumRule(TopMangaFilterEnum::class)]
];
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto;
use App\Contracts\DataRequest;
use App\Http\Resources\V4\PersonCollection;
/**
* @implements DataRequest<PersonCollection>
*/
final class QueryTopPeopleCommand extends QueryTopItemsCommand implements DataRequest
{
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Dto;
use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest;
use Illuminate\Http\JsonResponse;
/**
* @implements DataRequest<JsonResponse>
*/
final class QueryTopReviewsCommand extends QueryTopItemsCommand implements DataRequest
{
use HasRequestFingerprint;
}

44
app/Dto/SearchCommand.php Normal file
View File

@ -0,0 +1,44 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Enums\SortDirection;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\Validation\Alpha;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Size;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional;
class SearchCommand extends Data
{
/**
* The search keywords
* @var string|Optional
*/
#[Max(255), StringType]
public string|Optional $q;
#[IntegerType, Min(1)]
public int|Optional $limit;
#[WithCast(EnumCast::class, SortDirection::class)]
public SortDirection|Optional $sort;
#[Size(1), StringType, Alpha]
public string|Optional $letter;
public static function rules(): array
{
return [
"sort" => [new EnumRule(SortDirection::class)]
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Dto;
use App\Casts\EnumCast;
use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest;
use App\Enums\GenderEnum;
use App\Http\Resources\V4\UserCollection;
use Illuminate\Support\Optional;
use Spatie\Enum\Laravel\Rules\EnumRule;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\WithCast;
/**
* @implements DataRequest<UserCollection>
*/
final class UsersSearchCommand extends SearchCommand implements DataRequest
{
use HasRequestFingerprint;
public int|Optional $minAge;
public int|Optional $maxAge;
#[WithCast(EnumCast::class, GenderEnum::class)]
public GenderEnum|Optional $gender;
#[StringType]
public string|Optional $location;
public static function rules(): array
{
return [
...parent::rules(),
"gender" => [new EnumRule(GenderEnum::class)]
];
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self mal_id()
* @method static self title()
* @method static self type()
* @method static self rating()
* @method static self start_date()
* @method static self end_date()
* @method static self episodes()
* @method static self score()
* @method static self scored_by()
* @method static self rank
* @method static self popularity()
* @method static self members()
* @method static self favorites()
*
* @OA\Schema(
* schema="anime_search_query_orderby",
* description="Available Anime order_by properties",
* type="string",
* enum={"mal_id", "title", "type", "rating", "start_date", "end_date", "episodes", "score", "scored_by", "rank", "popularity", "members", "favorites" }
* )
*/
final class AnimeOrderByEnum extends Enum
{
protected static function labels(): array
{
return [
'start_date' => 'aired.from',
'end_date' => 'aired.to',
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self g()
* @method static self pg()
* @method static self pg13()
* @method static self r17()
* @method static self r()
* @method static self rx()
*
* @OA\Schema(
* schema="anime_search_query_rating",
* description="Available Anime audience ratings<br><br><b>Ratings</b><br><ul><li>G - All Ages</li><li>PG - Children</li><li>PG-13 - Teens 13 or older</li><li>R - 17+ (violence & profanity)</li><li>R+ - Mild Nudity</li><li>Rx - Hentai</li></ul>",
* type="string",
* enum={"g","pg","pg13","r17","r","rx"}
* )
*/
final class AnimeRatingEnum extends Enum
{
protected static function labels(): array
{
return [
"g" => "G - All Ages",
"pg" => "PG - Children",
"pg13" => "PG-13 - Teens 13 or older",
"r17" => "R - 17+ (violence & profanity)",
"r" => "R+ - Mild Nudity",
"rx" => "Rx - Hentai"
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self airing()
* @method static self complete()
* @method static self upcoming()
*
* @OA\Schema(
* schema="anime_search_query_status",
* description="Available Anime statuses",
* type="string",
* enum={"airing","complete","upcoming"}
* )
*/
final class AnimeStatusEnum extends Enum
{
protected static function labels(): array
{
return [
"airing" => "Currently Airing",
"complete" => "Finished Airing",
"upcoming" => "Not yet aired",
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self tv()
* @method static self movie()
* @method static self ova()
* @method static self special()
* @method static self ona()
* @method static self music()
*
* @OA\Schema(
* schema="anime_search_query_type",
* description="Available Anime types",
* type="string",
* enum={"tv","movie","ova","special","ona","music"}
* )
*/
final class AnimeTypeEnum extends Enum
{
protected static function labels(): array
{
return [
'tv' => 'TV',
'movie' => 'Movie',
'ova' => 'OVA',
'special' => 'Special',
'ona' => 'ONA',
'music' => 'Music'
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self mal_id()
* @method static self name()
* @method static self favorites()
*
* @OA\Schema(
* schema="characters_search_query_orderby",
* description="Available Character order_by properties",
* type="string",
* enum={"mal_id", "name", "favorites"}
* )
*/
final class CharacterOrderByEnum extends Enum
{
protected static function labels(): array
{
return [
"favorites" => "member_favorites"
];
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self anime()
* @method static self manga()
* @method static self actors_and_artists()
* @method static self characters()
* @method static self cities_and_neighborhoods()
* @method static self companies()
* @method static self conventions()
* @method static self games()
* @method static self japan()
* @method static self music()
* @method static self other()
* @method static self schools()
*
* @OA\Schema(
* schema="club_search_query_category",
* description="Club Search Query Category",
* type="string",
* enum={
* "anime","manga","actors_and_artists","characters",
* "cities_and_neighborhoods","companies","conventions","games",
* "japan","music","other","schools"
* }
* )
*/
final class ClubCategoryEnum extends Enum
{
protected static function labels(): array
{
return [
'anime' => 'Anime',
'manga' => 'Manga',
'actors_and_artists' => 'Actors & Artists',
'characters' => 'Characters',
'cities_and_neighborhoods' => 'Cities & Neighborhoods',
'companies' => 'Companies',
'conventions' => 'Conventions',
'games' => 'Games',
'japan' => 'Japan',
'music' => 'Music',
'other' => 'Other',
'schools' => 'Schools'
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self mal_id()
* @method static self title()
* @method static self members_count()
* @method static self pictures_count()
* @method static self created()
*
* @OA\Schema(
* schema="club_search_query_orderby",
* description="Club Search Query OrderBy",
* type="string",
* enum={"mal_id","title","members_count","pictures_count","created"}
* )
*/
final class ClubOrderByEnum extends Enum
{
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self public()
* @method static self private()
* @method static self secret()
*
* @OA\Schema(
* schema="club_search_query_type",
* description="Club Search Query Type",
* type="string",
* enum={"public","private","secret"}
* )
*/
final class ClubTypeEnum extends Enum
{
}

24
app/Enums/GenderEnum.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App\Enums;
use Jikan\Helper\Constants as JikanConstants;
/**
* @method static self any()
* @method static self male()
* @method static self female()
* @method static self nonbinary()
*/
final class GenderEnum extends \Spatie\Enum\Laravel\Enum
{
protected static function labels(): array
{
return [
'any' => JikanConstants::SEARCH_USER_GENDER_ANY,
'male' => JikanConstants::SEARCH_USER_GENDER_MALE,
'female' => JikanConstants::SEARCH_USER_GENDER_FEMALE,
'nonbinary' => JikanConstants::SEARCH_USER_GENDER_NONBINARY
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self genres()
* @method static self explicit_genres()
* @method static self themes()
* @method static self demographics()
*
* @OA\Schema(
* schema="genre_query_filter",
* description="Filter genres by type",
* type="string",
* enum={"genres","explicit_genres", "themes", "demographics"}
* )
*/
final class GenreFilterEnum extends Enum
{
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self mal_id()
* @method static self name()
* @method static self count()
*
* @OA\Schema(
* schema="magazines_query_orderby",
* description="Order by magazine data",
* type="string",
* enum={"mal_id", "name", "count"}
* )
*/
final class MagazineOrderByEnum extends Enum
{
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self mal_id()
* @method static self title()
* @method static self start_date()
* @method static self end_date()
* @method static self chapters()
* @method static self volumes()
* @method static self score()
* @method static self scored_by()
* @method static self rank()
* @method static self popularity()
* @method static self members()
* @method static self favorites()
*
* @OA\Schema(
* schema="manga_search_query_orderby",
* description="Available Manga order_by properties",
* type="string",
* enum={"mal_id", "title", "start_date", "end_date", "chapters", "volumes", "score", "scored_by", "rank", "popularity", "members", "favorites"}
* )
*/
final class MangaOrderByEnum extends Enum
{
protected static function labels(): array
{
return [
'start_date' => 'published.from',
'end_date' => 'published.to'
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self publishing()
* @method static self complete()
* @method static self hiatus()
* @method static self discontinued()
* @method static self upcoming()
*
* @OA\Schema(
* schema="manga_search_query_status",
* description="Available Manga statuses",
* type="string",
* enum={"publishing","complete","hiatus","discontinued","upcoming"}
* )
*/
final class MangaStatusEnum extends Enum
{
protected static function labels(): array
{
return [
"publishing" => "Publishing",
"complete" => "Finished",
"hiatus" => "On Hiatus",
"discontinued" => "Discontinued",
"upcoming" => "Not yet published"
];
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self manga()
* @method static self novel()
* @method static self lightnovel()
* @method static self oneshot()
* @method static self doujin()
* @method static self manhwa()
* @method static self manhua()
*
* @OA\Schema(
* schema="manga_search_query_type",
* description="Available Manga types",
* type="string",
* enum={"manga","novel", "lightnovel", "oneshot","doujin","manhwa","manhua"}
* )
*/
final class MangaTypeEnum extends Enum
{
protected static function labels(): array
{
return [
'manga' => 'Manga',
'novel' => 'Novel',
'lightnovel' => 'Light Novel',
'oneshot' => 'One-shot',
'doujin' => 'Doujinshi',
'manhwa' => 'Manhwa',
'manhua' => 'Manhua'
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self mal_id()
* @method static self name()
* @method static self birthday()
* @method static self favorites()
*
* @OA\Schema(
* schema="people_search_query_orderby",
* description="Available People order_by properties",
* type="string",
* enum={"mal_id", "name", "birthday", "favorites"}
* )
*/
final class PeopleOrderByEnum extends Enum
{
protected static function labels()
{
return [
"favorites" => "member_favorites"
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self mal_id()
* @method static self count()
* @method static self favorites()
* @method static self established()
*
* @OA\Schema(
* schema="producers_query_orderby",
* description="Producers Search Query Order By",
* type="string",
* enum={"mal_id", "count", "favorites", "established"}
* )
*/
final class ProducerOrderByEnum extends Enum
{
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self asc()
* @method static self desc()
*
* @OA\Schema(
* schema="search_query_sort",
* description="Search query sort direction",
* type="string",
* enum={"desc","asc"}
* )
*/
final class SortDirection extends Enum
{
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self airing()
* @method static self upcoming()
* @method static self bypopularity()
* @method static self favorite()
*
* @OA\Schema(
* schema="top_anime_filter",
* description="Top items filter types",
* type="string",
* enum={"airing","upcoming","bypopularity","favorite"}
* )
*/
final class TopAnimeFilterEnum extends Enum
{
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Enums;
use Spatie\Enum\Laravel\Enum;
/**
* @method static self publishing()
* @method static self upcoming()
* @method static self bypopularity()
* @method static self favorite()
*
* @OA\Schema(
* schema="top_manga_filter",
* description="Top items filter types",
* type="string",
* enum={"publishing","upcoming","bypopularity","favorite"}
* )
*/
final class TopMangaFilterEnum extends Enum
{
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Features;
use App\Dto\AnimeGenreListCommand;
/**
* @implements GenreListHandler<AnimeGenreListCommand>
*/
final class AnimeGenreListHandler extends GenreListHandler
{
/**
* @inheritDoc
*/
public function requestClass(): string
{
return AnimeGenreListCommand::class;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Features;
use App\Dto\AnimeSearchCommand;
use App\Http\Resources\V4\AnimeCollection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
/**
* @extends SearchRequestHandler<AnimeSearchCommand, AnimeCollection>
*/
class AnimeSearchHandler extends SearchRequestHandler
{
/**
* @inheritDoc
*/
public function requestClass(): string
{
return AnimeSearchCommand::class;
}
protected function renderResponse(LengthAwarePaginator $paginator): AnimeCollection
{
return new AnimeCollection($paginator);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Features;
use App\Dto\CharactersSearchCommand;
use App\Http\Resources\V4\CharacterCollection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
/**
* @extends SearchRequestHandler<CharactersSearchCommand, CharacterCollection>
*/
class CharacterSearchHandler extends SearchRequestHandler
{
/**
* @inheritDoc
*/
public function requestClass(): string
{
return CharactersSearchCommand::class;
}
/**
* @inheritDoc
*/
protected function renderResponse(LengthAwarePaginator $paginator)
{
return new CharacterCollection($paginator);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Features;
use App\Dto\ClubSearchCommand;
use App\Http\Resources\V4\ClubCollection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
/**
* @extends SearchRequestHandler<ClubSearchCommand, ClubCollection>
*/
class ClubSearchHandler extends SearchRequestHandler
{
/**
* @inheritDoc
*/
public function requestClass(): string
{
return ClubSearchCommand::class;
}
/**
* @inheritDoc
*/
protected function renderResponse(LengthAwarePaginator $paginator)
{
return new ClubCollection($paginator);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Features;
use App\Contracts\GenreRepository;
use App\Contracts\RequestHandler;
use App\Dto\GenreListCommand;
use App\Enums\GenreFilterEnum;
use App\Http\Resources\V4\GenreCollection;
/**
* @template TRequest of GenreListCommand
* @implements RequestHandler<TRequest, GenreCollection>
*/
abstract class GenreListHandler implements RequestHandler
{
public function __construct(private readonly GenreRepository $repository)
{
}
/**
* @param GenreListCommand $request
* @returns GenreCollection
*/
public function handle($request): GenreCollection
{
$requestParams = collect($request->all());
/**
* @var ?GenreFilterEnum $filterParam
*/
$filterParam = $requestParams->has("filter") ? $request->filter : null;
$results = match($filterParam) {
GenreFilterEnum::genres() => $this->repository->genres(),
GenreFilterEnum::explicit_genres() => $this->repository->getExplicitItems(),
GenreFilterEnum::themes() => $this->repository->getThemes(),
GenreFilterEnum::demographics() => $this->repository->getDemographics(),
default => $this->repository->all()
};
return new GenreCollection($results);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Features;
use App\Concerns\ScraperResultCache;
use App\Contracts\DataRequest;
use App\Contracts\Repository;
use App\Contracts\RequestHandler;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* @template TRequest of DataRequest<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)
{
}
/**
* @param TRequest $request
* @return TResponse
* @throws NotFoundHttpException
*/
public function handle($request)
{
$requestFingerprint = $request->getFingerPrint();
$results = $this->queryFromScraperCacheById(
$this->repository,
$request->id,
$request->getFingerPrint(),
);
$resource = $this->resource($results);
return $this->prepareResponse($requestFingerprint, $results, $resource->response());
}
protected abstract function resource(Collection $results): JsonResource;
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Features;
use App\Dto\MagazineSearchCommand;
use App\Http\Resources\V4\MagazineCollection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
/**
* @extends SearchRequestHandler<MagazineSearchCommand, MagazineCollection>
*/
class MagazineSearchHandler extends SearchRequestHandler
{
/**
* @inheritDoc
*/
public function requestClass(): string
{
return MagazineSearchCommand::class;
}
/**
* @inheritDoc
*/
protected function renderResponse(LengthAwarePaginator $paginator)
{
return new MagazineCollection($paginator);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Features;
use App\Dto\MangaGenreListCommand;
/**
* @implements GenreListHandler<MangaGenreListCommand>
*/
final class MangaGenreListHandler extends GenreListHandler
{
/**
* @inheritDoc
*/
public function requestClass(): string
{
return MangaGenreListCommand::class;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Features;
use App\Dto\MangaSearchCommand;
use App\Http\Resources\V4\MangaCollection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
/**
* @extends SearchRequestHandler<MangaSearchCommand, MangaCollection>
*/
class MangaSearchHandler extends SearchRequestHandler
{
/**
* @inheritDoc
*/
public function requestClass(): string
{
return MangaSearchCommand::class;
}
/**
* @inheritDoc
*/
protected function renderResponse(LengthAwarePaginator $paginator)
{
return new MangaCollection($paginator);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Features;
use App\Dto\PeopleSearchCommand;
use App\Http\Resources\V4\PersonCollection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
/**
* @extends SearchRequestHandler<PeopleSearchCommand, PersonCollection>
*/
class PeopleSearchHandler extends SearchRequestHandler
{
/**
* @inheritDoc
*/
public function requestClass(): string
{
return PeopleSearchCommand::class;
}
/**
* @inheritDoc
*/
protected function renderResponse(LengthAwarePaginator $paginator)
{
return new PersonCollection($paginator);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Features;
use App\Dto\ProducersSearchCommand;
use App\Http\Resources\V4\ProducerCollection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
/**
* @extends SearchRequestHandler<ProducersSearchCommand, ProducerCollection>
*/
class ProducerSearchHandler extends SearchRequestHandler
{
/**
* @inheritDoc
*/
public function requestClass(): string
{
return ProducersSearchCommand::class;
}
/**
* @inheritDoc
*/
protected function renderResponse(LengthAwarePaginator $paginator)
{
return new ProducerCollection($paginator);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Features;
use App\Concerns\ScraperResultCache;
use App\Contracts\AnimeRepository;
use App\Dto\QueryFullAnimeCommand;
use App\Http\Resources\V4\AnimeFullResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @extends ItemLookupHandler<QueryFullAnimeCommand, JsonResponse>
*/
final class QueryFullAnimeHandler extends ItemLookupHandler
{
public function __construct(AnimeRepository $repository)
{
parent::__construct($repository);
}
public function requestClass(): string
{
return QueryFullAnimeCommand::class;
}
protected function resource(Collection $results): JsonResource
{
return new AnimeFullResource($results->first());
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Features;
use App\Contracts\AnimeRepository;
use App\Contracts\RequestHandler;
use App\Dto\QueryTopAnimeItemsCommand;
use App\Enums\TopAnimeFilterEnum;
use App\Http\Resources\V4\AnimeCollection;
use App\Services\QueryBuilderPaginatorService;
use Spatie\LaravelData\Optional;
/**
* @implements RequestHandler<QueryTopAnimeItemsCommand, AnimeCollection>
*/
final class QueryTopAnimeItemsHandler implements RequestHandler
{
public function __construct(private readonly AnimeRepository $repository,
private readonly QueryBuilderPaginatorService $paginatorService)
{
}
/**
* @param QueryTopAnimeItemsCommand $request
* @returns AnimeCollection
*/
public function handle($request): AnimeCollection
{
$requestParams = collect($request->all());
/**
* @var ?TopAnimeFilterEnum $filterType
*/
$filterType = $requestParams->has("filter") ? $request->filter : null;
$builder = match($filterType) {
TopAnimeFilterEnum::airing() => $this->repository->getTopAiringItems(),
TopAnimeFilterEnum::upcoming() => $this->repository->getTopUpcomingItems(),
TopAnimeFilterEnum::bypopularity() => $this->repository->orderByPopularity(),
TopAnimeFilterEnum::favorite() => $this->repository->orderByFavoriteCount(),
default => $this->repository->orderByRank()
};
$builder = $builder->filter($requestParams);
return new AnimeCollection(
$this->paginatorService->paginate(
$builder, $requestParams->get("limit"), $requestParams->get("page")
)
);
}
public function requestClass(): string
{
return QueryTopAnimeItemsCommand::class;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Features;
use App\Contracts\CharacterRepository;
use App\Contracts\RequestHandler;
use App\Dto\QueryTopCharactersCommand;
use App\Http\Resources\V4\CharacterCollection;
use App\Services\QueryBuilderPaginatorService;
/**
* @implements RequestHandler<QueryTopCharactersCommand, CharacterCollection>
*/
final class QueryTopCharactersHandler implements RequestHandler
{
public function __construct(
private readonly CharacterRepository $repository,
private readonly QueryBuilderPaginatorService $paginatorService
)
{
}
/**
* @param QueryTopCharactersCommand $request
* @return CharacterCollection
*/
public function handle($request): CharacterCollection
{
$requestParams = collect($request->all());
$topItemsQuery = $this->repository->topCharacters()->filter($requestParams);
$results = $this->paginatorService->paginate(
$topItemsQuery, $requestParams->get("limit"), $requestParams->get("page")
);
return new CharacterCollection($results);
}
public function requestClass(): string
{
return QueryTopCharactersCommand::class;
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Features;
use App\Contracts\MangaRepository;
use App\Contracts\RequestHandler;
use App\Dto\QueryTopMangaItemsCommand;
use App\Enums\TopMangaFilterEnum;
use App\Http\Resources\V4\MangaCollection;
use App\Services\QueryBuilderPaginatorService;
/**
* @implements RequestHandler<QueryTopMangaItemsCommand, MangaCollection>
*/
class QueryTopMangaItemsHandler implements RequestHandler
{
public function __construct(private readonly MangaRepository $repository,
private readonly QueryBuilderPaginatorService $paginatorService)
{
}
/**
* @param QueryTopMangaItemsCommand $request
* @returns MangaCollection
*/
public function handle($request): MangaCollection
{
$requestParams = collect($request->all());
/**
* @var ?TopMangaFilterEnum $filterType
*/
$filterType = $requestParams->has("filter") ? $request->filter : null;
$builder = match($filterType) {
TopMangaFilterEnum::publishing() => $this->repository->getTopPublishingItems(),
TopMangaFilterEnum::upcoming() => $this->repository->getTopUpcomingItems(),
TopMangaFilterEnum::bypopularity() => $this->repository->orderByPopularity(),
TopMangaFilterEnum::favorite() => $this->repository->orderByFavoriteCount(),
default => $this->repository->orderByRank()
};
$builder = $builder->filter($requestParams);
return new MangaCollection(
$this->paginatorService->paginate(
$builder, $requestParams->get("limit"), $requestParams->get("page")
)
);
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryTopMangaItemsCommand::class;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Features;
use App\Contracts\PeopleRepository;
use App\Contracts\RequestHandler;
use App\Dto\QueryTopPeopleCommand;
use App\Http\Resources\V4\PersonCollection;
use App\Services\QueryBuilderPaginatorService;
/**
* @implements RequestHandler<QueryTopPeopleCommand, PersonCollection>
*/
class QueryTopPeopleHandler implements RequestHandler
{
public function __construct(
private readonly PeopleRepository $repository,
private readonly QueryBuilderPaginatorService $paginatorService
)
{
}
/**
* @param QueryTopPeopleCommand $request
* @returns PersonCollection
*/
public function handle($request)
{
$requestParams = collect($request->all());
$topItemsQuery = $this->repository->topPeople()->filter($requestParams);
$results = $this->paginatorService->paginate(
$topItemsQuery, $requestParams->get("limit"), $requestParams->get("page")
);
return new PersonCollection($results);
}
/**
* @inheritDoc
*/
public function requestClass(): string
{
return QueryTopPeopleCommand::class;
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Features;
use App\Concerns\ScraperResultCache;
use App\Contracts\RequestHandler;
use App\Dto\QueryTopReviewsCommand;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\JsonResponse;
use Jikan\Helper\Constants;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\Reviews\RecentReviewsRequest;
/**
* @implements RequestHandler<QueryTopReviewsCommand, JsonResponse>
*/
class QueryTopReviewsHandler implements RequestHandler
{
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
*/
public function requestClass(): string
{
return QueryTopReviewsCommand::class;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Features;
use App\Contracts\DataRequest;
use App\Contracts\RequestHandler;
use App\Services\QueryBuilderService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Response;
/**
* @template TRequest of DataRequest<TResponse>
* @template TResponse of ResourceCollection|JsonResource|Response
* @implements RequestHandler<TRequest, TResponse>
*/
abstract class SearchRequestHandler implements RequestHandler
{
public function __construct(
private readonly QueryBuilderService $queryBuilderService
) {}
/**
* @inheritDoc
*/
public function handle($request)
{
// note: ->all() doesn't transform the dto, all the parsed data is returned as it was parsed. (and validated)
$requestData = collect($request->all());
$builder = $this->queryBuilderService->query($requestData);
$page = $requestData->get("page");
$limit = $requestData->get("limit");
$paginator = $this->queryBuilderService->paginateBuilder($builder, $page, $limit);
return $this->renderResponse($paginator);
}
/**
* @param LengthAwarePaginator $paginator
* @return TResponse
*/
protected abstract function renderResponse(LengthAwarePaginator $paginator);
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Features;
use App\Concerns\ScraperResultCache;
use App\Contracts\RequestHandler;
use App\Dto\UsersSearchCommand;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\JsonResponse;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\Search\UserSearchRequest;
/**
* @implements RequestHandler<UsersSearchCommand, JsonResponse>
*/
final class UserSearchHandler implements RequestHandler
{
use ScraperResultCache;
/**
* @inheritDoc
*/
public function requestClass(): string
{
return UsersSearchCommand::class;
}
/**
* @param UsersSearchCommand $request
* @return JsonResponse
*/
public function handle($request): JsonResponse
{
$requestParams = collect($request->all());
$requestFingerPrint = $request->getFingerPrint();
$results = $this->queryFromScraperCacheByFingerPrint(
"users",
$requestFingerPrint,
fn (MalClient $jikan, int $page) => $jikan->getUserSearch((new UserSearchRequest())
->setQuery($requestParams->get("q"))
->setGender($requestParams->get("gender"))
->setLocation($requestParams->get("location"))
->setMaxAge($requestParams->get("maxAge"))
->setMinAge($requestParams->get("minAge"))
->setPage($page)),
$requestParams->get("page")
);
return $this->prepareResponse($requestFingerPrint, $results, (new ResultsResource(
$results->first()
))->response());
}
}

View File

@ -4,6 +4,7 @@ namespace App\Filters;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Support\Collection;
use Spatie\Enum\Enum;
trait FilterQueryString
{
@ -30,8 +31,17 @@ trait FilterQueryString
private function _normalizeOrderBy(Collection $filters): Collection
{
// If DTO is not transformed then the value can be Enum type.
// This scenario happens when we use ->all() on the DTO, instead of ->toArray().
// However it is preferred to have the parsed values passed down, not the transformed ones.
foreach(["order_by", "sort"] as $key) {
if ($filters->has($key) && $filters->get($key) instanceof Enum) {
$filters[$key] = $filters[$key]->label;
}
}
// fixme: this can be done more elegantly, for now this is here as a quick hack.
if ($filters->offsetExists("sort") && $filters->offsetExists("order_by")) {
if ($filters->has("sort") && $filters->has("order_by")) {
// we put the order by field and the sort direction in one array element.
// the OrderByClause class will explode the string by the comma and set the correct field.
$filters["order_by"] = $filters["order_by"] . "," . $filters["sort"];

View File

@ -1,6 +1,8 @@
<?php
namespace App\Filters;
use Illuminate\Support\Str;
trait FilterResolver
{
private function resolve($filterName, $values)
@ -26,7 +28,7 @@ trait FilterResolver
private function isCustomFilter($filterName)
{
return method_exists($this, $filterName);
return method_exists($this, $filterName) || method_exists($this, "filterBy" . ucfirst(Str::camel($filterName)));
}
private function getClosure($callable, $values)

View File

@ -14,7 +14,7 @@ class GenreAnime extends JikanApiSearchableModel
{
use HasFactory;
protected array $filters = ["order_by", "sort"];
protected array $filters = [];
/**
* The attributes that are mass assignable.

View File

@ -14,7 +14,7 @@ class GenreManga extends JikanApiSearchableModel
{
use HasFactory;
protected array $filters = ["order_by", "sort"];
protected array $filters = [];
/**
* The attributes that are mass assignable.

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\V4DB;
use App\Contracts\Mediator;
use App\Http\HttpHelper;
use App\Providers\SerializerFactory;
use Illuminate\Http\Request;
@ -70,17 +71,20 @@ class Controller extends BaseController
protected string $fingerprint;
protected Mediator $mediator;
/**
* AnimeController constructor.
*
* @param Request $request
* @param MalClient $jikan
*/
public function __construct(Request $request, MalClient $jikan)
public function __construct(Request $request, MalClient $jikan, Mediator $mediator)
{
$this->serializer = SerializerFactory::createV4();
$this->jikan = $jikan;
$this->fingerprint = HttpHelper::resolveRequestFingerprint($request);
$this->mediator = $mediator;
}
protected function isExpired($request, $results) : bool

View File

@ -54,12 +54,7 @@ class MagazineController extends ControllerWithQueryBuilderProvider
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
* @OA\Schema(
* schema="magazines_query_orderby",
* description="Order by magazine data",
* type="string",
* enum={"mal_id", "name", "count"}
* )
*
*/
public function main(Request $request): MagazineCollection
{

View File

@ -2,19 +2,19 @@
namespace App\Http\Controllers\V4DB;
use App\Http\QueryBuilder\SearchQueryBuilderUsers;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\CharacterCollection;
use App\Http\Resources\V4\ClubCollection;
use App\Http\Resources\V4\MangaCollection;
use App\Http\Resources\V4\PersonCollection;
use App\Http\Resources\V4\ProducerCollection;
use App\Dto\AnimeSearchCommand;
use App\Dto\CharactersSearchCommand;
use App\Dto\ClubSearchCommand;
use App\Dto\MangaSearchCommand;
use App\Dto\PeopleSearchCommand;
use App\Dto\ProducersSearchCommand;
use App\Dto\UsersSearchCommand;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Request\User\UsernameByIdRequest;
class SearchController extends ControllerWithQueryBuilderProvider
class SearchController extends Controller
{
/**
* @OA\Parameter(
@ -27,13 +27,6 @@ class SearchController extends ControllerWithQueryBuilderProvider
* in="query",
* @OA\Schema(type="integer")
* ),
*
* @OA\Schema(
* schema="search_query_sort",
* description="Characters Search Query Sort",
* type="string",
* enum={"desc","asc"}
* )
*/
/**
@ -163,9 +156,9 @@ class SearchController extends ControllerWithQueryBuilderProvider
* ),
* )
*/
public function anime(Request $request)
public function anime(AnimeSearchCommand $request)
{
return $this->preparePaginatedResponse(AnimeCollection::class, "anime", $request);
return $this->mediator->send($request);
}
/**
@ -289,9 +282,9 @@ class SearchController extends ControllerWithQueryBuilderProvider
* ),
* )
*/
public function manga(Request $request)
public function manga(MangaSearchCommand $request)
{
return $this->preparePaginatedResponse(MangaCollection::class, "manga", $request);
return $this->mediator->send($request);
}
/**
@ -339,9 +332,9 @@ class SearchController extends ControllerWithQueryBuilderProvider
* ),
* )
*/
public function people(Request $request)
public function people(PeopleSearchCommand $request)
{
return $this->preparePaginatedResponse(PersonCollection::class, "people", $request);
return $this->mediator->send($request);
}
/**
@ -391,9 +384,9 @@ class SearchController extends ControllerWithQueryBuilderProvider
* ),
* )
*/
public function character(Request $request)
public function character(CharactersSearchCommand $request)
{
return $this->preparePaginatedResponse(CharacterCollection::class, "character", $request);
return $this->mediator->send($request);
}
/**
@ -487,35 +480,9 @@ class SearchController extends ControllerWithQueryBuilderProvider
* },
* ),
*/
public function users(Request $request)
public function users(UsersSearchCommand $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$anime = $this->jikan->getUserSearch(
SearchQueryBuilderUsers::query(
$request
)
);
$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($request);
}
/**
@ -635,20 +602,12 @@ class SearchController extends ControllerWithQueryBuilderProvider
* ),
* )
*/
public function clubs(Request $request)
public function clubs(ClubSearchCommand $request)
{
return $this->preparePaginatedResponse(ClubCollection::class, "club", $request);
return $this->mediator->send($request);
}
/**
*
* @OA\Schema(
* schema="producers_query_orderby",
* description="Producers Search Query Order By",
* type="string",
* enum={"mal_id", "count", "favorites", "established"}
* )
*
* @OA\Get(
* path="/producers",
* operationId="getProducers",
@ -696,8 +655,8 @@ class SearchController extends ControllerWithQueryBuilderProvider
* ),
* )
*/
public function producers(Request $request)
public function producers(ProducersSearchCommand $request)
{
return $this->preparePaginatedResponse(ProducerCollection::class, "producers", $request);
return $this->mediator->send($request);
}
}

View File

@ -2,18 +2,13 @@
namespace App\Http\Controllers\V4DB;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\CharacterCollection;
use App\Http\Resources\V4\MangaCollection;
use App\Http\Resources\V4\PersonCollection;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Helper\Constants;
use Jikan\Request\Reviews\RecentReviewsRequest;
use App\Dto\QueryTopAnimeItemsCommand;
use App\Dto\QueryTopCharactersCommand;
use App\Dto\QueryTopMangaItemsCommand;
use App\Dto\QueryTopPeopleCommand;
use App\Dto\QueryTopReviewsCommand;
class TopController extends ControllerWithQueryBuilderProvider
class TopController extends Controller
{
/**
@ -33,7 +28,7 @@ class TopController extends ControllerWithQueryBuilderProvider
* name="filter",
* in="query",
* required=false,
* @OA\Schema(type="string",enum={"airing", "upcoming", "bypopularity", "favorite"})
* @OA\Schema(ref="#/components/schemas/top_anime_filter)
* ),
*
* @OA\Parameter(
@ -65,9 +60,9 @@ class TopController extends ControllerWithQueryBuilderProvider
* ),
* )
*/
public function anime(Request $request)
public function anime(QueryTopAnimeItemsCommand $request)
{
return $this->preparePaginatedResponse(AnimeCollection::class, "top_anime", $request);
return $this->mediator->send($request);
}
/**
@ -87,7 +82,7 @@ class TopController extends ControllerWithQueryBuilderProvider
* name="filter",
* in="query",
* required=false,
* @OA\Schema(type="string",enum={"publishing", "upcoming", "bypopularity", "favorite"})
* @OA\Schema(ref="#/components/schemas/top_manga_filter)
* ),
*
* @OA\Parameter(ref="#/components/parameters/page"),
@ -106,9 +101,9 @@ class TopController extends ControllerWithQueryBuilderProvider
* ),
* )
*/
public function manga(Request $request)
public function manga(QueryTopMangaItemsCommand $request)
{
return $this->preparePaginatedResponse(MangaCollection::class, "top_manga", $request);
return $this->mediator->send($request);
}
/**
@ -133,18 +128,9 @@ class TopController extends ControllerWithQueryBuilderProvider
* ),
* )
*/
public function people(Request $request)
public function people(QueryTopPeopleCommand $request)
{
$results = $this->getQueryBuilder("people", $request)
->whereNotNull('member_favorites')
->where('member_favorites', '>', 0)
->orderBy('member_favorites', 'desc');
$results = $this->getPaginator("people", $request, $results);
return new PersonCollection(
$results
);
return $this->mediator->send($request);
}
/**
@ -169,18 +155,9 @@ class TopController extends ControllerWithQueryBuilderProvider
* ),
* )
*/
public function characters(Request $request)
public function characters(QueryTopCharactersCommand $request)
{
$results = $this->getQueryBuilder("character", $request)
->whereNotNull('member_favorites')
->where('member_favorites', '>', 0)
->orderBy('member_favorites', 'desc');
$results = $this->getPaginator("character", $request, $results);
return new CharacterCollection(
$results
);
return $this->mediator->send($request);
}
/**
@ -276,33 +253,8 @@ class TopController extends ControllerWithQueryBuilderProvider
* ),
* ),
*/
public function reviews(Request $request)
public function reviews(QueryTopReviewsCommand $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$data = $this->jikan->getRecentReviews(
new RecentReviewsRequest(Constants::RECENT_REVIEW_BEST_VOTED, $page)
);
$response = \json_decode($this->serializer->serialize($data, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
return $this->mediator->send($request);
}
}

View File

@ -23,12 +23,12 @@ trait JikanApiQueryBuilder
/**
* @template T
* @param T $resourceCollectionClass
* @param class-string<T> $resourceCollectionClass
* @param string $resourceTypeName
* @param \Illuminate\Http\Request $request
* @return T
*/
protected function preparePaginatedResponse(string|object $resourceCollectionClass, string $resourceTypeName, Request $request)
protected function preparePaginatedResponse(string $resourceCollectionClass, string $resourceTypeName, Request $request)
{
$results = $this->getQueryBuilder($resourceTypeName, $request);
$paginator = $this->getPaginator($resourceTypeName, $request, $results);

View File

@ -7,13 +7,12 @@ use Illuminate\Http\Response;
class HttpResponse
{
public static function notFound(Request $request) : Response
{
return response(
\json_encode([
'status' => 404,
'type' => 'BadResponseException',
'type' => 'NotFoundException',
'message' => 'Resource not found',
'error' => '404 on ' . $request->getUri()
]),

View File

@ -35,7 +35,7 @@ abstract class MediaSearchQueryBuilder extends SearchQueryBuilder
{
$parameters = parent::sanitizeParameters($parameters);
if (!$parameters->offsetExists("score")) {
if (!$parameters->has("score")) {
$parameters["score"] = 0;
}

View File

@ -33,13 +33,6 @@ class GenreCollection extends ResourceCollection
* ref="#/components/schemas/genre"
* ),
* ),
* ),
*
* @OA\Schema(
* schema="genre_query_filter",
* description="Filter genres by type",
* type="string",
* enum={"genres","explicit_genres", "themes", "demographics"}
* )
*/
public function toArray($request)

View File

@ -2,6 +2,7 @@
namespace App;
use App\Concerns\FilteredByLetter;
use Jikan\Jikan;
use Jikan\Request\Magazine\MagazinesRequest;
@ -11,7 +12,8 @@ use Jikan\Request\Magazine\MagazinesRequest;
*/
class Magazine extends JikanApiSearchableModel
{
protected array $filters = ["order_by", "sort"];
use FilteredByLetter;
protected array $filters = ["order_by", "sort", "letter"];
/**
* The attributes that are mass assignable.
@ -29,6 +31,7 @@ class Magazine extends JikanApiSearchableModel
*/
protected $table = 'magazines';
protected ?string $displayNameFieldName = "name";
/**
* The attributes excluded from the model's JSON form.

View File

@ -2,7 +2,10 @@
namespace App;
use App\Concerns\FilteredByLetter;
use App\Concerns\MediaFilters;
use App\Http\HttpHelper;
use Carbon\CarbonImmutable;
use Database\Factories\MangaFactory;
use Illuminate\Support\Facades\App;
use Jikan\Jikan;
@ -11,11 +14,9 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
class Manga extends JikanApiSearchableModel
{
use HasFactory;
use HasFactory, MediaFilters, FilteredByLetter;
// note that here we skip "score", "min_score", "max_score", "rating" and others because they need special logic
// to set the correct filtering on the ORM.
protected array $filters = ["order_by", "status", "type", "sort"];
protected array $filters = ["order_by", "status", "type", "sort", "max_score", "min_score", "score", "start_date", "end_date", "magazine", "magazines", "letter"];
/**
* The attributes that are mass assignable.
@ -34,6 +35,7 @@ class Manga extends JikanApiSearchableModel
*/
protected $appends = [];
protected ?string $displayNameFieldName = "title";
/**
* The table associated with the model.
@ -51,6 +53,48 @@ class Manga extends JikanApiSearchableModel
'_id', 'expiresAt', 'request_hash'
];
/** @noinspection PhpUnused */
public function filterByStartDate(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, CarbonImmutable $date): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("published.from", $date->setTime(0, 0)->toAtomString());
}
/** @noinspection PhpUnused */
public function filterByEndDate(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, CarbonImmutable $date): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
return $query->where("published.to", $date->setTime(0, 0)->toAtomString());
}
public function filterByMagazine(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, string $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
if (empty($value)) {
return $query;
}
$magazine = (int)$value;
return $query
->orWhere('serializations.mal_id', $magazine);
}
/** @noinspection PhpUnused */
public function filterByMagazines(\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $query, string $value): \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder
{
if (empty($value)) {
return $query;
}
$magazines = explode(',', $value);
foreach ($magazines as $magazine) {
if (empty($magazine)) {
continue;
}
$query = $this->filterByMagazine($query, $value);
}
return $query;
}
public static function scrape(int $id)
{
$data = app('JikanParser')->getManga(new MangaRequest($id));

View File

@ -2,12 +2,13 @@
namespace App;
use Jenssegers\Mongodb\Eloquent\Model;
use App\Concerns\FilteredByLetter;
use Jikan\Request\Producer\ProducerRequest;
class Producers extends JikanApiSearchableModel
{
protected array $filters = ["order_by", "sort"];
use FilteredByLetter;
protected array $filters = ["order_by", "sort", "letter"];
/**
* The attributes that are mass assignable.
@ -25,6 +26,8 @@ class Producers extends JikanApiSearchableModel
*/
protected $table = 'producers';
protected ?string $displayNameFieldName = "titles.0.title";
/**
* The attributes excluded from the model's JSON form.
*

View File

@ -2,12 +2,13 @@
namespace App;
use Jenssegers\Mongodb\Eloquent\Model;
use App\Concerns\FilteredByLetter;
use Jikan\Request\User\UserProfileRequest;
class Profile extends JikanApiSearchableModel
{
protected array $filters = ["order_by", "sort"];
use FilteredByLetter;
protected array $filters = ["order_by", "sort", "letter"];
/**
* The attributes that are mass assignable.
@ -25,6 +26,8 @@ class Profile extends JikanApiSearchableModel
*/
protected $table = 'users';
protected ?string $displayNameFieldName = "username";
/**
* The attributes excluded from the model's JSON form.
*

View File

@ -2,6 +2,32 @@
namespace App\Providers;
use App\Contracts\AnimeRepository;
use App\Contracts\CharacterRepository;
use App\Contracts\ClubRepository;
use App\Contracts\MagazineRepository;
use App\Contracts\MangaRepository;
use App\Contracts\Mediator;
use App\Contracts\PeopleRepository;
use App\Contracts\ProducerRepository;
use App\Contracts\Repository;
use App\Contracts\RequestHandler;
use App\Contracts\UnitOfWork;
use App\Contracts\UserRepository;
use App\Dto\QueryTopPeopleCommand;
use App\Features\AnimeGenreListHandler;
use App\Features\AnimeSearchHandler;
use App\Features\CharacterSearchHandler;
use App\Features\ClubSearchHandler;
use App\Features\MagazineSearchHandler;
use App\Features\MangaSearchHandler;
use App\Features\PeopleSearchHandler;
use App\Features\ProducerSearchHandler;
use App\Features\QueryTopAnimeItemsHandler;
use App\Features\QueryTopCharactersHandler;
use App\Features\QueryTopMangaItemsHandler;
use App\Features\QueryTopReviewsHandler;
use App\Features\UserSearchHandler;
use App\GenreAnime;
use App\GenreManga;
use App\Http\QueryBuilder\AnimeSearchQueryBuilder;
@ -16,10 +42,29 @@ use App\Macros\To2dArrayWithDottedKeys;
use App\Magazine;
use App\Mixins\ScoutBuilderMixin;
use App\Producers;
use App\Repositories\AnimeGenresRepository;
use App\Repositories\DefaultAnimeRepository;
use App\Repositories\DefaultCharacterRepository;
use App\Repositories\DefaultClubRepository;
use App\Repositories\DefaultMagazineRepository;
use App\Repositories\DefaultMangaRepository;
use App\Repositories\DefaultPeopleRepository;
use App\Repositories\DefaultProducerRepository;
use App\Repositories\DefaultUserRepository;
use App\Repositories\MangaGenresRepository;
use App\Services\DefaultQueryBuilderService;
use App\Services\DefaultScoutSearchService;
use App\Services\ElasticScoutSearchService;
use App\Services\EloquentBuilderPaginatorService;
use App\Services\MongoSearchService;
use App\Services\ScoutBuilderPaginatorService;
use App\Services\ScoutSearchService;
use App\Services\SearchEngineSearchService;
use App\Services\TypeSenseScoutSearchService;
use App\Support\DefaultMediator;
use App\Support\JikanUnitOfWork;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Collection;
use Laravel\Scout\Builder as ScoutBuilder;
@ -48,14 +93,17 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
$this->app->singleton(ScoutSearchService::class, function($app) {
$scoutDriver = $this->getSearchIndexDriver($app);
return match ($scoutDriver) {
"typesense" => new TypeSenseScoutSearchService(),
"Matchish\ScoutElasticSearch\Engines\ElasticSearchEngine" => new ElasticScoutSearchService(),
default => new DefaultScoutSearchService()
};
});
// $this->app->singleton(ScoutSearchService::class, function($app) {
// $scoutDriver = $this->getSearchIndexDriver($app);
//
// return match ($scoutDriver) {
// "typesense" => new TypeSenseScoutSearchService(),
// "Matchish\ScoutElasticSearch\Engines\ElasticSearchEngine" => new ElasticScoutSearchService(),
// default => new DefaultScoutSearchService()
// };
// });
// todo: remove SearchQueryBuilders
$queryBuilders = [
AnimeSearchQueryBuilder::class,
@ -124,9 +172,139 @@ class AppServiceProvider extends ServiceProvider
});
}
private function registerModelRepositories()
{
// note: We deliberately not included here any of the GenreRepository implementations.
// We don't want to bind them to an abstract symbol.
$repositories = [
AnimeRepository::class => DefaultAnimeRepository::class,
MangaRepository::class => DefaultMangaRepository::class,
CharacterRepository::class => DefaultCharacterRepository::class,
ClubRepository::class => DefaultClubRepository::class,
MagazineRepository::class => DefaultMagazineRepository::class,
ProducerRepository::class => DefaultProducerRepository::class,
PeopleRepository::class => DefaultPeopleRepository::class,
UserRepository::class => DefaultUserRepository::class,
];
foreach ($repositories as $abstract => $concrete) {
$this->app->singleton($abstract, $concrete);
}
$this->app->singleton(AnimeGenresRepository::class);
$this->app->singleton(MangaGenresRepository::class);
$this->app->singleton(UnitOfWork::class, JikanUnitOfWork::class);
}
private function registerRequestHandlers()
{
/*
* This bit is about a "mediator" pattern for handling requests.
*/
$this->app->singleton(Mediator::class, DefaultMediator::class);
/*
* Each request is represented as a data transfer object, and spatie/laravel-data package's service provider
* registers them in the ioc container. For each request there is a request handler.
* 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.
*/
$this->app->when(DefaultMediator::class)
->needs(RequestHandler::class)
->give(function (Application $app) {
$searchIndexesEnabled = $this->getSearchIndexesEnabledConfig($app);
/**
* @var UnitOfWork $unitOfWorkInstance
*/
$unitOfWorkInstance = $app->make(UnitOfWork::class);
$searchRequestHandlersDescriptors = [
AnimeSearchHandler::class => $unitOfWorkInstance->anime(),
MangaSearchHandler::class => $unitOfWorkInstance->manga(),
CharacterSearchHandler::class => $unitOfWorkInstance->characters(),
PeopleSearchHandler::class => $unitOfWorkInstance->people(),
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, [
$app->make(DefaultQueryBuilderService::class, [
static::makeSearchService($app, $searchIndexesEnabled, $repositoryInstance),
$app->make($searchIndexesEnabled ? ScoutBuilderPaginatorService::class : EloquentBuilderPaginatorService::class)
])
]);
}
$queryTopItemsDescriptors = [
QueryTopAnimeItemsHandler::class => $unitOfWorkInstance->anime(),
QueryTopMangaItemsHandler::class => $unitOfWorkInstance->manga(),
QueryTopCharactersHandler::class => $unitOfWorkInstance->characters(),
QueryTopPeopleCommand::class => $unitOfWorkInstance->people(),
];
foreach ($queryTopItemsDescriptors as $handlerClass => $repositoryInstance) {
$requestHandlers[] = $this->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)
]);
}
$genreRequestHandlerDescriptors = [
AnimeGenreListHandler::class => $unitOfWorkInstance->animeGenres(),
MangaGenresRepository::class => $unitOfWorkInstance->mangaGenres()
];
foreach ($genreRequestHandlerDescriptors as $handlerClass => $repositoryInstance) {
$requestHandlers[] = $this->app->make($handlerClass, [$repositoryInstance]);
}
$requestHandlers[] = $this->app->make(QueryTopReviewsHandler::class);
return $requestHandlers;
});
}
/**
* Creates a search service instance.
* Search service knows how to do a full-text search on the database query builder instance.
* @throws BindingResolutionException
*/
private static function makeSearchService(Application $app, bool $searchIndexesEnabled, Repository $repositoryInstance)
{
return $searchIndexesEnabled ? $app->make( SearchEngineSearchService::class, [
static::makeScoutSearchService($app, $repositoryInstance), $repositoryInstance
]) : $app->make(MongoSearchService::class, [$repositoryInstance]);
}
/**
* Creates a scout search service instance.
* Scout search service knows about the configured search engine's implementation details.
* E.g. per search request configuration.
* @throws BindingResolutionException
*/
private static function makeScoutSearchService(Application $app, Repository $repositoryInstance)
{
// todo: cache result
$scoutDriver = static::getSearchIndexDriver($app);
$serviceClass = match ($scoutDriver) {
"typesense" => TypeSenseScoutSearchService::class,
"Matchish\ScoutElasticSearch\Engines\ElasticSearchEngine" => ElasticScoutSearchService::class,
default => DefaultScoutSearchService::class
};
return $app->make($serviceClass, [$repositoryInstance]);
}
/**
* @throws \ReflectionException
* @throws \Illuminate\Contracts\Container\BindingResolutionException
* @throws BindingResolutionException
* @return void
*/
private function registerMacros(): void
@ -158,7 +336,7 @@ class AppServiceProvider extends ServiceProvider
return $this->getSearchIndexDriver($app) != "null";
}
private function getSearchIndexDriver($app): string
private static function getSearchIndexDriver($app): string
{
return $app["config"]->get("scout.driver");
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Providers;
use Spatie\Enum\Laravel\EnumServiceProvider;
class JikanEnumServiceProvider extends EnumServiceProvider
{
protected function registerRouteBindingMacro(): void
{
// noop
}
}

Some files were not shown because too many files have changed in this diff Show More