henriquemattia/moeda-br
A Laravel package for working with Brazilian currency (BRL). Format numbers as R$, parse localized money strings, and convert amounts using simple, reusable helpers. Ideal for e-commerce, invoices, and financial dashboards, it keeps monetary values consistent across your application.
Confirm you are on Nova first. Despite the package description talking about "framework-agnostic PHP" and "parsing strings into floats", moeda-br is a Laravel Nova form field that applies a Brazilian currency input mask. If you are not running Laravel Nova (a paid license), this package has nothing to offer you — use NumberFormatter with pt_BR instead.
composer require henriquemattia/moeda-br
Read the source before the README. With 0 stars and a single maintainer, the entire package is likely a few hundred lines: a field class in src/ and a compiled input mask in dist/js/field.js. That dist/js file is the ground truth for what the mask actually does (decimals, minus sign, paste handling). Verify the exact class name and namespace there — the package description does not match the code.
First use case: a price field on a Nova resource.
// app/Nova/Product.php (verify namespace in src/)
use Henriquemattia\MoedaBr\MoedaBr;
public function fields(NovaRequest $request): array
{
return [
// ...
MoedaBr::make('Preço', 'price_cents')
->rules('required', 'integer', 'min:0'),
];
}
Decide your storage format on day one. Nova mask fields submit the masked string ("1.234,56"), not a number. Store cents as unsignedBigInteger and convert at the model boundary — do not store "1.234,56" in a decimal column and hope.
Wrap the field in your own class. This is the single highest-value pattern for a 0-star dependency: it lets you add rules, resolution logic, and help text in one place, and lets you swap the package out later without touching 40 resources.
namespace App\Nova\Fields;
use Henriquemattia\MoedaBr\MoedaBr;
final class BrlMoney extends MoedaBr
{
public static function make($name, $attribute = null, $resolveCallback = null): static
{
return parent::make($name, $attribute, $resolveCallback)
->rules('required', 'integer', 'min:0', 'max:99999999999')
->help('Valores em reais. Use vírgula para centavos.')
->resolveUsing(fn ($value) => $value === null
? null
: number_format($value / 100, 2, ',', '.')); // cents -> "1.234,56"
}
}
Normalize on the way in with a model mutator (not fillUsing). The mask runs in the browser; the server receives a string like "1.234,56". Parse it once, centrally:
protected function priceCents(): Attribute
{
return Attribute::make(
set: function ($value) {
if ($value === null || $value === '') {
return null;
}
// "R$ 1.234,56" | "1234,56" | "1234.56" -> 123456
$clean = preg_replace('/[^\d,\-]/', '', (string) $value);
$clean = str_replace(',', '.', $clean);
return (int) round(((float) $clean) * 100);
},
);
}
Keep formatting outside Nova duplicated deliberately. Nova fields only exist inside the admin panel. For API responses, PDFs, exports, and emails, use a shared helper rather than reaching for the package:
function brl(int $cents): string
{
return 'R$ ' . number_format($cents / 100, 2, ',', '.');
}
Pair with a Form Request sanitizer for non-Nova entry points. If the same value can also be edited through an API or Livewire, put the parsing in prepareForValidation() so both paths share one parser — not in the field.
Test the mask boundary, not the mask. Unit-test the accessor/mutator with a data provider covering "1.234,56", "1234,56", "1234.56", "R$ 0,00", "", "-10,00", and "1.234.567,89". The browser mask is untestable in PHP; the parsing contract is what breaks in production.
Reuse across resources via one static factory. If you have invoices, products, subscriptions, and payouts all holding money, expose BrlMoney::make() everywhere so a future change (decimals, help text, rules) is one edit.
R$ formatting and parsing helpers. The repository is a Nova field. Any plan built on "we'll call MoedaBr::format() from a job" will collapse. Grep src/ for public static methods before committing to an architecture."1.234,56" (MySQL stores 1.23 silently, or errors depending on STRICT_MODE). Assert the stored column type and cast."", "0,00", or the un-formatted value when untouched. Decide explicitly whether "no price" is null or 0 and write the test; Nova's edit form will exercise this path constantly.-, so a user editing a -10,00 row may see the sign silently dropped on save. Test editing a negative record; if it's broken, block negatives in validation and manage them via a separate field.R$ 1.234,56 or 1234.56 from a spreadsheet produces garbage the mask can't interpret. Defend on the server with the sanitizer above; never trust the client-side mask to produce valid input.dist/js/field.js — if it was compiled against an older Nova webpack/Vue build, it may not render at all in the panel. Smoke-test in the browser before rolling out to more than one resource.Currency::make('Preço')->currency('BRL'), which handles numeric storage and formatting natively. This package's only added value is the pt-BR input mask UX. If your team is comfortable typing 1234.56, you may not need a 0-star dependency at all.dd($request->all()) inside a FormRequest is the fastest way to see the real submitted value. If the field "isn't saving", the problem is almost always the payload format, not the package."henriquemattia/moeda-br": "~3.2.0" and route every usage through your App\Nova\Fields\BrlMoney subclass. That way, if the package goes stale or breaks against Nova 6, you migrate to a two-line custom field instead of a refactor.php artisan nova:publish (and a frontend build) is required for the field's compiled JS to be served; a stale public/vendor/nova is a classic "works on my machine, blank field in staging" cause.How can I help you explore Laravel packages today?