saumini/count
A lightweight Laravel package for efficient Eloquent counting. Get accurate totals for models and relationships without loading full collections, making it useful for dashboards, list cards, and search results where performance and simple code matter.
1. Reconcile the name before you composer require. The repo is nsaumini/nova-field-count, the package is saumini/count, and the description text advertises generic counting helpers for "controllers, views, and APIs." All three disagree. Confirm the exact Packagist vendor/name and publisher on packagist.org against the GitHub repo before installing — that mismatch is the classic shape of a typo-squat or dependency-confusion mistake. Then check the declared constraint:
composer show saumini/count # after require, or inspect composer.json on GitHub
2. Read src/ before you read the README. There is essentially one field class and one Vue component. Two questions decide everything else:
resolve()/resolveUsing() return a {relation}_count attribute that you must load via withCount, or does it fire a query per row? (The former is the whole point; the latter is an N+1 you're installing voluntarily.)^4.0, ^5.0, both? The installed Nova major is the real constraint here, not Laravel or PHP. Nova is paid and closed-source; a field compiled against Nova 4's Vue 3 / asset pipeline may silently fail to register on Nova 5. Verify against your actual composer.lock.3. Install and wire it up.
composer require saumini/count
Nova field packages typically register their JS from the package's service provider (Nova::script('count-field', __DIR__.'/../dist/js/field.js')) via auto-discovery — so no npm/Vite step in your host app if dist/ is committed. Check that it is; if it isn't, you've just inherited a front-end build chain for one integer, and you should stop and vendor instead.
4. First use case — one resource, index view. Start with something read-only and low-stakes: a Customer resource showing how many Tickets are open.
// app/Nova/Customer.php
public static function indexQuery(NovaRequest $request, $query)
{
return $query->withCount([
'tickets as open_tickets_count' => fn ($q) => $q->whereNull('closed_at'),
]);
}
public function fields(NovaRequest $request)
{
return [
ID::make()->sortable(),
Text::make('Name'),
Count::make('Open Tickets', 'open_tickets_count'), // confirm exact API in src/
];
}
Deploy that on staging, turn on query logging, and confirm the number of queries is constant as you page through 200 rows — not linear.
withCount('tickets') produces tickets_count. As soon as you need two variants of the same relation — all tickets vs. open tickets — you need aliases. Aliasing is also what keeps the field honest and debuggable:
$query->withCount([
'tickets as tickets_count',
'tickets as open_tickets_count' => fn ($q) => $q->whereNull('closed_at'),
'invoices as overdue_invoices_count' => fn ($q) => $q->where('due_at', '<', now()),
]);
Map each alias to exactly one field. Never let two fields share an attribute — that's how you get a stale or wrong number that still renders confidently.
indexQuery doesn't run on the detail page. In Nova 4+ add the same eager count to detailQuery; on Nova 3 you'll need resolveUsing with a guarded query, or a small accessor. Do it explicitly rather than assuming the field works identically on both screens — the most common report is "the count shows 0 on the detail page."
public static function detailQuery(NovaRequest $request, $query)
{
return $query->withCount('tickets');
}
If all you need is a plain integer on the index, withCount plus a core Number field already does the job:
Number::make('Open Tickets', 'open_tickets_count')->sortable(),
Adopt the package when you want its specific rendering or when you'd otherwise sprinkle one-off accessors across resources. The build-vs-buy calculus here is genuinely symmetric: ~100 lines of vendored field versus ~20–40 lines of in-house NovaField or Text::make()->displayUsing(). Don't accept lifecycle risk on a component you could own in an afternoon unless the time saved buys something else this sprint.
Given 28 stars, one maintainer, and zero dependents, the sane default is pin-and-copy:
cp -r vendor/saumini/count/src app/Nova/Fields/Count
cp -r vendor/saumini/count/dist app/Nova/Fields/Count/dist
Adjust the namespace, keep the asset registration in your own NovaServiceProvider. You now own the code, the Vue component, and the upgrade surface — and you can extend it (drill-down links, conditional coloring) without forking a repo nobody is watching.
The highest-value pattern for operators is a count that's clickable:
Text::make('Open Tickets', 'open_tickets_count')
->displayUsing(fn ($value) => $value === null ? '—' : (string) $value)
->asHtml(), // or wrap in a link to the related index with a filter
Check whether the package's Vue component already emits an index link; if not, this is the single extension most worth adding when you vendor.
The field is presentation, but its input is a query shape. Write a feature test asserting the attribute exists on the index payload:
$this->actingAs($user)->get('/nova-api/customers')
->assertJsonPath('resources.0.open_tickets_count', 3);
That catches the "silently renders 0 because the attribute name is wrong" failure, which is the only bug this package can realistically produce and also the worst one.
A missing or misnamed {relation}_count attribute renders as 0 or null, not an error. An operator reads "this account has zero open tickets" when it has twelve. Before shipping, confirm the behavior on a record that genuinely has zero and one that has several — if both render 0, you have an attribute-name bug, not empty data. Consider ->displayUsing(fn ($v) => $v === null ? '—' : $v) so "not loaded" is visually distinct from "genuinely zero." Never let the field fall back to a live query per row; you'll never notice it in dev with 10 records and you'll notice it in prod with 500.
->sortable() on a withCount alias requires Nova to translate the click into an ORDER BY on the count — which means either a correlated subquery or, worse, an in-PHP sort of one page. On a 200-row index against a multi-million-row tickets table, that's a visible page-load cost on a screen operators live in. Test sorting explicitly; if it's slow, drop ->sortable() and accept a fixed order, or move to a summary table / materialized counter.
Add this package to your Nova upgrade checklist as a blocking verification step. Budget roughly half a day per major upgrade to confirm the field still registers and renders. The failure mode is a blank cell in the browser, not a CI failure — Nova's asset registration and Vue 3 component contract churn between majors, and nothing in your test suite will tell you.
Run this before any Nova bump:
composer why-not laravel/nova
If dist/ is committed and pre-built, you get no JS toolchain in your app — a real de-risking factor. Verify on a staging deploy rather than by assumption: a field whose Nova::script() points at a missing or stale bundle fails to render with no server-side error. If dist/ is not committed, treat that as a hard stop and vendor the class, writing your own trivial Vue component.
Sums, averages, distinct counts, trends, time series, cross-record comparison — none of that belongs here. That's a metrics workstream with metric definition and ownership, not a field. If the request expands beyond "one integer per record," escalate and re-scope rather than accreting logic into the vendored class.
If you're not on Nova, this package is inert — use Eloquent withCount and a Blade component, or Filament, or your BI layer. If your dependency policy requires active maintenance, security response, or an SLA, 28 stars and a single maintainer won't clear it — vendor. And if the requirement is "a number the business trusts," the work is definition and reconciliation; the field is the trivial last step, not the project.
How can I help you explore Laravel packages today?