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.
Architecture fit. This is a single-purpose Filament form field: one Field subclass plus one Blade view, no service provider registration and no config publish in the common case. It captures how a quantity is entered — a bounded number with step, precision, and increment/decrement affordances — and nothing about what the quantity means. Reservations, per-warehouse availability, tiered pricing, and unit-of-measure conversion remain in the domain layer regardless of what renders the input. For a TPM, that is the right shape: the dependency is a leaf node in the admin UI, not a load-bearing architectural commitment. The package's own documentation extends this to a combined value + unit field, which is a meaningful scope decision — storing units is a schema choice made on day one and is materially costlier to reverse than the dependency itself.
Integration feasibility. Integration is a one-line install plus a fluent field definition:
composer require lara-zeus/quantity
Quantity::make('quantity')
->label('Stock adjustment')
->minValue(1)
->maxValue(500)
->default(1)
->required(),
There is no migration, no configuration, and no wiring. Realistic ramp-up to a first working drop-in is under an hour, provided the version matrix resolves. The documented use cases — stock levels, measurements, order quantities, seat/provisioning counts, recipe quantities — map to a recognizable cluster of internal screens that most Filament panels eventually need, so one decision covers several roadmap items with identical interaction.
Technical risk. The risks are concentrated and identifiable, which makes them manageable:
composer.json constraints against the target panel before a ticket is written; discovering incompatibility mid-sprint is the expensive version of this mistake.minValue/maxValue are client-side affordances and Filament validation rules at best. A screen that accepts a quantity the warehouse cannot fulfil because "the field had a max" is a live failure mode.decimal(10,3) column with a field emitting two decimals truncates measurements silently until a report is wrong.content paths missing the vendor directory, produce the classic silent breakage: the field renders unstyled but functional.Key questions to resolve before commitment.
composer.json pin for filament/filament, livewire/livewire, laravel/framework, and php — and does it resolve cleanly against our panels today?getStep() and getPrecision(), and do they agree with our column types and Eloquent casts?content?wire:model.live.debounce is roughly one to two days — that is the ceiling on this dependency's downside.Stack fit. The package targets Laravel + Filament 3 + Livewire 3 panels. If the panel is on that stack, fit is essentially perfect (a native Field subclass participating in normal schema, validation, and state handling). If the panel is on Filament 4, stop — this is not a compatibility warning but a re-scope. Confirm the exact namespace (LaraZeus\Quantity\...) and the fluent surface from the README rather than guessing from memory.
Wrap it, always. The highest-leverage day-one decision is a thin internal field component extending the upstream class, so product teams consume a stable internal API and a fork-shaped escape hatch exists from the start:
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();
}
}
Now "our quantity field" is one file, and swapping to a hand-rolled Alpine stepper is hours confined to that file rather than a grep across every resource. Keep the number of named variants small — two or three (StockQuantity, UnitCount, Measurement) is the sweet spot; fifteen signals that options should be passed instead of subclassed. Where the package's unit selector is used, store the unit in a separate, queryable column, not packed into a string, and decide immediately whether unit participates in any pricing or stock lookup key.
Migration path. There is no data migration inherent to adopting the field. The migration-shaped decisions are ours: column types and precision for quantity values, and whether a unit column exists. Set precision explicitly rather than relying on the component default, and align the field, the column type, and the Eloquent cast in one review. Treat any existing hand-rolled steppers or plain number inputs as candidates for consolidation, but convert them per-resource under the shared wrapper rather than in one sweep.
Compatibility and sequencing. Recommended order:
Repeater order-line or receiving editor only after the flat field is proven. This is where the field earns its keep and where row-scoped reactivity matters: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($state * $price, 2));
}),
])
->columns(3),
->live() only where something genuinely depends on the value.Prefer afterStateUpdated over computed properties for dependent UI, and keep maxValue and ->rules() fed by the same expression so client bounds and server rules cannot drift.
Validation architecture — two layers, deliberately. The field's minValue/maxValue plus Filament's ->rules() passthrough form the input-affordance layer: immediate feedback, typo prevention. The authoritative check lives in the action or domain service that mutates stock, with a database constraint where the invariant is real. Concurrency is explicitly out of scope — if two admins adjust the same stock row, that requires lockForUpdate() in a transaction or an atomic decrement(); no form field substitutes.
Maintenance. Ongoing cost is low and largely passive. The concrete obligations are: pin the upstream version in composer.json, keep the package on the dependency-review list, watch release notes for behavioural changes (3.0.2's delayed live update is the template), and keep the wrapper as the only integration surface. Prefer subclassing the wrapper for styling and behaviour over publishing and editing vendor views — a published view quietly diverges on the next composer update and nobody notices until a layout breaks. Because the surface area is a single field, "fork and vendor" is genuinely viable if upstream stalls.
Support. No SLA, single maintainer, negligible community. Support is entirely internal, which is acceptable for an admin-side input control and would not be for anything customer-facing or data-mutating. Plan against the feature set visible today: unit conversion, per-warehouse rules, and richer UX are not coming on request, and no roadmap item should assume otherwise. Document the fallback (one to two days of Alpine work) so the decision is reversible without debate.
Scaling. The component itself does not scale-load anything; it is a form control. What scales is the convention: one component with a documented option set means five squads render and validate quantities identically, which is a quality and consistency lever rather than a customer-facing differentiator. The scaling limits worth watching are process limits — every quantity field becoming ->live() turns form editing into a round-trip-per-change problem, and per-click server-side validation or per-click test assertions are now wrong under the batched update model. Add one explicit test that hammers the increment control in quick succession and asserts the final bound value and request count; it is cheap insurance and directly targets the 3.0.2 regression surface.
Failure modes to instrument or guard.
| Failure mode | Symptom | Guard |
|---|---|---|
| Filament 4 panel | Install or runtime break | Version-matrix check before ticketing |
| Stale/uncompiled CSS | Field renders unstyled but functional | Include vendor path in Tailwind content; verify after each bump |
| Over-max quantity accepted server-side | Warehouse cannot fulfil an accepted order | Authoritative validation in the action/domain layer + DB constraint |
| Precision truncation | Measurements silently lose digits; reports wrong | Set precision explicitly; align field, column type, cast |
| Batched live updates | Dependent totals feel laggy or tests assert per-click requests | Re-verify dependents; add rapid-click test |
| Concurrent stock adjustment | Lost update on the same row | lockForUpdate() or atomic decrement() in the domain layer |
| Locale decimal separator | Field renders fine but rejects 1,5 |
Verify numeric input behaviour on non-English panels before standardizing fractional quantities |
| Upstream abandonment | No fixes, no Filament 4 support | Pinned version, wrapper boundary, documented Alpine fallback |
Ramp-up. Under an hour to a first working drop-in for an engineer already fluent in Filament; half a day including the wrapper component and the three-point proof on a real screen. The first hour should be spent reading the package's composer.json, then the src/ directory and Blade view — specifically to confirm the serialization shape and the getStep()/getPrecision() defaults — rather than trusting the README's option list.
Overall posture. A narrow, low-regret buy: days saved rather than months, blast radius confined to a single form control, MIT licensed, and reversible within two days. The decision is not "is this a good package" but "are we on Filament 3, and do we accept single-maintainer risk for an admin input." If the first answer is yes and the second is mitigated by the wrapper and a pinned version, adopt it and move on. This belongs nowhere near a customer-facing roadmap slide; it is a consistency and quality play for back-office and operations tooling.
How can I help you explore Laravel packages today?