Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Metrics Laravel Package

square1/nova-metrics

Square1 Nova Metrics adds customizable metric cards to Laravel Nova, helping you display key application statistics and trends directly in your Nova dashboard with simple, configurable value and trend metrics.

View on GitHub
Deep Wiki
Context7

Getting Started

Day 0 — read the source before you composer require. This is a 27-star, zero-dependent package that reaches into closed-source Nova internals, so the compatibility check is the work. Spend half a day on the repo:

  • composer.json — note the declared Nova major, Laravel, and PHP constraints. Nova 4 → 5 was a Vue 2 → Vue 3 jump with a different asset pipeline (Nova.booting); card packages routinely break across that line. If it declares Nova 5, find the CI matrix that proves it. If it doesn't, assume Nova 4 is the ceiling until you've tested otherwise.
  • Does it ship pre-built JS? A PHP-only package that just shapes query builders is far safer than an unversioned front-end bundle you now own at every Nova upgrade.
  • LOC count. Under a few hundred lines, fork-and-own becomes a 1–2 day job and changes the entire risk calculus in your favour.
  • The core mechanism — this is the one that decides everything. Does the package re-evaluate Filter::apply($request, $query) server-side per metric request, or does it accept opaque filter state from the client and replay it? Server-side is auditable and inherits your existing canSee() rules. Client-supplied state is a tampering surface and a cross-tenant leak waiting to happen.

Install pinned — never with a caret:

composer require square1/nova-metrics:x.y.z

Record the Nova version you validated against in the PR description, not just in composer.lock. The lockfile tells the next person what you installed; the PR tells them what you tested.

Where to look first, in order: the trait or base class that does the filter threading → the service provider / card registration path → any published config → the tests (if there are any; if there aren't, that's your first contribution). Read the entry point end to end before wiring a card; you need to know whether the metric sees your TicketResource::filters() list or a copy of it.

First use case — one Value card, one resource, one simple filter. Pick a resource where users already apply a status or owner filter on the index page. Wire exactly one metric card that respects it. Then run the only check that matters:

// tests/Feature/Nova/FilteredMetricParityTest.php
$filters = ['status' => 'open'];

$indexCount = (new TicketResource::indexQuery(
    NovaRequest::create('/', 'GET', ['filter' => $filters]), Ticket::query()
))->count();

$metricValue = (new OpenTicketsMetric)->calculate(
    NovaRequest::create('/', 'GET', ['filters' => $filters])
)->value;

$this->assertSame($indexCount, $metricValue);

Green for no-filter, single-filter, and multi-filter cases earns you the right to add a second card. Red means stop — a metric that renders but silently ignores a filter is worse than a broken dashboard, because it looks trustworthy and gets screenshotted into a business review.

Implementation Patterns

Wrap it in your own base class from day one. Put an internal App\Nova\Metrics\FilterAwareMetric in place that extends or uses the package's trait. Every concrete metric extends that, never the package class directly. This is a single seam: when upstream goes stale, or breaks on your next Nova upgrade, the swap to first-party code or a vendored fork is one file, not a sweep through every dashboard.

namespace App\Nova\Metrics;

use Square1\NovaMetrics\...; // the only place the package is named in your app

abstract class FilterAwareMetric extends ...
{
    // hook for cross-cutting concerns: labelling, cache keys, guard rails
}

Single-source your filter definitions. The metric and the resource index must resolve from the same filter classes, not two hand-copied implementations of the same where clause. If TicketResource::filters() returns [StatusFilter::class, OwnerFilter::class], the card should resolve from that same list — ideally by reading it, not by duplicating it in a config array. Divergence here is the "the numbers don't match" bug you installed the package to fix, reintroduced in a new place.

Cache keyed on the filter payload and the user. If you wrap the calculation in Cache::remember, the key must include the resolved filter state and the authenticated user's ID. Otherwise a tenant- or owner-scoped metric serves a cached number across users. This is the highest-severity mistake available in this integration, and it will not show up in a single-user smoke test.

$key = sprintf(
    'metric:open-tickets:%s:%s',
    auth()->id(),
    md5(json_encode($resolvedFilters))
);

Build a parity harness as a data provider, not a single test. The point of the package is agreement between two query paths, so test the matrix: no filters, each filter alone, filter combinations, a filter with defaults, and a user with restricted scope.

public static function filterMatrix(): array
{
    return [
        'none'            => [[]],
        'status'          => [['status' => 'open']],
        'owner'           => [['owner' => 7]],
        'status + owner'  => [['status' => 'open', 'owner' => 7]],
    ];
}

Prefer Value and Partition over Trend for filtered cards. A Trend metric issues one query per bucket. With a cheap column predicate that's 30 queries and fine; with a whereHas filter it's 30 correlated subqueries and a dashboard that feels broken. Measure before you ship a trend card behind an expensive filter — and if the filter is expensive, precompute on a schedule instead.

Register cards where they already belong. Add the metric to the existing NovaServiceProvider::cards() or a dashboard's cards() array — don't introduce a parallel registration pathway. Keep each card's uriKey() stable from the first deploy; changing it later orphans any saved dashboard layouts user has customised.

Label filtered cards in the UI. A number that changes with filter state must say so in its name, its help text, or both — Open Tickets (filtered) rather than Open Tickets. The package solves query correctness; it does not solve the human problem of two people arguing over two different numbers that are both technically right.

Gotchas and Tips

  • Verify authorization is honoured server-side, with two users. canSee() and user-dependent options() on filters behave differently in a metric request context than on an index request. Test with two users of different tenant/owner scope and assert they get different numbers. If they don't, you have a data exposure, not a cosmetic bug.
  • options() runs on every metric request. Filters whose options() hits the database with a heavy query now run once per card, on every dashboard load. Watch the query count on a cold dashboard with three or four of these cards; it adds up faster than you'd guess.
  • Filter state doesn't reliably travel. Nova persists filter selection in browser storage scoped to a resource index; a dashboard card is a different render context entirely. Do not assume the index page's dropdown state is what the card receives. Confirm empirically what arrives in the metric request, and check whether filter defaults are applied on first load (or silently skipped).
  • Date ranges and timezones. DateFilter ranges are computed against the request; a metric resolved in a different context can be off by a day at the boundary. Test an explicit range edge — the last second of a period, and a user in a non-UTC timezone.
  • Debug by logging the final SQL, not the result. During a wrong-number spike, diff the metric's query with bindings against the index query with bindings. Divergence there explains most bugs faster than reading package source.
DB::listen(fn ($q) => logger()->debug($q->sql, $q->bindings));
  • Check the query plan, not just the row count. Run EXPLAIN on the filtered metric query. A filter that translates to whereHas may be fine on a small table and disastrous at production volume — and a dashboard loads it before anyone has applied anything.
  • Pin, and plan the fork. Zero dependents, a single maintainer, and dependence on closed-source Nova internals means upstream breakage will surface during your Nova upgrade with no committed fix timeline. Add "verify metric cards still render and still honour filters" to the permanent Nova upgrade checklist, and budget the fork as the realistic end state if the pilot spreads — not as a contingency.
  • Rollback is a deploy revert. This is a read-path change with no data migration. A bad pilot costs one deploy, not a rollback procedure — which is exactly why it's worth piloting cheaply rather than designing a platform around it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource