src/RefineQuery.php
<?php
namespace Laragear\Refine;
use Illuminate\Contracts\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Foundation\Precognition;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Laragear\Refine\Contracts\ValidatesRefiner;
use ReflectionMethod;
use ReflectionObject;
use function app;
use function array_flip;
use function array_values;
use function get_class;
use function get_class_methods;
use function is_string;
/**
* @internal
*
* @phpstan-consistent-constructor
*/
class RefineQuery
{
/**
* The array of cached methods list for the Refiner classes.
*
* @var array<class-string,string[]>
*/
protected static array $cachedMethods = [];
/**
* The Refiner abstract class internal methods.
*
* @var string[]|null
*/
protected static ?array $uncallableBaseRefinerMethods = null;
/**
* Create a new refine query instance.
*/
public function __construct(
protected Builder|EloquentBuilder $builder,
protected Request $request,
protected Refiner $refiner
) {
//
}
/**
* Refine the database query using the HTTP Request query.
*
* @param string[]|null $keys
*/
public function match(array $keys = null): void
{
$this->refiner->runBefore($this->builder, $this->request);
if ($this->refiner instanceof ValidatesRefiner) {
$this->validateRefiner();
}
// Take only the query keys that are going to be matched and run them.
$this->queryValuesFromRequest($keys);
$this->refiner->runAfter($this->builder, $this->request);
}
/**
* Validate the refiner.
*/
protected function validateRefiner(): void
{
$validator = app(ValidationFactory::class)->make(
$this->request->query(),
$this->refiner->validationRules(),
$this->refiner->validationMessages(),
$this->refiner->validationCustomAttributes()
);
if ($this->request->isPrecognitive()) {
$validator->after(Precognition::afterValidationHook($this->request))
->setRules($this->request->filterPrecognitiveRules($validator->getRulesWithoutPlaceholders()));
}
$validator->validate();
}
/**
* Retrieve all the query values from the keys to look for.
*
* @param string[]|null $keys
*/
protected function queryValuesFromRequest(?array $keys): void
{
Collection::make($keys ?? $this->getKeysFromRefiner($this->request))
// Transforms all items to $method => $key
->mapWithKeys(static function (string $key): array {
return [Str::camel($key) => $key];
})
// Remove all keys that are not present in the request query.
->filter(function (string $key): bool {
// @phpstan-ignore-next-line
return ($placeholder = (object) []) !== $this->request->query($key, $placeholder);
})
// Add "obligatory" keys set by the refiner that will always run.
->merge($this->getObligatoryKeysFromRefiner())
// Keep all items which method is present in the refiner object.
->intersectByKeys(array_flip($this->getPublicMethodsFromRefiner()))
// Remove all items which method are part of the abstract refiner object.
->diffKeys(array_flip($this->getRefinerClassMethods()))
// Run each method using the query value that matches.
->each(function (string $key, string $method): void {
$this->refiner->{$method}($this->builder, $this->request->query($key), $this->request);
});
}
/**
* Retrieves the key to use from the Refiner instance.
*
* @return string[]
*/
protected function getKeysFromRefiner(Request $request): array
{
// Get the array of keys without taking into account the internal methods.
return array_values($this->refiner->getKeys($request));
}
/**
* Return the obligatory keys from the refiner.
*
* @return \Illuminate\Support\Collection<string, string>
*/
protected function getObligatoryKeysFromRefiner(): Collection
{
return Collection::make($this->refiner->getObligatoryKeys($this->request))
->mapWithKeys(static function (string $key): array {
return [Str::camel($key) => $key];
});
}
/**
* Return the methods already used for the Refiner class.
*
* @return string[]
*/
protected function getRefinerClassMethods(): array
{
return static::$uncallableBaseRefinerMethods ??= get_class_methods(Refiner::class);
}
/**
* Return the public methods from the refiner to be called.
*
* @return string[]
*/
protected function getPublicMethodsFromRefiner(): array
{
$class = get_class($this->refiner);
if (! isset(static::$cachedMethods[$class])) {
static::$cachedMethods[$class] = Collection::make(
(new ReflectionObject($this->refiner))->getMethods(ReflectionMethod::IS_PUBLIC)
)
->filter(static function (ReflectionMethod $method): bool {
return $method->isUserDefined()
&& ! $method->isStatic()
&& ! $method->isAbstract();
})
->map(static function (ReflectionMethod $method): string {
return $method->name;
})
->toArray();
}
return static::$cachedMethods[$class];
}
/**
* Flushes the cache of refiner methods.
*/
public static function flushCachedRefinerMethods(): void
{
static::$cachedMethods[] = [];
}
/**
* Create a new refine query instance.
*
* @param \Laragear\Refine\Refiner|class-string|string $refiner
* @param string[]|null $keys
*/
public static function refine(
Builder|EloquentBuilder $builder,
Refiner|string $refiner,
array $keys = null
): Builder|EloquentBuilder {
$instance = new static($builder, app('request'), is_string($refiner) ? app($refiner) : $refiner);
$instance->match($keys);
return $builder;
}
}