Summary
Summary adalah figure di bawah sebuah kolom: total, rata-rata, jumlah, atau rentang. Gunakan ketika kolom berisi angka yang kemungkinan besar akan dijumlahkan atau dibandingkan pengguna secara mental. Summary dihitung oleh database berdasarkan query yang sudah difilter, bukan dengan menjumlahkan row yang sedang terlihat di layar. Total yang berubah setiap kali berpindah halaman akan menjadi angka berbeda yang memakai label sama.
Contoh minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Orders\Tables;
use PandaPanel\Tables\Columns\NumberColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Summaries\Average;
use PandaPanel\Tables\Summaries\Sum;
use PandaPanel\Tables\TableSchema;
final class OrdersTable
{
public static function configure(TableSchema $table): TableSchema
{
return $table->columns([
TextColumn::make('reference')->searchable(),
NumberColumn::make('total')
->prefix('$')
->decimals(2)
->summarize([Sum::make(), Average::make()]),
]);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Tabel sekarang merender dua footer row di bawah kolom total: sum dan average dari semua order yang tersisa setelah search dan filter aktif diterapkan.
Mendeklarasikan summary
use PandaPanel\Tables\Columns\Column;
use PandaPanel\Tables\Summaries\Summarizer;
Column::summarize(array $summarizers): static // array<array-key, Summarizer>
Column::getSummarizers(): array // list<Summarizer>
Column::hasSummaries(): bool
Column::summaryColumn(): string // aggregateAttribute() ?? getSortColumn()
Column::summaryUsesAggregate(): bool2
3
4
5
6
7
8
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Tables\Group;
use PandaPanel\Tables\TableSchema;
TableSchema::hasSummaries(): bool
TableSchema::summaries(Builder $query, array $records): array
TableSchema::groupSummaries(Builder $query, array $records, Group $group): array2
3
4
5
6
7
Tabel tanpa summarizer tidak membayar biaya tambahan: hasSummaries() bernilai false, halaman mengirim summaries: [], dan tidak ada aggregate query yang dijalankan.
Empat summarizer bawaan
| Class | aggregate() | Hasil | Nama default |
|---|---|---|---|
PandaPanel\Tables\Summaries\Sum | sum | float|int | sum |
PandaPanel\Tables\Summaries\Average | avg | ?float | average |
PandaPanel\Tables\Summaries\Count | count | int | count |
PandaPanel\Tables\Summaries\Range | null | array{min, max}|null | range |
Range membutuhkan dua aggregate, bukan satu, sehingga tidak cocok dengan pola single-aggregate dan menghitung keduanya sendiri. format() miliknya menampilkan min – max, satu value jika keduanya sama, atau — jika tidak ada data.
use PandaPanel\Tables\Columns\DateTimeColumn;
use PandaPanel\Tables\Columns\NumberColumn;
use PandaPanel\Tables\Summaries\Average;
use PandaPanel\Tables\Summaries\Count;
use PandaPanel\Tables\Summaries\Range;
use PandaPanel\Tables\Summaries\Sum;
NumberColumn::make('total')->summarize([
Sum::make(),
Average::make(),
Count::make(),
Range::make(),
]),
DateTimeColumn::make('created_at')->summarize([Range::make()->label('Between')]),2
3
4
5
6
7
8
9
10
11
12
13
14
15
API Summarizer
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder as QueryBuilder;
use PandaPanel\Tables\Summaries\Summarizer;
Summarizer::make(string $name = ''): static
Summarizer::label(string $label): static
Summarizer::formatUsing(Closure(mixed): string $callback): static
Summarizer::perPage(bool $perPage = true): static
Summarizer::isPerPage(): bool
Summarizer::getName(): string
Summarizer::getLabel(): string
Summarizer::aggregate(): ?string
Summarizer::summarize(QueryBuilder $query, string $column): mixed
Summarizer::summarizeRecords(array $records, string $column): mixed
Summarizer::format(mixed $value): string
Summarizer::toArray(mixed $value): array2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| Method | Default | Catatan |
|---|---|---|
make($name) | '' | nama kosong fallback ke lowercase class basename — sum, average, count, range |
label($label) | Str::headline(getName()) | caption di samping figure |
formatUsing($callback) | tidak ada | menggantikan formatting default sepenuhnya |
perPage($perPage) | false | menghitung halaman aktif, bukan seluruh result set |
use PandaPanel\Tables\Summaries\Sum;
Sum::make()->label('Total revenue')
->formatUsing(static fn (mixed $value): string => '$'.number_format((float) $value, 2)),
// Two summarizers of the same class on one column need distinct names: the
// name identifies the figure in the payload, and it defaults to the class.
Sum::make('sum_eur')->label('Total (EUR)')
->formatUsing(static fn (mixed $value): string => number_format((float) $value * 0.92, 2).' €'),2
3
4
5
6
7
8
9
Formatting default
format() dijalankan di server, sehingga yang dikirim adalah teks final beserta raw figure untuk kebutuhan lain:
| Value | Hasil render |
|---|---|
null | — |
float | number_format($v, 2) dengan nol dan titik akhir yang tidak diperlukan dihapus |
int | number_format($v) |
| scalar lain | di-cast menjadi string |
| value lain | — |
Bentuk serialization satu figure
[
'name' => 'sum',
'label' => 'Total revenue',
'value' => '$12,480.00', // format()
'raw' => 12480.0, // the scalar, or null when it is not one
'perPage' => false,
]2
3
4
5
6
7
TableSchema::summaries() mengembalikan figure yang di-key berdasarkan nama kolom. Setiap kolom berisi list sesuai urutan deklarasi summarizer:
[
'total' => [
['name' => 'sum', 'label' => 'Sum', 'value' => '12,480', 'raw' => 12480, 'perPage' => false],
['name' => 'average', 'label' => 'Average', 'value' => '124.8', 'raw' => 124.8, 'perPage' => false],
],
]2
3
4
5
6
Seluruh result atau halaman aktif
Default-nya menghitung seluruh result karena "total" biasanya berarti total keseluruhan. Angka yang diam-diam hanya berarti "dari dua puluh row ini" justru lebih mengejutkan.
use PandaPanel\Tables\Summaries\Count;
use PandaPanel\Tables\Summaries\Sum;
Sum::make()->label('All orders'), // the filtered result
Count::make()->label('On this page')->perPage(), // the records on screen2
3
4
5
| Dihitung oleh | Membaca | |
|---|---|---|
| default | database, berdasarkan filtered query | semua row yang tersisa setelah search dan filters |
perPage() | PHP, berdasarkan record yang diberikan | hanya row pada halaman aktif |
summarizeRecords() membaca setiap record dengan data_get(), membuang null, lalu melakukan reduction. Count menghitung non-null value, Sum meng-cast setiap value ke float lalu menjumlahkannya, Average membagi total, dan Range mengambil min serta max.
Cara figure untuk seluruh result dihitung
TableSchema::summarySource() menyiapkan query sekali per kolom dan hanya ketika benar-benar dibutuhkan. Figure perPage() tidak menyentuh database.
$base = $query->clone()->reorder()->toBase();
$base->limit = null;
$base->offset = null;2
3
Query di-clone dan di-reorder agar summary tidak meninggalkan order by pada builder yang baru saja digunakan table pagination. Limit dan offset dihapus karena paginate() menambahkannya pada builder yang diberikan; summary yang dihitung setelahnya tidak boleh hanya menggambarkan satu halaman sambil mengaku sebagai total result.
Setelah itu ada dua bentuk query, dan mencampur keduanya akan menjadi SQL error, bukan hanya angka salah:
- Plain column langsung di-aggregate dari tabel, misalnya
sum(total). - Generated alias — misalnya
posts_countdariwithCount()— hanya ada di SELECT list.sum(posts_count)langsung terhadap base table akan mengacu ke kolom yang tidak ada. Karena itu base query dibungkus dalam subquery (from (…) as panel_summary_source) dan aggregate dijalankan di luarnya.
Pembungkusan tidak dilakukan tanpa syarat karena subquery memiliki biaya. Query dengan correlated count column harus menghitung semuanya untuk setiap row jika selalu dibungkus, padahal plain sum(total) tidak membutuhkan biaya tersebut. Column::summaryUsesAggregate() menentukan strategi yang digunakan.
use PandaPanel\Tables\Columns\NumberColumn;
use PandaPanel\Tables\Summaries\Average;
use PandaPanel\Tables\Summaries\Sum;
// Wrapped: `passkeys_count` is an alias, not a column.
NumberColumn::make('passkeys_count')
->counts('passkeys')
->summarize([Sum::make()->label('Total'), Average::make()]),2
3
4
5
6
7
8
Column::summaryColumn() mengembalikan aggregate attribute jika tersedia dan getSortColumn() jika tidak. Dengan demikian summary selalu menghitung value yang benar-benar dipilih query. Lihat Kolom relasi.
Summary per grup
Ketika tabel dikelompokkan, setiap band mendapatkan figure-nya sendiri yang dihitung berdasarkan seluruh band, bukan hanya row pada halaman aktif.
use PandaPanel\Tables\Columns\NumberColumn;
use PandaPanel\Tables\Group;
use PandaPanel\Tables\Summaries\Count;
use PandaPanel\Tables\TableSchema;
TableSchema::make()
->columns([
NumberColumn::make('total')->summarize([Count::make()]),
])
->groups([Group::make('status')])
->defaultGroup('status');2
3
4
5
6
7
8
9
10
11
groupSummaries() membagi record pada halaman berdasarkan Group::keyFor(), lalu menjalankan kembali summaries() untuk setiap band dengan tambahan where(groupColumn, '=', key). Biayanya satu query per band yang tampil di layar, yang biasanya hanya beberapa. Figure ditampilkan di bawah band yang dijelaskannya. Summarizer perPage() tetap melakukan reduction terhadap record yang tampil.
Result di-key berdasarkan band lalu kolom:
[
'3' => ['total' => [['name' => 'count', 'label' => 'Count', 'value' => '3', 'raw' => 3, 'perPage' => false]]],
'7' => ['total' => [['name' => 'count', 'label' => 'Count', 'value' => '1', 'raw' => 1, 'perPage' => false]]],
]2
3
4
Tabel tanpa summarizer mengembalikan [] apa pun grouping-nya. Lihat Grouping.
Membuat summarizer sendiri
Ada dua abstract member: nama SQL aggregate dan implementasi ekuivalen di PHP untuk perPage().
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Tables\Summaries;
use Illuminate\Database\Query\Builder as QueryBuilder;
use PandaPanel\Tables\Summaries\Summarizer;
final class DistinctCount extends Summarizer
{
public function aggregate(): ?string
{
return null;
}
public function summarize(QueryBuilder $query, string $column): mixed
{
return $query->clone()->distinct()->count($column);
}
/**
* @param list<mixed> $values
*/
protected function reduce(array $values): int
{
return count(array_unique($values, SORT_REGULAR));
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Mengembalikan nama dari aggregate() adalah shortcut: base summarize() akan menjalankan $query->clone()->{$aggregate}($column). Kembalikan null lalu override summarize() jika figure membutuhkan lebih dari satu aggregate, seperti yang dilakukan Range.
Sumber figure pada waktu render
ListRecords memberikan builder yang sudah di-constrain dan record halaman aktif:
'summaries' => $schema->hasSummaries()
? $schema->summaries($query, array_values($records->items()))
: [],2
3
Builder yang diterima summaries() adalah instance yang sama dengan yang diberikan ke paginate(), sehingga seluruh constraint tabel sudah tersedia tanpa perlu membangun query kedua. RelationTable melakukan hal yang sama pada query milik relation, sehingga tabel relation manager merangkum tepat data yang sedang ditampilkan.
Catatan
- Summary meng-aggregate database column, bukan rendered cell.
formatUsing()pada kolom,prefix(), dandecimals()tidak terlibat. Format figure menggunakanSummarizer::formatUsing(). - Menjumlahkan kolom non-numerik adalah tanggung jawab database, bukan validation error framework. Gunakan
Sumpada kolom numerik, atau gunakanCount/Rangejika memang bermakna untuk tipe datanya. - Dua summarizer dari class yang sama pada satu kolom membutuhkan nama berbeda, karena
make()default ke class basename dan nama tersebut digunakan sebagai identifier figure di payload. Averagepada setperPage()kosong menghasilkannull, yang diformat sebagai—;avg()database atas data kosong juga menghasilkannull.- Figure mengabaikan pagination tetapi menghormati seluruh constraint lain — search, per-column search, filters, query builder, dan scope resource.
- Group summary membutuhkan satu query per band di layar. Dengan
perPagebesar dan group column ber-cardinality tinggi, jumlah band dapat banyak. Gunakan grouping pada data dengan jumlah kategori terbatas.