diff --git a/README.md b/README.md index b4d7c2b..0e22238 100644 --- a/README.md +++ b/README.md @@ -97,10 +97,6 @@ Run the tests with: composer test ``` -## Roadmap - -- Restructure/Optimize Builder class for better code quality - ## Contribution Pull requests are welcome :) diff --git a/src/Builder.php b/src/Builder.php index cd1256d..28d203c 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -5,20 +5,27 @@ namespace MarcReichel\IGDBLaravel; use Carbon\Carbon; -use Closure; -use DateTimeInterface; -use Illuminate\Http\Client\PendingRequest; use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use InvalidArgumentException; use JsonException; use MarcReichel\IGDBLaravel\Exceptions\InvalidParamsException; use MarcReichel\IGDBLaravel\Exceptions\MissingEndpointException; use MarcReichel\IGDBLaravel\Exceptions\ModelNotFoundException; -use MarcReichel\IGDBLaravel\Traits\{DateCasts, Operators}; +use MarcReichel\IGDBLaravel\Traits\{DateCasts, + HasLimits, + HasNestedWhere, + HasSearch, + HasSelect, + HasWhere, + HasWhereBetween, + HasWhereDate, + HasWhereHas, + HasWhereIn, + HasWhereLike, + Operators, + ValuePreparer}; use ReflectionClass; use ReflectionException; use stdClass; @@ -27,11 +34,17 @@ class Builder { use DateCasts; use Operators; - - /** - * The HTTP Client to request data from the API. - */ - private PendingRequest $client; + use HasSelect; + use HasLimits; + use HasSearch; + use ValuePreparer; + use HasWhere; + use HasWhereLike; + use HasNestedWhere; + use HasWhereIn; + use HasWhereBetween; + use HasWhereHas; + use HasWhereDate; /** * The endpoint of the API that should be requested. @@ -65,59 +78,9 @@ public function __construct(mixed $model = null) $this->init(); } - /** - * Set the fields to be selected. - */ - public function select(mixed $fields): self - { - $fields = is_array($fields) ? $fields : func_get_args(); - $collection = collect($fields); - - if ($collection->isEmpty()) { - $collection->push('*'); - } - - $collection = $collection->filter(fn (string $field) => !strpos($field, '.'))->flatten(); - - if ($collection->isEmpty()) { - $collection->push('*'); - } - - $this->query->put('fields', $collection); - - return $this; - } - protected function init(): void - { - $this->initClient(); - $this->initQuery(); - $this->resetCacheLifetime(); - } - - private function initClient(): void - { - $this->client = Http::withOptions([ - 'base_uri' => ApiHelper::IGDB_BASE_URI, - ])->withHeaders([ - 'Accept' => 'application/json', - 'Client-ID' => config('igdb.credentials.client_id'), - ]); - } - - /** - * Init the default query. - */ - protected function initQuery(): void { $this->query = new Collection(['fields' => new Collection(['*'])]); - } - - /** - * Reset the cache lifetime. - */ - protected function resetCacheLifetime(): void - { $cache = config('igdb.cache_lifetime'); if (!is_int($cache)) { throw new InvalidArgumentException('igdb.cache_lifetime needs to be int. ' . gettype($cache) . ' given.'); @@ -126,893 +89,6 @@ protected function resetCacheLifetime(): void $this->cacheLifetime = $cache; } - /** - * Set the "limit" value of the query. - */ - public function limit(int $limit): self - { - $limit = min($limit, 500); - $this->query->put('limit', $limit); - - return $this; - } - - /** - * Alias to set the "limit" value of the query. - */ - public function take(int $limit): self - { - return $this->limit($limit); - } - - /** - * Set the "offset" value of the query. - */ - public function offset(int $offset): self - { - $this->query->put('offset', $offset); - - return $this; - } - - /** - * Alias to set the "offset" value of the query. - */ - public function skip(int $offset): self - { - return $this->offset($offset); - } - - /** - * Set the limit and offset for a given page. - */ - public function forPage(int $page, int $perPage = 10): self - { - return $this->skip(($page - 1) * $perPage)->take($perPage); - } - - /** - * Search for the given string. - */ - public function search(string $query): self - { - $this->query->put('search', '"' . $query . '"'); - - return $this; - } - - /** - * Add a fuzzy search to the query. - * - * @throws ReflectionException - * @throws InvalidParamsException - * @throws JsonException - */ - public function fuzzySearch( - mixed $key, - string $query, - bool $caseSensitive = false, - string $boolean = '&', - ): self { - $tokenizedQuery = explode(' ', $query); - $keys = collect($key)->crossJoin($tokenizedQuery)->toArray(); - - return $this->whereNested(function (Builder $query) use ($keys, $caseSensitive): void { - foreach ($keys as $v) { - if (is_array($v)) { - $query->whereLike($v[0], $v[1], $caseSensitive, '|'); - } - } - }, $boolean); - } - - /** - * Add an "or fuzzy search" to the query. - * - * @throws ReflectionException|InvalidParamsException - */ - public function orFuzzySearch( - mixed $key, - string $query, - bool $caseSensitive = false, - string $boolean = '|', - ): self { - return $this->fuzzySearch($key, $query, $caseSensitive, $boolean); - } - - /** - * Add a basic where clause to the query. - * - * @throws ReflectionException - * @throws JsonException - * @throws InvalidParamsException - */ - public function where( - mixed $key, - mixed $operator = null, - mixed $value = null, - string $boolean = '&', - ): self { - if ($key instanceof Closure) { - return $this->whereNested($key, $boolean); - } - - if (is_array($key)) { - return $this->addArrayOfWheres($key, $boolean); - } - - if (!is_string($key)) { - throw new InvalidArgumentException('Parameter #1 $key needs to be string. ' . gettype($key) . ' given.'); - } - - [$value, $operator] = $this->prepareValueAndOperator( - $value, - $operator, - func_num_args() === 2, - ); - - $select = $this->query->get('fields', new Collection()); - if (!$select->contains($key) && !$select->contains('*')) { - $this->query->put('fields', $select->push($key)); - } - - $where = $this->query->get('where', new Collection()); - - if (collect($this->dates)->has($key) && $this->dates[$key] === 'date') { - $value = $this->castDate($value); - } - - if (is_string($value)) { - if ($operator === 'like') { - $this->whereLike($key, $value, true, $boolean); - } elseif ($operator === 'ilike') { - $this->whereLike($key, $value, false, $boolean); - } elseif ($operator === 'not like') { - $this->whereNotLike($key, $value, true, $boolean); - } elseif ($operator === 'not ilike') { - $this->whereNotLike($key, $value, false, $boolean); - } else { - $where->push(($where->count() ? $boolean . ' ' : '') . $key . ' ' . $operator . ' "' . $value . '"'); - $this->query->put('where', $where); - } - } else { - $value = !is_int($value) ? json_encode($value, JSON_THROW_ON_ERROR) : $value; - $where->push(($where->count() ? $boolean . ' ' : '') . $key . ' ' . $operator . ' ' . $value); - $this->query->put('where', $where); - } - - return $this; - } - - /** - * Add an "or where" clause to the query. - * - * @throws ReflectionException - * @throws JsonException - * @throws InvalidParamsException - */ - public function orWhere( - mixed $key, - string $operator = null, - mixed $value = null, - string $boolean = '|', - ): self { - if ($key instanceof Closure) { - return $this->whereNested($key, $boolean); - } - - if (is_array($key)) { - return $this->addArrayOfWheres($key, $boolean); - } - - [$value, $operator] = $this->prepareValueAndOperator( - $value, - $operator, - func_num_args() === 2, - ); - - return $this->where($key, $operator, $value, $boolean); - } - - /** - * Add a "where like" clause to the query. - * - * @throws JsonException - */ - public function whereLike( - string $key, - string $value, - bool $caseSensitive = true, - string $boolean = '&', - ): self { - $where = $this->query->get('where', new Collection()); - - $clause = $this->generateWhereLikeClause($key, $value, $caseSensitive, '=', '~'); - - $where->push(($where->count() ? $boolean . ' ' : '') . $clause); - - $this->query->put('where', $where); - - return $this; - } - - /** - * Add an "or where like" clause to the query. - * - * @throws JsonException - */ - public function orWhereLike( - string $key, - string $value, - bool $caseSensitive = true, - string $boolean = '|', - ): self { - return $this->whereLike($key, $value, $caseSensitive, $boolean); - } - - /** - * Add a "where not like" clause to the query. - * - * @throws JsonException - */ - public function whereNotLike( - string $key, - string $value, - bool $caseSensitive = true, - string $boolean = '&', - ): self { - $where = $this->query->get('where', new Collection()); - - $clause = $this->generateWhereLikeClause($key, $value, $caseSensitive, '!=', '!~'); - - $where->push(($where->count() ? $boolean . ' ' : '') . $clause); - - $this->query->put('where', $where); - - return $this; - } - - /** - * Add an "or where not like" clause to the query. - * - * @throws JsonException - */ - public function orWhereNotLike( - string $key, - string $value, - bool $caseSensitive = true, - string $boolean = '|', - ): self { - return $this->whereNotLike($key, $value, $caseSensitive, $boolean); - } - - /** - * @throws JsonException - */ - private function generateWhereLikeClause( - string $key, - string $value, - bool $caseSensitive, - string $operator, - string $insensitiveOperator, - ): string { - $hasPrefix = Str::startsWith($value, ['%', '*']); - $hasSuffix = Str::endsWith($value, ['%', '*']); - - if ($hasPrefix) { - $value = substr($value, 1); - } - if ($hasSuffix) { - $value = substr($value, 0, -1); - } - - $operator = $caseSensitive ? $operator : $insensitiveOperator; - $prefix = $hasPrefix || !$hasSuffix ? '*' : ''; - $suffix = $hasSuffix || !$hasPrefix ? '*' : ''; - $value = json_encode($value, JSON_THROW_ON_ERROR); - $value = Str::start(Str::finish($value, $suffix), $prefix); - - return implode(' ', [$key, $operator, $value]); - } - - /** - * Prepare the value and operator for a where clause. - */ - private function prepareValueAndOperator( - mixed $value, - mixed $operator, - bool $useDefault = false, - ): array { - if ($useDefault) { - return [$operator, '=']; - } - - if (!is_string($operator) && null !== $operator) { - throw new InvalidArgumentException('Parameter #2 $operator needs to be string or null. ' . gettype($operator) . ' given.'); - } - - if ($this->invalidOperatorAndValue($operator, $value)) { - throw new InvalidArgumentException('Illegal operator and value combination.'); - } - - return [$value, $operator]; - } - - /** - * Determine if the given operator and value combination is legal. - * - * Prevents using Null values with invalid operators. - */ - private function invalidOperatorAndValue(?string $operator, mixed $value): bool - { - return null === $value && in_array($operator, $this->operators, true) && !in_array($operator, ['=', '!=']); - } - - /** - * Add an array of where clauses to the query. - * - * @throws ReflectionException - * @throws InvalidParamsException - */ - protected function addArrayOfWheres( - array $arrayOfWheres, - string $boolean, - string $method = 'where', - ): self { - return $this->whereNested(function (Builder $query) use ( - $arrayOfWheres, - $method, - $boolean - ): void { - foreach ($arrayOfWheres as $key => $value) { - if (is_numeric($key) && is_array($value)) { - $query->$method(...array_values($value)); - } else { - $query->$method($key, '=', $value, $boolean); - } - } - }, $boolean); - } - - /** - * Add a nested where statement to the query. - * - * @throws ReflectionException - * @throws InvalidParamsException - */ - protected function whereNested(Closure $callback, string $boolean = '&'): self - { - if (isset($this->class) && $this->class) { - $class = $this->class; - $callback($query = new Builder(new $class())); - } else { - $callback($query = new Builder($this->endpoint)); - } - - return $this->addNestedWhereQuery($query, $boolean); - } - - /** - * Add another query builder as a nested where to the query builder. - */ - protected function addNestedWhereQuery(Builder $query, string $boolean): self - { - $where = $this->query->get('where', new Collection()); - - $nested = '(' . $query->query->get('where')->implode(' ') . ')'; - - $where->push(($where->count() ? $boolean . ' ' : '') . $nested); - - $this->query->put('where', $where); - - return $this; - } - - /** - * Add a "where in" clause to the query. - */ - public function whereIn( - string $key, - array $values, - string $boolean = '&', - string $operator = '=', - string $prefix = '(', - string $suffix = ')', - ): self { - if (($prefix === '(' && $suffix !== ')') || ($prefix === '[' && $suffix !== ']') || ($prefix === '{' && $suffix !== '}')) { - $message = 'Prefix and Suffix must be "(" and ")", "[" and "]" or "{" and "}".'; - - throw new InvalidArgumentException($message); - } - - $where = $this->query->get('where', new Collection()); - - $valuesString = collect($values)->map(fn (mixed $value) => !is_numeric($value) ? '"' . $value . '"' : $value)->implode(','); - - $where->push(($where->count() ? $boolean . ' ' : '') . $key . ' ' . $operator . ' ' . $prefix . $valuesString . $suffix); - - $this->query->put('where', $where); - - return $this; - } - - /** - * Add an "or where in" clause to the query. - */ - public function orWhereIn( - string $key, - array $value, - string $boolean = '|', - string $operator = '=', - string $prefix = '(', - string $suffix = ')', - ): self { - return $this->whereIn($key, $value, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add a "where in all" clause to the query. - */ - public function whereInAll( - string $key, - array $values, - string $boolean = '&', - string $operator = '=', - string $prefix = '[', - string $suffix = ']', - ): self { - return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add an "or where in all" clause to the query. - */ - public function orWhereInAll( - string $key, - array $values, - string $boolean = '|', - string $operator = '=', - string $prefix = '[', - string $suffix = ']', - ): self { - return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add a "where in exact" clause to the query. - */ - public function whereInExact( - string $key, - array $values, - string $boolean = '&', - string $operator = '=', - string $prefix = '{', - string $suffix = '}', - ): self { - return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add an "or where in exact" clause to the query. - */ - public function orWhereInExact( - string $key, - array $values, - string $boolean = '|', - string $operator = '=', - string $prefix = '{', - string $suffix = '}', - ): self { - return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add a "where not in" clause to the query. - */ - public function whereNotIn( - string $key, - array $values, - string $boolean = '&', - string $operator = '!=', - string $prefix = '(', - string $suffix = ')', - ): self { - return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add an "or where not in" clause to the query. - */ - public function orWhereNotIn( - string $key, - array $values, - string $boolean = '|', - string $operator = '!=', - string $prefix = '(', - string $suffix = ')', - ): self { - return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add a "where not in all" clause to the query. - */ - public function whereNotInAll( - string $key, - array $values, - string $boolean = '&', - string $operator = '!=', - string $prefix = '[', - string $suffix = ']', - ): self { - return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add an "or where not in all" clause to the query. - */ - public function orWhereNotInAll( - string $key, - array $values, - string $boolean = '|', - string $operator = '!=', - string $prefix = '[', - string $suffix = ']', - ): self { - return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add a "where not in exact" clause to the query. - */ - public function whereNotInExact( - string $key, - array $values, - string $boolean = '&', - string $operator = '!=', - string $prefix = '{', - string $suffix = '}', - ): self { - return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add an "or where not in exact" clause to the query. - */ - public function orWhereNotInExact( - string $key, - array $values, - string $boolean = '|', - string $operator = '!=', - string $prefix = '{', - string $suffix = '}', - ): self { - return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); - } - - /** - * Add a where between statement to the query. - * - * @throws ReflectionException - * @throws InvalidParamsException - * @throws JsonException - */ - public function whereBetween( - string $key, - mixed $first, - mixed $second, - bool $withBoundaries = true, - string $boolean = '&', - ): self { - if (collect($this->dates)->has($key) && $this->dates[$key] === 'date') { - $first = $this->castDate($first); - $second = $this->castDate($second); - } - - $this->whereNested(function (Builder $query) use ( - $key, - $first, - $second, - $withBoundaries, - ): void { - $operator = ($withBoundaries ? '=' : ''); - $query->where($key, '>' . $operator, $first)->where($key, '<' . $operator, $second); - }, $boolean); - - return $this; - } - - /** - * Add a or where between statement to the query. - * - * @throws ReflectionException - * @throws InvalidParamsException - */ - public function orWhereBetween( - string $key, - mixed $first, - mixed $second, - bool $withBoundaries = true, - string $boolean = '|', - ): self { - return $this->whereBetween($key, $first, $second, $withBoundaries, $boolean); - } - - /** - * Add a where not between statement to the query. - * - * @throws ReflectionException - * @throws InvalidParamsException - * @throws JsonException - */ - public function whereNotBetween( - string $key, - mixed $first, - mixed $second, - bool $withBoundaries = false, - string $boolean = '&', - ): self { - if (collect($this->dates)->has($key) && $this->dates[$key] === 'date') { - $first = $this->castDate($first); - $second = $this->castDate($second); - } - - $this->whereNested(function (Builder $query) use ( - $key, - $first, - $second, - $withBoundaries, - ): void { - $operator = ($withBoundaries ? '=' : ''); - $query->where($key, '<' . $operator, $first)->orWhere($key, '>' . $operator, $second); - }, $boolean); - - return $this; - } - - /** - * Add a or where not between statement to the query. - * - * @throws ReflectionException - * @throws InvalidParamsException - */ - public function orWhereNotBetween( - string $key, - mixed $first, - mixed $second, - bool $withBoundaries = false, - string $boolean = '|', - ): self { - return $this->whereNotBetween($key, $first, $second, $withBoundaries, $boolean); - } - - /** - * Add a "where has" statement to the query. - */ - public function whereHas(string $relationship, string $boolean = '&'): self - { - $where = $this->query->get('where', new Collection()); - - $currentWhere = $where; - - $where->push(($currentWhere->count() ? $boolean . ' ' : '') . $relationship . ' != null'); - - $this->query->put('where', $where); - - return $this; - } - - /** - * Add an "or where has" statement to the query. - */ - public function orWhereHas(string $relationship, string $boolean = '|'): self - { - return $this->whereHas($relationship, $boolean); - } - - /** - * Add a "where has not" statement to the query. - */ - public function whereHasNot(string $relationship, string $boolean = '&'): self - { - $where = $this->query->get('where', new Collection()); - - $currentWhere = $where; - - $where->push(($currentWhere->count() ? $boolean . ' ' : '') . $relationship . ' = null'); - - $this->query->put('where', $where); - - return $this; - } - - /** - * Add a "where has not" statement to the query. - */ - public function orWhereHasNot(string $relationship, string $boolean = '|'): self - { - return $this->whereHasNot($relationship, $boolean); - } - - /** - * Add a "where null" clause to the query. - */ - public function whereNull(string $key, string $boolean = '&'): self - { - return $this->whereHasNot($key, $boolean); - } - - /** - * Add an "or where null" clause to the query. - */ - public function orWhereNull(string $key, string $boolean = '|'): self - { - return $this->whereNull($key, $boolean); - } - - /** - * Add a "where not null" clause to the query. - */ - public function whereNotNull(string $key, string $boolean = '&'): self - { - return $this->whereHas($key, $boolean); - } - - /** - * Add an "or where not null" clause to the query. - */ - public function orWhereNotNull(string $key, string $boolean = '|'): self - { - return $this->whereNotNull($key, $boolean); - } - - /** - * Add a "where date" statement to the query. - * - * @throws ReflectionException - * @throws JsonException - * @throws InvalidParamsException - */ - public function whereDate(string $key, mixed $operator, mixed $value = null, string $boolean = '&'): self - { - [$value, $operator] = $this->prepareValueAndOperator( - $value, - $operator, - func_num_args() === 2, - ); - - $start = Carbon::parse($value)->startOfDay()->timestamp; - $end = Carbon::parse($value)->endOfDay()->timestamp; - - return match ($operator) { - '>' => $this->whereDateGreaterThan($key, $operator, $value, $boolean), - '>=' => $this->whereDateGreaterThanOrEquals($key, $operator, $value, $boolean), - '<' => $this->whereDateLowerThan($key, $operator, $value, $boolean), - '<=' => $this->whereDateLowerThanOrEquals($key, $operator, $value, $boolean), - '!=' => $this->whereNotBetween($key, $start, $end, false, $boolean), - default => $this->whereBetween($key, $start, $end, true, $boolean), - }; - } - - /** - * @throws JsonException - * @throws ReflectionException - * @throws InvalidParamsException - */ - private function whereDateGreaterThan(string $key, mixed $operator, mixed $value, string $boolean): self - { - if (is_string($value) || $value instanceof DateTimeInterface) { - $value = Carbon::parse($value)->addDay()->startOfDay()->timestamp; - } - - return $this->where($key, $operator, $value, $boolean); - } - - /** - * @throws JsonException - * @throws ReflectionException - * @throws InvalidParamsException - */ - private function whereDateGreaterThanOrEquals(string $key, mixed $operator, mixed $value, string $boolean): self - { - if (is_string($value) || $value instanceof DateTimeInterface) { - $value = Carbon::parse($value)->startOfDay()->timestamp; - } - - return $this->where($key, $operator, $value, $boolean); - } - - /** - * @throws JsonException - * @throws ReflectionException - * @throws InvalidParamsException - */ - private function whereDateLowerThan(string $key, mixed $operator, mixed $value, string $boolean): self - { - if (is_string($value) || $value instanceof DateTimeInterface) { - $value = Carbon::parse($value)->subDay()->endOfDay()->timestamp; - } - - return $this->where($key, $operator, $value, $boolean); - } - - /** - * @throws JsonException - * @throws ReflectionException - * @throws InvalidParamsException - */ - private function whereDateLowerThanOrEquals(string $key, mixed $operator, mixed $value, string $boolean): self - { - if (is_string($value) || $value instanceof DateTimeInterface) { - $value = Carbon::parse($value)->endOfDay()->timestamp; - } - - return $this->where($key, $operator, $value, $boolean); - } - - /** - * Add an "or where date" statement to the query. - * - * @throws ReflectionException - * @throws JsonException - * @throws InvalidParamsException - */ - public function orWhereDate(string $key, mixed $operator, mixed $value = null, string $boolean = '|'): self - { - return $this->whereDate($key, $operator, $value, $boolean); - } - - /** - * Add a "where year" statement to the query. - * - * @throws ReflectionException - * @throws JsonException - * @throws InvalidParamsException - */ - public function whereYear(string $key, mixed $operator, mixed $value = null, string $boolean = '&'): self - { - [$value, $operator] = $this->prepareValueAndOperator( - $value, - $operator, - func_num_args() === 2, - ); - - $value = Carbon::now()->setYear($value)->startOfYear(); - - if ($operator === '=') { - $start = $value->clone()->startOfYear()->timestamp; - $end = $value->clone()->endOfYear()->timestamp; - - return $this->whereBetween($key, $start, $end, true, $boolean); - } - - if ($operator === '>' || $operator === '<=') { - $value = $value->clone()->endOfYear()->timestamp; - } elseif ($operator === '>=' || $operator === '<') { - $value = $value->clone()->startOfYear()->timestamp; - } - - return $this->where($key, $operator, $value, $boolean); - } - - /** - * Add an "or where year" statement to the query. - * - * @throws ReflectionException - * @throws JsonException - * @throws InvalidParamsException - */ - public function orWhereYear(string $key, mixed $operator, mixed $value = null, string $boolean = '|'): self - { - [$value, $operator] = $this->prepareValueAndOperator( - $value, - $operator, - func_num_args() === 2, - ); - - return $this->whereYear($key, $operator, $value, $boolean); - } - /** * Add a "sort" clause to the query. * @@ -1105,6 +181,8 @@ public function getQuery(): string /** * Set the endpoint as string. + * + * @deprecated Will be removed in the next major release. */ public function endpoint(string $endpoint): self { @@ -1156,7 +234,7 @@ protected function setEndpoint(mixed $model): void /** * Cast a value as date. */ - private function castDate(mixed $date): mixed + protected function castDate(mixed $date): mixed { if (is_string($date)) { return Carbon::parse($date)->timestamp; @@ -1176,7 +254,11 @@ private function castDate(mixed $date): mixed */ public function get(): mixed { - $data = $this->fetchApi(); + if (!isset($this->endpoint) || $this->endpoint === '') { + throw new MissingEndpointException(); + } + + $data = Client::get($this->endpoint, $this->getQuery(), $this->cacheLifetime); if (isset($this->class) && $this->class) { $data = collect($data)->map(fn (mixed $result) => $this->mapToModel($result)); @@ -1278,9 +360,13 @@ public function firstOrFail(): mixed * * @throws MissingEndpointException */ - public function count(): mixed + public function count(): int { - $data = $this->fetchApi(true); + if (!isset($this->endpoint) || $this->endpoint === '') { + throw new MissingEndpointException(); + } + + $data = Client::count($this->endpoint, $this->getQuery(), $this->cacheLifetime); $this->init(); @@ -1307,57 +393,4 @@ public function paginate(int $limit = 10): Paginator $limit, ); } - - /** - * @throws MissingEndpointException - */ - private function fetchApi(bool $count = false): mixed - { - if (!isset($this->endpoint)) { - throw new MissingEndpointException(); - } - - $endpoint = $this->getEndpoint($count); - - $cacheKey = $this->handleCache($endpoint); - - return Cache::remember($cacheKey, $this->cacheLifetime, function () use ($endpoint, $count) { - $response = $this->client->withHeaders([ - 'Authorization' => 'Bearer ' . ApiHelper::retrieveAccessToken(), - ]) - ->withBody($this->getQuery(), 'plain/text') - ->retry(3, 100) - ->post($endpoint) - ->throw() - ->json(); - - if ($count && is_array($response)) { - return $response['count']; - } - - return $response; - }); - } - - private function getEndpoint(bool $count = false): string - { - $endpoint = $this->endpoint; - - if ($count) { - $endpoint = Str::finish($endpoint, '/count'); - } - - return $endpoint; - } - - private function handleCache(string $endpoint): string - { - $cacheKey = 'igdb_cache.' . md5($endpoint . $this->getQuery()); - - if ($this->cacheLifetime === 0) { - Cache::forget($cacheKey); - } - - return $cacheKey; - } } diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..6596f78 --- /dev/null +++ b/src/Client.php @@ -0,0 +1,73 @@ + self::request($endpoint, $query)); + } + + public static function count(string $endpoint, string $query, int $cacheLifetime): int + { + $endpoint = Str::finish($endpoint, '/count'); + $cacheKey = self::handleCache($endpoint, $query, $cacheLifetime); + + return Cache::remember($cacheKey, $cacheLifetime, static function () use ($endpoint, $query): int { + $response = self::request($endpoint, $query); + if (is_array($response)) { + return (int) $response['count']; + } + + return 0; + }); + } + + private static function handleCache(string $endpoint, string $query, int $cacheLifetime): string + { + $key = 'igdb_cache.' . md5($endpoint . $query); + + if ($cacheLifetime === 0) { + Cache::forget($key); + } + + return $key; + } + + /** + * @throws AuthenticationException + * @throws RequestException + */ + private static function request(string $endpoint, string $query): mixed + { + $client = Http::withOptions([ + 'base_uri' => ApiHelper::IGDB_BASE_URI, + ])->withHeaders([ + 'Accept' => 'application/json', + 'Client-ID' => config('igdb.credentials.client_id'), + ]); + + return $client->withHeaders([ + 'Authorization' => 'Bearer ' . ApiHelper::retrieveAccessToken(), + ]) + ->withBody($query, 'plain/text') + ->retry(3, 100) + ->post($endpoint) + ->throw() + ->json(); + } +} diff --git a/src/Models/Model.php b/src/Models/Model.php index 588900b..c4b4dbe 100644 --- a/src/Models/Model.php +++ b/src/Models/Model.php @@ -23,6 +23,7 @@ use MarcReichel\IGDBLaravel\Exceptions\InvalidWebhookMethodException; use MarcReichel\IGDBLaravel\Exceptions\WebhookSecretMissingException; use ReflectionException; +use RuntimeException; /** * @method static Builder select(mixed $fields) @@ -373,7 +374,7 @@ public static function createWebhook(Method | string $method, array $parameters ])->throw()->json(); if (!is_array($response)) { - throw new Exception('An error occurred while trying to create the webhook.'); + throw new RuntimeException('An error occurred while trying to create the webhook.'); } return new Webhook(...$response); diff --git a/src/Traits/DateCasts.php b/src/Traits/DateCasts.php index 1a5004e..1fd8131 100644 --- a/src/Traits/DateCasts.php +++ b/src/Traits/DateCasts.php @@ -4,6 +4,9 @@ namespace MarcReichel\IGDBLaravel\Traits; +/** + * @internal + */ trait DateCasts { /** diff --git a/src/Traits/HasLimits.php b/src/Traits/HasLimits.php new file mode 100644 index 0000000..09874f4 --- /dev/null +++ b/src/Traits/HasLimits.php @@ -0,0 +1,56 @@ +query->put('limit', $limit); + + return $this; + } + + /** + * Alias to set the "limit" value of the query. + */ + public function take(int $limit): self + { + return $this->limit($limit); + } + + /** + * Set the "offset" value of the query. + */ + public function offset(int $offset): self + { + $this->query->put('offset', $offset); + + return $this; + } + + /** + * Alias to set the "offset" value of the query. + */ + public function skip(int $offset): self + { + return $this->offset($offset); + } + + /** + * Set the limit and offset for a given page. + */ + public function forPage(int $page, int $perPage = 10): self + { + return $this->skip(($page - 1) * $perPage)->take($perPage); + } +} diff --git a/src/Traits/HasNestedWhere.php b/src/Traits/HasNestedWhere.php new file mode 100644 index 0000000..262c797 --- /dev/null +++ b/src/Traits/HasNestedWhere.php @@ -0,0 +1,51 @@ +class) && $this->class) { + $class = $this->class; + $callback($query = new Builder(new $class())); + } else { + $callback($query = new Builder($this->endpoint)); + } + + return $this->addNestedWhereQuery($query, $boolean); + } + + /** + * Add another query builder as a nested where to the query builder. + */ + protected function addNestedWhereQuery(Builder $query, string $boolean): self + { + $where = $this->query->get('where', new Collection()); + + $nested = '(' . $query->query->get('where', new Collection())->implode(' ') . ')'; + + $where->push(($where->count() ? $boolean . ' ' : '') . $nested); + + $this->query->put('where', $where); + + return $this; + } +} diff --git a/src/Traits/HasSearch.php b/src/Traits/HasSearch.php new file mode 100644 index 0000000..a0568d6 --- /dev/null +++ b/src/Traits/HasSearch.php @@ -0,0 +1,65 @@ +query->put('search', '"' . $query . '"'); + + return $this; + } + + /** + * Add a fuzzy search to the query. + * + * @throws ReflectionException + * @throws InvalidParamsException + * @throws JsonException + */ + public function fuzzySearch( + mixed $key, + string $query, + bool $caseSensitive = false, + string $boolean = '&', + ): self { + $tokenizedQuery = explode(' ', $query); + $keys = collect($key)->crossJoin($tokenizedQuery)->toArray(); + + return $this->whereNested(function (Builder $query) use ($keys, $caseSensitive): void { + foreach ($keys as $v) { + if (is_array($v)) { + $query->whereLike($v[0], $v[1], $caseSensitive, '|'); + } + } + }, $boolean); + } + + /** + * Add an "or fuzzy search" to the query. + * + * @throws ReflectionException|InvalidParamsException|JsonException + */ + public function orFuzzySearch( + mixed $key, + string $query, + bool $caseSensitive = false, + string $boolean = '|', + ): self { + return $this->fuzzySearch($key, $query, $caseSensitive, $boolean); + } +} diff --git a/src/Traits/HasSelect.php b/src/Traits/HasSelect.php new file mode 100644 index 0000000..71c863b --- /dev/null +++ b/src/Traits/HasSelect.php @@ -0,0 +1,34 @@ +isEmpty()) { + $collection->push('*'); + } + + $collection = $collection->filter(fn (string $field) => !strpos($field, '.'))->flatten(); + + if ($collection->isEmpty()) { + $collection->push('*'); + } + + $this->query->put('fields', $collection); + + return $this; + } +} diff --git a/src/Traits/HasWhere.php b/src/Traits/HasWhere.php new file mode 100644 index 0000000..107b3d1 --- /dev/null +++ b/src/Traits/HasWhere.php @@ -0,0 +1,139 @@ +whereNested($key, $boolean); + } + + if (is_array($key)) { + return $this->addArrayOfWheres($key, $boolean); + } + + if (!is_string($key)) { + throw new InvalidArgumentException('Parameter #1 $key needs to be string. ' . gettype($key) . ' given.'); + } + + [$value, $operator] = $this->prepareValueAndOperator( + $value, + $operator, + func_num_args() === 2, + ); + + $select = $this->query->get('fields', new Collection()); + if (!$select->contains($key) && !$select->contains('*')) { + $this->query->put('fields', $select->push($key)); + } + + $where = $this->query->get('where', new Collection()); + + if (collect($this->dates)->has($key) && $this->dates[$key] === 'date') { + $value = $this->castDate($value); + } + + if (is_string($value)) { + if ($operator === 'like') { + $this->whereLike($key, $value, true, $boolean); + } elseif ($operator === 'ilike') { + $this->whereLike($key, $value, false, $boolean); + } elseif ($operator === 'not like') { + $this->whereNotLike($key, $value, true, $boolean); + } elseif ($operator === 'not ilike') { + $this->whereNotLike($key, $value, false, $boolean); + } else { + $where->push(($where->count() ? $boolean . ' ' : '') . $key . ' ' . $operator . ' "' . $value . '"'); + $this->query->put('where', $where); + } + } else { + $value = !is_int($value) ? json_encode($value, JSON_THROW_ON_ERROR) : $value; + $where->push(($where->count() ? $boolean . ' ' : '') . $key . ' ' . $operator . ' ' . $value); + $this->query->put('where', $where); + } + + return $this; + } + + /** + * Add an "or where" clause to the query. + * + * @throws ReflectionException + * @throws JsonException + * @throws InvalidParamsException + */ + public function orWhere( + mixed $key, + string $operator = null, + mixed $value = null, + string $boolean = '|', + ): self { + if ($key instanceof Closure) { + return $this->whereNested($key, $boolean); + } + + if (is_array($key)) { + return $this->addArrayOfWheres($key, $boolean); + } + + [$value, $operator] = $this->prepareValueAndOperator( + $value, + $operator, + func_num_args() === 2, + ); + + return $this->where($key, $operator, $value, $boolean); + } + + /** + * Add an array of where clauses to the query. + * + * @throws ReflectionException + * @throws InvalidParamsException + */ + protected function addArrayOfWheres( + array $arrayOfWheres, + string $boolean, + string $method = 'where', + ): self { + return $this->whereNested(function (Builder $query) use ( + $arrayOfWheres, + $method, + $boolean + ): void { + foreach ($arrayOfWheres as $key => $value) { + if (is_numeric($key) && is_array($value)) { + $query->$method(...array_values($value)); + } else { + $query->$method($key, '=', $value, $boolean); + } + } + }, $boolean); + } +} diff --git a/src/Traits/HasWhereBetween.php b/src/Traits/HasWhereBetween.php new file mode 100644 index 0000000..df1d604 --- /dev/null +++ b/src/Traits/HasWhereBetween.php @@ -0,0 +1,111 @@ +dates)->has($key) && $this->dates[$key] === 'date') { + $first = $this->castDate($first); + $second = $this->castDate($second); + } + + $this->whereNested(function (Builder $query) use ( + $key, + $first, + $second, + $withBoundaries, + ): void { + $operator = ($withBoundaries ? '=' : ''); + $query->where($key, '>' . $operator, $first)->where($key, '<' . $operator, $second); + }, $boolean); + + return $this; + } + + /** + * Add a or where between statement to the query. + * + * @throws ReflectionException + * @throws InvalidParamsException + * @throws JsonException + */ + public function orWhereBetween( + string $key, + mixed $first, + mixed $second, + bool $withBoundaries = true, + string $boolean = '|', + ): self { + return $this->whereBetween($key, $first, $second, $withBoundaries, $boolean); + } + + /** + * Add a where not between statement to the query. + * + * @throws ReflectionException + * @throws InvalidParamsException + * @throws JsonException + */ + public function whereNotBetween( + string $key, + mixed $first, + mixed $second, + bool $withBoundaries = false, + string $boolean = '&', + ): self { + if (collect($this->dates)->has($key) && $this->dates[$key] === 'date') { + $first = $this->castDate($first); + $second = $this->castDate($second); + } + + $this->whereNested(function (Builder $query) use ( + $key, + $first, + $second, + $withBoundaries, + ): void { + $operator = ($withBoundaries ? '=' : ''); + $query->where($key, '<' . $operator, $first)->orWhere($key, '>' . $operator, $second); + }, $boolean); + + return $this; + } + + /** + * Add a or where not between statement to the query. + * + * @throws ReflectionException + * @throws InvalidParamsException + * @throws JsonException + */ + public function orWhereNotBetween( + string $key, + mixed $first, + mixed $second, + bool $withBoundaries = false, + string $boolean = '|', + ): self { + return $this->whereNotBetween($key, $first, $second, $withBoundaries, $boolean); + } +} diff --git a/src/Traits/HasWhereDate.php b/src/Traits/HasWhereDate.php new file mode 100644 index 0000000..9e6bd8a --- /dev/null +++ b/src/Traits/HasWhereDate.php @@ -0,0 +1,164 @@ +prepareValueAndOperator( + $value, + $operator, + func_num_args() === 2, + ); + + $start = Carbon::parse($value)->startOfDay()->timestamp; + $end = Carbon::parse($value)->endOfDay()->timestamp; + + return match ($operator) { + '>' => $this->whereDateGreaterThan($key, $operator, $value, $boolean), + '>=' => $this->whereDateGreaterThanOrEquals($key, $operator, $value, $boolean), + '<' => $this->whereDateLowerThan($key, $operator, $value, $boolean), + '<=' => $this->whereDateLowerThanOrEquals($key, $operator, $value, $boolean), + '!=' => $this->whereNotBetween($key, $start, $end, false, $boolean), + default => $this->whereBetween($key, $start, $end, true, $boolean), + }; + } + + /** + * @throws JsonException + * @throws ReflectionException + * @throws InvalidParamsException + */ + private function whereDateGreaterThan(string $key, mixed $operator, mixed $value, string $boolean): self + { + if (is_string($value) || $value instanceof DateTimeInterface) { + $value = Carbon::parse($value)->addDay()->startOfDay()->timestamp; + } + + return $this->where($key, $operator, $value, $boolean); + } + + /** + * @throws JsonException + * @throws ReflectionException + * @throws InvalidParamsException + */ + private function whereDateGreaterThanOrEquals(string $key, mixed $operator, mixed $value, string $boolean): self + { + if (is_string($value) || $value instanceof DateTimeInterface) { + $value = Carbon::parse($value)->startOfDay()->timestamp; + } + + return $this->where($key, $operator, $value, $boolean); + } + + /** + * @throws JsonException + * @throws ReflectionException + * @throws InvalidParamsException + */ + private function whereDateLowerThan(string $key, mixed $operator, mixed $value, string $boolean): self + { + if (is_string($value) || $value instanceof DateTimeInterface) { + $value = Carbon::parse($value)->subDay()->endOfDay()->timestamp; + } + + return $this->where($key, $operator, $value, $boolean); + } + + /** + * @throws JsonException + * @throws ReflectionException + * @throws InvalidParamsException + */ + private function whereDateLowerThanOrEquals(string $key, mixed $operator, mixed $value, string $boolean): self + { + if (is_string($value) || $value instanceof DateTimeInterface) { + $value = Carbon::parse($value)->endOfDay()->timestamp; + } + + return $this->where($key, $operator, $value, $boolean); + } + + /** + * Add an "or where date" statement to the query. + * + * @throws ReflectionException + * @throws JsonException + * @throws InvalidParamsException + */ + public function orWhereDate(string $key, mixed $operator, mixed $value = null, string $boolean = '|'): self + { + return $this->whereDate($key, $operator, $value, $boolean); + } + + /** + * Add a "where year" statement to the query. + * + * @throws ReflectionException + * @throws JsonException + * @throws InvalidParamsException + */ + public function whereYear(string $key, mixed $operator, mixed $value = null, string $boolean = '&'): self + { + [$value, $operator] = $this->prepareValueAndOperator( + $value, + $operator, + func_num_args() === 2, + ); + + $value = Carbon::now()->setYear($value)->startOfYear(); + + if ($operator === '=') { + $start = $value->clone()->startOfYear()->timestamp; + $end = $value->clone()->endOfYear()->timestamp; + + return $this->whereBetween($key, $start, $end, true, $boolean); + } + + if ($operator === '>' || $operator === '<=') { + $value = $value->clone()->endOfYear()->timestamp; + } elseif ($operator === '>=' || $operator === '<') { + $value = $value->clone()->startOfYear()->timestamp; + } + + return $this->where($key, $operator, $value, $boolean); + } + + /** + * Add an "or where year" statement to the query. + * + * @throws ReflectionException + * @throws JsonException + * @throws InvalidParamsException + */ + public function orWhereYear(string $key, mixed $operator, mixed $value = null, string $boolean = '|'): self + { + [$value, $operator] = $this->prepareValueAndOperator( + $value, + $operator, + func_num_args() === 2, + ); + + return $this->whereYear($key, $operator, $value, $boolean); + } +} diff --git a/src/Traits/HasWhereHas.php b/src/Traits/HasWhereHas.php new file mode 100644 index 0000000..1192641 --- /dev/null +++ b/src/Traits/HasWhereHas.php @@ -0,0 +1,93 @@ +query->get('where', new Collection()); + + $currentWhere = $where; + + $where->push(($currentWhere->count() ? $boolean . ' ' : '') . $relationship . ' != null'); + + $this->query->put('where', $where); + + return $this; + } + + /** + * Add an "or where has" statement to the query. + */ + public function orWhereHas(string $relationship, string $boolean = '|'): self + { + return $this->whereHas($relationship, $boolean); + } + + /** + * Add a "where has not" statement to the query. + */ + public function whereHasNot(string $relationship, string $boolean = '&'): self + { + $where = $this->query->get('where', new Collection()); + + $currentWhere = $where; + + $where->push(($currentWhere->count() ? $boolean . ' ' : '') . $relationship . ' = null'); + + $this->query->put('where', $where); + + return $this; + } + + /** + * Add a "where has not" statement to the query. + */ + public function orWhereHasNot(string $relationship, string $boolean = '|'): self + { + return $this->whereHasNot($relationship, $boolean); + } + + /** + * Add a "where null" clause to the query. + */ + public function whereNull(string $key, string $boolean = '&'): self + { + return $this->whereHasNot($key, $boolean); + } + + /** + * Add an "or where null" clause to the query. + */ + public function orWhereNull(string $key, string $boolean = '|'): self + { + return $this->whereNull($key, $boolean); + } + + /** + * Add a "where not null" clause to the query. + */ + public function whereNotNull(string $key, string $boolean = '&'): self + { + return $this->whereHas($key, $boolean); + } + + /** + * Add an "or where not null" clause to the query. + */ + public function orWhereNotNull(string $key, string $boolean = '|'): self + { + return $this->whereNotNull($key, $boolean); + } +} diff --git a/src/Traits/HasWhereIn.php b/src/Traits/HasWhereIn.php new file mode 100644 index 0000000..3768f85 --- /dev/null +++ b/src/Traits/HasWhereIn.php @@ -0,0 +1,196 @@ +query->get('where', new Collection()); + + $valuesString = collect($values)->map(fn (mixed $value) => !is_numeric($value) ? '"' . $value . '"' : $value)->implode(','); + + $where->push(($where->count() ? $boolean . ' ' : '') . $key . ' ' . $operator . ' ' . $prefix . $valuesString . $suffix); + + $this->query->put('where', $where); + + return $this; + } + + /** + * Add an "or where in" clause to the query. + */ + public function orWhereIn( + string $key, + array $value, + string $boolean = '|', + string $operator = '=', + string $prefix = '(', + string $suffix = ')', + ): self { + return $this->whereIn($key, $value, $boolean, $operator, $prefix, $suffix); + } + + /** + * Add a "where in all" clause to the query. + */ + public function whereInAll( + string $key, + array $values, + string $boolean = '&', + string $operator = '=', + string $prefix = '[', + string $suffix = ']', + ): self { + return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); + } + + /** + * Add an "or where in all" clause to the query. + */ + public function orWhereInAll( + string $key, + array $values, + string $boolean = '|', + string $operator = '=', + string $prefix = '[', + string $suffix = ']', + ): self { + return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); + } + + /** + * Add a "where in exact" clause to the query. + */ + public function whereInExact( + string $key, + array $values, + string $boolean = '&', + string $operator = '=', + string $prefix = '{', + string $suffix = '}', + ): self { + return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); + } + + /** + * Add an "or where in exact" clause to the query. + */ + public function orWhereInExact( + string $key, + array $values, + string $boolean = '|', + string $operator = '=', + string $prefix = '{', + string $suffix = '}', + ): self { + return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); + } + + /** + * Add a "where not in" clause to the query. + */ + public function whereNotIn( + string $key, + array $values, + string $boolean = '&', + string $operator = '!=', + string $prefix = '(', + string $suffix = ')', + ): self { + return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); + } + + /** + * Add an "or where not in" clause to the query. + */ + public function orWhereNotIn( + string $key, + array $values, + string $boolean = '|', + string $operator = '!=', + string $prefix = '(', + string $suffix = ')', + ): self { + return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); + } + + /** + * Add a "where not in all" clause to the query. + */ + public function whereNotInAll( + string $key, + array $values, + string $boolean = '&', + string $operator = '!=', + string $prefix = '[', + string $suffix = ']', + ): self { + return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); + } + + /** + * Add an "or where not in all" clause to the query. + */ + public function orWhereNotInAll( + string $key, + array $values, + string $boolean = '|', + string $operator = '!=', + string $prefix = '[', + string $suffix = ']', + ): self { + return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); + } + + /** + * Add a "where not in exact" clause to the query. + */ + public function whereNotInExact( + string $key, + array $values, + string $boolean = '&', + string $operator = '!=', + string $prefix = '{', + string $suffix = '}', + ): self { + return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); + } + + /** + * Add an "or where not in exact" clause to the query. + */ + public function orWhereNotInExact( + string $key, + array $values, + string $boolean = '|', + string $operator = '!=', + string $prefix = '{', + string $suffix = '}', + ): self { + return $this->whereIn($key, $values, $boolean, $operator, $prefix, $suffix); + } +} diff --git a/src/Traits/HasWhereLike.php b/src/Traits/HasWhereLike.php new file mode 100644 index 0000000..2741777 --- /dev/null +++ b/src/Traits/HasWhereLike.php @@ -0,0 +1,116 @@ +query->get('where', new Collection()); + + $clause = $this->generateWhereLikeClause($key, $value, $caseSensitive, '=', '~'); + + $where->push(($where->count() ? $boolean . ' ' : '') . $clause); + + $this->query->put('where', $where); + + return $this; + } + + /** + * Add an "or where like" clause to the query. + * + * @throws JsonException + */ + public function orWhereLike( + string $key, + string $value, + bool $caseSensitive = true, + string $boolean = '|', + ): self { + return $this->whereLike($key, $value, $caseSensitive, $boolean); + } + + /** + * Add a "where not like" clause to the query. + * + * @throws JsonException + */ + public function whereNotLike( + string $key, + string $value, + bool $caseSensitive = true, + string $boolean = '&', + ): self { + $where = $this->query->get('where', new Collection()); + + $clause = $this->generateWhereLikeClause($key, $value, $caseSensitive, '!=', '!~'); + + $where->push(($where->count() ? $boolean . ' ' : '') . $clause); + + $this->query->put('where', $where); + + return $this; + } + + /** + * Add an "or where not like" clause to the query. + * + * @throws JsonException + */ + public function orWhereNotLike( + string $key, + string $value, + bool $caseSensitive = true, + string $boolean = '|', + ): self { + return $this->whereNotLike($key, $value, $caseSensitive, $boolean); + } + + /** + * @throws JsonException + */ + private function generateWhereLikeClause( + string $key, + string $value, + bool $caseSensitive, + string $operator, + string $insensitiveOperator, + ): string { + $hasPrefix = Str::startsWith($value, ['%', '*']); + $hasSuffix = Str::endsWith($value, ['%', '*']); + + if ($hasPrefix) { + $value = substr($value, 1); + } + if ($hasSuffix) { + $value = substr($value, 0, -1); + } + + $operator = $caseSensitive ? $operator : $insensitiveOperator; + $prefix = $hasPrefix || !$hasSuffix ? '*' : ''; + $suffix = $hasSuffix || !$hasPrefix ? '*' : ''; + $value = json_encode($value, JSON_THROW_ON_ERROR); + $value = Str::start(Str::finish($value, $suffix), $prefix); + + return implode(' ', [$key, $operator, $value]); + } +} diff --git a/src/Traits/Operators.php b/src/Traits/Operators.php index 856d442..bbe8e5f 100644 --- a/src/Traits/Operators.php +++ b/src/Traits/Operators.php @@ -4,6 +4,9 @@ namespace MarcReichel\IGDBLaravel\Traits; +/** + * @internal + */ trait Operators { /** diff --git a/src/Traits/ValuePreparer.php b/src/Traits/ValuePreparer.php new file mode 100644 index 0000000..9056854 --- /dev/null +++ b/src/Traits/ValuePreparer.php @@ -0,0 +1,46 @@ +invalidOperatorAndValue($operator, $value)) { + throw new InvalidArgumentException('Illegal operator and value combination.'); + } + + return [$value, $operator]; + } + + /** + * Determine if the given operator and value combination is legal. + * + * Prevents using Null values with invalid operators. + */ + private function invalidOperatorAndValue(?string $operator, mixed $value): bool + { + return null === $value && in_array($operator, $this->operators, true) && !in_array($operator, ['=', '!=']); + } +}