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

Better Units Laravel Package

asika/better-units

Better Units is a PHP library for working with units of measure. Convert values between compatible units, define custom units, and format quantities consistently in any PHP or Laravel project.

View on GitHub
Deep Wiki
Context7

Getting Started

asika/better-units is a framework-agnostic PHP conversion library — there is no Laravel service provider, no config file to publish, and no facade. Treat it as a pure library you wrap yourself.

Install and verify:

composer require asika/better-units

Then immediately read three things, in this order — with 2 stars and 0 dependents, these are the documentation:

  1. composer.json — confirm the PHP constraint and whether it pulls brick/math, bcmath, or works in plain floats. This single detail determines how you handle precision for the rest of the project.
  2. README.md — the surface API (converter entry point vs. unit value objects).
  3. tests/ — executable examples of every supported unit and alias. When the README is thin, the test suite is the spec.

First use case. The highest-value pattern for this library is canonical storage, converted display: store every measurement in a single base unit in the database (grams, millimeters, seconds), and convert only at the edges — form input, API response, Blade output. That way a later switch to brick/units or php-units-of-measure is a change in one adapter class, not a migration.

A minimal adapter, which is also the right place to start regardless of the package's exact class names:

// app/Support/Units/Converter.php
namespace App\Support\Units;

use Asika\BetterUnits\...; // confirm namespace from src/

final class Converter
{
    public function convert(float $value, string $from, string $to): float
    {
        // wrap the package call here — nothing else in the app touches the package
    }
}

Then bind it once so tests can fake it:

// app/Providers/AppServiceProvider.php
$this->app->singleton(\App\Support\Units\Converter::class);

Before writing any of this, open src/ and identify two things: (a) the object that performs a conversion, and (b) the registry/table that defines supported units. Everything below depends on which of those two is public API.

Implementation Patterns

Wrap, don't sprinkle. The library should appear in exactly one class in your codebase. Every controller, cast, and job calls your adapter. This is the single most important decision with a 2-star dependency — it makes the package replaceable and makes its exceptions yours to control.

Validate units before converting. The package will throw on an unknown unit identifier, and an uncaught library exception in a controller becomes a 500. Validate at the FormRequest boundary against the supported-unit list you derive from the registry:

public function rules(): array
{
    return [
        'weight'      => ['required', 'numeric', 'min:0'],
        'weight_unit' => ['required', Rule::in(app(SupportedUnits::class)->for('mass'))],
    ];
}

Build SupportedUnits from the package registry once and cache it (it is static data — cache it forever, or generate it at build time) so you can feed the same list to <select> dropdowns and API metadata.

Eloquent cast for measurement pairs. Two columns — value (canonical base unit) and unit (the unit the user entered, kept for faithful round-tripping) — plus a cast that exposes a small value object:

class MeasurementCast implements CastsAttributes
{
    public function get($model, $key, $value, array $attributes): ?Measurement
    {
        return $value === null ? null : new Measurement(
            value: (float) $value,
            unit:  $attributes['unit'],
            canonical: 'g',
        );
    }

    public function set($model, $key, $value, array $attributes): array
    {
        return [
            'value' => $value->toCanonical(),   // always grams in the DB
            'unit'  => $value->unit,            // what the user typed
        ];
    }
}

Storing both is worth the extra column: you can render "500 g" back as "1.1 lb" or as the original "500 g" without guessing intent.

Convert at the API boundary, not in queries. Keep ORDER BY value and aggregations in the canonical unit. Convert in the API Resource / presenter:

class ProductResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'weight' => [
                'value' => round($this->weight->to($request->query('unit', 'kg')), 3),
                'unit'  => $request->query('unit', 'kg'),
            ],
        ];
    }
}

This keeps sorting, filtering, and reporting sane, and avoids WHERE-clause conversion entirely (which no units library can help you with anyway).

Batch conversions. Don't construct a converter per row inside a loop over 10,000 records. Construct once, loop over values. If the package exposes a unit value object, prefer the scalar convert($value, $from, $to) path in bulk contexts to avoid 10,000 object allocations.

Testing. The library is small and pure, so your tests should be data-provider driven and assert round-tripping:

#[DataProvider('conversions')]
public function test_converts(float $v, string $from, string $to, float $expected): void
{
    $this->assertEqualsWithDelta($expected, app(Converter::class)->convert($v, $from, $to), 1e-9);
}

Also write one integration test that asserts your storage layer never receives a non-canonical unit — that is the invariant that actually breaks in production.

Reporting and exports are the second natural use case: a spreadsheet export column labelled "Weight (kg)" pulling from gram storage. Same adapter, same conversion, zero new concepts.

Gotchas and Tips

Temperature will break a naive linear converter. °C ↔ °F ↔ K requires an offset, not just a scale factor. Check how the package models this before you promise the client temperature support — if its unit definitions are pure factor multipliers, temperature will be silently wrong. Verify with a known pair: convert(0, 'celsius', 'fahrenheit') must be 32.

Find out what "number" means here. If the package works in native PHP floats, 3.5 kg → lb may yield 7.716179176999999. Never store the converted result; store the canonical value and convert only for display, rounding at the presentation layer (round(), or NumberFormatter for locale-aware output — the library does not do formatting).

Unit identifiers are exact strings. km, KM, kilometer, and kilometre are not interchangeable unless the registry explicitly aliases them. Normalise input (strtolower, trim) in your adapter, and keep a single canonical spelling in your own database. Don't let user-supplied strings reach the library unvalidated.

Zero and negative values. Confirm behaviour on 0 and on negative inputs — weight and length are non-negative in your domain, but a generic converter may accept negatives happily. Enforce your domain rules in validation, not by relying on the library.

No service provider means no auto-discovery surprises — but also no config. There is nothing to publish, no config/units.php in your app. If you need project-specific unit lists (e.g. only the four units your warehouse uses), maintain that list in your config and filter the registry output through it.

Pin the version. With 0 dependents and 2 stars, semver discipline is unproven. Use an exact constraint ("asika/better-units": "1.0.*" or a commit hash) and commit composer.lock. An unreviewed patch release should not be able to change your conversion results.

Have an exit plan. Because all calls go through one adapter, swapping to brick/units or php-units-of-measure is a one-file change plus re-running your data-provider tests. Keep that adapter free of package-specific types in its public signature — accept and return scalars and your own value object.

Debugging an unexpected result. Dump the resolved unit definition before blaming the math: find the registry entry for both the source and target units and compare their factors. Nine times out of ten, the bug is a unit-name mismatch resolving to a slightly different unit (e.g. oz as avoirdupois vs. troy), not the conversion arithmetic.

Exception mapping. Catch the package's exception in your adapter and rethrow as ValidationException (for user input) or a domain exception (for imports/jobs). Users should never see a raw library stack trace.

Where to extend. If the package exposes a registry you can append to, add custom/industry units there rather than forking. If it does not, define the custom unit's factor in your own adapter and delegate the standard units to the package — that keeps the dependency at arm's length.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core