Skip to content

Commit

Permalink
support nulls in belongsTo
Browse files Browse the repository at this point in the history
  • Loading branch information
gpibarra committed Jan 21, 2025
1 parent d96c963 commit dcfb45c
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 39 deletions.
95 changes: 56 additions & 39 deletions src/Filters/FiltersBelongsTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Arr;

/**
Expand All @@ -17,67 +17,84 @@ class FiltersBelongsTo implements Filter
/** {@inheritdoc} */
public function __invoke(Builder $query, $value, string $property)
{
$values = array_values(Arr::wrap($value));
$values = $this->prepareValues($value);
$valuesWithoutNulls = $this->filterNullValues($values);
$withWhereNull = count($values) !== count($valuesWithoutNulls);

$propertyParts = collect(explode('.', $property));
$relation = $propertyParts->pop();
$relationParent = $propertyParts->implode('.');
$relatedModel = $this->getRelatedModel($query->getModel(), $relation, $relationParent);

$relatedCollection = $relatedModel->newCollection();
array_walk($values, fn ($v) => $relatedCollection->add(
tap($relatedModel->newInstance(), fn ($m) => $m->setAttribute($m->getKeyName(), $v))
));

if ($relatedCollection->isEmpty()) {
if (empty($valuesWithoutNulls) && ! $withWhereNull) {
return $query;
}

$propertyParts = collect(explode('.', $property));
$relation = $propertyParts->pop();
$relationParent = $propertyParts->implode('.');
if ($relationParent) {
$query->whereHas($relationParent, fn (Builder $q) => $q->whereBelongsTo($relatedCollection, $relation));
$relationObject = $this->getRelationModelFromRelationName(
$this->getModelFromRelationName($query->getModel(), $relationParent),
$relation
);
$query->whereHas($relationParent, function (Builder $q) use ($relationObject, $withWhereNull, $valuesWithoutNulls) {
$this->applyLastLevelWhere($q, $relationObject, $withWhereNull, $valuesWithoutNulls);
});
} else {
$query->whereBelongsTo($relatedCollection, $relation);
$relationObject = $this->getRelationModelFromRelationName($query->getModel(), $relation);
$this->applyLastLevelWhere($query, $relationObject, $withWhereNull, $valuesWithoutNulls);
}
}

protected function getRelatedModel(Model $modelQuery, string $relationName, string $relationParent): Model
protected function prepareValues($values): array
{
if ($relationParent) {
$modelParent = $this->getModelFromRelation($modelQuery, $relationParent);
} else {
$modelParent = $modelQuery;
}

$relatedModel = $this->getRelatedModelFromRelation($modelParent, $relationName);

return $relatedModel;
return array_values(Arr::wrap($values));
}

protected function getRelatedModelFromRelation(Model $model, string $relationName): ?Model
protected function filterNullValues(array $values): array
{
$relationObject = $model->$relationName();
if (! is_subclass_of($relationObject, Relation::class)) {
throw RelationNotFoundException::make($model, $relationName);
}

$relatedModel = $relationObject->getRelated();
return array_filter(
$values,
fn ($v) => ! in_array($v, [null, 0, 'null', '0'], true)
);
}

return $relatedModel;
protected function applyLastLevelWhere(Builder $query, BelongsTo $relation, bool $withWhereNull, array $values)
{
$relationColumn = $relation->getQualifiedForeignKeyName();
$query->where(function (Builder $q) use ($relationColumn, $withWhereNull, $values) {
if ($withWhereNull) {
$q->orWhereNull($relationColumn);
}
if (! empty($values)) {
$q->orWhereIn($relationColumn, $values);
}
});
}

protected function getModelFromRelation(Model $model, string $relation, int $level = 0): ?Model
protected function getModelFromRelationName(Model $model, string $relation, int $level = 0): Model
{
$relationParts = explode('.', $relation);
if (count($relationParts) == 1) {
return $this->getRelatedModelFromRelation($model, $relation);
$relationObject = $this->getRelationModelFromRelationName($model, $relation);

return $relationObject->getRelated();
}

$firstRelation = $relationParts[0];
$firstRelatedModel = $this->getRelatedModelFromRelation($model, $firstRelation);
if (! $firstRelatedModel) {
return null;
$firstRelationObject = $this->getRelationModelFromRelationName($model, $firstRelation);

// recursion
return $this->getModelFromRelationName(
$firstRelationObject->getRelated(),
implode('.', array_slice($relationParts, 1)),
$level + 1
);
}

protected function getRelationModelFromRelationName(Model $model, string $relationName): BelongsTo
{
$relationObject = $model->$relationName();
if (! $relationObject instanceof BelongsTo) {
throw RelationNotFoundException::make($model, $relationName);
}

return $this->getModelFromRelation($firstRelatedModel, implode('.', array_slice($relationParts, 1)), $level + 1);
return $relationObject;
}
}
26 changes: 26 additions & 0 deletions tests/FilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Spatie\QueryBuilder\Filters\Filter as FilterInterface;
use Spatie\QueryBuilder\Filters\FiltersExact;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\Tests\TestClasses\Models\NestedNullableRelatedModel;
use Spatie\QueryBuilder\Tests\TestClasses\Models\NestedRelatedModel;
use Spatie\QueryBuilder\Tests\TestClasses\Models\RelatedModel;
use Spatie\QueryBuilder\Tests\TestClasses\Models\TestModel;
Expand Down Expand Up @@ -296,6 +297,31 @@
expect($modelsResult)->toHaveCount(1);
});

