assoconnect/absolute-percent-value-bundle
Represents either an absolute value or a percent, with a type (PERCENT or ABSOLUTE) and a positive numeric string value; percent values must be under 10000.
1. Install
composer require assoconnect/absolute-percent-value-bundle
PHP 8.2+ is required (support for older versions was dropped in v1.4.0). The package is tiny — no Laravel service provider exists, nothing to register, nothing to publish.
2. Read one file first
Ignore the bundle plumbing (DependencyInjection/, the Symfony serializer normalizer) and open src/Object/AbsolutePercentValue.php. That class is the package. Budget ten minutes: check the constructor/factory names, the constants for PERCENT / ABSOLUTE, the getType()/getValue() accessors, and the exact exception thrown on invalid input. Everything else in this assessment follows from that file.
3. Understand the unit before writing a line of code
The README's constraint — "value must be less than 10000 (100%)" — tells you the value is stored in basis points, not percent. 100 means 1%, 1500 means 15%. The value is always a string (a numeric positive string), never a float, and it carries no currency.
4. First use case: a discount that can be "$10 off" or "15% off"
This is the whole reason the package exists — one column/field that models two shapes of the same business concept.
use Assoconnect\AbsolutePercentValueBundle\Object\AbsolutePercentValue; // verify namespace in the file header
$fixed = AbsolutePercentValue::absolute('1000'); // 10.00 in your minor unit
$percent = AbsolutePercentValue::percent('1500'); // 15% (basis points)
Then apply it yourself — the package deliberately does not know how to apply a value to a base amount:
function discountedTotal(AbsolutePercentValue $v, int $baseMinor): int
{
return $v->getType() === AbsolutePercentValue::PERCENT
? intdiv($baseMinor * (int) $v->getValue(), 10000)
: max(0, $baseMinor - (int) $v->getValue());
}
Wire the first use case into a single feature (a coupon or a promotion on one model), assert the boundary values (0, 100, 10000), and only then think about persisting it.
Persist it as two columns, not one
Store discount_type (PERCENT/ABSOLUTE) and discount_value (string/unsignedBigInteger) side by side. This keeps your SQL sortable/filterable and avoids lock-in to the package's serialization format. A JSON blob couples your schema to a one-star dependency.
Custom Eloquent cast at the boundary
class AbsolutePercentValueCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes): ?AbsolutePercentValue
{
$type = $attributes[$key.'_type'] ?? null;
if ($type === null || $value === null) {
return null;
}
return AbsolutePercentValue::{$type === 'PERCENT' ? 'percent' : 'absolute'}($value);
}
public function set($model, $key, $value, $attributes): array
{
return [
$key.'_type' => $value?->getType(),
$key => $value?->getValue(),
];
}
}
Validate in the FormRequest, construct in the DTO
Don't let InvalidArgumentException leak as a 500. Validate the shape with Laravel's rules, then construct the VO inside a try in your DTO/action and convert the failure into a validation error:
'type' => ['required', Rule::in(['PERCENT', 'ABSOLUTE'])],
'value' => ['required', 'string', 'regex:/^\d+$/'],
// plus: if type === PERCENT, value must be < 10000
Rules like required_if / prohibited_if on the type field get you most of the way; the package's own check is the second line of defence, not the first.
Normalize at the edge, compute with integers
Treat the VO as a transport/domain-boundary type. The moment a service needs to do arithmetic, flatten it to an int (plus an explicit currency/unit) and work with intdiv/bcmath. Never let the VO wander into a pricing engine.
Serialize explicitly in API Resources
return ['type' => $this->discount->getType(), 'value' => (string) $this->discount->getValue()];
Do not rely on the bundled Symfony normalizer. Laravel doesn't autowire Symfony's serializer, so unless you install and configure symfony/serializer yourself, that integration point is inert — and it drags a framework you don't use into your dependency graph.
Pair with a money library for anything user-facing
The VO has no currency, no rounding, no precision policy. For 15% off €19.99, use moneyphp/money or brick/money for the amount and rounding mode, and use this package only to describe which kind of adjustment is being applied.
Keep it behind your own interface
interface DiscountValue { public function apply(int $baseMinor): int; }
One adapter class wraps AbsolutePercentValue. When this package dies (see below), you swap a 40-line enum + readonly class in behind the same interface and nothing else changes.
ServiceProvider, no config merge, no DI extension that Laravel will ever run. Autoloading works fine, but ~half the package is dead weight. Vendor-lock risk is low precisely because so little of it is used.1500 = 15%, 100 = 1%. If your forms accept "15" for 15%, multiply by 100 on the way in — or you'll ship a 0.15%-off bug that QA will not catch. Put the conversion in exactly one place.10000 is the boundary. README says the percent value must be less than 10000, so a literal 100% may throw. Test it; if you need "100% off", special-case it outside the VO or fork the check.(float) it — you'll lose precision and reintroduce the float-money bug the design was avoiding. Cast to int and use intdiv, or use bcmath if you allow fractional basis points.denormalize return type, which implies static factories plus a serializer normalizer. Confirm the exact factory method names in src/Object/AbsolutePercentValue.php before writing ten call sites — the README doesn't document them."1.5", leading +, huge values) are unverified. Write your own table-driven tests around those inputs and keep them adjacent to your cast."assoconnect/absolute-percent-value-bundle": "^1.5" and let CI tell you when it breaks, rather than floating *.100 vs 10000 scaling mistake upstream in a form request, not a package bug.How can I help you explore Laravel packages today?