- 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:
pushrbx 2022-06-05 18:53:53 +01:00
parent 271c750417
commit 194a17881b
16 changed files with 821 additions and 77 deletions

View File

@ -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")
);
}
/**

View File

@ -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;

View File

@ -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
);
}

View 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";
}
}

View 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";
}
}

View 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;
}

View File

@ -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)

View 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;
}
}

View 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;
}

View File

@ -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
View 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
View 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 [];
}
}

View File

@ -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();

View File

@ -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"));
});
}
}

View 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];
}
}

View File

@ -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);