Share
Embed
Persister des Value Objects avec Doctrine
Lightning talk de SymfonyLive Paris 2016
Hugo Hamon
April 08, 2016
Transcript
27 slides
-
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. -
• Il n’a pas d’identité propre • Il est immuable • Il est
interchangeable Caractéristiques d’un « Value Object » -
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]); -
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; } } -
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; } } -
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'; } } -
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() ); } } -
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); } } -
<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> -
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) ); -
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 -
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 -
<!-- 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 -
<!-- 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 -
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 -
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 ! -
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(); } }