wamesk/rating-field
A lightweight Laravel package that adds a ready-to-use star rating field to your forms, letting users pick a score with a simple, configurable star widget. Quick to install and easy to drop into existing forms.
Confirm the gate before you type a command. This is a Nova-only field. If composer show laravel/nova doesn't return a licensed install, you cannot use it at all — in that case read the source as a reference implementation instead (it's one PHP field class plus one Vue SFC, roughly 45 minutes of reading).
1. Establish your Nova major first. Nova 4 is Vue 2; Nova 5 is Vue 3. A field last released 2023-04-20 with zero stars and zero dependents is almost certainly Nova 4 / Vue 2. Open resources/js/components/Field.vue and check the <script> block for Options API vs. defineComponent / <script setup>. That single check decides whether you proceed or scaffold your own field.
2. Install and pin exactly.
composer require wamesk/rating-field:0.1.0 # pin exactly, no caret
Do not use an open constraint. Assume the version you install is the version you will run forever.
3. Publish what it ships, then clear config.
php artisan vendor:publish --provider="Wamesk\RatingField\FieldServiceProvider"
php artisan config:clear
Run --tag once you've read the provider to see which tags exist. Check whether dist/ ships a compiled bundle (assets-only publish) or raw sources (which means building inside your Nova asset pipeline). If you edit or republish the Vue component, you own the upgrade path forever — there won't be one anyway.
4. Read two files before wiring anything up:
src/Rating.php — the Field subclass. This tells you the exact fluent method names for star count, half-stars, icons, and colors. The docs describe capabilities; the method names must come from source.resources/js/components/Field.vue — the input. Confirms the Vue version and reveals whether the widget is keyboard/ARIA accessible.5. First use case — a nullable quality score on one internal resource. Migrate first, field second:
// whole stars only
$table->unsignedTinyInteger('quality_score')->nullable()->after('status');
// OR half-stars
$table->decimal('quality_score', 2, 1)->nullable()->after('status');
use App\Nova\Fields\Rating; // your wrapper, see below
public function fields(NovaRequest $request): array
{
return [
Rating::score(),
];
}
protected $casts = [
'quality_score' => 'integer', // or 'decimal:1' for half-stars
];
Smoke-test in the browser on one resource behind a flag, then widen. The first day's work is the storage contract, not the widget.
Never reference Wamesk\RatingField\Rating from a resource directly. One thin subclass gives you a single swap point when (not if) the package dies:
namespace App\Nova\Fields;
use Wamesk\RatingField\Rating as BaseRating;
class Rating extends BaseRating
{
public function __construct($name, $attribute = null, ?callable $resolveCallback = null)
{
parent::__construct($name, $attribute, $resolveCallback);
$this->withMeta([
'max' => 5,
]);
}
public static function score(string $name = 'Quality Score', ?string $attribute = 'quality_score'): static
{
return static::make($name, $attribute)
->rules('nullable', 'integer', 'between:1,5')
->nullable();
}
}
Every call site becomes Rating::score(). Replacing it later with an in-house field or a plain Number is a one-line change.
This is the only genuinely expensive decision; everything else is a UI swap.
| Requirement | Column | Cast | Nova rules |
|---|---|---|---|
| Whole stars, optional | unsignedTinyInteger nullable |
integer |
nullable|integer|between:1,5 |
| Half stars | decimal(2,1) nullable |
decimal:1 |
nullable|numeric|between:0.5,5 |
| Always rated | unsignedTinyInteger (no null) |
integer |
required|integer|between:1,5 |
Half-stars come back from Vue as strings ("3.5") over the wire. The decimal:1 cast handles reads, but validate with numeric, not integer, or you'll reject the very feature you configured.
0 stars and "not yet rated" are different facts. Use nullable() everywhere unless business rules require a rating on creation — otherwise staff enter fake 1s to clear the form.
Nova's ->rules() is a convenience skipped by non-Nova write paths (imports, artisan commands, API endpoints). Put the truth in a rule class and let the field echo it:
class ScoreRequest extends FormRequest
{
public function rules(): array
{
return ['quality_score' => ['nullable', 'integer', 'between:1,5']];
}
}
The field submits a plain integer/decimal, so it composes with standard Laravel validation with zero ceremony — that's its main real virtue.
Verify all three rendering modes in a scratch resource rather than assuming:
Rating::score()
->onlyOnForms()
->hideWhenCreating(); // for values stamped by a workflow, not typed
If sorting is supported, it sorts the underlying column — which is what you want, but only if the column type matches. Sorting decimals lexically in JS will bite you. Test ?orderBy=quality_score in the browser; if the index column can't render stars, use ->exceptOnIndex() and add a plain Number for the list view.
it('persists a whole-star score', function () {
$listing = Listing::factory()->create();
$this->put("/nova-api/listings/{$listing->id}", ['quality_score' => 4])
->assertOk();
expect($listing->fresh()->quality_score)->toBe(4);
});
it('rejects out-of-range scores', function () {
// assert 422 with quality_score in the error bag
});
Add a Dusk or Playwright smoke test that loads one Nova form and asserts the star widget mounted. That is the only test that catches the failure mode that actually happens: stale assets, silent degradation.
The field writes a plain integer, so factories need no package awareness:
'quality_score' => fake()->optional(0.7)->numberBetween(1, 5),
If your factory ever needs to know about the rating field, presentation has leaked into the domain.
Gate the field on config so a Nova upgrade can't break a visible form on deploy day:
if (config('features.nova_rating_field')) {
$fields[] = Rating::score();
}
The rollback is a Number field with min/max and a suffix. Document that in the runbook — it costs nothing and it's your entire safety net.
This field fails quietly, and that's the whole risk. A missing asset bundle doesn't throw; the form renders a plain number input or nothing, and submits fine. Users see a worse form and say nothing. Pair every Nova-asset deploy with a one-page smoke check, and put it in the release checklist rather than tribal knowledge.
Pin, vendor, attribute. The honest posture for a 0-star dependency on an admin surface:
composer require wamesk/rating-field:0.1.0
# then copy src/ into app/Nova/Fields/Vendor/Rating/ and drop the composer dep
Check the license permits it (most Nova field packages are MIT). Keep the copyright header and a NOTICE line. Treat it as internal code with a named owner.
Config caching bites on publish. After vendor:publish, run php artisan config:clear — and remember config:cache in production freezes the published config. If star count or colors change with no effect, it's cache, not the package.
Publishing views means forking-lite. If you publish and edit the Vue component or Blade view, upstream changes will never merge cleanly (and there won't be any). Prefer configuration and per-field meta over publish-everything, and note the fork in the runbook.
Half-star type drift is the silent data bug. decimal(2,1) + between:1,5 + integer validation = every half-star rejected with a message users won't read. If you enable half-stars, change the column, cast, validation, and client-side step in one commit, and add a test asserting 3.5 round-trips.
0 vs null on display. If the index renders stars for every row, unrated rows show empty stars indistinguishable from "rated zero." Use ->hideFromIndex() or render an explicit — for null via ->displayUsing().
Accessibility is on you. Star widgets are notoriously keyboard- and screen-reader-hostile. Tab to the field, use arrow keys, and confirm the value is announced. If the component renders <i class="fa fa-star"> with a click handler and no focusable control or ARIA value, wrap or replace it. In an internal tool this is a complaint, not an incident — but it will be a complaint.
Icons and colors: prefer per-field meta over global config. The fluent/meta route avoids republishing to tweak one resource and avoids a config:cache cycle on a theme change.
Add it to the Nova upgrade runbook now. One line: "Rating field (app/Nova/Fields/Rating.php, vendored from wamesk/rating-field) — re-validate form/detail/index under the new Nova major; fallback is Number with min/max." Small blast radius, quiet failure; the runbook entry is the mitigation.
Know when to walk away. If the requirement grows into averages, per-user vote history, "sort by rating," or public-facing stars, this package is done and you're building a domain model. Keep the field as a form input and put aggregation in a query or a cached column of your own design.
How can I help you explore Laravel packages today?