18) { throw new InvalidArgumentException('Invalid scale for EncryptedDecimal.'); } $this->scale = $scale; } public function get($model, string $key, $value, array $attributes) { if ($value === null || $value === '') { // Treat null as zero but do not mutate DB implicitly return number_format(0, $this->scale, '.', ''); } try { $plain = Crypt::decryptString((string) $value); } catch (\Throwable $e) { // If value is not decryptable (legacy/plain), try to normalize as plain string $plain = (string) $value; } return $this->normalize($plain); } public function set($model, string $key, $value, array $attributes) { if ($value === null || $value === '') { return [$key => null]; } $normalized = $this->normalize($value); return [$key => Crypt::encryptString($normalized)]; } private function normalize($value): string { // Accept numeric, string with comma/dot; normalize to string with fixed scale $s = is_string($value) ? trim($value) : (string) $value; $s = str_replace([',', ' '], ['', ''], $s); if (!preg_match('/^-?\d*(?:\.\d+)?$/', $s)) { throw new InvalidArgumentException('Invalid decimal value.'); } if ($s === '' || $s === '-') { $s = '0'; } // Use integer math on scaled value to avoid float precision $scale = $this->scale; // Split integer and fractional part $neg = str_starts_with($s, '-') ? '-' : ''; if ($neg) $s = substr($s, 1); [$int, $frac] = array_pad(explode('.', $s, 2), 2, ''); $frac = substr($frac . str_repeat('0', $scale), 0, $scale); $intPart = ltrim($int === '' ? '0' : $int, '0'); if ($intPart === '') { $intPart = '0'; } $out = ($neg ? '-' : '') . $intPart; if ($scale > 0) { $out .= '.' . $frac; } return $out; } }