diff --git a/packages/forms/src/Components/CheckboxList.php b/packages/forms/src/Components/CheckboxList.php index 2b92451a4f..a89337f0e8 100644 --- a/packages/forms/src/Components/CheckboxList.php +++ b/packages/forms/src/Components/CheckboxList.php @@ -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, [ diff --git a/packages/forms/src/Components/Select.php b/packages/forms/src/Components/Select.php index ac97f17c0b..82d9c6ac03 100644 --- a/packages/forms/src/Components/Select.php +++ b/packages/forms/src/Components/Select.php @@ -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(); diff --git a/packages/support/src/Services/RelationshipJoiner.php b/packages/support/src/Services/RelationshipJoiner.php new file mode 100644 index 0000000000..a7ac50494c --- /dev/null +++ b/packages/support/src/Services/RelationshipJoiner.php @@ -0,0 +1,98 @@ +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 + */ + 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; + } +} diff --git a/packages/tables/src/Actions/AttachAction.php b/packages/tables/src/Actions/AttachAction.php index 0ee4781414..f3641663a1 100644 --- a/packages/tables/src/Actions/AttachAction.php +++ b/packages/tables/src/Actions/AttachAction.php @@ -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)); }) diff --git a/packages/tables/src/Concerns/CanSummarizeRecords.php b/packages/tables/src/Concerns/CanSummarizeRecords.php index 005da55ccb..9f5a9d09da 100644 --- a/packages/tables/src/Concerns/CanSummarizeRecords.php +++ b/packages/tables/src/Concerns/CanSummarizeRecords.php @@ -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; diff --git a/packages/tables/src/Filters/Concerns/HasRelationship.php b/packages/tables/src/Filters/Concerns/HasRelationship.php index c2fce0dea8..097f63a6c9 100644 --- a/packages/tables/src/Filters/Concerns/HasRelationship.php +++ b/packages/tables/src/Filters/Concerns/HasRelationship.php @@ -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, [ diff --git a/packages/tables/src/Support/RelationshipJoiner.php b/packages/tables/src/Support/RelationshipJoiner.php deleted file mode 100644 index 7f0da2d826..0000000000 --- a/packages/tables/src/Support/RelationshipJoiner.php +++ /dev/null @@ -1,44 +0,0 @@ -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 - */ - public function getLeftJoinsForRelationship(Builder $query, string $relationship): array - { - /** @phpstan-ignore-next-line */ - $query->leftJoinRelationship($relationship); - - return $query->toBase()->joins; - } -}