Версія v1.6.5 Попередні версії: документація на GitHub
Метрики
На цій сторінці
- Генератори
- Перша метрика
- Картка
- Періоди (ranges)
- Часовий пояс
- Джерело даних і безпека
- Value
- Trend
- Partition
- Підписи з пов'язаної моделі
- Власні значення
- Progress
- Форматування
- Кешування
- Що перевіряється при попаданні в кеш
- Від чого ще залежить результат
- Обмеження частоти запитів
- MetricRequest
- Ендпоінти
- Відомі обмеження
- Формат відповіді ендпоінта
Метрика це картка з числом або графіком, яку панель показує на дашборді або над списком Entity. Кожна картка завантажує дані окремим JSON-запитом, тож повільна чи зламана метрика не блокує сторінку та інші картки.
| Тип | Базовий клас | Що показує | Результат calculate() |
|---|---|---|---|
| Value | Maxcxam\Larsadmin\Metrics\Value |
одне число за період і зміну відносно попереднього | ValueResult |
| Trend | Maxcxam\Larsadmin\Metrics\Trend |
лінійний графік по днях, тижнях або місяцях | TrendResult |
| Partition | Maxcxam\Larsadmin\Metrics\Partition |
кільцеву діаграму за значеннями колонки | PartitionResult |
| Progress | Maxcxam\Larsadmin\Metrics\Progress |
прогрес до цілі | ProgressResult |
Результати лежать у просторі імен Maxcxam\Larsadmin\Metrics\Results.
Генератори
php artisan larsadmin:value NewPosts
php artisan larsadmin:trend PostsPerDay
php artisan larsadmin:partition PostsByStatus
php artisan larsadmin:progress MonthlyPublishingGoal
Класи створюються в app/Larsadmin/Metrics (App\Larsadmin\Metrics\NewPosts). Вкладені назви
працюють: Reports/Revenue дає App\Larsadmin\Metrics\Reports\Revenue. --force перезаписує
існуючий клас. Класи метрик у app/Larsadmin не реєструються як Entity.
Згенерований клас рахує
App\Models\User(клас моделі, без обмежень доступу), щоб картка працювала одразу; рядок позначено коментарем// SECURITY: unscoped. Замініть джерело на свою Entity (див. «Джерело даних і безпека»). Шаблон Value вже містить->allowZeroResult(), шаблон Trend властивість$unit.
Перша метрика
namespace App\Larsadmin\Metrics;
use App\Larsadmin\Post;
use Maxcxam\Larsadmin\Metrics\MetricRequest;
use Maxcxam\Larsadmin\Metrics\Results\ValueResult;
use Maxcxam\Larsadmin\Metrics\Value;
class NewPosts extends Value
{
public function name(): string
{
return 'Нові пости';
}
public function ranges(): array
{
return $this->labelled([7, 30, 90, 'MTD']);
}
public function calculate(MetricRequest $request): ValueResult
{
// Entity: рахуються лише рядки, які бачить користувач; 0 показується як число, а не «Немає даних»
return $this->count($request, Post::class)->allowZeroResult();
}
}
Підключіть картку в cards() дашборда або Entity:
public function cards(Request $request): array
{
return [
NewPosts::make()->help('Дописи, створені за період.'),
];
}
Картка
Спільні методи всіх карток (клас Maxcxam\Larsadmin\Metrics\Card):
| Метод | За замовчуванням | Опис |
|---|---|---|
make(...$args) |
створює екземпляр (аргументи йдуть у конструктор) | |
name() |
назва класу в Title Case через __() |
заголовок картки; перевизначте метод |
uriKey() |
kebab-case назви класу | ключ картки в URL. Має бути унікальним у межах дашборда чи Entity: дві картки з однаковим ключем (видимі чи ні) кидають InvalidArgumentException |
width(string) |
'1/3' |
'1/3', '1/2', '2/3' або 'full'. Інше значення кидає InvalidArgumentException |
help(string) |
підказка: іконка «?» біля заголовка | |
canSee(Closure) |
fn (Request $request): bool. Прихована картка не показується, а її ендпоінт відповідає 404 |
|
withMeta(array) |
[] |
додаткові ключі серіалізації картки (з 1.5), додаються після базових; базові ключі (component, uriKey, name, width, helpText, endpoint) кидають InvalidArgumentException |
Ширина діє на великих екранах (сітка з шести частин). На планшеті картки 1/3 і 1/2 займають
половину рядка, 2/3 і full весь рядок; на телефоні кожна картка на всю ширину.
Власні картки з власним Vue-компонентом (статичні Card і картки з даними DataCard з тим самим
ендпоінтом, авторизацією й кешем) описано в Розширення: картки.
Періоди (ranges)
ranges() повертає доступні періоди у вигляді ключ => підпис. Користувач перемикає період у
списку на картці (список показується, коли періодів більше одного). defaultRange() повертає
період за замовчуванням: перший ключ ranges(). Невідомий або некоректний range у запиті
замінюється періодом за замовчуванням.
| Ключ | Поточний період | З чим порівнюється (Value) |
|---|---|---|
N (ціле число) |
від початку дня N − 1 днів тому до цього моменту |
ті самі N днів перед ним |
'TODAY' |
від початку сьогоднішнього дня | вчора, той самий проміжок часу від півночі |
'MTD' |
від початку місяця | попередній місяць, той самий проміжок від його початку |
'QTD' |
від початку кварталу | попередній квартал, так само |
'YTD' |
від початку року | попередній рік, так само |
'ALL' |
без обмеження дати | не порівнюється |
Періоди напіввідкриті, [початок, кінець): рядок рівно на межі потрапляє лише в один період, а
попередній період ніколи не заходить у поточний.
$this->labelled([...]) будує ranges() з перекладеними підписами (файл larsadmin::metrics):
TODAY, MTD, QTD, YTD, ALL мають власні підписи, ціле число підписується кількістю днів
у правильній формі множини («14 днів», «21 день»). Підписи можна задати й самому:
public function ranges(): array
{
return [7 => 'Тиждень', 30 => 'Місяць', 'MTD' => 'Цей місяць'];
}
public function defaultRange(): int
{
return 30;
}
Значення за замовчуванням:
| Тип | ranges() |
|---|---|
| Value, Progress | 30, 60, 365, TODAY, MTD, QTD, YTD |
| Trend | кількість точок в одиницях $unit: днів 7, 30, 90, тижнів 4, 12, 26, місяців 3, 6, 12 (див. «Trend») |
| Partition | порожньо: діаграма завжди рахує всі рядки |
Часовий пояс
Межі періодів («сьогодні», «з початку місяця», точки графіка) рахуються в часовому поясі панелі:
Larsadmin::timezoneUsing(fn (Request $request) => ...), наприклад пояс із профілю користувача;config('larsadmin.timezone')(LARSADMIN_TIMEZONE);config('app.timezone').
Невідомі ідентифікатори поясу пропускаються. Дати в базі порівнюються в app.timezone.
// app/Providers/LarsadminServiceProvider.php, boot()
Larsadmin::timezoneUsing(fn (Request $request) => $request->user()?->timezone);
Джерело даних і безпека
Хелпери (count, sum, countByDays тощо) приймають джерело $model. Від нього залежить, які
рядки потраплять у число.
| Джерело | Обмеження |
|---|---|
клас Entity: Post::class з App\Larsadmin |
рядки з indexQuery() цієї Entity; без права viewAny картка показує «Немає доступу до цих даних.» (відповідь 403) |
$this->scoped(Post::class, fn (Builder $q) => ...) або EntitySource::make(Post::class, fn ...) |
те саме, що Entity, плюс ваша умова |
клас моделі: \App\Models\Post::class |
жодних обмежень |
Eloquent builder (Post::query()->where(...)) або DB::table(...) |
жодних обмежень |
Увага. Клас моделі та builder рахують усі рядки, до яких дотягується запит, без
indexQuery()і без політик. Користувач, якомуindexQuery()показує лише власні записи, побачить у такій метриці число по всій таблиці. Використовуйте їх лише для загальних показників і ховайте картку черезcanSee(), якщо число чутливе.
Звужуйте Entity через scoped(), а не через модель:
use Illuminate\Database\Eloquent\Builder;
public function calculate(MetricRequest $request): ValueResult
{
$live = $this->scoped(Post::class, fn (Builder $query) => $query->where('status', 'live'));
return $this->sum($request, $live, 'views', 'published_at');
}
Умова scoped() додається вкладеним where (...), тому orWhere у ній не вийде за межі
indexQuery(). scoped() (метод метрики) і EntitySource::make() (клас
Maxcxam\Larsadmin\Metrics\EntitySource) рівнозначні; для класу, що не є Entity, вони кидають
InvalidArgumentException.
Інше про джерела:
- Для Eloquent-джерел діють глобальні скоупи моделі (наприклад,
SoftDeletes). - Колонка дати за замовчуванням це
created_atмоделі (getCreatedAtColumn()). ДляDB::table()моделі немає, тому колонку дати потрібно передати явно, інакшеInvalidArgumentException. AuthorizationException, кинутий уcalculate(), дає відповідь 403 і стан «Немає доступу».
Value
protected function count(MetricRequest $request, $model, ?string $dateColumn = null): ValueResult
protected function sum(MetricRequest $request, $model, string $column, ?string $dateColumn = null): ValueResult
protected function average(MetricRequest $request, $model, string $column, ?string $dateColumn = null): ValueResult
protected function min(MetricRequest $request, $model, string $column, ?string $dateColumn = null): ValueResult
protected function max(MetricRequest $request, $model, string $column, ?string $dateColumn = null): ValueResult
protected function result(int|float|null $value): ValueResult // власне значення
Хелпери рахують поточний період і попередній (див. таблицю періодів) і повертають обидва числа. Картка показує зміну у відсотках:
| Стан | Що видно |
|---|---|
| значення зросло / зменшилося | «Зросло на N%» / «Зменшилося на N%» зі стрілкою |
| зміна округлюється до 0,0% | «Без змін» |
попереднє значення 0 |
«Немає даних за попередній період» |
попереднього періоду немає (ALL, result() без previous()) |
лише число |
значення null (наприклад, average без рядків) |
«Немає даних» |
значення 0 без allowZeroResult() |
«Немає даних», без порівняння |
Методи ValueResult:
| Метод | Опис |
|---|---|
previous(int|float|null) |
значення попереднього періоду для власних обчислень |
allowZeroResult(bool $allow = true) |
показувати 0 як справжній результат (з порівнянням). Без нього 0 вважається відсутністю даних. Для лічильників на кшталт «Нові пости» зазвичай потрібен |
| форматування | див. «Форматування» |
public function calculate(MetricRequest $request): ValueResult
{
$value = Order::query()->where('paid', true)->sum('total'); // модель: без обмежень доступу
return $this->result($value)->currency('UAH');
}
Trend
class NewUsersPerWeek extends Trend
{
protected string $unit = 'week'; // одиниця періодів за замовчуванням: day, week або month
public function calculate(MetricRequest $request): TrendResult
{
return $this->countByWeeks($request, User::class)->showSumValue();
}
}
Властивість $unit ('day' за замовчуванням) визначає лише періоди за замовчуванням і їхні підписи:
днів 7, 30, 90, тижнів 4, 12, 26, місяців 3, 6, 12 («12 тижнів»). Одиницю точок задає хелпер
(countByWeeks() рахує тижнями), тож тримайте $unit у відповідності до нього. Власні періоди
Trend задавайте цілими числами; $this->bucketRanges([4, 8, 13]) підпише їх в одиницях $unit.
Рядковий ключ періоду ('MTD' тощо) у Trend кидає InvalidArgumentException. Невідомий $unit
теж кидає InvalidArgumentException.
Хелпери за одиницею часу й агрегатом:
| днями | тижнями | місяцями | |
|---|---|---|---|
| кількість | countByDays($r, $model, ?$dateColumn) |
countByWeeks(...) |
countByMonths(...) |
| сума | sumByDays($r, $model, $column, ?$dateColumn) |
sumByWeeks(...) |
sumByMonths(...) |
| середнє | averageByDays(...) |
averageByWeeks(...) |
averageByMonths(...) |
| мінімум | minByDays(...) |
minByWeeks(...) |
minByMonths(...) |
| максимум | maxByDays(...) |
maxByWeeks(...) |
maxByMonths(...) |
Також є загальний aggregate($request, $model, string $unit, string $function, ?string $column = null, ?string $dateColumn = null)
з $unit = day|week|month і $function = count|sum|avg|average|min|max.
- Період означає кількість точок:
30дає 30 днів (тижнів, місяців), останньою йде поточна, ще не завершена. Тижні починаються з понеділка. Максимум 366 точок. - Межі точок рахуються в часовому поясі панелі й коректні під час переходу на літній/зимовий час.
- Точки без рядків мають
0дляcount/sumі порожнє значення дляaverage/min/max. - Підписи точок форматує сервер мовою панелі (
D MMMдля днів і тижнів,MMM YYYYдля місяців).
Головне число над графіком вмикається явно:
Метод TrendResult |
Число над графіком |
|---|---|
showSumValue() |
сума всіх точок; лише для count/sum, для average/min/max кидає LogicException |
showLatestValue() |
значення останньої (поточної) точки |
| нічого | лише графік |
public function calculate(MetricRequest $request): TrendResult
{
$live = $this->scoped(Post::class, fn (Builder $query) => $query->where('status', 'live'));
return $this->countByDays($request, $live, 'published_at')->showSumValue();
}
Partition
protected function count(MetricRequest $request, $model, string $groupBy): PartitionResult
protected function sum(MetricRequest $request, $model, string $column, string $groupBy): PartitionResult
protected function average(MetricRequest $request, $model, string $column, string $groupBy): PartitionResult
protected function min(MetricRequest $request, $model, string $column, string $groupBy): PartitionResult
protected function max(MetricRequest $request, $model, string $column, string $groupBy): PartitionResult
Рядки групуються за колонкою $groupBy, сектори йдуть від найбільшого. Період не враховується.
Метод PartitionResult |
Опис |
|---|---|
label(Closure) |
підпис сектора: fn (string $key): string. Ключ завжди рядок, '' для NULL |
labelsFrom(string|Builder $model, string $titleColumn = 'name', ?string $missing = null) |
підписи з пов'язаної моделі за первинним ключем (див. нижче) |
booleanLabels(?string $yes = null, ?string $no = null) |
підписи «Так»/«Ні» для ключів '1'/'0' булевої колонки ('' дає «Без значення») |
colors(array) |
CSS-кольори за ключем: ['live' => '#16a34a']. Приймаються #hex, rgb()/rgba(), hsl()/hsla(), oklch() і var(--token); інші значення й сектори без кольору беруть палітру теми |
limit(int $limit = 8) |
максимум секторів. Для count/sum решта збирається в сектор «Інше», для average/min/max решта відкидається. Менше 1 кидає InvalidArgumentException |
label(), labelsFrom() і booleanLabels() замінюють одне одного: діє останній виклик. Без них
сектор підписаний ключем, а NULL-група підписом «Без значення».
- Булеві колонки. Значення
true/falseстають ключами'1'/'0'на будь-якій СУБД. Якщо модель має для колонки кастbool/boolean,booleanLabels()застосовується автоматично. average/min/max. Значення секторів не складаються, тому картка не показує ні загальної суми, ні відсотків (у JSON"additive": false,"total": null).
public function calculate(MetricRequest $request): PartitionResult
{
return $this->count($request, Post::class, 'status')
->label(fn (string $status) => ['draft' => 'Чернетка', 'live' => 'Опубліковано'][$status] ?? $status)
->colors(['live' => '#16a34a', 'draft' => '#94a3b8']);
}
Підписи з пов'язаної моделі
Коли групуєте за зовнішнім ключем, labelsFrom() підставляє назву пов'язаного запису:
return $this->count($request, Post::class, 'category_id')
->labelsFrom(Category::class, 'name', 'Без категорії')
->limit(6);
- Назви завантажуються одним запитом
whereInі лише для показаних секторів (післяlimit()). NULL-ключ отримує$missing(за замовчуванням «Без значення»), ключ без запису отримує#<ключ>.- Можна передати builder і власну колонку назви:
labelsFrom(Category::query()->where('active', true), 'title'). - Пов'язана модель читається без
indexQuery()і політик, але лише для ключів, які вже є в результаті користувача. Тобто розкриваються назви тих груп, які він і так бачить у числах.
Власні значення
use Maxcxam\Larsadmin\Metrics\Results\PartitionResult;
$values = ['Підтверджені' => 12, 'Непідтверджені' => 3]; // ключ => значення, від найбільшого
arsort($values);
return new PartitionResult($values);
Progress
protected function count(MetricRequest $request, $model, int|float $target, ?string $dateColumn = null): ProgressResult
protected function sum(MetricRequest $request, $model, string $column, int|float $target, ?string $dateColumn = null): ProgressResult
protected function result(int|float|null $value, int|float $target): ProgressResult
Рахує значення за поточний період (без порівняння) і показує відсоток від $target.
avoid() означає «менше краще» й інвертує кольори (наприклад, для кількості скарг).
class MonthlyPublishingGoal extends Progress
{
public function ranges(): array
{
return $this->labelled(['MTD']);
}
public function calculate(MetricRequest $request): ProgressResult
{
$live = $this->scoped(Post::class, fn (Builder $query) => $query->where('status', 'live'));
return $this->count($request, $live, 80, 'published_at');
}
}
Форматування
Числа форматує браузер через Intl.NumberFormat мовою панелі. Порожнє значення виводиться як «—».
| Метод | Value | Trend | Partition | Progress | Опис |
|---|---|---|---|---|---|
currency(string $iso) |
так | так | так | так | валюта ISO 4217: ->currency('UAH') |
format(array) |
так | так | так | так | опції Intl.NumberFormat: ['maximumFractionDigits' => 1], ['notation' => 'compact'], ['style' => 'percent'] |
prefix(string), suffix(string) |
так | так | ні | так | текст до й після числа: ->suffix(' шт.'). У PartitionResult цих методів немає; використовуйте currency() або format() |
return $this->sum($request, $live, 'views', 'published_at')->format(['notation' => 'compact']);
Кешування
За замовчуванням метрика рахується на кожен запит. Щоб кешувати, перевизначте cacheFor():
public function cacheFor(): int
{
return 300; // секунди; також DateInterval або DateTimeInterface
}
- Ключ кешу містить власника картки (дашборд чи Entity), клас і
uriKey()метрики, період, часовий пояс, мову, guard і користувача (клас моделі + ідентифікатор), а також ваші частини (див. нижче). Результат одного користувача не потрапить іншому. - Сховище:
config('larsadmin.metrics.cache_store'),nullозначає сховище за замовчуванням. null,0або від'ємне значення вимикають кеш. Відмова в доступі (403) не кешується.- Закешоване число застаріває до кінця строку. Для показників, за якими користувачі діють, тримайте строк коротким.
Що перевіряється при попаданні в кеш
Авторизація не кешується:
canSee()дашборда й картки виконується на кожен запит;- для кожного Entity-джерела, яке використала метрика (
Post::class,scoped(...)),viewAnyперевіряється знову; якщо право відкликали, відповідь 403.
calculate() при попаданні в кеш не викликається. Тому власні перевірки доступу всередині
calculate() (наприклад, abort_unless(...) чи AuthorizationException) на закешованому
результаті не спрацюють. Переносьте їх у canSee() картки.
Від чого ще залежить результат
Якщо calculate() залежить від чогось, крім користувача й періоду (поточна команда чи тенант,
роль, піддомен, сесія), додайте це в ключ, інакше користувач отримає число чужої області:
// для однієї метрики
public function cacheKeyParts(MetricRequest $request): array
{
return [$request->request->user()->current_team_id];
}
// для всіх метрик, у boot() провайдера застосунку
Larsadmin::metricCacheScopeUsing(fn (Request $request, Metric $metric) => tenant()?->getKey());
cacheKeyParts() повертає масив (за замовчуванням []), callback metricCacheScopeUsing()
будь-яке значення. Обидва мають бути придатні для json_encode. null вимикає callback.
Той самий callback отримує й картки з даними (DataCard, з 1.5). Callback, чий другий параметр
типізовано лише як Metric, картку отримати не може, тож така картка не кешується зовсім. Щоб кешувати
картки з даними, типізуйте параметр як Metric|DataCard, Card чи mixed
(див. Розширення: кешування карток).
Обмеження частоти запитів
Ендпоінти метрик обмежені іменованим лімітером larsadmin-metrics: larsadmin.metrics.throttle
запитів на хвилину (за замовчуванням 120) на користувача, для гостей на IP. null знімає
обмеження. Кожна картка робить окремий запит, а зміна періоду ще один, тож для великих дашбордів
ліміт може знадобитися збільшити.
MetricRequest
calculate() отримує Maxcxam\Larsadmin\Metrics\MetricRequest:
| Властивість | Опис |
|---|---|
$request->request |
поточний Illuminate\Http\Request |
$request->range |
перевірений ключ періоду (або null) |
$request->timezone |
часовий пояс панелі |
$request->now |
поточний момент (CarbonImmutable) у цьому поясі |
[$from, $to] = [$request->now->startOfWeek(), $request->now];
Ендпоінти
Картки завантажують дані з GET {path}/api/dashboards/{dashboard}/metrics/{metric}?range= і
GET {path}/api/entities/{entity}/metrics/{metric}?range=. Обидва потребують доступу до панелі.
Ті самі ендпоінти віддають дані карток DataCard (Розширення).
| Ситуація | Відповідь |
|---|---|
| метрика | 200 {"range": …, "result": …} (формат) |
картка DataCard |
200 {"data": …}; невалідні параметри за rules() дають 422 {"message", "errors"} |
дашборд або картка невидимі (canSee), картка не є метрикою чи DataCard |
404 |
немає viewAny для Entity, чиї картки запитуються |
403 |
джерело Entity без viewAny (і при попаданні в кеш), AuthorizationException у calculate() |
403 |
перевищено ліміт larsadmin-metrics |
429 |
Відомі обмеження
- SQL метрик у CI перевіряється лише на SQLite. На SQLite колонки лише з датою (
DATE) у трендах потрапляють у точку попереднього дня; використовуйтеDATETIME/TIMESTAMP. - Кожна картка завантажується окремим HTTP-запитом.
Формат відповіді ендпоінта
Метрика відповідає {"range": <ключ періоду або null>, "result": {…}}. Форма result стабільна в 1.x:
на неї спираються перевизначення вбудованих карток
(у мінорах ключі лише додаються). Сама картка серіалізується з ключами Card плюс ranges
([{ value, label }]) і defaultRange.
type |
Ключі result |
|---|---|
value |
value, previous, change (відсотки, null без попереднього значення чи з нулем), zeroResult, format, prefix, suffix |
trend |
unit (day, week, month), points ([{ label, start, value }]), value (підсумок для showLatestValue()/showSumValue(), інакше null), valueKind, format, prefix, suffix |
partition |
slices ([{ key, label, value, color }]), total (null для неадитивних агрегатів), additive, format |
progress |
value, target, percentage, avoid, format, prefix, suffix |
format є об'єктом опцій Intl.NumberFormat (порожній об'єкт, якщо їх немає).
Далі: Дашборди.