lara-zeus/quantity
Lara Zeus Quantity is a Laravel package for working with quantities and units of measure. Define units, convert values between them, and format quantities cleanly, making it easy to handle product weights, lengths, and other measurements in your applications.
Verify the version matrix before you write a line of code. This is a Filament 3–era package on a low-velocity release line (3.0.2 is docs restructuring, two CI bumps, and one behavioural fix). Diff its composer.json constraints for php, laravel/framework, filament/filament, and livewire/livewire against your panel first. If you're on Filament 4, this is a hard stop, not a warning. On a Filament 3 / Livewire 3 panel, composer require lara-zeus/quantity resolves cleanly — there is no service provider to register and no config to publish for the common case.
Where to look first, in this order:
vendor/lara-zeus/quantity/composer.json — the constraints, as above.vendor/lara-zeus/quantity/src/ — the field class is the entire package. Read it, don't skim the README's option list. You specifically want to know: what the class is actually named and namespaced, whether it extends Filament's Field or TextInput, which fluent methods genuinely exist, what the defaults are for step and precision, and how the value serializes (one scalar, or number + unit as an array/string). That last answer dictates your migration shape.resources/views/ — this tells you what the markup is, where the increment/decrement buttons live, and whether the delay introduced in 3.0.2 is configurable from PHP or hard-coded in the template.First use case: one flat quantity field on one internal resource. Not a Repeater row, not an order-line editor. Pick the lowest-blast-radius screen you have — an inventory adjustment or a receiving form — put the field behind a feature flag, and prove three things in one sitting: it renders inside your panel theme (not unstyled), it round-trips a value to the database unchanged, and the increment/decrement controls behave under rapid clicking.
use LaraZeus\Quantity\Quantity; // confirm exact namespace from source
Quantity::make('quantity')
->label('Quantity')
->minValue(1)
->maxValue(500)
->default(1)
->required(),
The boundary, said once. This package captures how a quantity is entered — a bounded number with a step, precision, and stepper controls. It does not know what a quantity means. Reservations, tiered pricing, per-warehouse availability, unit-of-measure conversion: all of that lives in your domain layer and stays there regardless of which widget renders the input. Every decision below follows from that line.
Wrap it from day one. The highest-leverage decision you'll make is a thin internal field that extends the upstream class, so product teams consume a stable internal API and you retain a fork-shaped escape hatch.
namespace App\Filament\Forms\Components;
use LaraZeus\Quantity\Quantity;
class StockQuantity extends Quantity
{
protected function setUp(): void
{
parent::setUp();
$this->label('Quantity')
->minValue(1)
->maxValue(9999)
->step(1)
->default(1)
->required();
}
}
Swapping to a hand-rolled Alpine stepper with wire:model.live.debounce later is then hours of work in one file, not a grep across every resource.
One component, many resources. Inventory counts, receiving lines, provisioning/seat counts, recipe quantities, order quantities — all should reach for StockQuantity (or two or three siblings like UnitCount, Measurement) rather than each resource hand-rolling bounds. Two or three named components is the sweet spot; fifteen means you should be passing options instead of subclassing.
Inside a Repeater for order lines — this is where it earns its keep and where the sharp edges are. The state is scoped per row, so anything reactive must be row-aware:
Repeater::make('lines')
->schema([
Select::make('product_id')->options(Product::pluck('name', 'id'))->live(),
StockQuantity::make('quantity')
->live(onBlur: true)
->afterStateUpdated(function ($state, $set, $get) {
$price = Product::find($get('product_id'))?->price ?? 0;
$set('subtotal', round((float) $state * $price, 2));
}),
])
->columns(3),
Prefer afterStateUpdated over computed properties for dependent UI. If a line subtotal or a cart badge recomputes from the quantity, hang it off the field's callback rather than deriving it from the model value elsewhere. Reading state inside the callback keeps you on the supported path — and since 3.0.2 batches the increment/decrement update, the recompute now happens on a cadence, not per tap (see Gotchas).
Live vs. deferred is a per-field decision, not a default. Leave the field deferred for a plain stock input. Add ->live() only where something genuinely depends on the value — you're paying a round-trip per debounced change. Don't reflexively make every quantity field live.
Validation: two layers, deliberately. Use minValue/maxValue plus Filament's ->rules() passthrough for the input affordance layer — it stops typos and gives immediate feedback. Then re-validate the same bounds server-side in the action or domain service that actually mutates stock. maxValue(10) is not a stock check:
StockQuantity::make('quantity')
->minValue(1)
->maxValue($record->available_stock) // optimistic UX only
->rules(['integer', 'max:' . $record->available_stock])
->required(),
If you use the unit selector, store the unit as its own column. A second column is queryable and report-friendly; packing "5 kg" into one string is not. Decide on day one whether the unit participates in the key for any pricing or stock lookup — that schema choice is far costlier to reverse than the dependency.
Read-only and display contexts. Reuse the same component with ->disabled() in infolists rather than formatting numbers differently in a TextEntry. Format consistency is half the reason to adopt this package at all.
Testing. Filament fields test like any other schema element:
livewire(CreateAdjustment::class)
->fillForm(['quantity' => 5])
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas('adjustments', ['quantity' => 5]);
Then add one explicit test that hammers the increment control in quick succession and asserts the final bound value and the request count. That's the regression target created by 3.0.2 and it's cheap insurance against a silent behaviour change on the next bump.
The 3.0.2 change deserves real QA attention. Increment/decrement now delays the live update so rapid clicking doesn't fire a request per tap. Consequences to re-verify in your own suite: any test asserting one request per click, any per-click server-side validation, and any dependent UI (line subtotal, running total, badge) that now observes batched updates. Check whether the delay interval is configurable from the field and what the default is — it directly sets how laggy a dependent total feels.
Bounds are UX, never enforcement. minValue / maxValue / step are client-side affordances and Filament validation at best. The failure mode to watch for is a screen shipping that accepts a quantity the warehouse can't fulfil because "the field had a max." Re-validate in the action/domain layer, and add a database constraint if the invariant is real.
Precision and casts are a silent data-loss vector. Confirm the field's precision default from source and make it agree with your column type and Eloquent cast. A decimal(10,3) column behind a field emitting two decimals will truncate a measurement happily, and nobody notices until a report is wrong. Set precision explicitly rather than inheriting a default.
Keep client bounds and server rules in agreement by construction. When maxValue comes from a record ($record->available_stock), feed the identical expression into ->rules(). Two hand-typed numbers drift within a sprint.
Concurrency is out of scope — build the guard yourself. The component captures an entered number; it does not make decrements race-free. Two admins adjusting the same stock row need lockForUpdate() inside a transaction or an atomic decrement(). No form field will save you there.
Locale and decimal separators. On non-English panels, confirm what the underlying numeric input accepts as a decimal separator before standardizing on fractional quantities. A field that renders perfectly but rejects 1,5 generates a very specific and confusing support ticket.
Assets are the classic silent breakage. Confirm whether CSS is precompiled or published: stale published assets after a version bump, and Tailwind content paths that don't include the vendor directory, both produce a field that renders unstyled but functional. If it looks wrong and behaves right, check this before anything else.
Where to extend. The Blade view is the extension point for markup and styling; the field class is the extension point for behaviour. Prefer subclassing your internal wrapper over publishing and editing vendor views — a published view quietly diverges on the next composer update and you won't notice until a layout breaks.
Plan around the bus factor rather than arguing about it. Single maintainer, small adoption, no SLA. Mitigations are cheap: the internal wrapper component, the upstream version pinned in composer.json, the package on your dependency-review list, and a documented fallback — a thin Alpine stepper with wire:model.live.debounce is one to two days, not a quarter. Because the surface area is a single field, "fork and vendor" is genuinely viable here.
Don't wait for the roadmap. 3.0.2 is housekeeping plus one behavioural fix. Read that as stable and cared for, not actively expanding. Plan against the feature set visible in the source today; unit conversion, per-warehouse rules, and richer UX are not arriving on request.
How can I help you explore Laravel packages today?