Slide 1

Slide 1 text

SensioLabs Persister des « Value Objects » avec Doctrine.

Slide 2

Slide 2 text

Value Objects

Slide 3

Slide 3 text

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.

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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]);

Slide 6

Slide 6 text

Usage

Slide 7

Slide 7 text

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; } }

Slide 8

Slide 8 text

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; } }

Slide 9

Slide 9 text

Types Doctrine Personnalisés

Slide 10

Slide 10 text

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'; } }

Slide 11

Slide 11 text

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() ); } }

Slide 12

Slide 12 text

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); } }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Slide 15

Slide 15 text

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) );

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Entités Imbriquées

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Value Object Mapping

Slide 21

Slide 21 text

Entity Mapping

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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 !  

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Requêtes DQL

Slide 26

Slide 26 text

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(); } }

Slide 27

Slide 27 text

SensioLabs Merci!