$30 off During Our Annual Pro Sale. View Details »

Persister des Value Objects avec Doctrine

Persister des Value Objects avec Doctrine

Lightning talk de SymfonyLive Paris 2016

Hugo Hamon

April 08, 2016
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. SensioLabs
    Persister des
    « Value Objects »
    avec Doctrine.

    View Slide

  2. Value Objects

    View Slide

  3. Un Objet de Valeur (ou Value Object)
    est un petit objet qui représente une
    donnée (ou concept) simple et atomique.
    Qu’est-ce qu’un « Value Object » ?
    Quelques exemples :
    monnaie, devise, coordonnées 2D, coordonnées 3D, adresse postale,
    volume, distance, adresse e-mail, élément chimique, date, intervalle de
    date, numéro de téléphone, UUID, etc.

    View Slide

  4. • Il n’a pas d’identité propre
    • Il est immuable
    • Il est interchangeable
    Caractéristiques d’un « Value Object »

    View Slide

  5. Example d’un objet Money
    use SebastianBergmann\Money\Currency;
    use SebastianBergmann\Money\Money;
    $money1 = Money::fromString('1725.78', new Currency('EUR'));
    $money2 = Money::fromString('12.22', new Currency('EUR'));
    $money3 = $money1->add($money2);
    $money4 = $money3->multiply(10);
    $shares = $money4->allocateToTargets(5);
    $shares = $money4->allocateByRatios([3, 7]);

    View Slide

  6. Usage

    View Slide

  7. class Order
    {
    /** @var Money */
    private $unitPrice, $grossAmount, $vatAmount, $totalAmount;
    public function checkout($vatRate)
    {
    if (self::PENDING !== $this->status) {
    throw new OrderException('...');
    }
    $this->grossAmount = $this->unitPrice->multiply($this->quantity);
    $this->vatAmount = $this->grossAmount->multiply($vatRate);
    $this->totalAmount = $this->grossAmount->add($this->vatAmount);
    $this->status = self::CHECKOUT;
    }
    }

    View Slide

  8. Entité Doctrine + Value Object
    class Order
    {
    // ...
    public function pay(Money $paidAmount)
    {
    if (self::PAID === $this->status) {
    throw new OrderAlreadyPaidException();
    }
    if (!$paidAmount->equals($this->totalAmount)) {
    throw new OrderPartiallyPaidException();
    }
    $this->status = self::PAID;
    }
    }

    View Slide

  9. Types Doctrine
    Personnalisés

    View Slide

  10. namespace Money\Doctrine;
    use Doctrine\DBAL\Platforms\AbstractPlatform;
    use Doctrine\DBAL\Types\Type;
    class MoneyType extends Type
    {
    function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
    return $platform->getVarcharTypeDeclarationSQL($fieldDeclaration);
    }
    function requiresSQLCommentHint(AbstractPlatform $platform)
    {
    return true;
    }
    function getName()
    {
    return 'money';
    }
    }

    View Slide

  11. class MoneyType extends Type
    {
    function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
    if (null === $value || '' === $value) {
    return null;
    }
    if (!$value instanceof \SebastianBergmann\Money\Money) {
    throw new \InvalidArgumentException('Expected Money object.');
    }
    return sprintf(
    '%s %s',
    $value->getConvertedAmount(),
    $value->getCurrency()
    );
    }
    }

    View Slide

  12. use SebastianBergmann\Money\Money;
    class MoneyType extends Type
    {
    function convertToPHPValue($value, AbstractPlatform $platform)
    {
    if (null === $value || '' === $value) {
    return $value;
    }
    list($amount, $currency) = explode(' ', $value);
    return Money::fromString($amount, $currency);
    }
    }

    View Slide

  13. # app/config/config.yml
    doctrine:
    dbal:
    # ...
    types:
    money: Money\Doctrine\MoneyType

    View Slide













  14. View Slide

  15. CREATE TABLE `sl_orders` (
    uuid CHAR(36) NOT NULL COMMENT '(DC2Type:guid)',
    designation VARCHAR(255) NOT NULL,
    unit_price VARCHAR(10) NOT NULL COMMENT '(DC2Type:money)',
    quantity SMALLINT NOT NULL,
    gross_amount VARCHAR(10) NOT NULL COMMENT '(DC2Type:money)',
    vat_amount VARCHAR(10) NOT NULL COMMENT '(DC2Type:money)',
    total_amount VARCHAR(10) NOT NULL COMMENT '(DC2Type:money)',
    status VARCHAR(15) NOT NULL,
    PRIMARY KEY(uuid)
    );

    View Slide

  16. $uuid = Uuid::fromString('a71b50d3-7144-461d-a868-8625df3892a7');
    $order = $repository->find($uuid);

    View Slide

  17. Avantages vs inconvénients
    + Simplicité de mise en place
    + Approche entièrement objet
    -  Une seule colonne SQL de stockage
    -  Requêtes SQL d’aggrégation impossible

    View Slide

  18. Entités
    Imbriquées

    View Slide

  19. Doctrine 2.5 vient avec le mécanisme des
    entités imbriquées qui permet de
    stocker (et récupérer) automatiquement
    des sous-entités dans la même table SQL
    que l’entité principale.
    Depuis Doctrine 2.5

    View Slide














  20. Value Object Mapping

    View Slide














  21. Entity Mapping

    View Slide

  22. doctrine:
    orm:
    auto_generate_proxy_classes: "%kernel.debug%"
    entity_managers:
    default:
    naming_strategy: doctrine.orm.naming_strategy.underscore
    auto_mapping: false
    mappings:
    Money:
    type: xml
    dir: "%kernel.root_dir%/../src/Money/Resources/config/doctrine"
    prefix: SebastianBergmann\Money
    is_bundle: false
    alias: Money
    AppBundle:
    type: xml
    prefix: AppBundle\Entity
    is_bundle: true
    alias: AppBundle
    DoctrineBundle Configuration

    View Slide

  23. MariaDB [demo]> select * from sl_orders\G;
    *************************** 1. row ***************************
    uuid: f89f1395-71dc-402f-afb9-efb3035920eb
    designation: Symfony Live
    quantity: 2
    status: PAID
    unit_price_amount: 29000
    unit_price_currency_code: EUR
    gross_amount_amount: 58000
    gross_amount_currency_code: EUR
    vat_amount_amount: 11600
    vat_amount_currency_code: EUR
    total_amount_amount: 69600
    total_amount_currency_code: EUR
    /!\ Les montants sont ici tous exprimés en centimes d’Euro !  

    View Slide

  24. $uuid = Uuid::fromString('f89f1395-71dc-402f-afb9-efb3035920eb');
    $order = $repository->find($uuid);

    View Slide

  25. Requêtes DQL

    View Slide

  26. namespace AppBundle\Entity;
    use Doctrine\ORM\EntityRepository;
    use SebastianBergmann\Money\Money;
    class OrderRepository extends EntityRepository
    {
    public function findGreaterThan(Money $amount)
    {
    $q = $this
    ->createQueryBuilder('o')
    ->where('o.grossAmount.amount >= :amount')
    ->andWhere('o.grossAmount.currency.currencyCode >= :currency')
    ->setParameter('amount', $amount->getAmount())
    ->setParameter('currency', (string) $amount->getCurrency())
    ->getQuery()
    ;
    return $q->getResult();
    }
    }

    View Slide

  27. SensioLabs
    Merci!

    View Slide