Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. 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.
  2. • Il n’a pas d’identité propre • Il est immuable • Il est

    interchangeable Caractéristiques d’un « Value Object »
  3. 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]);
  4. 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; } }
  5. 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; } }
  6. 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'; } }
  7. 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() ); } }
  8. 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); } }
  9. <doctrine-mapping> <entity name="AppBundle\Entity\Order" name="sl_orders"> <id name="uuid" length="36" type="guid"/> <field name="designation"

    type="string"/> <field name="unitPrice" type="money" length="10"/> <field name="quantity" type="smallint" /> <field name="grossAmount" type="money" length="10"/> <field name="vatAmount" type="money" length="10"/> <field name="totalAmount" type="money" length="10"/> <field name="status" type="string" length="15"/> </entity> </doctrine-mapping>
  10. 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) );
  11. 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
  12. 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
  13. <!-- src/Money/Resources/config/doctrine/Money.orm.xml --> <doctrine-mapping> <embeddable name="SebastianBergmann\Money\Money"> <field name="amount" column="amount" type="integer"

    /> <embedded name="currency" class="SebastianBergmann\Money\Currency"/> </embeddable> </doctrine-mapping> <!-- src/Money/Resources/config/doctrine/Currency.orm.xml --> <doctrine-mapping> <embeddable name="SebastianBergmann\Money\Currency"> <field name="currencyCode" column="code" type="string" length="3" /> </embeddable> </doctrine-mapping> Value Object Mapping
  14. <!-- src/AppBundle/Resources/config/doctrine/Order.orm.xml --> <doctrine-mapping> <entity name="AppBundle\Entity\Order" table="sl_orders"> <id name="uuid" type="guid"/>

    <field name="designation" type="string"/> <field name="quantity" type="smallint" /> <field name="status" type="string" length="15"/> <embedded name="unitPrice" class="SebastianBergmann\Money\Money"/> <embedded name="grossAmount" class="SebastianBergmann\Money\Money"/> <embedded name="vatAmount" class="SebastianBergmann\Money\Money"/> <embedded name="totalAmount" class="SebastianBergmann\Money\Money"/> </entity> </doctrine-mapping> Entity Mapping
  15. 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
  16. 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 !  
  17. 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(); } }