it('can filter results by belongs to with null', function () {
$relatedModel = RelatedModel::create(['name' => 'John Related Doe', 'test_model_id' => 0]);
$nestedModel = NestedNullableRelatedModel::create(['name' => 'John Nested Doe', 'related_model_id' => $relatedModel->id]);
$nestedNullModel = NestedNullableRelatedModel::create(['name' => 'John Nested Doe']);

$modelsResult = createQueryFromFilterRequest(['relatedModel' => [null]], NestedNullableRelatedModel::class)
->allowedFilters(AllowedFilter::belongsTo('relatedModel'))
->get();

expect($modelsResult)->toHaveCount(1);

$modelsResult = createQueryFromFilterRequest(['relatedModel' => [0]], NestedNullableRelatedModel::class)
->allowedFilters(AllowedFilter::belongsTo('relatedModel'))
->get();

expect($modelsResult)->toHaveCount(1);


$modelsResult = createQueryFromFilterRequest(['relatedModel' => [$relatedModel->id, null]], NestedNullableRelatedModel::class)
->allowedFilters(AllowedFilter::belongsTo('relatedModel'))
->get();

expect($modelsResult)->toHaveCount(2);
});

it('can filter results by belongs to no match', function () {
$relatedModel = RelatedModel::create(['name' => 'John Related Doe', 'test_model_id' => 0]);
$nestedModel = NestedRelatedModel::create(['name' => 'John Nested Doe', 'related_model_id' => $relatedModel->id + 1]);
Expand Down
5 changes: 5 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ protected function setUpDatabase(Application $app)
$table->integer('related_model_id');
$table->string('name');
});
$app['db']->connection()->getSchemaBuilder()->create('nested_nullable_related_models', function (Blueprint $table) {
$table->increments('id');
$table->integer('related_model_id')->nullable();
$table->string('name');
});

$app['db']->connection()->getSchemaBuilder()->create('pivot_models', function (Blueprint $table) {
$table->increments('id');
Expand Down
18 changes: 18 additions & 0 deletions tests/TestClasses/Models/NestedNullableRelatedModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Spatie\QueryBuilder\Tests\TestClasses\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class NestedNullableRelatedModel extends Model
{
protected $guarded = [];

public $timestamps = false;

public function relatedModel(): BelongsTo
{
return $this->belongsTo(RelatedModel::class);
}
}

0 comments on commit dcfb45c

Please sign in to comment.