diff --git a/CHANGELOG.md b/CHANGELOG.md index 53626ba..f0da69e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- Full PHPUnit test suite alongside the `.phpt` tests; **100% line coverage**. + +### Changed +- Default unit registration moved from the autoload shim into a testable + `DefaultUnits::register()` class. + +### Fixed +- `humanize()` no longer throws when the registry is only partially populated; + it now skips ladder units that are not registered. + ## [1.2.0] - 2026-07-10 ### Added diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 038b91f..0815244 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,11 +3,16 @@ tests + tests src + + + src/Unit/bootstrap.php + diff --git a/src/Unit/DefaultUnits.php b/src/Unit/DefaultUnits.php new file mode 100644 index 0000000..3d0e050 --- /dev/null +++ b/src/Unit/DefaultUnits.php @@ -0,0 +1,136 @@ +label(), $name, $factor, $dimension, $offset)); + } + + public static function register(): void + { + $length = new Dimension(['L' => 1]); + $mass = new Dimension(['M' => 1]); + $time = new Dimension(['T' => 1]); + $area = new Dimension(['L' => 2]); + $volume = new Dimension(['L' => 3]); + $temperature = new Dimension(['Θ' => 1]); + $speed = new Dimension(['L' => 1, 'T' => -1]); + $data = new Dimension(['B' => 1]); + $force = new Dimension(['M' => 1, 'L' => 1, 'T' => -2]); + $energy = new Dimension(['M' => 1, 'L' => 2, 'T' => -2]); + $power = new Dimension(['M' => 1, 'L' => 2, 'T' => -3]); + $pressure = new Dimension(['M' => 1, 'L' => -1, 'T' => -2]); + $frequency = new Dimension(['T' => -1]); + $angle = new Dimension(['A' => 1]); + + // Length (base unit: metre) + self::add(Name::MM, 0.001, $length); + self::add(Name::CM, 0.01, $length); + self::add(Name::M, 1.0, $length); + self::add(Name::KM, 1000.0, $length); + self::add(Name::INCH, 0.0254, $length); + self::add(Name::FT, 0.3048, $length); + self::add(Name::YD, 0.9144, $length); + self::add(Name::MI, 1609.344, $length); + + // Mass (base unit: kilogram) + self::add(Name::MG, 1e-6, $mass); + self::add(Name::G, 0.001, $mass); + self::add(Name::KG, 1.0, $mass); + self::add(Name::TON, 1000.0, $mass); + self::add(Name::LB, 0.45359237, $mass); + self::add(Name::OZ, 0.028349523125, $mass); + + // Time (base unit: second) + self::add(Name::MS, 0.001, $time); + self::add(Name::S, 1.0, $time); + self::add(Name::MIN, 60.0, $time); + self::add(Name::H, 3600.0, $time); + self::add(Name::DAY, 86400.0, $time); + self::add(Name::WK, 604800.0, $time); + + // Area (base unit: square metre) + self::add(Name::CM2, 0.0001, $area); + self::add(Name::M2, 1.0, $area); + self::add(Name::KM2, 1e6, $area); + self::add(Name::FT2, 0.09290304, $area); + self::add(Name::HA, 10000.0, $area); + self::add(Name::ACRE, 4046.8564224, $area); + + // Volume (base unit: cubic metre) + self::add(Name::ML, 1e-6, $volume); + self::add(Name::L, 0.001, $volume); + self::add(Name::M3, 1.0, $volume); + self::add(Name::GAL, 0.003785411784, $volume); + + // Temperature (base unit: kelvin) — affine scales use factor + offset. + self::add(Name::K, 1.0, $temperature, 0.0); + self::add(Name::C, 1.0, $temperature, 273.15); + self::add(Name::F, 5.0 / 9.0, $temperature, 273.15 - (32.0 * 5.0 / 9.0)); + + // Speed (base unit: metre per second) + self::add(Name::MPS, 1.0, $speed); + self::add(Name::KMH, 1000.0 / 3600.0, $speed); + self::add(Name::MPH, 0.44704, $speed); + self::add(Name::KNOT, 1852.0 / 3600.0, $speed); + + // Data (base unit: byte) + self::add(Name::BIT, 0.125, $data); + self::add(Name::BYTE, 1.0, $data); + self::add(Name::KB, 1e3, $data); + self::add(Name::MB, 1e6, $data); + self::add(Name::GB, 1e9, $data); + self::add(Name::TB, 1e12, $data); + self::add(Name::KIB, 1024.0, $data); + self::add(Name::MIB, 1024.0 ** 2, $data); + self::add(Name::GIB, 1024.0 ** 3, $data); + self::add(Name::TIB, 1024.0 ** 4, $data); + + // Force (base unit: newton) + self::add(Name::N, 1.0, $force); + self::add(Name::KN, 1000.0, $force); + + // Energy (base unit: joule) + self::add(Name::J, 1.0, $energy); + self::add(Name::KJ, 1000.0, $energy); + self::add(Name::CAL, 4.184, $energy); + self::add(Name::KCAL, 4184.0, $energy); + self::add(Name::WH, 3600.0, $energy); + self::add(Name::KWH, 3.6e6, $energy); + + // Power (base unit: watt) + self::add(Name::W, 1.0, $power); + self::add(Name::KW, 1000.0, $power); + self::add(Name::HP, 745.6998715822702, $power); + + // Pressure (base unit: pascal) + self::add(Name::PA, 1.0, $pressure); + self::add(Name::KPA, 1000.0, $pressure); + self::add(Name::BAR, 1e5, $pressure); + self::add(Name::ATM, 101325.0, $pressure); + self::add(Name::PSI, 6894.757293168361, $pressure); + + // Frequency (base unit: hertz) + self::add(Name::HZ, 1.0, $frequency); + self::add(Name::KHZ, 1e3, $frequency); + self::add(Name::MHZ, 1e6, $frequency); + self::add(Name::GHZ, 1e9, $frequency); + + // Angle (base unit: radian) + self::add(Name::RAD, 1.0, $angle); + self::add(Name::DEG, M_PI / 180.0, $angle); + self::add(Name::GRAD, M_PI / 200.0, $angle); + self::add(Name::TURN, 2.0 * M_PI, $angle); + } +} diff --git a/src/Unit/Quantity.php b/src/Unit/Quantity.php index e99446d..9df5a05 100644 --- a/src/Unit/Quantity.php +++ b/src/Unit/Quantity.php @@ -135,19 +135,19 @@ public function humanize(): self return $this; } + $base = $this->toBase(); foreach (self::humanizeLadders() as $symbols) { - if (!UnitRegistry::has($symbols[0])) { + $registered = array_values(array_filter($symbols, UnitRegistry::has(...))); + if ($registered === []) { continue; } - if (!UnitRegistry::get($symbols[0])->dimension->equals($this->unit->dimension)) { + if (!UnitRegistry::get($registered[0])->dimension->equals($this->unit->dimension)) { continue; } - $base = $this->toBase(); - $chosen = $symbols[0]; - foreach ($symbols as $symbol) { - $candidate = UnitRegistry::get($symbol); - if (abs($base / $candidate->factor) >= 1.0) { + $chosen = $registered[0]; + foreach ($registered as $symbol) { + if (abs($base / UnitRegistry::get($symbol)->factor) >= 1.0) { $chosen = $symbol; } else { break; diff --git a/src/Unit/bootstrap.php b/src/Unit/bootstrap.php index 6f2f7c4..eeac815 100644 --- a/src/Unit/bootstrap.php +++ b/src/Unit/bootstrap.php @@ -1,132 +1,16 @@ label(), $name, $factor, $dimension, $offset)); -}; - -$length = new Dimension(['L' => 1]); -$mass = new Dimension(['M' => 1]); -$time = new Dimension(['T' => 1]); -$area = new Dimension(['L' => 2]); -$volume = new Dimension(['L' => 3]); -$temperature = new Dimension(['Θ' => 1]); -$speed = new Dimension(['L' => 1, 'T' => -1]); -$data = new Dimension(['B' => 1]); -$force = new Dimension(['M' => 1, 'L' => 1, 'T' => -2]); -$energy = new Dimension(['M' => 1, 'L' => 2, 'T' => -2]); -$power = new Dimension(['M' => 1, 'L' => 2, 'T' => -3]); -$pressure = new Dimension(['M' => 1, 'L' => -1, 'T' => -2]); -$frequency = new Dimension(['T' => -1]); -$angle = new Dimension(['A' => 1]); - -// Length (base unit: metre) -$register(Name::MM, 0.001, $length); -$register(Name::CM, 0.01, $length); -$register(Name::M, 1.0, $length); -$register(Name::KM, 1000.0, $length); -$register(Name::INCH, 0.0254, $length); -$register(Name::FT, 0.3048, $length); -$register(Name::YD, 0.9144, $length); -$register(Name::MI, 1609.344, $length); - -// Mass (base unit: kilogram) -$register(Name::MG, 1e-6, $mass); -$register(Name::G, 0.001, $mass); -$register(Name::KG, 1.0, $mass); -$register(Name::TON, 1000.0, $mass); -$register(Name::LB, 0.45359237, $mass); -$register(Name::OZ, 0.028349523125, $mass); - -// Time (base unit: second) -$register(Name::MS, 0.001, $time); -$register(Name::S, 1.0, $time); -$register(Name::MIN, 60.0, $time); -$register(Name::H, 3600.0, $time); -$register(Name::DAY, 86400.0, $time); -$register(Name::WK, 604800.0, $time); - -// Area (base unit: square metre) -$register(Name::CM2, 0.0001, $area); -$register(Name::M2, 1.0, $area); -$register(Name::KM2, 1e6, $area); -$register(Name::FT2, 0.09290304, $area); -$register(Name::HA, 10000.0, $area); -$register(Name::ACRE, 4046.8564224, $area); - -// Volume (base unit: cubic metre) -$register(Name::ML, 1e-6, $volume); -$register(Name::L, 0.001, $volume); -$register(Name::M3, 1.0, $volume); -$register(Name::GAL, 0.003785411784, $volume); - -// Temperature (base unit: kelvin) — affine scales use factor + offset. -$register(Name::K, 1.0, $temperature, 0.0); -$register(Name::C, 1.0, $temperature, 273.15); -$register(Name::F, 5.0 / 9.0, $temperature, 273.15 - (32.0 * 5.0 / 9.0)); - -// Speed (base unit: metre per second) -$register(Name::MPS, 1.0, $speed); -$register(Name::KMH, 1000.0 / 3600.0, $speed); -$register(Name::MPH, 0.44704, $speed); -$register(Name::KNOT, 1852.0 / 3600.0, $speed); - -// Data (base unit: byte) -$register(Name::BIT, 0.125, $data); -$register(Name::BYTE, 1.0, $data); -$register(Name::KB, 1e3, $data); -$register(Name::MB, 1e6, $data); -$register(Name::GB, 1e9, $data); -$register(Name::TB, 1e12, $data); -$register(Name::KIB, 1024.0, $data); -$register(Name::MIB, 1024.0 ** 2, $data); -$register(Name::GIB, 1024.0 ** 3, $data); -$register(Name::TIB, 1024.0 ** 4, $data); - -// Force (base unit: newton) -$register(Name::N, 1.0, $force); -$register(Name::KN, 1000.0, $force); - -// Energy (base unit: joule) -$register(Name::J, 1.0, $energy); -$register(Name::KJ, 1000.0, $energy); -$register(Name::CAL, 4.184, $energy); -$register(Name::KCAL, 4184.0, $energy); -$register(Name::WH, 3600.0, $energy); -$register(Name::KWH, 3.6e6, $energy); - -// Power (base unit: watt) -$register(Name::W, 1.0, $power); -$register(Name::KW, 1000.0, $power); -$register(Name::HP, 745.6998715822702, $power); - -// Pressure (base unit: pascal) -$register(Name::PA, 1.0, $pressure); -$register(Name::KPA, 1000.0, $pressure); -$register(Name::BAR, 1e5, $pressure); -$register(Name::ATM, 101325.0, $pressure); -$register(Name::PSI, 6894.757293168361, $pressure); - -// Frequency (base unit: hertz) -$register(Name::HZ, 1.0, $frequency); -$register(Name::KHZ, 1e3, $frequency); -$register(Name::MHZ, 1e6, $frequency); -$register(Name::GHZ, 1e9, $frequency); +use KhaledAlam\Unit\DefaultUnits; -// Angle (base unit: radian) -$register(Name::RAD, 1.0, $angle); -$register(Name::DEG, M_PI / 180.0, $angle); -$register(Name::GRAD, M_PI / 200.0, $angle); -$register(Name::TURN, 2.0 * M_PI, $angle); +DefaultUnits::register(); diff --git a/tests/DefaultUnitsTest.php b/tests/DefaultUnitsTest.php new file mode 100644 index 0000000..3f6ea9a --- /dev/null +++ b/tests/DefaultUnitsTest.php @@ -0,0 +1,63 @@ +assertTrue( + UnitRegistry::has($case->value), + "Unit {$case->value} was not registered" + ); + } + + $this->assertCount(count(Name::cases()), UnitRegistry::all()); + } + + /** + * @return array + */ + public static function conversions(): array + { + return [ + 'km/h -> mph' => [100.0, 'km/h', 'mph', 62.137119, 6], + 'GiB -> MiB' => [1.0, 'GiB', 'MiB', 1024.0, 6], + 'kWh -> J' => [1.0, 'kWh', 'J', 3_600_000.0, 6], + 'mi -> km' => [1.0, 'mi', 'km', 1.609344, 6], + 'atm -> psi' => [1.0, 'atm', 'psi', 14.695949, 6], + 'deg -> rad' => [180.0, 'deg', 'rad', 3.141593, 6], + 'hp -> W' => [1.0, 'hp', 'W', 745.699872, 6], + 'lb -> g' => [1.0, 'lb', 'g', 453.59237, 6], + 'acre -> m²' => [1.0, 'acre', 'm²', 4046.856422, 6], + 'gal -> L' => [1.0, 'gal', 'L', 3.785412, 6], + ]; + } + + #[DataProvider('conversions')] + public function test_conversions_are_accurate( + float $value, + string $from, + string $to, + float $expected, + int $precision + ): void { + $result = Quantity::of($value, $from)->convertTo($to)->getValue(); + $this->assertEqualsWithDelta($expected, $result, 10 ** -$precision); + } +} diff --git a/tests/DimensionTest.php b/tests/DimensionTest.php new file mode 100644 index 0000000..10f63a7 --- /dev/null +++ b/tests/DimensionTest.php @@ -0,0 +1,48 @@ + 0, 'L' => 1]); + $this->assertSame(['L' => 1], $d->exponents); + $this->assertFalse($d->isDimensionless()); + } + + public function test_multiply_and_divide(): void + { + $length = new Dimension(['L' => 1]); + $time = new Dimension(['T' => 1]); + + $speed = $length->divide($time); + $this->assertSame(['L' => 1, 'T' => -1], $speed->exponents); + + $area = $length->multiply($length); + $this->assertSame(['L' => 2], $area->exponents); + } + + public function test_division_by_same_dimension_is_dimensionless(): void + { + $length = new Dimension(['L' => 1]); + $cancelled = $length->divide($length); + + $this->assertTrue($cancelled->isDimensionless()); + $this->assertTrue($cancelled->equals(new Dimension())); + } + + public function test_equals_and_to_string(): void + { + $a = new Dimension(['L' => 1, 'T' => -1]); + $b = new Dimension(['T' => -1, 'L' => 1]); + + $this->assertTrue($a->equals($b)); + $this->assertFalse($a->equals(new Dimension(['L' => 1]))); + $this->assertSame('{"L":1,"T":-1}', (string) $a); + $this->assertSame('[]', (string) new Dimension()); + } +} diff --git a/tests/LaravelIntegrationTest.php b/tests/LaravelIntegrationTest.php new file mode 100644 index 0000000..9538c27 --- /dev/null +++ b/tests/LaravelIntegrationTest.php @@ -0,0 +1,91 @@ +markTestSkipped('illuminate packages are not installed.'); + } + } + + public function test_service_provider_registers_custom_units_from_config(): void + { + $furlong = new Unit('furlong', 'fur', 201.168, new Dimension(['L' => 1])); + + $container = new Container(); + $container->instance('config', new class ($furlong) { + /** @param Unit $furlong */ + public function __construct(private $furlong) + { + } + + public function get(string $key, mixed $default = null): mixed + { + return $key === 'unit.units' ? [$this->furlong, 'not-a-unit'] : $default; + } + }); + + (new UnitServiceProvider($container))->boot(); + + $this->assertTrue(UnitRegistry::has('fur')); + $this->assertEqualsWithDelta( + 201.168, + Quantity::of(1, 'fur')->convertTo('m')->getValue(), + 1e-9 + ); + } + + public function test_cast_round_trips_a_quantity(): void + { + $cast = new AsQuantity(); + $model = new class extends Model { + }; + + // set() from a Quantity + $stored = $cast->set($model, 'distance', Quantity::of(2.5, 'km'), []); + $this->assertSame(['distance' => '2.5 km'], $stored); + + // set() from a string (parsed + normalized) + $fromString = $cast->set($model, 'distance', '5 ft 3 in', []); + $this->assertSame(['distance' => '5.25 ft'], $fromString); + + // set() null + $this->assertSame(['distance' => null], $cast->set($model, 'distance', null, [])); + + // get() hydrates a Quantity + $quantity = $cast->get($model, 'distance', '2.5 km', []); + $this->assertInstanceOf(Quantity::class, $quantity); + $this->assertSame('2500 m', (string) $quantity->to('m')); + + // get() null / empty + $this->assertNull($cast->get($model, 'distance', null, [])); + $this->assertNull($cast->get($model, 'distance', '', [])); + + // get() passes through an existing Quantity + $existing = Quantity::of(1, 'm'); + $this->assertSame($existing, $cast->get($model, 'distance', $existing, [])); + } + + public function test_cast_set_rejects_invalid_type(): void + { + $cast = new AsQuantity(); + $model = new class extends Model { + }; + + $this->expectException(\InvalidArgumentException::class); + $cast->set($model, 'distance', 123, []); + } +} diff --git a/tests/NameTest.php b/tests/NameTest.php new file mode 100644 index 0000000..487cae4 --- /dev/null +++ b/tests/NameTest.php @@ -0,0 +1,35 @@ +label(); + $this->assertNotSame('', $label, "Missing label for {$case->value}"); + $this->assertMatchesRegularExpression('/^[A-Z]/', $label); + } + } + + public function test_labels_are_unique(): void + { + $labels = array_map(static fn (Name $n): string => $n->label(), Name::cases()); + $this->assertSame(count($labels), count(array_unique($labels))); + } + + public function test_specific_labels(): void + { + $this->assertSame('Meter', Name::M->label()); + $this->assertSame('Kilometer per hour', Name::KMH->label()); + $this->assertSame('Gibibyte', Name::GIB->label()); + $this->assertSame('Pound per square inch', Name::PSI->label()); + } +} diff --git a/tests/QuantityEdgeTest.php b/tests/QuantityEdgeTest.php new file mode 100644 index 0000000..61eacee --- /dev/null +++ b/tests/QuantityEdgeTest.php @@ -0,0 +1,87 @@ +expectException(\InvalidArgumentException::class); + Quantity::of(1, 'm', -1); + } + + public function test_subtract_across_units(): void + { + $result = Quantity::of(2, 'm')->subtract(Quantity::of(50, 'cm')); + $this->assertEqualsWithDelta(1.5, $result->getValue(), 1e-9); + } + + public function test_divide_by_zero_quantity_throws(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot divide by a zero quantity.'); + Quantity::of(10, 'm')->divide(Quantity::of(0, 's')); + } + + public function test_division_of_same_unit_cancels_symbol(): void + { + $ratio = Quantity::of(10, 'm')->divide(Quantity::of(2, 'm')); + $this->assertSame(5.0, $ratio->getValue()); + $this->assertSame('', $ratio->getUnit()->symbolString()); + $this->assertTrue($ratio->getUnit()->dimension->isDimensionless()); + } + + public function test_multiply_same_unit_squares_symbol(): void + { + $area = Quantity::of(2, 'm')->multiply(Quantity::of(3, 'm')); + $this->assertSame('m²', $area->getUnit()->symbolString()); + } + + public function test_comparison_helpers(): void + { + $this->assertTrue(Quantity::of(1, 'm')->isLessThan(Quantity::of(200, 'cm'))); + $this->assertFalse(Quantity::of(1, 'm')->isLessThan(Quantity::of(50, 'cm'))); + $this->assertFalse(Quantity::of(1, 'm')->equals(Quantity::of(3, 's'))); + } + + public function test_humanize_returns_self_when_no_ladder_applies(): void + { + // Frequency has no humanize ladder — value/unit stay unchanged. + $freq = Quantity::of(2, 'kHz'); + $this->assertSame('2 kHz', (string) $freq->humanize()); + } + + public function test_humanize_skips_ladders_whose_units_are_not_registered(): void + { + // Only data units registered: humanize must skip the length/mass/etc. + // ladders (their base symbols are absent) and still find the data ladder. + UnitRegistry::clear(); + $data = new Dimension(['B' => 1]); + UnitRegistry::register(new Unit('Byte', 'B', 1.0, $data)); + UnitRegistry::register(new Unit('Kilobyte', 'KB', 1e3, $data)); + UnitRegistry::register(new Unit('Megabyte', 'MB', 1e6, $data)); + + $this->assertSame('2.5 MB', (string) Quantity::of(2_500_000, 'B')->humanize()); + } + + public function test_withPrecision_and_json(): void + { + $q = Quantity::of(1.23456, 'm')->withPrecision(2); + $this->assertSame('1.23 m', (string) $q); + $this->assertSame('{"value":1.23456,"unit":"m"}', json_encode($q)); + } +} diff --git a/tests/UnitRegistryTest.php b/tests/UnitRegistryTest.php new file mode 100644 index 0000000..c2cd579 --- /dev/null +++ b/tests/UnitRegistryTest.php @@ -0,0 +1,48 @@ +assertFalse(UnitRegistry::has('yd')); + + $yard = new Unit('yard', 'yd', 0.9144, new Dimension(['L' => 1])); + UnitRegistry::register($yard); + + $this->assertTrue(UnitRegistry::has('yd')); + $this->assertSame($yard, UnitRegistry::get('yd')); + $this->assertSame(['yd' => $yard], UnitRegistry::all()); + } + + public function test_get_unknown_unit_throws(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unknown unit: nope'); + UnitRegistry::get('nope'); + } + + public function test_clear_empties_the_registry(): void + { + DefaultUnits::register(); + $this->assertNotEmpty(UnitRegistry::all()); + + UnitRegistry::clear(); + $this->assertEmpty(UnitRegistry::all()); + } +} diff --git a/tests/UnitTest.php b/tests/UnitTest.php new file mode 100644 index 0000000..7dfb719 --- /dev/null +++ b/tests/UnitTest.php @@ -0,0 +1,34 @@ + 1])); + + $this->assertSame('m', $unit->symbolString()); + $this->assertSame('m', (string) $unit); + $this->assertSame(0.0, $unit->offset); + } + + public function test_string_symbol_does_not_error(): void + { + $derived = new Unit('derived', 'm/s', 1.0, new Dimension(['L' => 1, 'T' => -1])); + + $this->assertSame('m/s', $derived->symbolString()); + $this->assertSame('m/s', (string) $derived); + } + + public function test_offset_is_retained(): void + { + $celsius = new Unit('Celsius', Name::C, 1.0, new Dimension(['Θ' => 1]), 273.15); + $this->assertSame(273.15, $celsius->offset); + } +} diff --git a/tests/laravel/cast.phpt b/tests/laravel/cast.phpt deleted file mode 100644 index 40eff3d..0000000 --- a/tests/laravel/cast.phpt +++ /dev/null @@ -1,42 +0,0 @@ ---TEST-- -Laravel AsQuantity Eloquent cast round-trips quantities ---SKIPIF-- - ---FILE-- - AsQuantity::class]; - } -}; - -$model->distance = Quantity::of(2.5, 'km'); -echo $model->getAttributes()['distance'] . "\n"; // stored string -echo $model->distance->to('m') . "\n"; // hydrated + converted - -$model->distance = '5 ft 3 in'; -echo $model->getAttributes()['distance'] . "\n"; // parsed + stored -echo $model->distance->humanize()->format(4) . "\n"; // hydrated + humanized - -$model->distance = null; -var_dump($model->distance); -?> ---EXPECT-- -2.5 km -2500 m -5.25 ft -1.6002 m -NULL