francescobruno-cmv/decimal-input
Decimal Input for Laravel provides a dedicated form field for decimal numbers, validating, formatting, and safely casting user input to and from PHP values so money and precision-sensitive fields stay consistent across Blade forms, requests, and models.
Read the source before you write a line of Blade. At 0.0.1 the release notes are an auto-generated changelog link and there is no README worth the name, so the package's actual contract lives in vendor/francescobruno-cmv/decimal-input/src. Install it pinned, then read three files in this order:
composer require francescobruno-cmv/decimal-input:0.0.1 --prefer-dist
ls vendor/francescobruno-cmv/decimal-input/src
<x-decimal-input>, <x-decimal>, something else), and the FQCN of any validation rule.<input type="text" inputmode="decimal"> (locale-safe, the whole point) or falls back to type="number" (which most browsers refuse to accept a comma in).(float) or does it stay a string/bcmath? If it's a float anywhere on the write path, stop and keep it out of money fields.If the provider declares publishes(), publish and commit the config:
php artisan vendor:publish --tag=decimal-input-config
First use case: one price field on an admin create/edit form. Not invoice line items, not a bulk CSV importer — a single bounded form with a DECIMAL(12,2) column behind it, so you can round-trip the value and prove nothing is being silently truncated before you widen adoption.
<x-decimal-input
name="unit_price"
:value="old('unit_price', $product->unit_price)"
:precision="2"
:min="0"
step="0.01"
/>
Attribute and alias names above are the conventional shape — confirm them against the component class. Then pair it with a request-level guard so the field is validated server-side even if someone posts to the endpoint directly.
Normalise in prepareForValidation, validate with the rule, persist via a cast. Three separate layers, and the value crosses the string → number boundary exactly once:
class StoreProductRequest extends FormRequest
{
protected function prepareForValidation(): void
{
$this->merge([
'unit_price' => $this->normaliseDecimal($this->input('unit_price'), 2),
]);
}
public function rules(): array
{
return [
'unit_price' => ['required', 'numeric', 'min:0', new Decimal(2)],
];
}
}
Keep the normalisation helper on your own base FormRequest (or a small App\Support\DecimalInput adapter) rather than calling the package class from every controller. That adapter is your escape hatch: if the package stalls at 0.0.1, you reimplement ~100 lines behind the same method signature and nothing else moves.
Put display formatting in the model, not the Blade file. The DB column stores a canonical DECIMAL(12,2); the component renders a locale-formatted string; the model is the only place that knows how to cross between them:
protected $casts = ['unit_price' => 'decimal:2'];
public function unitPriceForInput(): string
{
return number_format((float) $this->unit_price, 2, $this->decimalSeparator(), '');
}
Old-input round-trip. The most common day-one bug is a failed validation re-rendering a different value than the user typed, because the component formats on the way back in. Decide explicitly: either pass old('unit_price') raw and let the component parse it, or normalise it the same way prepareForValidation does. Never mix the two paths.
Locale is a per-request decision, not a config constant. If the app serves both en-US and de-DE, resolve the separator from the authenticated user's locale (or the session) and feed it to both the component and the normaliser. A config-level default is a fallback, not the answer.
Wrap it once. Register your own <x-money-input> that internally delegates to the package component with your house defaults (precision 2, min=0, step=0.01, inputmode="decimal"). Every form in the codebase then targets your alias, and swapping the underlying package later is a one-file change.
Where not to use it: API endpoints receiving strict JSON decimals, console commands, imports. Those are machine inputs with no separator ambiguity — routing them through a human-input formatter adds a failure mode for zero benefit.
^0.0.1 in a 0.0.x line effectively means 0.0.1 today but no stability promise tomorrow. Use "francescobruno-cmv/decimal-input": "0.0.1" and treat every bump as a review-required change.brick/math, bcmath strings, or integer minor units are safe; float is not.step and precision must agree. precision=2 with step="0.1" makes browsers with native constraint validation reject 12.34 with a vague "Please enter a valid value" — and if your form carries novalidate, the same mismatch is silently ignored instead. Keep them derived from one source of truth."1.234,56", "1,234.56", "1 234,56", "1\u{00A0}234,56" (non-breaking space), ".5", "1e3", "-0", empty string, and null. What comes out the other side is the package's real spec, and 0.0.1 almost certainly doesn't document it.0. For a nullable DECIMAL column, an empty input must become null, not 0.00. Check what the normaliser returns for "" — this bug shows up months later as zero-priced line items.12.345 against precision=2 may round half-up, truncate, or throw. Test it, and decide which you actually want; round-half-up at the boundary is usually correct for money, but truncation is common in naive implementations and it corrupts totals quietly.type="number" ignores commas in nearly every locale. If the component ever renders type="number", users typing 1,50 get an empty field posted and your required rule fires on a value they can see on screen. Verify the rendered markup; prefer type="text" inputmode="decimal"."1.234,56" through the form path, re-read from the database, re-render into the input, and assert you get "1.234,56" back and a DECIMAL value of 1234.56. Unit tests on the parser alone will not catch a driver-level truncation.php artisan config:cache after publishing config, and remember that a cached config masks a missing .env key — the component will render with the package default precision in production and your locally-tested formatting will look different.aria-*/label association. At this maturity, budget for adding your own <label for> and error-text wiring rather than assuming it's there.0.1.0 — or immediately if you see a commit touching the parser.How can I help you explore Laravel packages today?