mirror of
https://github.com/jikan-me/jikan-rest.git
synced 2025-02-20 11:23:35 +08:00
wip
- refactored query builders - added function for transforming nested fields on models to typesense compatible format. - readded the AppServiceProvider class to the service container - todo: create a base class for all models so they would share the common typesense specific implementations
This commit is contained in:
parent
271c750417
commit
194a17881b
@ -179,9 +179,7 @@ class Anime extends Model implements TypesenseDocument
|
||||
'mal_id' => (string) $this->mal_id,
|
||||
'start_date' => $this->aired['from'] ? Parser::parseDate($this->aired['from'])->getTimestamp() : 0,
|
||||
'end_date' => $this->aired['to'] ? Parser::parseDate($this->aired['to'])->getTimestamp() : 0,
|
||||
'url' => $this->url,
|
||||
'images' => $this->images,
|
||||
'trailer' => $this->trailer,
|
||||
'title' => $this->title,
|
||||
'title_english' => $this->title_english,
|
||||
'title_japanese' => $this->title_japanese,
|
||||
@ -191,10 +189,8 @@ class Anime extends Model implements TypesenseDocument
|
||||
'episodes' => $this->episodes,
|
||||
'status' => $this->status,
|
||||
'airing' => $this->airing,
|
||||
'duration' => $this->duration,
|
||||
'rating' => $this->rating,
|
||||
'score' => $this->score,
|
||||
'scored_by' => $this->scored_by,
|
||||
'rank' => $this->rank,
|
||||
'popularity' => $this->popularity,
|
||||
'members' => $this->members,
|
||||
@ -203,17 +199,16 @@ class Anime extends Model implements TypesenseDocument
|
||||
'background' => $this->background,
|
||||
'season' => $this->season,
|
||||
'year' => $this->year,
|
||||
'broadcast' => $this->broadcast,
|
||||
'producers' => $serializer->serialize($this->producers, 'json'),
|
||||
'licensors' => $serializer->serialize($this->licensors, 'json'),
|
||||
'studios' => $serializer->serialize($this->studios, 'json'),
|
||||
'genres' => $serializer->serialize($this->genres, 'json'),
|
||||
'explicit_genres' => $serializer->serialize($this->explicit_genres, 'json'),
|
||||
'themes' => $serializer->serialize($this->themes, 'json'),
|
||||
'demographics' => $serializer->serialize($this->demographics, 'json'),
|
||||
];
|
||||
|
||||
return $result;
|
||||
// todo: test this with artisan::tinker
|
||||
return array_merge($result,
|
||||
// this will add nested fields in the format of "producers.0.mal_id=123" and etc
|
||||
$this->toTypeSenseCompatibleNestedField("producers"),
|
||||
$this->toTypeSenseCompatibleNestedField("studios"),
|
||||
$this->toTypeSenseCompatibleNestedField("genres"),
|
||||
$this->toTypeSenseCompatibleNestedField("explicit_genres")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,34 +3,24 @@
|
||||
namespace App\Http\Controllers\V4DB;
|
||||
|
||||
use App\Anime;
|
||||
use App\DatabaseHandler;
|
||||
use App\Http\HttpHelper;
|
||||
use App\Http\HttpResponse;
|
||||
use App\Http\Resources\V4\AnimeCharactersResource;
|
||||
use App\Http\Resources\V4\AnimeCollection;
|
||||
use App\Http\Resources\V4\AnimeEpisodeResource;
|
||||
use App\Http\Resources\V4\AnimeEpisodesResource;
|
||||
use App\Http\Resources\V4\ExternalLinksResource;
|
||||
use App\Http\Resources\V4\AnimeForumResource;
|
||||
use App\Http\Resources\V4\AnimeRelationsCollection;
|
||||
use App\Http\Resources\V4\AnimeRelationsResource;
|
||||
use App\Http\Resources\V4\AnimeThemesResource;
|
||||
use App\Http\Resources\V4\MoreInfoResource;
|
||||
use App\Http\Resources\V4\AnimeNewsResource;
|
||||
use App\Http\Resources\V4\PicturesResource;
|
||||
use App\Http\Resources\V4\RecommendationsResource;
|
||||
use App\Http\Resources\V4\ResultsResource;
|
||||
use App\Http\Resources\V4\ReviewsResource;
|
||||
use App\Http\Resources\V4\AnimeStaffResource;
|
||||
use App\Http\Resources\V4\AnimeStatisticsResource;
|
||||
use App\Http\Resources\V4\StreamingLinksResource;
|
||||
use App\Http\Resources\V4\UserUpdatesResource;
|
||||
use App\Http\Resources\V4\AnimeVideosResource;
|
||||
use App\Http\Resources\V4\CommonResource;
|
||||
use App\Http\Resources\V4\ForumResource;
|
||||
use App\Http\Resources\V4\NewsResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Jikan\Request\Anime\AnimeCharactersAndStaffRequest;
|
||||
use Jikan\Request\Anime\AnimeEpisodeRequest;
|
||||
|
@ -2,20 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers\V4DB;
|
||||
|
||||
use App\Anime;
|
||||
use App\Character;
|
||||
use App\Club;
|
||||
use App\Http\HttpHelper;
|
||||
use App\Http\HttpResponse;
|
||||
use App\Http\Middleware\Throttle;
|
||||
use App\Http\QueryBuilder\SearchQueryBuilderAnime;
|
||||
use App\Http\QueryBuilder\ScoutSearchQueryBuilderAnime;
|
||||
use App\Http\QueryBuilder\SearchQueryBuilderCharacter;
|
||||
use App\Http\QueryBuilder\SearchQueryBuilderClub;
|
||||
use App\Http\QueryBuilder\SearchQueryBuilderManga;
|
||||
use App\Http\QueryBuilder\SearchQueryBuilderPeople;
|
||||
use App\Http\QueryBuilder\SearchQueryBuilderUsers;
|
||||
use App\Http\Resources\V4\AnimeCharactersResource;
|
||||
use App\Http\Resources\V4\AnimeCollection;
|
||||
use App\Http\Resources\V4\CharacterCollection;
|
||||
use App\Http\Resources\V4\ClubCollection;
|
||||
@ -25,26 +18,36 @@ use App\Http\Resources\V4\ResultsResource;
|
||||
use App\Http\SearchQueryBuilder;
|
||||
use App\Manga;
|
||||
use App\Person;
|
||||
use App\Providers\SearchQueryBuilderProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Jikan\Jikan;
|
||||
use Jikan\MyAnimeList\MalClient;
|
||||
use Jikan\Request\Anime\AnimeCharactersAndStaffRequest;
|
||||
use Jikan\Request\Search\AnimeSearchRequest;
|
||||
use Jikan\Request\Search\MangaSearchRequest;
|
||||
use Jikan\Request\Search\CharacterSearchRequest;
|
||||
use Jikan\Request\Search\PersonSearchRequest;
|
||||
use Jikan\Helper\Constants as JikanConstants;
|
||||
use Jikan\Request\Search\UserSearchRequest;
|
||||
use Jikan\Request\User\UsernameByIdRequest;
|
||||
use JMS\Serializer\Serializer;
|
||||
use MongoDB\BSON\UTCDateTime;
|
||||
use phpDocumentor\Reflection\Types\Object_;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
private $request;
|
||||
const MAX_RESULTS_PER_PAGE = 25;
|
||||
private SearchQueryBuilderProvider $searchQueryBuilderProvider;
|
||||
|
||||
public function __construct(Request $request, MalClient $jikan, SearchQueryBuilderProvider $searchQueryBuilderProvider)
|
||||
{
|
||||
parent::__construct($request, $jikan);
|
||||
$this->searchQueryBuilderProvider = $searchQueryBuilderProvider;
|
||||
}
|
||||
|
||||
private function getQueryBuilder(string $name, Request $request): \Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder
|
||||
{
|
||||
$queryBuilder = $this->searchQueryBuilderProvider->getQueryBuilder($name);
|
||||
return $queryBuilder->query($request);
|
||||
}
|
||||
|
||||
private function getPaginator(string $name, Request $request, \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder $results): \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
{
|
||||
$queryBuilder = $this->searchQueryBuilderProvider->getQueryBuilder($name);
|
||||
return $queryBuilder->paginateBuilder($request, $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Parameter(
|
||||
@ -211,18 +214,11 @@ class SearchController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$results = ScoutSearchQueryBuilderAnime::query(
|
||||
$request
|
||||
);
|
||||
|
||||
$results = $results
|
||||
->paginate(
|
||||
$limit,
|
||||
$page
|
||||
);
|
||||
$results = $this->getQueryBuilder("anime", $request);
|
||||
$paginator = $this->getPaginator("anime", $request, $results);
|
||||
|
||||
return new AnimeCollection(
|
||||
$results
|
||||
$paginator
|
||||
);
|
||||
}
|
||||
|
||||
@ -365,21 +361,11 @@ class SearchController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$results = SearchQueryBuilderManga::query(
|
||||
$request,
|
||||
Manga::query()
|
||||
);
|
||||
|
||||
$results = $results
|
||||
->paginate(
|
||||
$limit,
|
||||
['*'],
|
||||
null,
|
||||
$page
|
||||
);
|
||||
$results = $this->getQueryBuilder("manga", $request);
|
||||
$paginator = $this->getPaginator("manga", $request, $results);
|
||||
|
||||
return new MangaCollection(
|
||||
$results
|
||||
$paginator
|
||||
);
|
||||
}
|
||||
|
||||
|
137
app/Http/QueryBuilder/AnimeSearchQueryBuilder.php
Normal file
137
app/Http/QueryBuilder/AnimeSearchQueryBuilder.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\QueryBuilder;
|
||||
|
||||
use App\Anime;
|
||||
|
||||
class AnimeSearchQueryBuilder extends MediaSearchQueryBuilder
|
||||
{
|
||||
protected array $parameterNames = ["producer", "producers", "rating"];
|
||||
protected string $displayNameFieldName = "title";
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="anime_search_query_type",
|
||||
* description="Available Anime types",
|
||||
* type="string",
|
||||
* enum={"tv","movie","ova","special","ona","music"}
|
||||
* )
|
||||
*/
|
||||
const MAP_TYPES = [
|
||||
'tv' => 'TV',
|
||||
'movie' => 'Movie',
|
||||
'ova' => 'OVA',
|
||||
'special' => 'Special',
|
||||
'ona' => 'ONA',
|
||||
'music' => 'Music'
|
||||
];
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="anime_search_query_status",
|
||||
* description="Available Anime statuses",
|
||||
* type="string",
|
||||
* enum={"airing","complete","upcoming"}
|
||||
* )
|
||||
*/
|
||||
const MAP_STATUS = [
|
||||
'airing' => 'Currently Airing',
|
||||
'complete' => 'Finished Airing',
|
||||
'upcoming' => 'Not yet aired',
|
||||
];
|
||||
|
||||
/**
|
||||
* @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"}
|
||||
* )
|
||||
*/
|
||||
const MAP_RATING = [
|
||||
'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'
|
||||
];
|
||||
|
||||
/**
|
||||
* @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" }
|
||||
* )
|
||||
*/
|
||||
const ORDER_BY = [
|
||||
'start_date' => 'aired.from',
|
||||
'end_date' => 'aired.to',
|
||||
'episodes' => 'episodes',
|
||||
'rating' => 'rating',
|
||||
'type' => 'type',
|
||||
];
|
||||
|
||||
protected function buildQuery(array $requestParameters, \Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder $results): \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder
|
||||
{
|
||||
$builder = parent::buildQuery($requestParameters, $results);
|
||||
extract($requestParameters);
|
||||
|
||||
if (!is_null($rating)) {
|
||||
$builder = $builder->where('rating', $rating);
|
||||
}
|
||||
|
||||
if (!is_null($producer)) {
|
||||
$producer = (int)$producer;
|
||||
|
||||
$builder = $builder
|
||||
->where('producers.mal_id', $producer)
|
||||
->orWhere('licensors.mal_id', $producer)
|
||||
->orWhere('studios.mal_id', $producer);
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
protected function filterByStartDate(\Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder $builder, string $startDate): \Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder
|
||||
{
|
||||
return $builder->where('aired.from', '>=', $startDate);
|
||||
}
|
||||
|
||||
protected function filterByEndDate(\Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder $builder, string $endDate): \Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder
|
||||
{
|
||||
return $builder->where('aired.to', '<=', $endDate);
|
||||
}
|
||||
|
||||
protected function getStatusMap(): array
|
||||
{
|
||||
return self::MAP_STATUS;
|
||||
}
|
||||
|
||||
protected function getTypeMap(): array
|
||||
{
|
||||
return self::MAP_TYPES;
|
||||
}
|
||||
|
||||
protected function getModelClass(): object|string
|
||||
{
|
||||
return Anime::class;
|
||||
}
|
||||
|
||||
protected function getOrderByFieldMap(): array
|
||||
{
|
||||
$map = parent::getOrderByFieldMap();
|
||||
return array_merge($map, self::ORDER_BY);
|
||||
}
|
||||
|
||||
protected function getAdultRating(): string
|
||||
{
|
||||
return self::MAP_RATING['rx'];
|
||||
}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return "anime";
|
||||
}
|
||||
}
|
126
app/Http/QueryBuilder/MangaSearchQueryBuilder.php
Normal file
126
app/Http/QueryBuilder/MangaSearchQueryBuilder.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\QueryBuilder;
|
||||
|
||||
use App\Manga;
|
||||
|
||||
class MangaSearchQueryBuilder extends MediaSearchQueryBuilder
|
||||
{
|
||||
protected array $parameterNames = ["magazine", "magazines", "rating"];
|
||||
protected string $displayNameFieldName = "title";
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="manga_search_query_type",
|
||||
* description="Available Manga types",
|
||||
* type="string",
|
||||
* enum={"manga","novel", "lightnovel", "oneshot","doujin","manhwa","manhua"}
|
||||
* )
|
||||
*/
|
||||
const MAP_TYPES = [
|
||||
'manga' => 'Manga',
|
||||
'novel' => 'Novel',
|
||||
'lightnovel' => 'Light Novel',
|
||||
'oneshot' => 'One-shot',
|
||||
'doujin' => 'Doujinshi',
|
||||
'manhwa' => 'Manhwa',
|
||||
'manhua' => 'Manhua'
|
||||
];
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="manga_search_query_status",
|
||||
* description="Available Manga statuses",
|
||||
* type="string",
|
||||
* enum={"publishing","complete","hiatus","discontinued","upcoming"}
|
||||
* )
|
||||
*/
|
||||
const MAP_STATUS = [
|
||||
'publishing' => 'Publishing',
|
||||
'complete' => 'Finished',
|
||||
'hiatus' => 'On Hiatus',
|
||||
'discontinued' => 'Discontinued',
|
||||
'upcoming' => 'Not yet published'
|
||||
];
|
||||
|
||||
/**
|
||||
* @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"}
|
||||
* )
|
||||
*/
|
||||
const ORDER_BY = [
|
||||
'chapters' => 'chapters',
|
||||
'volumes' => 'volumes',
|
||||
'start_date' => 'published.from',
|
||||
'end_date' => 'published.to',
|
||||
];
|
||||
|
||||
protected function buildQuery(array $requestParameters, \Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder $results): \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder
|
||||
{
|
||||
$builder = parent::buildQuery($requestParameters, $results);
|
||||
extract($requestParameters);
|
||||
|
||||
if (!is_null($magazine)) $magazines = $magazine;
|
||||
|
||||
if (!is_null($magazines)) {
|
||||
$magazines = explode(',', $magazines);
|
||||
|
||||
foreach ($magazines as $magazine) {
|
||||
if (empty($magazine)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$magazine = (int)$magazine;
|
||||
|
||||
$builder = $builder
|
||||
->orWhere('serializations.mal_id', $magazine);
|
||||
}
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
protected function filterByStartDate(\Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder $builder, string $startDate): \Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder
|
||||
{
|
||||
return $builder->where('published.from', '>=', $startDate);
|
||||
}
|
||||
|
||||
protected function filterByEndDate(\Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder $builder, string $endDate): \Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder
|
||||
{
|
||||
return $builder->where('published.to', '<=', $endDate);
|
||||
}
|
||||
|
||||
protected function getStatusMap(): array
|
||||
{
|
||||
return self::MAP_STATUS;
|
||||
}
|
||||
|
||||
protected function getTypeMap(): array
|
||||
{
|
||||
return self::MAP_TYPES;
|
||||
}
|
||||
|
||||
protected function getAdultRating(): string
|
||||
{
|
||||
return "Doujinshi";
|
||||
}
|
||||
|
||||
protected function getModelClass(): object|string
|
||||
{
|
||||
return Manga::class;
|
||||
}
|
||||
|
||||
protected function getOrderByFieldMap(): array
|
||||
{
|
||||
$map = parent::getOrderByFieldMap();
|
||||
return array_merge($map, self::ORDER_BY);
|
||||
}
|
||||
|
||||
function getIdentifier(): string
|
||||
{
|
||||
return "manga";
|
||||
}
|
||||
}
|
164
app/Http/QueryBuilder/MediaSearchQueryBuilder.php
Normal file
164
app/Http/QueryBuilder/MediaSearchQueryBuilder.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\QueryBuilder;
|
||||
|
||||
use App\IsoDateFormatter;
|
||||
|
||||
abstract class MediaSearchQueryBuilder extends SearchQueryBuilder
|
||||
{
|
||||
use IsoDateFormatter;
|
||||
|
||||
private array $mediaParameterNames = ["score", "sfw", "genres", "genres_exclude", "min_score", "max_score",
|
||||
"start_date", "end_date", "status"];
|
||||
|
||||
const ORDER_BY = [
|
||||
'mal_id' => 'mal_id',
|
||||
'title' => 'title',
|
||||
'score' => 'score',
|
||||
'scored_by' => 'scored_by',
|
||||
'rank' => 'rank',
|
||||
'popularity' => 'popularity',
|
||||
'members' => 'members',
|
||||
'favorites' => 'favorites'
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string|null $status
|
||||
* @return string|null
|
||||
*/
|
||||
public function mapStatus(?string $status = null): ?string
|
||||
{
|
||||
$status = strtolower($status);
|
||||
|
||||
return $this->getStatusMap()[$status] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $type
|
||||
* @return string|null
|
||||
*/
|
||||
public function mapType(?string $type = null): ?string
|
||||
{
|
||||
$type = strtolower($type);
|
||||
|
||||
return $this->getTypeMap()[$type] ?? null;
|
||||
}
|
||||
|
||||
protected function getParameterNames(): array
|
||||
{
|
||||
$parameterNames = parent::getParameterNames();
|
||||
return array_merge($parameterNames, $this->mediaParameterNames);
|
||||
}
|
||||
|
||||
protected function sanitizeParameters($parameters): array
|
||||
{
|
||||
$parameters = parent::sanitizeParameters($parameters);
|
||||
|
||||
if (!array_key_exists("score", $parameters) || empty($parameters["score"])) {
|
||||
$parameters["score"] = 0;
|
||||
}
|
||||
|
||||
$parameters["status"] = $this->mapStatus($parameters["status"]);
|
||||
$parameters["type"] = $this->mapType($parameters["type"]);
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
private function filterByGenre(\Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder $builder, int $genre, $exclude = false): \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder
|
||||
{
|
||||
return $builder->where(function ($query) use ($genre, $exclude) {
|
||||
$operator = $exclude ? '!=' : null;
|
||||
return $query
|
||||
->where('genres.mal_id', $operator, $genre)
|
||||
->where('demographics.mal_id', $operator, $genre)
|
||||
->where('themes.mal_id', $operator, $genre)
|
||||
->where('explicit_genres.mal_id', $operator, $genre);
|
||||
});
|
||||
}
|
||||
|
||||
private function filterByGenres(\Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder $builder, string $genres, $exclude = false): \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder
|
||||
{
|
||||
$genres = explode(',', $genres);
|
||||
foreach ($genres as $genre) {
|
||||
if (empty($genre)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$genre = (int)$genre;
|
||||
|
||||
$builder = $this->filterByGenre($builder, $genre, $exclude);
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
protected function buildQuery(array $requestParameters, \Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder $results): \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder
|
||||
{
|
||||
$builder = $results;
|
||||
extract($requestParameters);
|
||||
|
||||
if (!is_null($start_date)) {
|
||||
$builder = $this->filterByStartDate($builder, $this->formatIsoDateTime($start_date));
|
||||
}
|
||||
|
||||
if (!is_null($end_date)) {
|
||||
$builder = $this->filterByEndDate($builder, $this->formatIsoDateTime($end_date));
|
||||
}
|
||||
|
||||
if (!is_null($type)) {
|
||||
$builder = $builder
|
||||
->where('type', $type);
|
||||
}
|
||||
|
||||
if ($score !== 0) {
|
||||
$score = (float)$score;
|
||||
|
||||
$builder = $builder
|
||||
->where('score', '>=', $score);
|
||||
}
|
||||
|
||||
if ($min_score !== null) {
|
||||
$min_score = (float)$min_score;
|
||||
|
||||
$builder = $builder
|
||||
->where('score', '>=', $min_score);
|
||||
}
|
||||
|
||||
if ($max_score !== null) {
|
||||
$max_score = (float)$max_score;
|
||||
|
||||
$builder = $builder
|
||||
->where('score', '<=', $max_score);
|
||||
}
|
||||
|
||||
if (!is_null($genres)) {
|
||||
$builder = $this->filterByGenres($builder, $genres);
|
||||
}
|
||||
|
||||
if (!is_null($genresExclude)) {
|
||||
$builder = $this->filterByGenres($builder, $genresExclude, true);
|
||||
}
|
||||
|
||||
if (!is_null($sfw)) {
|
||||
$builder = $builder
|
||||
->where('type', '!=', $this->getAdultRating());
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
protected function getOrderByFieldMap(): array
|
||||
{
|
||||
return self::ORDER_BY;
|
||||
}
|
||||
|
||||
protected abstract function filterByStartDate(\Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder $builder, string $startDate): \Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder;
|
||||
|
||||
protected abstract function filterByEndDate(\Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder $builder, string $endDate): \Jenssegers\Mongodb\Eloquent\Builder|\Laravel\Scout\Builder;
|
||||
|
||||
protected abstract function getStatusMap(): array;
|
||||
|
||||
protected abstract function getTypeMap(): array;
|
||||
|
||||
protected abstract function getAdultRating(): string;
|
||||
}
|
@ -119,6 +119,11 @@ class ScoutSearchQueryBuilderAnime
|
||||
->orderBy('mal_id');
|
||||
}
|
||||
|
||||
if (!is_null($letter)) {
|
||||
$results = $results
|
||||
->where('title', 'like', "{$letter}%");
|
||||
}
|
||||
|
||||
if (!is_null($startDate)) {
|
||||
|
||||
$startDate = explode('-', $startDate);
|
||||
@ -208,8 +213,8 @@ class ScoutSearchQueryBuilderAnime
|
||||
$genre = (int) $genre;
|
||||
|
||||
$results = $results
|
||||
->where(function($query) use ($genre) {
|
||||
$query
|
||||
->where(function ($query) use ($genre) {
|
||||
return $query
|
||||
->where('genres.mal_id', $genre)
|
||||
->orWhere('demographics.mal_id', $genre)
|
||||
->orWhere('themes.mal_id', $genre)
|
||||
|
218
app/Http/QueryBuilder/SearchQueryBuilder.php
Normal file
218
app/Http/QueryBuilder/SearchQueryBuilder.php
Normal file
@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\QueryBuilder;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Jenssegers\Mongodb\Eloquent\Model;
|
||||
|
||||
abstract class SearchQueryBuilder implements SearchQueryBuilderService
|
||||
{
|
||||
protected array $commonParameterNames = ["q", "order_by", "sort", "letter"];
|
||||
protected array $parameterNames = [];
|
||||
protected string $displayNameFieldName = "name";
|
||||
protected bool $searchIndexesEnabled;
|
||||
|
||||
public function __construct(bool $searchIndexesEnabled)
|
||||
{
|
||||
$this->searchIndexesEnabled = $searchIndexesEnabled;
|
||||
}
|
||||
|
||||
protected function getParametersFromRequest(Request $request): array
|
||||
{
|
||||
$paramNames = $this->getParameterNames();
|
||||
$parameters = [];
|
||||
|
||||
foreach ($paramNames as $paramName) {
|
||||
$parameters[$paramName] = $request->get($paramName);
|
||||
}
|
||||
|
||||
if (!array_key_exists("q", $parameters)) {
|
||||
$parameters["q"] = "";
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
protected function getParameterNames(): array
|
||||
{
|
||||
return array_merge($this->commonParameterNames, $this->parameterNames);
|
||||
}
|
||||
|
||||
protected function getSanitizedParametersFromRequest(Request $request): array
|
||||
{
|
||||
return $this->sanitizeParameters($this->getParametersFromRequest($request));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getQueryBuilder($requestParameters): \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder
|
||||
{
|
||||
$modelClass = $this->getModelClass();
|
||||
$traits = class_uses_recursive($modelClass);
|
||||
|
||||
if (!in_array(Model::class, class_parents($modelClass))) {
|
||||
throw new \Exception("Programming error: The getModelClass method should return a class which
|
||||
inherits from \Jenssegers\Mongodb\Eloquent\Model.");
|
||||
}
|
||||
|
||||
if ($this->isSearchIndexUsed()) {
|
||||
return $modelClass::search($requestParameters["q"]);
|
||||
}
|
||||
|
||||
return $modelClass::query();
|
||||
}
|
||||
|
||||
public function isSearchIndexUsed(): bool
|
||||
{
|
||||
$modelClass = $this->getModelClass();
|
||||
$traits = class_uses_recursive($modelClass);
|
||||
return in_array(Searchable::class, $traits) && $this->searchIndexesEnabled;
|
||||
}
|
||||
|
||||
protected function sanitizeParameters($parameters): array
|
||||
{
|
||||
$parameters["sort"] = $this->mapSort($parameters["sort"]);
|
||||
$parameters["order_by"] = $this->mapOrderBy($parameters["order_by"]);
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
protected abstract function getModelClass(): object|string;
|
||||
|
||||
protected abstract function buildQuery(array $requestParameters, \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder $results): \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder;
|
||||
|
||||
protected abstract function getOrderByFieldMap(): array;
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function query(Request $request): \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder
|
||||
{
|
||||
$requestParameters = $this->getSanitizedParametersFromRequest($request);
|
||||
extract($requestParameters);
|
||||
$results = $this->getQueryBuilder($requestParameters);
|
||||
|
||||
if (!is_null($letter)) {
|
||||
$results = $results
|
||||
->where($this->displayNameFieldName, 'like', "{$letter}%");
|
||||
}
|
||||
|
||||
if (!is_null($order_by)) {
|
||||
$results = $results
|
||||
->orderBy($order_by, $sort ?? 'asc');
|
||||
}
|
||||
|
||||
if (empty($q)) {
|
||||
$results = $results
|
||||
->orderBy('mal_id');
|
||||
}
|
||||
|
||||
// if search index is disabled, use mongo's full text-search
|
||||
if (empty($q) && is_null($letter) && !$this->isSearchIndexUsed()) {
|
||||
$results = $results
|
||||
->whereRaw([
|
||||
'$text' => [
|
||||
'$search' => $query
|
||||
],
|
||||
], [
|
||||
'score' => [
|
||||
'$meta' => 'textScore'
|
||||
]
|
||||
])
|
||||
->orderBy('score', ['$meta' => 'textScore']);
|
||||
}
|
||||
|
||||
return $this->buildQuery($requestParameters, $results);
|
||||
}
|
||||
|
||||
#[ArrayShape(['per_page' => "int", 'total' => "int", 'current_page' => "int", 'last_page' => "int", 'data' => "array"])]
|
||||
public function paginate(Request $request, \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder $results): array
|
||||
{
|
||||
$paginated = $this->paginateBuilder($request, $results);
|
||||
|
||||
$items = $paginated->items();
|
||||
foreach ($items as &$item) {
|
||||
unset($item['_id']);
|
||||
}
|
||||
|
||||
return [
|
||||
'per_page' => $paginated->perPage(),
|
||||
'total' => $paginated->total(),
|
||||
'current_page' => $paginated->currentPage(),
|
||||
'last_page' => $paginated->lastPage(),
|
||||
'data' => $items
|
||||
];
|
||||
}
|
||||
|
||||
private function getPaginateParameters(Request $request): array
|
||||
{
|
||||
$page = $request->get('page') ?? 1;
|
||||
$limit = $request->get('limit') ?? env('MAX_RESULTS_PER_PAGE', 25);
|
||||
|
||||
$limit = (int)$limit;
|
||||
|
||||
if ($limit <= 0) {
|
||||
$limit = 1;
|
||||
}
|
||||
|
||||
if ($limit > env('MAX_RESULTS_PER_PAGE', 25)) {
|
||||
$limit = env('MAX_RESULTS_PER_PAGE', 25);
|
||||
}
|
||||
|
||||
if ($page <= 0) {
|
||||
$page = 1;
|
||||
}
|
||||
|
||||
return compact("page", "limit");
|
||||
}
|
||||
|
||||
public function paginateBuilder(Request $request, \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder $results): \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
{
|
||||
extract($this->getPaginateParameters($request));
|
||||
|
||||
if ($this->isSearchIndexUsed()) {
|
||||
$paginated = $results
|
||||
->paginate(
|
||||
$limit,
|
||||
null,
|
||||
null,
|
||||
$page
|
||||
);
|
||||
} else {
|
||||
$paginated = $results
|
||||
->paginate(
|
||||
$limit,
|
||||
['*'],
|
||||
null,
|
||||
$page
|
||||
);
|
||||
}
|
||||
|
||||
return $paginated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $sort
|
||||
* @return string|null
|
||||
*/
|
||||
public function mapSort(?string $sort = null): ?string
|
||||
{
|
||||
$sort = strtolower($sort);
|
||||
|
||||
return $sort === 'desc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $orderBy
|
||||
* @return string|null
|
||||
*/
|
||||
public function mapOrderBy(?string $orderBy): ?string
|
||||
{
|
||||
$orderBy = strtolower($orderBy);
|
||||
|
||||
return $this->getOrderByFieldMap()[$orderBy] ?? null;
|
||||
}
|
||||
}
|
18
app/Http/QueryBuilder/SearchQueryBuilderService.php
Normal file
18
app/Http/QueryBuilder/SearchQueryBuilderService.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\QueryBuilder;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
interface SearchQueryBuilderService
|
||||
{
|
||||
function query(Request $request): \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder;
|
||||
|
||||
function paginate(Request $request, \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder $results): array;
|
||||
|
||||
function paginateBuilder(Request $request, \Laravel\Scout\Builder|\Jenssegers\Mongodb\Eloquent\Builder $results): \Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
function getIdentifier(): string;
|
||||
|
||||
function isSearchIndexUsed(): bool;
|
||||
}
|
@ -14,10 +14,10 @@ class MangaCollection extends ResourceCollection
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @OA\Schema(
|
||||
* @OA\Schema(
|
||||
* schema="manga_search",
|
||||
* description="Manga Search Resource",
|
||||
*
|
||||
*
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/pagination_plus"),
|
||||
* @OA\Schema(
|
||||
@ -38,7 +38,7 @@ class MangaCollection extends ResourceCollection
|
||||
|
||||
private $pagination;
|
||||
|
||||
public function __construct(LengthAwarePaginator $resource)
|
||||
public function __construct($resource)
|
||||
{
|
||||
$this->pagination = [
|
||||
'last_visible_page' => $resource->lastPage(),
|
||||
@ -73,7 +73,7 @@ class MangaCollection extends ResourceCollection
|
||||
public function withResponse($request, $response)
|
||||
{
|
||||
$jsonResponse = json_decode($response->getContent(), true);
|
||||
unset($jsonResponse['links'],$jsonResponse['meta']);
|
||||
unset($jsonResponse['links'], $jsonResponse['meta']);
|
||||
$response->setContent(json_encode($jsonResponse));
|
||||
}
|
||||
}
|
18
app/IsoDateFormatter.php
Normal file
18
app/IsoDateFormatter.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
trait IsoDateFormatter
|
||||
{
|
||||
protected function formatIsoDateTime(string $d): string
|
||||
{
|
||||
$dt = explode('-', $d);
|
||||
return (new \DateTime())
|
||||
->setDate(
|
||||
$start_date[0] ?? date('Y'),
|
||||
$start_date[1] ?? 1,
|
||||
$start_date[2] ?? 1
|
||||
)
|
||||
->format(\DateTimeInterface::ISO8601);
|
||||
}
|
||||
}
|
17
app/JikanModel.php
Normal file
17
app/JikanModel.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Jenssegers\Mongodb\Eloquent\Model;
|
||||
use Typesense\LaravelTypesense\Interfaces\TypesenseDocument;
|
||||
|
||||
abstract class JikanModel extends Model implements TypesenseDocument
|
||||
{
|
||||
public abstract function typesenseQueryBy(): array;
|
||||
|
||||
public function getCollectionSchema(): array
|
||||
{
|
||||
// TODO: Implement getCollectionSchema() method.
|
||||
return [];
|
||||
}
|
||||
}
|
@ -8,7 +8,35 @@ trait JikanSearchable
|
||||
{
|
||||
use Searchable;
|
||||
|
||||
public function queryScoutModelsByIds(Builder $builder, array $ids)
|
||||
private function flattenArrayWithKeys($array): array {
|
||||
$result = array();
|
||||
foreach($array as $key=>$value) {
|
||||
if(is_array($value)) {
|
||||
$result = $result + $this->flattenArrayWithKeys($value, $key . '.');
|
||||
}
|
||||
else {
|
||||
$result[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function toTypeSenseCompatibleNestedField(string $fieldName): array {
|
||||
$field = $this->{$fieldName};
|
||||
if (!is_array($field) && !is_object($field)) {
|
||||
return $field;
|
||||
}
|
||||
|
||||
return $this->flattenArrayWithKeys($field);
|
||||
}
|
||||
|
||||
protected function getMalIdsOfField(mixed $field): array {
|
||||
return array_map(function($elem) {
|
||||
return $elem->mal_id;
|
||||
}, $field);
|
||||
}
|
||||
|
||||
public function queryScoutModelsByIds(Builder $builder, array $ids): Builder
|
||||
{
|
||||
$query = static::usesSoftDelete()
|
||||
? $this->withTrashed() : $this->newQuery();
|
||||
|
@ -2,8 +2,9 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Http\QueryBuilder\AnimeSearchQueryBuilder;
|
||||
use App\Http\QueryBuilder\MangaSearchQueryBuilder;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Jenssegers\Mongodb\Eloquent\Builder;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@ -12,13 +13,22 @@ class AppServiceProvider extends ServiceProvider
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
public function register(): void
|
||||
{
|
||||
// //
|
||||
// $this->app->alias('bugsnag.logger', \Illuminate\Contracts\Logging\Log::class);
|
||||
// $this->app->alias('bugsnag.logger', \Psr\Log\LoggerInterface::class);
|
||||
Builder::macro('getName', function() {
|
||||
return 'mongodb';
|
||||
$this->app->singleton(AnimeSearchQueryBuilder::class, function($app) {
|
||||
$searchIndexesEnabled = $app["config"]->get("scout.driver") != "null";
|
||||
return new AnimeSearchQueryBuilder($searchIndexesEnabled);
|
||||
});
|
||||
|
||||
$this->app->singleton(MangaSearchQueryBuilder::class, function($app) {
|
||||
$searchIndexesEnabled = $app["config"]->get("scout.driver") != "null";
|
||||
return new MangaSearchQueryBuilder($searchIndexesEnabled);
|
||||
});
|
||||
|
||||
$this->app->tag([AnimeSearchQueryBuilder::class, MangaSearchQueryBuilder::class], "searchQueryBuilders");
|
||||
|
||||
$this->app->singleton(SearchQueryBuilderProvider::class, function($app) {
|
||||
return new SearchQueryBuilderProvider($app->tagged("searchQueryBuilders"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
31
app/Providers/SearchQueryBuilderProvider.php
Normal file
31
app/Providers/SearchQueryBuilderProvider.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Http\QueryBuilder\SearchQueryBuilderService;
|
||||
|
||||
class SearchQueryBuilderProvider
|
||||
{
|
||||
private array $searchQueryBuilders = [];
|
||||
|
||||
public function __construct(array $searchQueryBuilders)
|
||||
{
|
||||
foreach($searchQueryBuilders as $searchQueryBuilder)
|
||||
{
|
||||
$this->searchQueryBuilders[$searchQueryBuilder->getIdentifier()] = $searchQueryBuilder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getQueryBuilder(string $name): SearchQueryBuilderService
|
||||
{
|
||||
if (!array_key_exists($name, $this->searchQueryBuilders))
|
||||
{
|
||||
throw new \InvalidArgumentException("Invalid argument: name.");
|
||||
}
|
||||
|
||||
return $this->searchQueryBuilders[$name];
|
||||
}
|
||||
}
|
@ -117,6 +117,7 @@ $app->register(\SwaggerLume\ServiceProvider::class);
|
||||
$app->register(Flipbox\LumenGenerator\LumenGeneratorServiceProvider::class);
|
||||
$app->register(\App\Providers\SourceHeartbeatProvider::class);
|
||||
$app->register(Illuminate\Database\Eloquent\LegacyFactoryServiceProvider::class);
|
||||
$app->register(\App\Providers\AppServiceProvider::class);
|
||||
|
||||
if (env('REPORTING') && env('REPORTING_DRIVER') === 'sentry') {
|
||||
$app->register(\Sentry\Laravel\ServiceProvider::class);
|
||||
|
Loading…
x
Reference in New Issue
Block a user