Skip to content

Commit ea3ba65

Browse files
committed
chore: Cleanup bulk sort and move to service for reuse
1 parent 8b82c3b commit ea3ba65

File tree

7 files changed

+146
-48
lines changed

7 files changed

+146
-48
lines changed

app/Facades/SortFacade.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace App\Facades;
4+
5+
use Illuminate\Support\Facades\Facade;
6+
7+
/**
8+
* @method static null bulkSortGroupChannels(\App\Models\Group $record, string $order = 'ASC')
9+
* @method static null bulkRecountGroupChannels(\App\Models\Group $record, int $start = 1)
10+
*/
11+
class SortFacade extends Facade
12+
{
13+
protected static function getFacadeAccessor()
14+
{
15+
return 'sort';
16+
}
17+
}

app/Filament/Resources/Groups/GroupResource.php

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Filament\Resources\Groups;
44

5+
use App\Facades\SortFacade;
56
use App\Filament\Resources\Groups\Pages\ListGroups;
67
use App\Filament\Resources\Groups\Pages\ViewGroup;
78
use App\Filament\Resources\Groups\RelationManagers\ChannelsRelationManager;
@@ -249,10 +250,7 @@ public static function table(Table $table): Table
249250
])
250251
->action(function (Group $record, array $data): void {
251252
$start = (int) $data['start'];
252-
$channels = $record->channels()->orderBy('sort')->cursor();
253-
foreach ($channels as $channel) {
254-
$channel->update(['channel' => $start++]);
255-
}
253+
SortFacade::bulkRecountGroupChannels($record, $start);
256254
})
257255
->after(function () {
258256
Notification::make()
@@ -280,13 +278,7 @@ public static function table(Table $table): Table
280278
->action(function (Group $record, array $data): void {
281279
// Sort by title_custom (if present) then title, matching the UI column sort
282280
$order = $data['sort'] ?? 'ASC';
283-
$channels = $record->channels()
284-
->orderByRaw("COALESCE(title_custom, title) $order")
285-
->cursor();
286-
$sort = 1;
287-
foreach ($channels as $channel) {
288-
$channel->update(['sort' => $sort++]);
289-
}
281+
SortFacade::bulkSortGroupChannels($record, $order);
290282
})
291283
->after(function () {
292284
Notification::make()

app/Filament/Resources/Groups/Pages/ViewGroup.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Filament\Resources\Groups\Pages;
44

5+
use App\Facades\SortFacade;
56
use App\Filament\Resources\Groups\GroupResource;
67
use App\Models\CustomPlaylist;
78
use App\Models\Group;
@@ -123,12 +124,10 @@ protected function getHeaderActions(): array
123124
])
124125
->action(function (Group $record, array $data): void {
125126
$start = (int) $data['start'];
126-
$channels = $record->channels()->orderBy('sort')->cursor();
127-
foreach ($channels as $channel) {
128-
$channel->update(['channel' => $start++]);
129-
}
127+
SortFacade::bulkRecountGroupChannels($record, $start);
130128
})
131-
->after(function () {
129+
->after(function ($livewire) {
130+
$livewire->dispatch('refreshRelation');
132131
Notification::make()
133132
->success()
134133
->title('Channels Recounted')
@@ -154,15 +153,10 @@ protected function getHeaderActions(): array
154153
->action(function (Group $record, array $data): void {
155154
// Sort by title_custom (if present) then title, matching the UI column sort
156155
$order = $data['sort'] ?? 'ASC';
157-
$channels = $record->channels()
158-
->orderByRaw("COALESCE(title_custom, title) $order")
159-
->cursor();
160-
$sort = 1;
161-
foreach ($channels as $channel) {
162-
$channel->update(['sort' => $sort++]);
163-
}
156+
SortFacade::bulkSortGroupChannels($record, $order);
164157
})
165-
->after(function () {
158+
->after(function ($livewire) {
159+
$livewire->dispatch('refreshRelation');
166160
Notification::make()
167161
->success()
168162
->title('Channels Sorted')

app/Filament/Resources/VodGroups/Pages/ViewVodGroup.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Filament\Resources\VodGroups\Pages;
44

5+
use App\Facades\SortFacade;
56
use App\Filament\Resources\VodGroups\VodGroupResource;
67
use App\Models\CustomPlaylist;
78
use App\Models\Group;
@@ -123,12 +124,10 @@ protected function getHeaderActions(): array
123124
])
124125
->action(function (Group $record, array $data): void {
125126
$start = (int) $data['start'];
126-
$channels = $record->channels()->orderBy('sort')->cursor();
127-
foreach ($channels as $channel) {
128-
$channel->update(['channel' => $start++]);
129-
}
127+
SortFacade::bulkRecountGroupChannels($record, $start);
130128
})
131-
->after(function () {
129+
->after(function ($livewire) {
130+
$livewire->dispatch('refreshRelation');
132131
Notification::make()
133132
->success()
134133
->title('Channels Recounted')
@@ -154,15 +153,10 @@ protected function getHeaderActions(): array
154153
->action(function (Group $record, array $data): void {
155154
// Sort by title_custom (if present) then title, matching the UI column sort
156155
$order = $data['sort'] ?? 'ASC';
157-
$channels = $record->channels()
158-
->orderByRaw("COALESCE(title_custom, title) $order")
159-
->get();
160-
$sort = 1;
161-
foreach ($channels as $channel) {
162-
$channel->update(['sort' => $sort++]);
163-
}
156+
SortFacade::bulkSortGroupChannels($record, $order);
164157
})
165-
->after(function () {
158+
->after(function ($livewire) {
159+
$livewire->dispatch('refreshRelation');
166160
Notification::make()
167161
->success()
168162
->title('Channels Sorted')

app/Filament/Resources/VodGroups/VodGroupResource.php

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Filament\Resources\VodGroups;
44

5+
use App\Facades\SortFacade;
56
use App\Filament\Resources\Playlists\PlaylistResource;
67
use App\Filament\Resources\VodGroups\Pages\ListVodGroups;
78
use App\Filament\Resources\VodGroups\Pages\ViewVodGroup;
@@ -249,10 +250,7 @@ public static function table(Table $table): Table
249250
])
250251
->action(function (Group $record, array $data): void {
251252
$start = (int) $data['start'];
252-
$channels = $record->channels()->orderBy('sort')->cursor();
253-
foreach ($channels as $channel) {
254-
$channel->update(['channel' => $start++]);
255-
}
253+
SortFacade::bulkRecountGroupChannels($record, $start);
256254
})
257255
->after(function () {
258256
Notification::make()
@@ -280,13 +278,7 @@ public static function table(Table $table): Table
280278
->action(function (Group $record, array $data): void {
281279
// Sort by title_custom (if present) then title, matching the UI column sort
282280
$order = $data['sort'] ?? 'ASC';
283-
$channels = $record->channels()
284-
->orderByRaw("COALESCE(title_custom, title) $order")
285-
->cursor();
286-
$sort = 1;
287-
foreach ($channels as $channel) {
288-
$channel->update(['sort' => $sort++]);
289-
}
281+
SortFacade::bulkSortGroupChannels($record, $order);
290282
})
291283
->after(function () {
292284
Notification::make()

app/Providers/AppServiceProvider.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use App\Services\GitInfoService;
2727
use App\Services\PlaylistService;
2828
use App\Services\ProxyService;
29+
use App\Services\SortService;
2930
use App\Settings\GeneralSettings;
3031
use Dedoc\Scramble\Scramble;
3132
use Dedoc\Scramble\Support\Generator\OpenApi;
@@ -637,6 +638,11 @@ public function setupServices(): void
637638
$this->app->singleton('playlist', function () {
638639
return new PlaylistService;
639640
});
641+
642+
// Register the sort service
643+
$this->app->singleton('sort', function () {
644+
return new SortService;
645+
});
640646
}
641647

642648
/**

app/Services/SortService.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use App\Models\Group;
6+
use Illuminate\Support\Facades\DB;
7+
8+
class SortService
9+
{
10+
/**
11+
* Bulk-update channels' sort order using DB window functions when available,
12+
* falling back to a single CASE-based UPDATE to avoid N queries.
13+
*/
14+
public function bulkSortGroupChannels(Group $record, string $order = 'ASC'): void
15+
{
16+
$direction = strtoupper($order) === 'DESC' ? 'DESC' : 'ASC';
17+
$driver = DB::getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
18+
19+
// MySQL (8+)
20+
if ($driver === 'mysql') {
21+
DB::statement("UPDATE channels c JOIN (SELECT id, ROW_NUMBER() OVER (ORDER BY COALESCE(title_custom, title) {$direction}) AS rn FROM channels WHERE group_id = ?) t ON c.id = t.id SET c.sort = t.rn", [$record->id]);
22+
23+
return;
24+
}
25+
26+
// Postgres
27+
if (str_starts_with($driver, 'pgsql') || $driver === 'postgresql' || $driver === 'postgres') {
28+
DB::statement("UPDATE channels SET sort = t.rn FROM (SELECT id, ROW_NUMBER() OVER (ORDER BY COALESCE(title_custom, title) {$direction}) AS rn FROM channels WHERE group_id = ?) t WHERE channels.id = t.id", [$record->id]);
29+
30+
return;
31+
}
32+
33+
// SQLite
34+
if ($driver === 'sqlite') {
35+
DB::statement("WITH ranked AS (SELECT id, ROW_NUMBER() OVER (ORDER BY COALESCE(title_custom, title) {$direction}) AS rn FROM channels WHERE group_id = ?) UPDATE channels SET sort = (SELECT rn FROM ranked WHERE ranked.id = channels.id) WHERE group_id = ?", [$record->id, $record->id]);
36+
37+
return;
38+
}
39+
40+
// Fallback: single CASE update
41+
$ids = $record->channels()->orderByRaw("COALESCE(title_custom, title) {$order}")->pluck('id')->all();
42+
if (empty($ids)) {
43+
return;
44+
}
45+
46+
$cases = [];
47+
$i = 1;
48+
foreach ($ids as $id) {
49+
$cases[] = "WHEN {$id} THEN {$i}";
50+
$i++;
51+
}
52+
53+
$casesSql = implode(' ', $cases);
54+
$idsSql = implode(',', $ids);
55+
56+
DB::statement("UPDATE channels SET sort = CASE id {$casesSql} END WHERE id IN ({$idsSql})");
57+
}
58+
59+
/**
60+
* Bulk recount channel numbers.
61+
*/
62+
public function bulkRecountGroupChannels(Group $record, int $start = 1): void
63+
{
64+
$offset = max(0, $start - 1);
65+
$driver = DB::getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
66+
67+
if ($driver === 'mysql') {
68+
DB::statement('UPDATE channels c JOIN (SELECT id, ROW_NUMBER() OVER (ORDER BY sort) AS rn FROM channels WHERE group_id = ?) t ON c.id = t.id SET c.channel = t.rn + ?', [$record->id, $offset]);
69+
70+
return;
71+
}
72+
73+
if (str_starts_with($driver, 'pgsql') || $driver === 'postgresql' || $driver === 'postgres') {
74+
DB::statement('UPDATE channels SET channel = t.rn + ? FROM (SELECT id, ROW_NUMBER() OVER (ORDER BY sort) AS rn FROM channels WHERE group_id = ?) t WHERE channels.id = t.id', [$offset, $record->id]);
75+
76+
return;
77+
}
78+
79+
if ($driver === 'sqlite') {
80+
DB::statement('WITH ranked AS (SELECT id, ROW_NUMBER() OVER (ORDER BY sort) AS rn FROM channels WHERE group_id = ?) UPDATE channels SET channel = (SELECT rn FROM ranked WHERE ranked.id = channels.id) + ? WHERE group_id = ?', [$record->id, $offset, $record->id]);
81+
82+
return;
83+
}
84+
85+
// Fallback: CASE update
86+
$ids = $record->channels()->orderBy('sort')->pluck('id')->all();
87+
if (empty($ids)) {
88+
return;
89+
}
90+
91+
$cases = [];
92+
$i = $start;
93+
foreach ($ids as $id) {
94+
$cases[] = "WHEN {$id} THEN {$i}";
95+
$i++;
96+
}
97+
98+
$casesSql = implode(' ', $cases);
99+
$idsSql = implode(',', $ids);
100+
101+
DB::statement("UPDATE channels SET channel = CASE id {$casesSql} END WHERE id IN ({$idsSql})");
102+
}
103+
}

0 commit comments

Comments
 (0)