Merge branch '3.x' into 4.x

This commit is contained in:
Dan Harrin
2025-01-22 08:54:45 +00:00
3 changed files with 133 additions and 3 deletions
@@ -5,6 +5,7 @@ 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\Expression;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
@@ -91,15 +92,32 @@ class RelationshipJoiner
/** @phpstan-ignore-next-line */
foreach (($relationshipQuery->getQuery()->orders ?? []) as $order) {
if (! array_key_exists('column', $order)) {
// Regular orders: { column: string, direction: 'asc' | 'desc' }
// Sub-query orders: { column: Illuminate\Database\Query\Expression, direction: 'asc' | 'desc' }
// Raw orders: { type: 'Raw', sql: string }
if (! array_key_exists('column', $order) && ! array_key_exists('sql', $order)) {
continue;
}
if (str($order['column'])->startsWith("{$relationshipQuery->getModel()->getTable()}.")) {
$columnValue = $order['column'] ?? new Expression($order['sql']);
if (
$columnValue instanceof Expression
&& str($columnValue->getValue($relationship->getGrammar()))->contains('?')
) {
// Heuristic to determine if the expression contains (a) binding(s), if so, as of
// yet we cannot reliably determine (which) bindings are used in the expression.
continue;
}
$relationshipQuery->addSelect($order['column']);
if (
str($columnValue instanceof Expression ? $columnValue->getValue($relationship->getGrammar()) : $columnValue)
->startsWith("{$relationshipQuery->getModel()->getTable()}.")
) {
continue;
}
$relationshipQuery->addSelect($columnValue);
}
}
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('team_user', function (Blueprint $table): void {
$table->id();
$table->foreignId('team_id')->constrained();
$table->foreignId('user_id')->constrained();
$table->string('role')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('team_user');
}
};
@@ -0,0 +1,88 @@
<?php
use Filament\Support\Services\RelationshipJoiner;
use Filament\Tests\Models\Team;
use Filament\Tests\Models\User;
use Filament\Tests\TestCase;
use Illuminate\Database\Query\Expression;
uses(TestCase::class);
it('can prepare query for no constraints for a BelongsToMany relationship', function () {
$user = User::factory()->create();
expect($user->teams()->toBase())
->distinct->toBeFalse()
->getColumns()->toBe([])
->orders->toBeNull();
$preparedQuery = app(RelationshipJoiner::class)->prepareQueryForNoConstraints($user->teams());
expect($preparedQuery->toBase())
->distinct->toBeTrue()
->getColumns()->toBe(['teams.*'])
->orders->toBeNull();
$preparedQuery = app(RelationshipJoiner::class)->prepareQueryForNoConstraints(
$user
->teams()
->orderBy('id')
->orderBy((new Team)->qualifyColumn('name'))
->orderBy('team_user.role')
);
expect($preparedQuery->toBase())
->distinct->toBeTrue()
->getColumns()->toBe([
(new Team)->qualifyColumn('*'), // Default select...
'id', // Select without a qualified table also included just to be sure...
// Select for `team.name` not included as that is already included in the `team.*`...
'team_user.role', // Select for a qualitified other table included...
])
->orders->toBe([
[
'column' => 'id',
'direction' => 'asc',
],
[
'column' => 'teams.name',
'direction' => 'asc',
],
[
'column' => 'team_user.role',
'direction' => 'asc',
],
]);
$preparedQuery = app(RelationshipJoiner::class)->prepareQueryForNoConstraints(
$user->teams()->orderByRaw("CASE WHEN role = 'admin' THEN 1 ELSE 2 END")
);
expect($preparedQuery->toBase())
->distinct->toBeTrue()
->getColumns()->toBe([
(new Team)->qualifyColumn('*'),
"CASE WHEN role = 'admin' THEN 1 ELSE 2 END", // Select added from `orderByRaw`...
])
->orders->toBe([
[
'type' => 'Raw',
'sql' => "CASE WHEN role = 'admin' THEN 1 ELSE 2 END",
],
]);
$preparedQuery = app(RelationshipJoiner::class)->prepareQueryForNoConstraints(
$user->teams()->orderBy(new Expression("CASE WHEN role = 'some_other_role' THEN 1 ELSE 2 END"))
);
expect($preparedQuery->toBase())
->distinct->toBeTrue()
->getColumns()->toBe([
(new Team)->qualifyColumn('*'),
"CASE WHEN role = 'some_other_role' THEN 1 ELSE 2 END", // Select added from `orderByRaw`...
])
->orders->toHaveCount(1)
->and($preparedQuery->toBase()->orders[0])
->column->getValue($user->teams()->getGrammar())->toBe("CASE WHEN role = 'some_other_role' THEN 1 ELSE 2 END")
->direction->toBe('asc');
});