Patrón Moneda. Representar importes monetarios de forma correcta con PHP (y otros)

Manu Pijierro
6 min readNov 12, 2017

--

Como programador seguramente alguna vez te habrá tocado implementar una aplicación que gestione importes monetarios como precios de productos, salarios de empleados, transacciones monetarias…etc. Y es muy probable también que te hayas encontrado con las dudas sobre que tipo de dato utilizar para guardar o calcular importes no exactos, es decir, con decimales. Por ejemplo, ¿qué tipo de dato utilizaríamos para guardar 3,99 Euros? ¿un float? ¿un decimal? ¿cuántas cifras para la parte entera y cuántas para la decimal? ¿estamos seguros que las operaciones de redondeo son idempotentes y nos retornan siempre los mismos resultados con estos tipos de datos? y todo esto sin entrar en otra problemática como podría ser el uso de conversiones entre distintos tipos de divisas.

¿Qué es el patrón Moneda?

La solución a todas estas dudas están en el patrón Money (en adelante usaremos este nombre por ser el original del patrón), definido por Martin Fowler que mediante la representación de un importe monetario de forma correcta nos proporciona un acceso simple y estandarizado para trabajar con divisas. Básicamente lo que se propone con este patrón es que se almacenen dos informaciones; una para indicar la cantidad del importe monetario y otra que especifique la divisa del importe.

Para trabajar con el dato del importe monetario el tipo de datos que lo va a manejar es un número entero que representa el valor de dicho importe en la mínima unidad divisible de la moneda que estamos utilizando. Esto que suena tan rimbombante es sencillo de entender cuando se explica con un ejemplo: Si atendemos al caso anterior de 3,99 Euros, lo que manejamos realmente sería el entero 399. Es decir, como estamos usando Euros, para trabajar con cualquier importe monetario antes lo pasaríamos a céntimos por ser el mínimo valor divisible de la moneda. Si fueran dólares, también coincidiríamos con los céntimos de dólar. Si fuera cualquier otra moneda igualmente tendríamos que convertirla a su ‘moneda mínima’.

Para almacenar la divisa, se suele usar un código de tres letras. El estándar internacional ISO 4217 especifica todas las monedas del mundo.

Este patrón indica que esta estructura de datos compuesta por un número entero y una cadena de tres letras se conforman dentro de un Value Object. Un Value Object es una objeto cuya identidad no viene dada por un identificador sino por el valor de los atributos que contiene. Son objetos inmutables, es decir, el estado del mismo no se pude modificar, lo que quiere decir que cualquier operación y modificación del mismo se traduce en el retorno de una nueva instancia del objeto con la modificación deseada.

Primer acercamiento en PHP

Como patrón de implementación es agnóstico del lenguaje y debe funcionar igual sea cual sea el lenguaje que utilice. Yo voy a utilizar PHP porque es el que conozco.

En el siguiente ejemplo (muy simplificado) se puede ver la estructura de datos del objeto Money y como se trabajaría con una operación de suma. (está escrito del tirón, sin comprobar sintaxis ni nada, así que puede contener algún error que otro)

<?phpclass Money {  private $amount;
private $currency;

public function __construct (int $amount, string $currency){

$this->amount = $amount;
$this->currency = $currency;

}

public function amount(){
return $this->amount();
}

public function currency (){
return $this->currency;
}

public function assertSameCurrencyAs (self $otherMoney){
if ( $this->currency != $ohterMoney->currency() ){
throw new InvalidArgumentException('Currency mismatch');
}
}

public function sum (self $otherMoney): self {
$this->assertSameCurrencyAs($otherMoney);

return new self ($this->amount + $otherMoney->amount(),
$this->currency);
}

}
$price = new Money (533, 'EUR'); // price is 5,33€$otherPrice = new Money (299, 'EUR'); // price is 2,99€$sum = $price->add($otherPrice);// is a new object with 832 cents

Como se puede ver, antes de crear la instancia del objeto Money este ya recibe como parámetro el importe como un entero además de la divisa. Aquí lo interesante está en ver como conseguimos la inmutabilidad del objeto evitando ‘setters’ sobre los atributos o como en la operación suma lo que se hace es retornar un objeto del tipo Money cuya cantidad es la operación de la suma de las dos cantidades.

Con un poco de imaginación podríamos hacernos una idea como sería implementar operaciones de resta, multiplicación, conversión de divisas..etc

Ejemplo con importes que tienen muchos decimales

Ahora la pregunta que puede surgir es, ¿y qué ocurre si por ejemplo al hacer una conversión de divisa tenemos un resultado de 9,66666666… euros? ¿o al aplicar un descuento de un 5% en un precio tenemos un importe de 65,67812 euros? ¿cómo afecta el redondeo a estas cantidades?

La solución dependerá en primera instancia de la forma de redondear concreta que tengamos especificada en nuestra aplicación como regla de negocio, pero, de forma general, ojo que lo acabo de leer en el BOE, con la entrada del euro los decimales de los importes son como máximo dos y los redondeos se hacen al alza, es decir, si el tercer decimal está entre 0 y 4 el segundo decimal se mantiene y si el tercer decimal está entre el 5 y el 9 el segundo decimal se redondea al alza, por lo tanto, tendríamos el importe de 9,67 euros que al pasarlo a céntimos tendríamos los 967 con los que tendríamos que construir el objeto Money.

Componente MoneyPhp

Por suerte para PHP, y apostaría a que para casi todos los lenguajes mayormente usados existe una implementación de este patrón de uso público y libre. MoneyPHP es un componente que implementa el patrón del que estamos hablando. No solo tiene una clase bien construída sino que además tiene soporte para especificación de monedas, comparación de divisas, operaciones, formateo de importes, parser, facilidades para la extensión de sus características. Se instala por Composer, el uso es muy sencillo y la documetación bastante amplia. Es importante destacar que desde la versión 3.0 ya no utiliza de manera interna números enteros para representar los importes ya que se han cambiado a strings de la longitud que queramos. Esto hace que prácticamente podamos usar importes por cantidades ilimitadas ya que los enteros al fin y al cabo tienen un límite máximo de representación.

Logo de MoneyPHP

Por ejemplo, con este componente, vemos lo sencillo que serían instanciar un objeto que representase 5 dólares americanos. Es parecido al acercamiento que dejé anteriormente pero al especificar la moneda vemos que instancia la clase Currency.

use Money\Currency;
use Money\Money;

$fiver = new Money(500, new Currency('USD'));
// or shorter:
$fiver = Money::USD(500);

Otros ejemplos de uso y los tipos de datos de entrada que acepta.

use Money\Currency;
use Money\Money;

// int is accepted
$fiver = new Money(500, new Currency('USD'));

// string is accepted if integer
$fiver = new Money('500', new Currency('USD'));

// string is accepted if fractional part is zero
$fiver = new Money('500.00', new Currency('USD'));

// leading zero's are not accepted
$fiver = new Money('00500', new Currency('USD'));

// multiple zero's are not accepted
$fiver = new Money('000', new Currency('USD'));

// plus sign is not accepted
$fiver = new Money('+500', new Currency('USD'));

Gracias a este componente podremos tener toda la estructura del patrón Money montada en nuestro proyecto y utilizarla de una forma muy sencilla y correcta.

Eso es todo, si conocías este patrón y sus componentes probablemente no te habré mostrado nada nuevo y en caso contrario, ya sabes, a partir de este momento utiliza este patrón para trabajar correctamente con importes monetarios en tus aplicaciones, es muy sencillo y te ahorrará muchos dolores de cabeza.

Chimpún.

Bibliografía

--

--

No responses yet