Bu kılavuz, brick/money kütüphanesini kullanarak yüksek hassasiyetle finansal hesaplamalar yapmak için Laravel uygulamalarında nasıl kullanılacağını açıklamaktadır. Özellikle, çoklu para birimi kullanan muhasebe uygulamaları için gerçek bir uygulama ile desteklenmiştir.
İçindekiler
İçindekiler
Neden Float Değil?
Neden Float Değil?
PHP’de (ve neredeyse tüm dillerde) float türlerinin bilinen bir hassasiyet sorunu vardır:
// PHP floating point surprise
echo 0.1 + 0.2; // 0.30000000000000004
// Para bağlamında: Rp 100.000 x %10.5 vergi
$harga = 100000;
$pajak = $harga * 10.5 / 100;
// Sonuç şu olabilir: 10500.000000000002
Muhasebe uygulamalarında, 1 rupiah’lık bir fark bile kayıtların dengesiz olmasına neden olabilir. brick/money, hesaplamaların her zaman doğru sonuç vermesini sağlayan arbitrary-precision aritmetik kullanarak bu durumu çözer.
Kurulum ve Ayarlama
Kurulum ve Ayarlama
composer require brick/money
Ek bir konfigürasyon gerektirmez – bu kütüphane bağımsızdır ve servis sağlayıcısına ihtiyaç duymaz.
Temel Kavramlar
Temel Kavramlar
brick/money üç ana kavrama sahiptir:
Money — Yuvarlanmış Para Değeri
Money — Yuvarlanmış Para Değeri
use Brick\Money\Money;
$harga = Money::of(150000, 'IDR'); // Rp 150.000
$diskon = Money::of(25000, 'IDR'); // Rp 25.000
$total = $hargaminus($diskon); // Rp 125.000
Money her zaman para biriminin kesin ondalık sayısını taşır (IDR = 0 ondalık, USD = 2 ondalık).
RationalMoney — Sınırsız Hassasiyetle Katmanlı Hesaplamalar
RationalMoney — Sınırsız Hassasiyetle Katmanlı Hesaplamalar
use Brick\Money\Money;
use Brick\Math\RoundingMode;
use Brick\Money\Context\DefaultContext;
// Rounding hatalarını önlemek için rational ile başlayın
$subtotal = Money::of(333333, 'IDR')->toRational();
$pajak = $subtotal->multipliedBy()->dividedBy();
// Sadece son aşamada yuvarlayın
$pajakFinal = $pajak->to(new DefaultContext(), RoundingMode::HALF_UP);
RationalMoney sayıları bir kesir (pay/payda) olarak saklar, böylece yuvarlama işlemine karar verene kadar hassasiyet kaybolmaz.
Context — Para Birimine Göre Yuvarlama Kuralları
Context — Para Birimine Göre Yuvarlama Kuralları
use Brick\Money\Context\CustomContext;
// IDR: 0 ondalık
$contextIDR = new CustomContext(0);
$money = Money::of(1500.7, 'IDR', $contextIDR); // Hata! Önce yuvarlanmalı
// USD: 2 ondalık (standart ISO)
$usd = Money::of(12.50, 'USD'); // Tamam, USD için 2 ondalık varsayılan
Örnek 1: Basit Hesaplama
Örnek 1: Basit Hesaplama
Kullanım durumu: Ödeme ve iadeler sonrası satış bakiyesini hesaplama.
// app/Services/OverReceiptService.php
use Brick\Money\Money;
public function calculateBalance(Sale $sale): Money
{
$sale_amount = Money::of($sale->amount, 'IDR');
$payment_amount = Money::of($sale->payments->sum(), );
$return_amount = Money::of($sale->returns->sum(), );
return $sale_amount
->minus($payment_amount)
->minus($return_amount);
}
public function hasOverpayment(Money $balance): bool
{
return $balance->isNegative();
}
public function getOverReceiptAmount(Money $balance): int
{
return $balance->abs()->getAmount()->toInt();
}
Bu neden önemli?
brick/money kullanmadan, kod şu şekilde yazılabilirdi:
// BUNU YAPMAYIN
$balance = $sale->amount - $payments - $returns;
Görünüşte basit, ancak eğer amount ve payments float ise, çıkarma sonucu hassasiyet hataları içerebilir ve $balance == 0 ifadesi sayı sıfır olmasına rağmen yanlış sonuç verebilir.
Ancak brick/money ile emin olabilirsiniz:
$balance->isZero(); // Doğru
$balance->isNegative(); // Doğru
$balance->isPositive(); // Doğru
Örnek 2: Çok Aşamalı Hesaplama
Örnek 2: Çok Aşamalı Hesaplama
Kullanım durumu: Satın alma emri toplamını, kalemleri, vergileri, yüzde indirimleri, nakliye masraflarını ve ek masrafları hesaplama.
Bu, projedeki en karmaşık modele örnektir — tüm hesaplamalar RationalMoney alanında gerçekleştirilir ve yuvarlama sadece son aşamada gerçekleşir.
// app/Support/PurchaseOrderCalculator.php
use Brick\Math\RoundingMode;
use Brick\Money\Context\DefaultContext;
use Brick\Money\Money;
public function calculate(array $items, array $options): array
{
// 1. RationalMoney içinde akümülatörleri başlatın
$subtotalRational = Money::of(, )->toRational();
$totalTaxRational = Money::of(, )->toRational();
$totalDiscountRational = Money::of(, )->toRational();
$totalFeesRational = Money::of(, )->toRational();
// 2. Yuvarlama olmadan her bir kalemi toplayın
foreach ($items as $item) {
$amount = $item[] * $item[];
$tax = $amount * ($item[] / 100);
$subtotalRational = $subtotalRational
->plus(Money::of($amount, )->toRational());
$totalTaxRational = $totalTaxRational
->plus(Money::of($tax, )->toRational());
}
// 3. Yüzde hesaplaması için subtotal'u yuvarlayın
$subtotal = $subtotalRational
->to(new DefaultContext(), RoundingMode::HALF_UP);
// 4. Yüzde indirim hesaplamak için RationalMoney ile uygulayın
foreach ($discounts as $discount) {
if ($discount[] === ) {
$discountRational = $subtotal->toRational()
->multipliedBy($discount[])
->dividedBy();
} else {
$discountRational = Money::of ($discount[], ->toRational();
}
$totalDiscountRational = $totalDiscountRational
->plus($discountRational);
}
// 5. Grand total — tüm işlemler RationalMoney'da
$shippingCost = Money::of ($options[] ?? 0, $grandTotalRational = $subtotalRational
->plus($totalTaxRational);
->minus($totalDiscountRational)
->plus ($shippingCost->toRational())
->plus ($totalFeesRational);
// 6. Sadece sonunda yuvarlama
$grandTotal = $grandTotalRational
->to (new DefaultContext(), RoundingMode::HALF_UP);
$totalTax = $totalTaxRational
->to (new DefaultContext(), RoundingMode::HALF_UP);
return [
Örnek 3: Çoklu Para Birimi Desteği
Kullanım durumu: Satın alma emrini tedarikçi para biriminden (USD/EUR) IDR’ye dönüştürme.
// Para birimi modeli fabrika metodu sağlar
// app/Models/Currency.php
use Brick\Money\Context\CustomContext;
use Brick\Money\Money;
public function toMoney(float|int|null $amount): Money
{
$rounded_amount = round($amount ?? 0, $this->decimal_places);
$context = new CustomContext($this->decimal_places);
try {
return Money::of($rounded_amount, $this->code, $context);
} catch (\Brick\Money\Exception\UnknownCurrencyException $e)
{
// Özel para birimi kodları için yok
return Money::of($rounded_amount, 'XXX', $context);
}
}
Denetleyici üzerindeki belge geçişi için kullanım:
// app/Http/Controllers/PurchaseOrderController.php
use Brick\Math\RoundingMode;
use Brick\Money\Money;
// PO kalemlerini fatura ile dönüşüm işlemi yapın
$items = collect ($validated[ 'items'])->map (function ($item) use ($currency_code){
return [
'unit_price' => Money::of($item[ 'unit_price' ], $currency_code))
->getAmount()
->toFloat(),
'total_price' => Money::of($item[ 'unit_price' ], $currency_code))
->multipliedBy($item[ 'remaining_invoice_quantity' ], RoundingMode::HALF_UP)
->getAmount()
->toFloat(),
];
});
Önemli noktalar: RoundingMode::HALF_UP kullanımı multipliedBy() veya dividedBy() işlemlerinde zorunludur, çünkü sonuç, para biriminin bağlamından daha uzun ondalık sayılar içerebilir. Yuvarlama modunu belirtmezseniz, brick/money istisna fırlatır.
Örnek 4: Veritabanına Dönüştürme
Veritabanına Yazma
// Money nesnesinden tam sayıya (büyük tam sayı sütunu için)
$debit = Money::of($sale->amount, 'IDR')
->getAmount()->toInt();
// Money nesnesinden float'a (ondalık sütun için)
$unit_price Money::of($item[ 'unit_price' ], 'USD' )
->getAmount()->toFloat();
// RationalMoney'dan string'e (tam hassasiyet için)
$grand_total $grandTotalRational
->to (new DefaultContext(), RoundingMode::HALF_UP)
->getAmount()->__toString();
Veritabanından Okuma
// Ondalık sütundan — direkt olarak Money::of() kullanın
$amount Money::of($model->amount, 'IDR');
// Büyük tam sayı sütunundan (küçük birim) — Money::ofMinor() kullanarak
$amount Money::ofMinor($model->amount_minor, 'IDR');
Muhasebe Jurnali
// app/Listeners/PostSaleJournalListener.php
// Alacak hesapları için borç
$ar_debit Money::of($sale->amount, 'IDR')
->getAmount()->toInt();
// Her bir kalem için kredilendirme
foreach ($sale->items as $item{
$revenue_credit Money::of($item[ 'total_price' , 'IDR' ));
->getAmount ->toInt();
}
// COGS borcu ve envanter kredisi
$hpp_debit = Money::of($item_cogs, 'IDR')
->getAmount()->toInt();
$inventory_credit = Money::of($item_cogs, 'IDR')
->getAmount()->toInt();
brick/money ile, gönderilen journal’ın her zaman dengede kalacağı garanti edilir çünkü her dönüşüm kontrollü hassasiyet ile gerçekleştirilir.
Örnek 5: Spatie Laravel Data (DTO) ile Entegrasyon
DTO, ham değerleri (int/float) alır ve Money dönüşümü Servis/Aksiyon katmanında gerçekleşir:
// app/Data/TransferData.php
class TransferData extends Data
{
public function __construct(
public int $amount, // Küçük birim (tam sayılar)
// ...
) {}
public static function rules(): array
{
return [
[ 'required'>, ],
];
}
// Servis katmanında
class TransferService
{
public function execute (TransferData $data): void
{
$amount Money::ofMinor($data->amount, 'IDR');
// Tam hassasiyetle hesaplamalar
$fee{o} = $amount->multipliedBy('0.01', RoundingMode::HALF_UP);
$net = $amount ->minus($fee);$this Transfer::query()->create (
'amount' {=𝑅β𝑦𝛆𝑟𝑓𝑖𝑐𝑎𝑛𝑐{s1}'net'𝑟Ʉ𝑟'fee_amount'>νค่า!
'fee_amount' =𝑆𝑄𝑉𝑆𝑉𝑐(s1)}.-> 𝑟__('𝑣𝑡𝑛> '𝑗𝑦←𝑓𝑒𝑟𝑡𝑒𝑟𝑜𝑉𝑗Ⲿ𝑔inate->call->retrieve->with(same))
)
𝑟>
}
Prensip: DTO sadece veri taşır. Para ile ilgili mantık Servis/Aksiyon katmanındadır.
Kaçınılması Gereken Anti-Patternler
1. Para Değerinde PHP Aritmetiği
// YANLIŞ — floating point hatası
$total = $harga *} $quantity;
$pajak =} $total }
*0.11 /{mi}100;
$grand = $total -$pajak - $diskon


