Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Number To Words Laravel Package

phpviet/number-to-words

phpviet/number-to-words converts numeric values into readable Vietnamese words. It handles integers, decimals, and currency amounts, making it easy to generate invoices, receipts, and payment notices with properly formatted Vietnamese text. Simple API, framework-agnostic, installs via Composer.

Deep Wiki
Context7

Getting Started

Install

composer require phpviet/number-to-words

Plain PHP — the two-minute version

use PHPViet\NumberToWords\Transformer;

$transformer = new Transformer();

echo $transformer->toWords(123456789);
// "một trăm hai mươi ba triệu bốn trăm năm mươi sáu nghìn bảy trăm tám mươi chín"

Laravel — the package ships a service provider that is auto-discovered, so you can either resolve the transformer out of the container or use the facade:

use PHPViet\NumberToWords\Facades\NumberToWords;

NumberToWords::toWords(105); // "một trăm lẻ năm"

Confirm the exact FQCNs and facade alias in vendor/phpviet/number-to-words/composer.json (extra.laravel) and src/ before you build on them — the surface is tiny, so a two-minute read of Transformer.php is worth more than any docs page.

Where to look first, in this order

  1. src/Transformer.php — this is the package. The whole algorithm is one class; read it end to end so you know exactly which reading conventions it picks (see Gotchas).
  2. composer.json — PHP constraint, illuminate/support constraint, and whether there are any runtime deps at all.
  3. tests/ — this is a 21-star, single-maintainer package last released in October 2021. The test suite is your only real specification; run it locally (vendor/bin/phpunit) on your target PHP version before committing to the dependency.

First use case to ship: the Bằng chữ: line on a Vietnamese invoice, receipt, or quotation. That single field is 90% of why anyone installs this, and it's a perfect smoke test because accounting staff will immediately tell you if the wording is wrong.

Implementation Patterns

1. Wrap it — don't sprinkle the facade everywhere

The package's API is one method and it hasn't changed since 2021. Give yourself a seam so a fork or replacement is a one-file change:

namespace App\Support;

use PHPViet\NumberToWords\Transformer;

class AmountInWords
{
    public function __construct(private Transformer $transformer) {}

    /** Reads a VND amount the way an invoice does: "Một triệu đồng chẵn" */
    public function vnd(int|string $amount): string
    {
        $words = $this->transformer->toWords((int) $amount);

        return mb_convert_case($words, MB_CASE_TITLE, 'UTF-8') . ' đồng chẵn';
    }
}

Bind it as a singleton in a service provider (or just let Laravel autowire it) and inject it. Never call NumberToWords:: from a controller or Blade file.

2. Eloquent accessor for the "in words" column

use Illuminate\Database\Eloquent\Casts\Attribute;

protected function totalInWords(): Attribute
{
    return Attribute::get(
        fn () => app(\App\Support\AmountInWords::class)->vnd($this->total)
    );
}

Add total_in_words to $appends only on the resources that need it — it's a pure-compute field with no cache, so appending it to every list response is wasted CPU on index pages.

3. API resources: keep it explicit and optional

return [
    'total'          => $this->total,
    'total_in_words' => $this->when(
        $request->boolean('with_words'),
        fn () => app(AmountInWords::class)->vnd($this->total)
    ),
];

Printing/payment clients ask for it; dashboards don't. Gate it behind a query flag or a dedicated InvoicePrintResource.

4. Blade component for PDFs

<p class="amount-in-words">
    Bằng chữ: <strong>{{ app(\App\Support\AmountInWords::class)->vnd($invoice->total) }}</strong>
</p>

Critical: Vietnamese diacritics require a Unicode TTF in dompdf/mpdf — DejaVu Sans or Noto Sans, embedded and subset. The default Helvetica will render một as m?t and you'll spend an afternoon blaming the package.

5. Store money as integer đồng

VND has no minor unit, so bigInteger đồng is exact. This sidesteps every float-precision argument about toWords(1e15) and lets you hand the package a clean int:

$table->unsignedBigInteger('total'); // đồng, not a decimal

If you must support decimals (foreign currency), split the integer and fractional parts yourself and render them as two calls — the package has no built-in decimal phrasing.

6. Test with a fixture table, not with the package's output

Don't assert toWords(15) === $someOutputYouCopiedFromThePackage — that tests nothing. Write down what your accountant says is correct and assert that:

#[DataProvider('amounts')]
public function test_vnd_readings(int $amount, string $expected): void
{
    $this->assertSame($expected, app(AmountInWords::class)->vnd($amount));
}

