Merge pull request #8873 from cheesegrits/3.x-relationship-joiner

Handle pivot clauses in relationship joiner
This commit is contained in:
Dan Harrin
2023-10-06 09:19:28 +02:00
committed by GitHub
7 changed files with 112 additions and 203 deletions
+2 -18
View File
@@ -5,13 +5,13 @@ namespace Filament\Forms\Components;
use Closure;
use Filament\Forms\Components\Actions\Action;
use Filament\Support\Enums\ActionSize;
use Filament\Support\Services\RelationshipJoiner;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Str;
class CheckboxList extends Field implements Contracts\HasNestedRecursiveValidationRules
@@ -133,23 +133,7 @@ class CheckboxList extends Field implements Contracts\HasNestedRecursiveValidati
$this->options(static function (CheckboxList $component) use ($modifyQueryUsing): array {
$relationship = Relation::noConstraints(fn () => $component->getRelationship());
$relationshipQuery = $relationship->getQuery();
// By default, `BelongsToMany` relationships use an inner join to scope the results to only
// those that are attached in the pivot table. We need to change this to a left join so
// that we can still get results when the relationship is not attached to the record.
if ($relationship instanceof BelongsToMany) {
/** @var ?JoinClause $firstRelationshipJoinClause */
$firstRelationshipJoinClause = $relationshipQuery->getQuery()->joins[0] ?? null;
if ($firstRelationshipJoinClause) {
$firstRelationshipJoinClause->type = 'left';
}
$relationshipQuery
->distinct() // Ensure that results are unique when fetching options.
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relationshipQuery = (new RelationshipJoiner())->prepareQueryForNoConstraints($relationship);
if ($modifyQueryUsing) {
$relationshipQuery = $component->evaluate($modifyQueryUsing, [
+5 -69
View File
@@ -8,6 +8,7 @@ use Filament\Forms\ComponentContainer;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Form;
use Filament\Support\Concerns\HasExtraAlpineAttributes;
use Filament\Support\Services\RelationshipJoiner;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Connection;
@@ -18,7 +19,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Exists;
@@ -704,23 +704,7 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
$this->getSearchResultsUsing(static function (Select $component, ?string $search) use ($modifyQueryUsing): array {
$relationship = Relation::noConstraints(fn () => $component->getRelationship());
$relationshipQuery = $relationship->getQuery();
// By default, `BelongsToMany` relationships use an inner join to scope the results to only
// those that are attached in the pivot table. We need to change this to a left join so
// that we can still get results when the relationship is not attached to the record.
if ($relationship instanceof BelongsToMany) {
/** @var ?JoinClause $firstRelationshipJoinClause */
$firstRelationshipJoinClause = $relationshipQuery->getQuery()->joins[0] ?? null;
if ($firstRelationshipJoinClause) {
$firstRelationshipJoinClause->type = 'left';
}
$relationshipQuery
->distinct() // Ensure that results are unique when fetching options.
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relationshipQuery = (new RelationshipJoiner())->prepareQueryForNoConstraints($relationship);
if ($modifyQueryUsing) {
$relationshipQuery = $component->evaluate($modifyQueryUsing, [
@@ -786,23 +770,7 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
$relationship = Relation::noConstraints(fn () => $component->getRelationship());
$relationshipQuery = $relationship->getQuery();
// By default, `BelongsToMany` relationships use an inner join to scope the results to only
// those that are attached in the pivot table. We need to change this to a left join so
// that we can still get results when the relationship is not attached to the record.
if ($relationship instanceof BelongsToMany) {
/** @var ?JoinClause $firstRelationshipJoinClause */
$firstRelationshipJoinClause = $relationshipQuery->getQuery()->joins[0] ?? null;
if ($firstRelationshipJoinClause) {
$firstRelationshipJoinClause->type = 'left';
}
$relationshipQuery
->distinct() // Ensure that results are unique when fetching options.
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relationshipQuery = (new RelationshipJoiner())->prepareQueryForNoConstraints($relationship);
if ($modifyQueryUsing) {
$relationshipQuery = $component->evaluate($modifyQueryUsing, [
@@ -912,23 +880,7 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
$this->getSelectedRecordUsing(static function (Select $component, $state) use ($modifyQueryUsing): ?Model {
$relationship = Relation::noConstraints(fn () => $component->getRelationship());
$relationshipQuery = $relationship->getQuery();
// By default, `BelongsToMany` relationships use an inner join to scope the results to only
// those that are attached in the pivot table. We need to change this to a left join so
// that we can still get results when the relationship is not attached to the record.
if ($relationship instanceof BelongsToMany) {
/** @var ?JoinClause $firstRelationshipJoinClause */
$firstRelationshipJoinClause = $relationshipQuery->getQuery()->joins[0] ?? null;
if ($firstRelationshipJoinClause) {
$firstRelationshipJoinClause->type = 'left';
}
$relationshipQuery
->distinct() // Ensure that results are unique when fetching options.
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relationshipQuery = (new RelationshipJoiner())->prepareQueryForNoConstraints($relationship);
if ($relationship instanceof BelongsToMany) {
$relatedKeyName = $relationship->getRelatedKeyName();
@@ -952,23 +904,7 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
$this->getOptionLabelsUsing(static function (Select $component, array $values) use ($modifyQueryUsing): array {
$relationship = Relation::noConstraints(fn () => $component->getRelationship());
$relationshipQuery = $relationship->getQuery();
// By default, `BelongsToMany` relationships use an inner join to scope the results to only
// those that are attached in the pivot table. We need to change this to a left join so
// that we can still get results when the relationship is not attached to the record.
if ($relationship instanceof BelongsToMany) {
/** @var ?JoinClause $firstRelationshipJoinClause */
$firstRelationshipJoinClause = $relationshipQuery->getQuery()->joins[0] ?? null;
if ($firstRelationshipJoinClause) {
$firstRelationshipJoinClause->type = 'left';
}
$relationshipQuery
->distinct() // Ensure that results are unique when fetching options.
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relationshipQuery = (new RelationshipJoiner())->prepareQueryForNoConstraints($relationship);
if ($relationship instanceof BelongsToMany) {
$relatedKeyName = $relationship->getQualifiedRelatedKeyName();
@@ -0,0 +1,98 @@
<?php
namespace Filament\Support\Services;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Kirschbaum\PowerJoins\JoinsHelper;
use Kirschbaum\PowerJoins\PowerJoins;
class RelationshipJoiner
{
use PowerJoins;
public function leftJoinRelationship(Builder $query, string $relationship): Builder
{
if (str($relationship)->contains('.')) {
/** @phpstan-ignore-next-line */
$query->joinNestedRelationship(
$relationship,
callback: null,
joinType: JoinsHelper::$joinMethodsMap['leftJoin'] ?? 'leftJoin',
);
return $query;
}
/** @phpstan-ignore-next-line */
$query->joinRelationship(
$relationship,
callback: null,
joinType: 'leftJoin',
);
return $query;
}
/**
* @return array<JoinClause>
*/
public function getLeftJoinsForRelationship(Builder $query, string $relationship): array
{
/** @phpstan-ignore-next-line */
$query->leftJoinRelationship($relationship);
return $query->toBase()->joins;
}
public function prepareQueryForNoConstraints(Relation $relationship): Builder
{
$relationshipQuery = $relationship->getQuery();
// By default, `BelongsToMany` relationships use an inner join to scope the results to only
// those that are attached in the pivot table. We need to change this to a left join so
// that we can still get results when the relationship is not attached to the record.
if ($relationship instanceof BelongsToMany) {
/** @var ?JoinClause $firstRelationshipJoinClause */
$firstRelationshipJoinClause = $relationshipQuery->getQuery()->joins[0] ?? null;
if ($firstRelationshipJoinClause) {
$firstRelationshipJoinClause->type = 'left';
// Any "where" clauses that are scoped to the pivot table need to be moved to the join.
// It's expected that any scopes that don't apply to the pivot table do not have
// a `column` attribute set.
$relationshipQueryPivotWheres = Arr::where(
$relationshipQuery->getQuery()->wheres,
function (array $where) use ($relationship): bool {
if (! array_key_exists('column', $where)) {
return false;
}
return Str::startsWith($where['column'], "{$relationship->getTable()}.");
},
);
$firstRelationshipJoinClause->wheres = array_merge(
$firstRelationshipJoinClause->wheres,
$relationshipQueryPivotWheres,
);
$relationshipQuery->getQuery()->wheres = Arr::except(
$relationshipQuery->getQuery()->wheres,
array_keys($relationshipQueryPivotWheres),
);
}
$relationshipQuery
->distinct()
->select($relationshipQuery->getModel()->getTable() . '.*');
}
return $relationshipQuery;
}
}
+4 -52
View File
@@ -6,6 +6,7 @@ use Closure;
use Filament\Actions\Concerns\CanCustomizeProcess;
use Filament\Forms\Components\Select;
use Filament\Forms\Form;
use Filament\Support\Services\RelationshipJoiner;
use Filament\Tables\Table;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
@@ -13,7 +14,6 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
@@ -70,23 +70,7 @@ class AttachAction extends Action
/** @var BelongsToMany $relationship */
$relationship = Relation::noConstraints(fn () => $table->getRelationship());
$relationshipQuery = $relationship->getQuery();
// By default, `BelongsToMany` relationships use an inner join to scope the results to only
// those that are attached in the pivot table. We need to change this to a left join so
// that we can still get results when the relationship is not attached to the record.
if ($relationship instanceof BelongsToMany) {
/** @var ?JoinClause $firstRelationshipJoinClause */
$firstRelationshipJoinClause = $relationshipQuery->getQuery()->joins[0] ?? null;
if ($firstRelationshipJoinClause) {
$firstRelationshipJoinClause->type = 'left';
}
$relationshipQuery
->distinct() // Ensure that results are unique when fetching records to attach.
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relationshipQuery = (new RelationshipJoiner())->prepareQueryForNoConstraints($relationship);
$isMultiple = is_array($data['recordId']);
@@ -198,23 +182,7 @@ class AttachAction extends Action
/** @var BelongsToMany $relationship */
$relationship = Relation::noConstraints(fn () => $table->getRelationship());
$relationshipQuery = $relationship->getQuery();
// By default, `BelongsToMany` relationships use an inner join to scope the results to only
// those that are attached in the pivot table. We need to change this to a left join so
// that we can still get results when the relationship is not attached to the record.
if ($relationship instanceof BelongsToMany) {
/** @var ?JoinClause $firstRelationshipJoinClause */
$firstRelationshipJoinClause = $relationshipQuery->getQuery()->joins[0] ?? null;
if ($firstRelationshipJoinClause) {
$firstRelationshipJoinClause->type = 'left';
}
$relationshipQuery
->distinct() // Ensure that results are unique when fetching options.
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relationshipQuery = (new RelationshipJoiner())->prepareQueryForNoConstraints($relationship);
if ($this->modifyRecordSelectOptionsQueryUsing) {
$relationshipQuery = $this->evaluate($this->modifyRecordSelectOptionsQueryUsing, [
@@ -295,23 +263,7 @@ class AttachAction extends Action
->getOptionLabelUsing(function ($value) use ($table): string {
$relationship = Relation::noConstraints(fn () => $table->getRelationship());
$relationshipQuery = $relationship->getQuery();
// By default, `BelongsToMany` relationships use an inner join to scope the results to only
// those that are attached in the pivot table. We need to change this to a left join so
// that we can still get results when the relationship is not attached to the record.
if ($relationship instanceof BelongsToMany) {
/** @var ?JoinClause $firstRelationshipJoinClause */
$firstRelationshipJoinClause = $relationshipQuery->getQuery()->joins[0] ?? null;
if ($firstRelationshipJoinClause) {
$firstRelationshipJoinClause->type = 'left';
}
$relationshipQuery
->distinct() // Ensure that results are unique when fetching options.
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relationshipQuery = (new RelationshipJoiner())->prepareQueryForNoConstraints($relationship);
return $this->getRecordTitle($relationshipQuery->find($value));
})
@@ -3,7 +3,7 @@
namespace Filament\Tables\Concerns;
use Closure;
use Filament\Tables\Support\RelationshipJoiner;
use Filament\Support\Services\RelationshipJoiner;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Facades\DB;
@@ -3,10 +3,9 @@
namespace Filament\Tables\Filters\Concerns;
use Closure;
use Filament\Support\Services\RelationshipJoiner;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\JoinClause;
trait HasRelationship
{
@@ -79,23 +78,7 @@ trait HasRelationship
{
$relationship = Relation::noConstraints(fn () => $this->getRelationship());
$relationshipQuery = $relationship->getQuery();
// By default, `BelongsToMany` relationships use an inner join to scope the results to only
// those that are attached in the pivot table. We need to change this to a left join so
// that we can still get results when the relationship is not attached to the record.
if ($relationship instanceof BelongsToMany) {
/** @var ?JoinClause $firstRelationshipJoinClause */
$firstRelationshipJoinClause = $relationshipQuery->getQuery()->joins[0] ?? null;
if ($firstRelationshipJoinClause) {
$firstRelationshipJoinClause->type = 'left';
}
$relationshipQuery
->distinct() // Ensure that results are unique when fetching options and indicating.
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relationshipQuery = (new RelationshipJoiner())->prepareQueryForNoConstraints($relationship);
if ($this->getModifyRelationshipQueryUsing()) {
$relationshipQuery = $this->evaluate($this->modifyRelationshipQueryUsing, [
@@ -1,44 +0,0 @@
<?php
namespace Filament\Tables\Support;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Kirschbaum\PowerJoins\JoinsHelper;
class RelationshipJoiner
{
public function leftJoinRelationship(Builder $query, string $relationship): Builder
{
if (str($relationship)->contains('.')) {
/** @phpstan-ignore-next-line */
$query->joinNestedRelationship(
$relationship,
callback: null,
joinType: JoinsHelper::$joinMethodsMap['leftJoin'] ?? 'leftJoin',
);
return $query;
}
/** @phpstan-ignore-next-line */
$query->joinRelationship(
$relationship,
callback: null,
joinType: 'leftJoin',
);
return $query;
}
/**
* @return array<JoinClause>
*/
public function getLeftJoinsForRelationship(Builder $query, string $relationship): array
{
/** @phpstan-ignore-next-line */
$query->leftJoinRelationship($relationship);
return $query->toBase()->joins;
}
}