2014-12-04 3 views
11

Qual è la vostra strategia per memorizzare i valori monetari con Doctrine? Il campo money di Symfony è abbastanza utile ma come mapparlo alla colonna di Doctrine? Esiste un pacchetto per questo che fornisce il tipo DBAL?Symfony tipo di campo di denaro e dottrina

float otipi di colonne sono insufficienti perché quando si ha a che fare con il denaro spesso si gestisce anche la valuta. Sto usando due campi per questo, ma è difficile da gestire manualmente.

+0

Avete bisogno di memorizzare numeri interi o numeri floating? Penso che avrai un numero limitatore di tipi di campo. –

+0

@ A.L Non importa molto. Posso sempre usare l'opzione [divisor] (http://symfony.com/doc/2.5/reference/forms/types/money.html#divisor) per convertire i valori in float. Per ora sto usando 'int' (importo) +' stringa' (valuta) ma è così imbarazzante. – SiliconMind

+0

Cosa fai con il segno '+'? Hai un campo per l'importo e un campo per la valuta? Se devi lavorare con valute diverse, ti suggerisco di aggiungere una nota nella tua domanda perché lo rende un po 'più complesso rispetto a una sola valuta. –

risposta

2

È possibile definire un proprio tipo di campo se si dice alla doctrina come gestirlo. Per spiegare questo ho inventato un "negozio" e un "ordine" dove un "denaro" - ValueObject viene usato.

Per iniziare abbiamo bisogno di un entità e un ValueObject, che viene utilizzato nel soggetto:

order.php:

<?php 

namespace Shop\Entity; 

/** 
* @Entity 
*/ 
class Order 
{ 
    /** 
    * @Column(type="money") 
    * 
    * @var \Shop\ValueObject\Money 
    */ 
    private $money; 

    /** 
    * ... other variables get defined here 
    */ 

    /** 
    * @param \Shop\ValueObject\Money $money 
    */ 
    public function setMoney(\Shop\ValueObject\Money $money) 
    { 
     $this->money = $money; 
    } 

    /** 
    * @return \Shop\ValueObject\Money 
    */ 
    public function getMoney() 
    { 
     return $this->money; 
    } 

    /** 
    * ... other getters and setters are coming here ... 
    */ 
} 

Money.php:

<?php 

namespace Shop\ValueObject; 

class Money 
{ 

    /** 
    * @param float $value 
    * @param string $currency 
    */ 
    public function __construct($value, $currency) 
    { 
     $this->value = $value; 
     $this->currency = $currency; 
    } 

    /** 
    * @return float 
    */ 
    public function getValue() 
    { 
     return $this->value; 
    } 

    /** 
    * @return string 
    */ 
    public function getCurrency() 
    { 
     return $this->currency; 
    } 
} 

Finora niente di speciale. La "magia" viene qui:

MoneyType.php:

<?php 

namespace Shop\Types; 

use Doctrine\DBAL\Types\Type; 
use Doctrine\DBAL\Platforms\AbstractPlatform; 

use Shop\ValueObject\Money; 

class MoneyType extends Type 
{ 
    const MONEY = 'money'; 

    public function getName() 
    { 
     return self::MONEY; 
    } 

    public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform) 
    { 
     return 'MONEY'; 
    } 

    public function convertToPHPValue($value, AbstractPlatform $platform) 
    { 
     list($value, $currency) = sscanf($value, 'MONEY(%f %d)'); 

     return new Money($value, $currency); 
    } 

    public function convertToDatabaseValue($value, AbstractPlatform $platform) 
    { 
     if ($value instanceof Money) { 
      $value = sprintf('MONEY(%F %D)', $value->getValue(), $value->getCurrency()); 
     } 

     return $value; 
    } 

    public function canRequireSQLConversion() 
    { 
     return true; 
    } 

    public function convertToPHPValueSQL($sqlExpr, AbstractPlatform $platform) 
    { 
     return sprintf('AsText(%s)', $sqlExpr); 
    } 

    public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform) 
    { 
     return sprintf('PointFromText(%s)', $sqlExpr); 
    } 
} 

quindi è possibile utilizzare il seguente codice:

// preparing everything for example getting the EntityManager... 

// Store a Location object 
use Shop\Entity\Order; 
use Shop\ValueObject\Money; 

$order = new Order(); 

// set whatever needed 
$order->setMoney(new Money(99.95, 'EUR')); 
// other setters get called here. 

$em->persist($order); 
$em->flush(); 
$em->clear(); 

Si potrebbe scrivere un mapper che mappa l'input proveniente dal campo monetario di Symfony in un Money-ValueObject per semplificare ulteriormente.

un altro paio di dettagli sono spiegati qui: http://doctrine-orm.readthedocs.org/en/latest/cookbook/advanced-field-value-conversion-using-custom-mapping-types.html

testato, ma ho usato questo concetto prima e ha funzionato. Fammi sapere se hai domande.

+1

Grazie. È praticamente la cosa a cui stavo pensando. Ma userei 'int' per memorizzare l'importo nella sua unità più piccola (ad esempio centesimi per EUR). Lo svantaggio in questo caso è che non puoi facilmente filtrare o ordinare i dati nella tabella per valuta o importo. E questo è il più grande show stopper per me. – SiliconMind

+0

Questo è anche disponibile qui: https://github.com/leaphly/price – qooplmao

+0

Quali sono i vantaggi rispetto a un tipo di colonna flottante in SQL? –

1

Mi stavo preparando per una soluzione a questo problema e su Google sono arrivato a this page.

Qui è illustrato lo Embeddable field disponibile da Doctrine 2.5.

Con qualcosa di simile è possibile gestire i valori come quelli monetari con più "parametri".

Un esempio:

/** @Entity */ 
class Order 
{ 
    /** @Id */ 
    private $id; 

    /** @Embedded(class = "Money") */ 
    private $money; 
} 

/** @Embeddable */ 
class Money 
{ 
    /** @Column(type = "int") */ // better then decimal see the mathiasverraes/money documentation 
    private $amount; 

    /** @Column(type = "string") */ 
    private $currency; 
} 

Spero che questo vi aiuterà.

UPDATE

ho scritto un PHP library that contains some useful value objects.

C'è anche un oggetto valore per gestire i valori monetari (che racchiude il grande MoneyPHP library) e li persistono nel database utilizzando un Doctrine type.

Questo tipo salva il valore nel database sotto forma di 100-EUR che corrisponde a 1 euro.

9

Considerare utilizzando il tipo decimal:

/** 
* @ORM\Column(type="decimal", precision=7, scale=2) 
*/ 
protected $price = 0; 

noti che ci sono valute che hanno tre posizioni decimali. Se si intende utilizzare tali valute, il parametro scale deve essere 3. Se si intende mescolare le valute con due e tre posizioni decimali, aggiungere un trailing 0 se ci sono solo due posizioni decimali.

Attenzione: $price sarà una stringa in PHP. Puoi lanciarlo su float o moltiplicarlo con 100 (o 1000, nel caso di valute con tre posizioni decimali) e trasmetterlo a int.


La valuta stessa è un campo separato; può essere una stringa con il codice di valuta a tre lettere. Oppure - in modo pulito - puoi creare una tabella con tutte le valute che stai utilizzando e quindi creare una relazione ManyToOne per la voce della valuta.

+2

NON prendere in considerazione l'uso del tipo 'decimale'! PHP non può rappresentare accuratamente i decimali, il che è sicuramente un problema quando si tratta di soldi! Conservare sempre i soldi nell'unità più piccola (centesimi, penny ecc.). – Jonny

+1

Se hai bisogno di dimostrare che questa * non è una buona idea *, esegui questo nella tua shell: "echo" Jonny

+1

@Jonny: Sì, ma questo è solo un problema se confronti direttamente i valori in virgola mobile. Un programmatore serio saprà che controllano un epsilon assoluto o relativo. Inoltre, Doctrine ti fornisce il valore come stringa numerica, quindi sei libero di moltiplicare la stringa con 1000 per ottenere un int. (Sì, applicare le operazioni matematiche sulle stringhe è complicato, ma questo è PHP per te.) – lxg

4

Si consiglia di utilizzare un oggetto valore come Money\Money.

# app/Resources/Money/doctrine/Money.orm.yml 
Money\Money: 
    type: embeddable 
    fields: 
    amount: 
     type: integer 
    embedded: 
    currency: 
     class: Money\Currency 
# app/Resources/Money/doctrine/Currency.orm.yml 
Money\Currency: 
    type: embeddable 
    fields: 
    code: 
     type: string 
     length: 3 
# app/config.yml 
doctrine: 
    orm: 
    mappings: 
     Money: 
     type: yml 
     dir: "%kernel.root_dir%/../app/Resources/Money/doctrine" 
     prefix: Money 
class YourEntity 
{ 
    /** 
    * @ORM\Embedded(class="\Money\Money") 
    */ 
    private $value; 

    public function __construct(string $currencyCode) 
    { 
     $this->value = new \Money\Money(0, new \Money\Currency($currencyCode)); 
    } 

    public function getValue(): \Money\Money 
    { 
     return $this->value; 
    } 
} 
+0

Questa soluzione è buona, ma solo se non hai bisogno di oggetti incorporabili nullable, perché purtroppo Doctrine2 non supporta l'incorporabilità nullable. – baris1892