public static function amounts(): array
{
    return [
        [0,         'Không đồng chẵn'],
        [15,        'Mười lăm đồng chẵn'],
        [21,        'Hai mươi mốt đồng chẵn'],
        [105,       'Một trăm lẻ năm đồng chẵn'],
        [1_000_000, 'Một triệu đồng chẵn'],
    ];
}

This table becomes your regression suite the day the package goes stale and you have to fork it.

7. Pair with a validation rule

If you already use phpviet/laravel-validation, Vietnamese-specific rules (phone, citizen ID, bank account) live there and pair naturally with this package — e.g. validating a citizen ID and echoing it back "bằng chữ" on a contract PDF. Keep the two packages independently upgradable.

Gotchas and Tips

It has been frozen since October 2021. That's the headline risk. Before adopting:

  • Run the package's own tests against your PHP target (8.2/8.3/8.4). Old code often trips on implicit-nullable parameter deprecations and dynamic-property warnings.
  • Check the illuminate/support constraint in composer.json. If it caps below your Laravel version, you'll be fighting the resolver — composer require --with-all-dependencies won't save you, you'll need a fork or a replace entry.
  • Budget for vendoring. The algorithm is a few hundred lines with no exotic deps. Copying Transformer.php into app/Support/ and deleting the dependency is a legitimate, low-drama option for a package this size — and it lets you fix wording without waiting for a maintainer who may have moved on.

Convention mismatches are the #1 support ticket. Vietnamese number reading has regional/domain variants and the package picks exactly one:

Case Variants you'll see in the wild
105 một trăm lẻ năm vs một trăm linh năm
24 hai mươi tư vs hai mươi bốn
25 hai mươi lăm (never năm)
1,000,000 một triệu vs một ngàn triệu
0 không vs empty string

There is almost certainly no config file to switch these. Read Transformer.php, note which it produces, and if accounting demands another form, wrap with a targeted str_replace post-processor (fragile) or fork (correct). Decide up front; don't discover it in production.

It will not preserve leading zeros. Feeding it "0123" or a phone number gives you một trăm hai mươi ba — the zeros are gone before the algorithm ever sees them. This package reads quantities, not digits. For phone numbers, ID numbers, or account numbers you want digit-by-digit reading (không một hai ba), which you'd have to write yourself.

Type coercion traps. If the signature is toWords(int $number), passing a "1.000.000" string or a formatted value from your DB is a silent 1 in PHP 7 semantics and a TypeError in strict mode. Cast at the boundary ((int) $amount) and validate the raw input before the cast, or "abc" becomes 0 becomes Không đồng.

Negative and zero. Refunds produce negative totals. Check what toWords(-5) does before it ends up on a credit note — most likely you need to handle the sign yourself and emit Âm ... or restructure the document.

Int range. PHP_INT_MAX is ~9.2×10¹⁸ on 64-bit. Anything approaching that (hyperinflation scenarios, aggregate sums, test fixtures like PHP_INT_MAX) is worth a manual check; on 32-bit PHP the ceiling is 2.1×10⁹, which a real invoice total can exceed.

Capitalisation is on you. The output is lowercase. Don't reach for ucfirst() — it operates on bytes and will mangle the first multibyte character. Use mb_convert_case($s, MB_CASE_TITLE, 'UTF-8') or mb_strtoupper(mb_substr($s, 0, 1, 'UTF-8'), 'UTF-8') . mb_substr($s, 1, null, 'UTF-8').

UTF-8 discipline everywhere downstream. The package emits UTF-8; your DB connection charset (utf8mb4), your Content-Type header, your Excel export library, and your PDF font must all agree. The package is never the bug here, but it's always the first place people look.

Long-running workers / Octane. If the transformer caches parsed dictionaries in instance properties, a singleton resolved once may carry state across requests. It's a one-method, stateless-looking class so this is likely fine — but grep for private array/static before you register it as a singleton under Octane. app(Transformer::class) per call is cheap insurance.

Debugging recipe. When a reading looks wrong, isolate: (a) is the input the integer you think it is — var_dump($n) right before the call; (b) is the reading wrong, or is it your wrapper's suffix/chẵn/capitalisation; (c) does the package's own test suite pass for that number? If (c) is "no", you've found a genuine gap — write the failing case into your fixture table and fix it in the fork.

Prefer the fork over the PR. With 21 stars and a 2021 release cadence, upstreaming a fix is a coin flip. Since the code is MIT and self-contained, own the file, keep the license header, and add your own tests. That's the honest assessment for a dependency this small and this quiet.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels