diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 23b6018..49eac9b 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -2,7 +2,6 @@ namespace Padam87\MoneyBundle\DependencyInjection; -use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -11,7 +10,7 @@ class Configuration implements ConfigurationInterface /** * {@inheritdoc} */ - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('padam87_money'); @@ -31,6 +30,9 @@ public function getConfigTreeBuilder() ->scalarPrototype()->end() ->defaultValue(['EUR']) ->end() + ->arrayNode('currency_digits') + ->scalarPrototype()->end() + ->end() ->end() ; diff --git a/DependencyInjection/Padam87MoneyExtension.php b/DependencyInjection/Padam87MoneyExtension.php index 98ddb4d..744cebc 100644 --- a/DependencyInjection/Padam87MoneyExtension.php +++ b/DependencyInjection/Padam87MoneyExtension.php @@ -2,29 +2,26 @@ namespace Padam87\MoneyBundle\DependencyInjection; -use Money\Currencies; -use Money\Currencies\CurrencyList; -use Padam87\MoneyBundle\Doctrine\Type\CurrencyType; -use Padam87\MoneyBundle\Doctrine\Type\MoneyAmountType; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Padam87\MoneyBundle\Doctrine\Mapping\Driver\MoneyEmbeddedDriver; +use Padam87\MoneyBundle\Money\EmbeddedMoney; +use Padam87\MoneyBundle\Money\NullableMoney; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\DependencyInjection\Loader; -class Padam87MoneyExtension extends Extension implements PrependExtensionInterface, CompilerPassInterface +class Padam87MoneyExtension extends Extension implements CompilerPassInterface { /** * {@inheritdoc} */ - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yaml'); $container->setParameter('padam87_money.config', $config); @@ -33,57 +30,23 @@ public function load(array $configs, ContainerBuilder $container) /** * {@inheritdoc} */ - public function prepend(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { - $container->prependExtensionConfig( - 'doctrine', - [ - 'dbal' => [ - 'types' => [ - 'money_amount' => MoneyAmountType::class, - 'currency' => CurrencyType::class, - ] - ], - ] - ); - } - - /** - * {@inheritdoc} - */ - public function process(ContainerBuilder $container) - { - $config = $container->getParameter('padam87_money.config'); - $driver = $container->getDefinition('doctrine.orm.default_metadata_driver'); - $driver->addMethodCall( 'addDriver', [ - $container->getDefinition('Padam87\MoneyBundle\Doctrine\Mapping\Driver\MoneyEmbeddedDriver'), - 'Money\Money' + $container->getDefinition(MoneyEmbeddedDriver::class), + EmbeddedMoney::class ] ); - $driver->addMethodCall( 'addDriver', [ - $container->getDefinition('Padam87\MoneyBundle\Doctrine\Mapping\Driver\CurrencyPairEmbeddedDriver'), - 'Money\CurrencyPair' + $container->getDefinition(MoneyEmbeddedDriver::class), + NullableMoney::class ] ); - - $container->setDefinition( - CurrencyList::class, - new Definition( - CurrencyList::class, - [ - array_fill_keys($config['currencies'], $config['scale']) - ] - ) - ); - - $container->setAlias(Currencies::class, CurrencyList::class); } } diff --git a/Doctrine/Mapping/Driver/CurrencyPairEmbeddedDriver.php b/Doctrine/Mapping/Driver/CurrencyPairEmbeddedDriver.php deleted file mode 100644 index 7c495c3..0000000 --- a/Doctrine/Mapping/Driver/CurrencyPairEmbeddedDriver.php +++ /dev/null @@ -1,59 +0,0 @@ -isEmbeddedClass = true; - - $metadata->mapField( - [ - 'fieldName' => 'baseCurrency', - 'type' => 'currency', - ] - ); - - $metadata->mapField( - [ - 'fieldName' => 'counterCurrency', - 'type' => 'currency', - ] - ); - - $metadata->mapField( - [ - 'fieldName' => 'conversionRatio', - 'type' => 'float', - ] - ); - } - - /** - * {@inheritdoc} - */ - public function getAllClassNames() - { - return [ - CurrencyPair::class - ]; - } - - /** - * {@inheritdoc} - */ - public function isTransient($className) - { - return false; - } -} diff --git a/Doctrine/Mapping/Driver/MoneyEmbeddedDriver.php b/Doctrine/Mapping/Driver/MoneyEmbeddedDriver.php index 02b89df..feb96da 100644 --- a/Doctrine/Mapping/Driver/MoneyEmbeddedDriver.php +++ b/Doctrine/Mapping/Driver/MoneyEmbeddedDriver.php @@ -4,21 +4,19 @@ use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\Driver\MappingDriver; -use Money\Money; +use Padam87\MoneyBundle\Money\EmbeddedMoney; +use Padam87\MoneyBundle\Money\NullableMoney; class MoneyEmbeddedDriver implements MappingDriver { - private $config; - - public function __construct(array $config) + public function __construct(private array $config) { - $this->config = $config; } /** * {@inheritdoc} */ - public function loadMetadataForClass($className, ClassMetadata $metadata) + public function loadMetadataForClass($className, ClassMetadata $metadata): void { /* @var \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata */ @@ -27,9 +25,10 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) $metadata->mapField( [ 'fieldName' => 'amount', - 'type' => 'money_amount', + 'type' => 'decimal_object', 'precision' => $this->config['precision'], 'scale' => $this->config['scale'], + 'nullable' => $className === NullableMoney::class, ] ); @@ -37,6 +36,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) [ 'fieldName' => 'currency', 'type' => 'currency', + 'nullable' => $className === NullableMoney::class, ] ); } @@ -44,17 +44,18 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) /** * {@inheritdoc} */ - public function getAllClassNames() + public function getAllClassNames(): array { return [ - Money::class + EmbeddedMoney::class, + NullableMoney::class, ]; } /** * {@inheritdoc} */ - public function isTransient($className) + public function isTransient($className): bool { return false; } diff --git a/Doctrine/Type/CurrencyType.php b/Doctrine/Type/CurrencyType.php index 2f6ed10..e451c6d 100644 --- a/Doctrine/Type/CurrencyType.php +++ b/Doctrine/Type/CurrencyType.php @@ -2,16 +2,16 @@ namespace Padam87\MoneyBundle\Doctrine\Type; +use Brick\Money\Currency; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; -use Money\Currency; class CurrencyType extends Type { /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'currency'; } @@ -19,17 +19,25 @@ public function getName() /** * {@inheritdoc} */ - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string { - $fieldDeclaration['length'] = 3; + $column['length'] = $this->getDefaultLength($platform); // enforce column length even if specified - return $platform->getVarcharTypeDeclarationSQL($fieldDeclaration); + return $platform->getStringTypeDeclarationSQL($column); } /** * {@inheritdoc} */ - public function requiresSQLCommentHint(AbstractPlatform $platform) + public function getDefaultLength(AbstractPlatform $platform): int + { + return 3; + } + + /** + * {@inheritdoc} + */ + public function requiresSQLCommentHint(AbstractPlatform $platform): bool { return true; } @@ -37,24 +45,28 @@ public function requiresSQLCommentHint(AbstractPlatform $platform) /** * {@inheritdoc} */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue($value, AbstractPlatform $platform): ?Currency { if (null === $value) { return null; } - return new Currency($value); + return Currency::of($value); } /** * {@inheritdoc} */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string { + if ($value === null) { + return null; + } + if (!$value instanceof Currency) { - throw new \LogicException(); + throw new \LogicException(sprintf('Only instances of "%s" can be persisted as currency', Currency::class)); } - return $value->getCode(); + return (string) $value->getCurrencyCode(); } } diff --git a/Doctrine/Type/DecimalObjectType.php b/Doctrine/Type/DecimalObjectType.php new file mode 100644 index 0000000..e56e45b --- /dev/null +++ b/Doctrine/Type/DecimalObjectType.php @@ -0,0 +1,67 @@ +config['precision']; + } + + if (!isset($column['scale'])) { + $column['scale'] = $this->config['scale']; + } + + return $platform->getDecimalTypeDeclarationSQL($column); + } + + /** + * {@inheritdoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?BigDecimal + { + if (null === $value) { + return null; + } + + return BigDecimal::of($value); + } + + /** + * {@inheritdoc} + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if ($value === null) { + return null; + } + + if (!$value instanceof BigDecimal) { + throw new \LogicException(sprintf('Only instances of "%s" can be persisted as decimal', BigDecimal::class)); + } + + return (string) $value; + } +} diff --git a/Doctrine/Type/MoneyAmountType.php b/Doctrine/Type/MoneyAmountType.php deleted file mode 100644 index 733cf6a..0000000 --- a/Doctrine/Type/MoneyAmountType.php +++ /dev/null @@ -1,54 +0,0 @@ -getCurrency() === null) { + return null; + } + + return Money::of($amount, $this->getCurrency(), new BundleContext()); + } + + private function setMoney(?BigDecimal &$amount, ?Money $value, bool $strict = true): self + { + if ($strict && $this->getCurrency() !== null && !$this->getCurrency()->is($value->getCurrency())) { + throw new \LogicException( + sprintf('Class "%s" has currency of "%s", tried to set "%s"', self::class, $this->getCurrency(), $value->getCurrency()) + ); + } + + $amount = $value->getAmount(); + + return $this; + } +} diff --git a/Exchange/DatabaseExchange.php b/Exchange/DatabaseExchange.php deleted file mode 100644 index bff2545..0000000 --- a/Exchange/DatabaseExchange.php +++ /dev/null @@ -1,48 +0,0 @@ -doctrine = $doctrine; - } - - /** - * {@inheritdoc} - */ - public function quote(Currency $baseCurrency, Currency $counterCurrency) - { - if ($baseCurrency->getCode() === $counterCurrency->getCode()) { - return new CurrencyPair($baseCurrency, $counterCurrency, 1); - } - - $repo = $this->doctrine->getRepository(ExchangeRateInterface::class); - - if (!$repo instanceof ExchangeRateRepositoryInterface) { - throw new \LogicException( - sprintf('"%s" must implement %s', $repo->getClassName(), ExchangeRateRepositoryInterface::class) - ); - } - - if (null === $exchangeRate = $repo->getExchangeRate($baseCurrency, $counterCurrency)) { - throw new UnresolvableCurrencyPairException( - sprintf('%s - >%s ratio not found in the database.', $baseCurrency->getCode(), $counterCurrency->getCode()) - ); - } - - return $exchangeRate->getCurrencyPair(); - } -} diff --git a/Form/CurrencyType.php b/Form/CurrencyType.php index aa8c59a..da24228 100644 --- a/Form/CurrencyType.php +++ b/Form/CurrencyType.php @@ -2,45 +2,41 @@ namespace Padam87\MoneyBundle\Form; -use Money\Currency; -use Money\Money; -use Padam87\MoneyBundle\Service\MoneyHelper; +use Brick\Money\Currency; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class CurrencyType extends AbstractType { - private $config; - - public function __construct(array $config) + public function __construct(private array $config) { - $this->config = $config; } /** * {@inheritdoc} */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->addModelTransformer( new CallbackTransformer( - function (Currency $model = null) { - return $model ? $model->getCode() : null; - }, - function ($form) { - return new Currency($form); + fn(?Currency $modelData = null): ?string => $modelData !== null ? $modelData->getCurrencyCode() : null, + function ($formData): ?Currency { + if ($formData === null) { + return null; + } + + return Currency::of($formData); } ) ) ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver ->setDefaults( @@ -51,12 +47,12 @@ public function configureOptions(OptionsResolver $resolver) ; } - public function getParent() + public function getParent(): ?string { return ChoiceType::class; } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'moneyphp_currency'; } diff --git a/Form/DecimalType.php b/Form/DecimalType.php new file mode 100644 index 0000000..5f42b5c --- /dev/null +++ b/Form/DecimalType.php @@ -0,0 +1,71 @@ +addModelTransformer( + new CallbackTransformer( + function (?BigDecimal $modelData = null) use ($options): ?string { + if ($modelData === null) { + return null; + } + + if ($options['integer_only']) { + return $modelData->getIntegralPart(); + } + + if ($options['strip_trailing_zeros']) { + $modelData = $modelData->stripTrailingZeros(); + } + + return (string) $modelData; + }, + function (?string $formData): ?BigDecimal { + if ($formData === null) { + return null; + } + + return BigDecimal::of(str_replace([' ', ','], ['', '.'], $formData)); + } + ) + ) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefaults( + [ + 'integer_only' => false, + 'strip_trailing_zeros' => true, + 'html5' => true, + ] + ) + ; + } + + public function getParent(): ?string + { + return NumberType::class; + } + + public function getBlockPrefix(): string + { + return 'decimal'; + } +} diff --git a/Form/MoneyType.php b/Form/MoneyType.php index 410f1a3..0fa0e7d 100644 --- a/Form/MoneyType.php +++ b/Form/MoneyType.php @@ -2,47 +2,54 @@ namespace Padam87\MoneyBundle\Form; -use Money\Currency; -use Money\Money; -use Padam87\MoneyBundle\Service\MoneyHelper; +use Brick\Math\RoundingMode; +use Brick\Money\Currency; +use Brick\Money\Money; +use Padam87\MoneyBundle\Money\Context\BundleContext; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\CallbackTransformer; -use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolver; class MoneyType extends AbstractType { - private $moneyHelper; - private $config; - - public function __construct(MoneyHelper $moneyHelper, array $config) - { - $this->moneyHelper = $moneyHelper; - $this->config = $config; - } - /** * {@inheritdoc} */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('amount', $options['amount_type'], $options['amount_options']) ->addModelTransformer( new CallbackTransformer( - function (Money $model = null) use ($options) { - if ($model === null) { - $model = new Money(0, new Currency($options['default_currency_code'])); + function (?Money $modelData = null): ?array { + if ($modelData === null) { + return null; } return [ - 'amount' => $this->moneyHelper->getAmount($model), - 'currency' => $model->getCurrency(), + 'amount' => $modelData->getAmount(), + 'currency' => $modelData->getCurrency(), ]; }, - function ($form) { - return $this->moneyHelper->createMoney($form['amount'], $form['currency']); + function (?array $formData = null) use ($options): ?Money { + if ($formData === null) { + return null; + } + + if (null === $amount = $formData['amount']) { + return null; + } + + if (array_key_exists('currency', $formData) && null !== $formData['currency']) { + $currency = $formData['currency']; + } else { + $currency = Currency::of($options['default_currency_code']); + } + + return Money::of($amount, $currency, $options['context'], $options['rounding_mode']); } ) ) @@ -53,28 +60,37 @@ function ($form) { } } - public function configureOptions(OptionsResolver $resolver) + public function buildView(FormView $view, FormInterface $form, array $options): void + { + $view->vars['currency_enabled'] = $options['currency_enabled']; + $view->vars['default_currency_code'] = $options['default_currency_code']; + } + + public function configureOptions(OptionsResolver $resolver): void { $resolver ->setDefaults( [ - 'amount_type' => TextType::class, + 'amount_type' => DecimalType::class, 'amount_options' => [ 'label' => false, ], - 'default_currency_code' => $this->config['default_currency'], + 'default_currency_code' => 'HUF', 'currency_enabled' => false, 'currency_type' => CurrencyType::class, 'currency_options' => [ 'label' => false, ], + 'addon_text' => null, + 'context' => new BundleContext(), + 'rounding_mode' => RoundingMode::UNNECESSARY, ] ) ; } - public function getBlockPrefix() + public function getBlockPrefix(): string { - return 'moneyphp_money'; + return 'money_object'; } } diff --git a/Money/Context/BundleContext.php b/Money/Context/BundleContext.php new file mode 100644 index 0000000..c2bd1fc --- /dev/null +++ b/Money/Context/BundleContext.php @@ -0,0 +1,48 @@ +toScale(self::$scale, $roundingMode); + } + + /** + * {@inheritdoc} + */ + public function getStep() : int + { + return 1; + } + + /** + * {@inheritdoc} + */ + public function isFixedScale() : bool + { + return true; + } + + public static function getScale(): int + { + return self::$scale; + } + + public static function setScale(int $scale): void + { + self::$scale = $scale; + } +} diff --git a/Money/EmbeddedMoney.php b/Money/EmbeddedMoney.php new file mode 100644 index 0000000..1aabcf4 --- /dev/null +++ b/Money/EmbeddedMoney.php @@ -0,0 +1,45 @@ +amount = $money->getAmount(); + $this->currency = $money->getCurrency(); + } + + public static function of(BigNumber|int|float|string $amount, Currency $currency): self + { + return new EmbeddedMoney(Money::of($amount, $currency, new BundleContext())); + } + + public static function zero(Currency $currency): self + { + return new EmbeddedMoney(Money::zero($currency, new BundleContext())); + } + + public function __invoke(): Money + { + return Money::of($this->amount, $this->currency, new BundleContext()); + } +} diff --git a/Money/Formatter/NumberFormatterFactory.php b/Money/Formatter/NumberFormatterFactory.php deleted file mode 100644 index 097a5f5..0000000 --- a/Money/Formatter/NumberFormatterFactory.php +++ /dev/null @@ -1,19 +0,0 @@ -getCurrentRequest()) { - $locale = $defaultLocale; - } else { - $locale = $requestStack->getCurrentRequest()->getLocale(); - } - - return new \NumberFormatter($locale, \NumberFormatter::CURRENCY); - } -} diff --git a/Money/NullableMoney.php b/Money/NullableMoney.php new file mode 100644 index 0000000..c9daac7 --- /dev/null +++ b/Money/NullableMoney.php @@ -0,0 +1,39 @@ +amount = $money !== null ? $money->getAmount() : null; + $this->currency = $money !== null ? $money->getCurrency() : null; + } + + public function __invoke(): ?Money + { + if ($this->amount === null || $this->currency === null) { + return null; + } + + return Money::of($this->amount, $this->currency, new BundleContext()); + } +} diff --git a/Padam87MoneyBundle.php b/Padam87MoneyBundle.php index 903d030..2c926b9 100644 --- a/Padam87MoneyBundle.php +++ b/Padam87MoneyBundle.php @@ -2,16 +2,27 @@ namespace Padam87\MoneyBundle; -use Padam87\MoneyBundle\Doctrine\Type\MoneyAmountType; +use Doctrine\DBAL\Types\Type; +use Padam87\MoneyBundle\Doctrine\Type\CurrencyType; +use Padam87\MoneyBundle\Doctrine\Type\DecimalObjectType; +use Padam87\MoneyBundle\Money\Context\BundleContext; use Symfony\Component\HttpKernel\Bundle\Bundle; class Padam87MoneyBundle extends Bundle { - public function boot() + public function boot(): void { $config = $this->container->getParameter('padam87_money.config'); - MoneyAmountType::$precision = $config['precision']; - MoneyAmountType::$scale = $config['scale']; + BundleContext::setScale($config['scale']); + + // @TODO: Keep an eye on https://github.com/doctrine/DoctrineBundle/issues/1867 for a better way to do this. + if (!Type::hasType('decimal_object')) { + Type::addType('decimal_object', new DecimalObjectType($config)); + } + + if (!Type::hasType('currency')) { + Type::addType('currency', CurrencyType::class); + } } } diff --git a/README.md b/README.md index b6a870b..c22ec3d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MoneyBundle -Symfony bundle for https://github.com/moneyphp/money +Symfony bundle for https://github.com/brick/money As an **opinionated** bundle, this money bundle uses the following principles as it's main guide: - Storage is just as important as calculation. @@ -13,9 +13,6 @@ To achieve these, the following restrictions apply: - precision and scale are mandatory (but have default values) - amounts are mapped as DECIMAL (changable, but not recommended to change) - ext-bcmath is mandatory. -(DECIMAL database values are converted to string by PDO. -This bundle uses bcmath to multiple these values by ˙pow(10, $scale)˙, and pass integer values to the `Money` object. -https://github.com/Padam87/MoneyBundle/blob/master/Doctrine/Type/MoneyAmountType.php#L45) ## Installation @@ -37,19 +34,80 @@ padam87_money: ### Doctrine +#### A) Using the embedded money type + ```php - /** - * @var Money - * - * @ORM\Embedded(class="Money\Money") - */ - private $price; + #[ORM\Embedded(class: EmbeddedMoney::class)] + protected EmbeddedMoney $netPrice; + + public function getNetPrice(): ?Money + { + return ($this->netPrice)(); + } + + public function setNetPrice(?Money $netPrice): self + { + $this->netPrice = new EmbeddedMoney($netPrice); + + return $this; + } +``` + +#### B) Using separate fields for amount and currency + +_This is recommended when multiple amounts share the same currency_ + +```php + use MoneyFromDecimalTrait; + + #[ORM\Column(type: 'currency')] + private ?Currency $currency = null; + + #[ORM\Column(type: 'decimal_object')] + private ?BigDecimal $netPrice = null; + + #[ORM\Column(type: 'decimal_object')] + private ?BigDecimal $grossPrice = null; + + + public function getCurrency(): ?Currency + { + return $this->currency; + } + + + public function getNetPrice(): Money + { + return $this->getMoney($this->netPrice); + } + + protected function setNetPrice(Money $netPrice): self + { + $this->setMoney($this->netPrice, $netPrice); + + return $this; + } + + + public function getGrossPrice(): Money + { + return $this->getMoney($this->grossPrice); + } + + protected function setGrossPrice(Money $grossPrice): self + { + $this->setMoney($this->grossPrice, $grossPrice); + + return $this; + } ``` ### Formatting -The bundle adds 2 services. +#### Twig + +`{{ netPrice|money }}` -> €100 -`padam87_money.number_formatter` - A simple `\NumberFormatter` object, with the current request's locale, and currency style. +`{{ netPrice|money_amount }}` -> 100 -`Money\Formatter\IntlMoneyFormatter` - Intl money formatter +`{{ netPrice|money_currency }}` -> € diff --git a/Repository/ExchangeRateRepositoryInterface.php b/Repository/ExchangeRateRepositoryInterface.php index ef8c67d..26e6a3d 100644 --- a/Repository/ExchangeRateRepositoryInterface.php +++ b/Repository/ExchangeRateRepositoryInterface.php @@ -2,10 +2,9 @@ namespace Padam87\MoneyBundle\Repository; -use Money\Currency; use Padam87\MoneyBundle\Entity\ExchangeRateInterface; interface ExchangeRateRepositoryInterface { - public function getExchangeRate(Currency $baseCurrency, Currency $counterCurrency): ?ExchangeRateInterface; + public function getExchangeRate(string $sourceCurrencyCode, string $targetCurrencyCode): ?ExchangeRateInterface; } diff --git a/Resources/config/services.yaml b/Resources/config/services.yaml index 4db17e1..679068d 100644 --- a/Resources/config/services.yaml +++ b/Resources/config/services.yaml @@ -1,50 +1,45 @@ services: _defaults: public: false - autowire: true - autoconfigure: true Padam87\MoneyBundle\Doctrine\Mapping\Driver\MoneyEmbeddedDriver: arguments: $config: '%padam87_money.config%' - Padam87\MoneyBundle\Doctrine\Mapping\Driver\CurrencyPairEmbeddedDriver: ~ - - Padam87\MoneyBundle\Money\Formatter\NumberFormatterFactory: ~ - - padam87_money.number_formatter: - class: \NumberFormatter - factory: 'Padam87\MoneyBundle\Money\Formatter\NumberFormatterFactory:createNumberFormatter' - arguments: ['@request_stack', '%locale%'] - - Money\Formatter\IntlMoneyFormatter: + Padam87\MoneyBundle\Service\MoneyFormatter: arguments: - - '@padam87_money.number_formatter' - - '@Money\Currencies\CurrencyList' + $requestStack: '@request_stack' + $defaultLocale: '%kernel.default_locale%' + $config: '%padam87_money.config%' - Padam87\MoneyBundle\Service\MoneyHelper: - public: true + Padam87\MoneyBundle\Form\CurrencyType: arguments: $config: '%padam87_money.config%' + tags: + - 'form.type' + + Padam87\MoneyBundle\Form\DecimalType: + tags: + - 'form.type' Padam87\MoneyBundle\Form\MoneyType: - arguments: - $config: '%padam87_money.config%' + tags: + - 'form.type' - Padam87\MoneyBundle\Form\CurrencyType: + Padam87\MoneyBundle\Twig\Extension\MoneyExtension: arguments: - $config: '%padam87_money.config%' + $formatter: '@Padam87\MoneyBundle\Service\MoneyFormatter' + $converter: '@Brick\Money\CurrencyConverter' + tags: + - 'twig.extension' - Padam87\MoneyBundle\Exchange\DatabaseExchange: - arguments: - - '@Doctrine\Persistence\ManagerRegistry' + Padam87\MoneyBundle\Money\Context\BundleContext: + shared: false - Money\Exchange\ReversedCurrenciesExchange: + Padam87\MoneyBundle\Service\DatabaseExchangeRateProvider: arguments: - - '@Padam87\MoneyBundle\Exchange\DatabaseExchange' + $doctrine: '@doctrine' - Money\Converter: - class: Money\Converter + Brick\Money\CurrencyConverter: arguments: - - '@Money\Currencies\CurrencyList' - - '@Money\Exchange\ReversedCurrenciesExchange' + $exchangeRateProvider: '@Padam87\MoneyBundle\Service\DatabaseExchangeRateProvider' diff --git a/Service/DatabaseExchangeRateProvider.php b/Service/DatabaseExchangeRateProvider.php new file mode 100644 index 0000000..46573cb --- /dev/null +++ b/Service/DatabaseExchangeRateProvider.php @@ -0,0 +1,38 @@ +doctrine->getRepository(ExchangeRateInterface::class); + + if (!$repo instanceof ExchangeRateRepositoryInterface) { + throw new \LogicException( + sprintf('"%s" must implement %s', $repo->getClassName(), ExchangeRateRepositoryInterface::class) + ); + } + + if (null === $exchangeRate = $repo->getExchangeRate($sourceCurrencyCode, $targetCurrencyCode)) { + throw CurrencyConversionException::exchangeRateNotAvailable($sourceCurrencyCode, $targetCurrencyCode); + } + + return $exchangeRate->getConversionRatio(); + } +} diff --git a/Service/MoneyFormatter.php b/Service/MoneyFormatter.php new file mode 100644 index 0000000..c5ba629 --- /dev/null +++ b/Service/MoneyFormatter.php @@ -0,0 +1,84 @@ +getCurrencyDigits($money->getCurrency()); + } + + return $money->formatWith($this->createFormatter($digits)); + } + + public function amount(?Money $money, ?int $digits = null): ?string + { + if ($money === null) { + return null; + } + + if ($digits === null) { + $digits = $this->getCurrencyDigits($money->getCurrency()); + } + + return $this->createFormatter($digits, \NumberFormatter::DECIMAL)->format((string) $money->getAmount()); + } + + public function currency(?Money $money): ?string + { + if ($money === null) { + return null; + } + + return Currencies::getSymbol($money->getCurrency()->getCurrencyCode()); + } + + private function createFormatter(?int $digits = null, int $formatStyle = \NumberFormatter::CURRENCY): \NumberFormatter + { + $locale = $this->getLocale(); + $formatter = new \NumberFormatter($locale, $formatStyle); + $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, \NumberFormatter::ROUND_CEILING); + + if ($digits !== null) { + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $digits); + $formatter->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, $digits); + $formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $digits); + $formatter->setAttribute(\NumberFormatter::DECIMAL_ALWAYS_SHOWN, $digits); + } + + return $formatter; + } + + private function getCurrencyDigits(Currency $currency): ?int + { + if (array_key_exists($currency->getCurrencyCode(), $this->config['currency_digits'])) { + return (int) $this->config['currency_digits'][$currency->getCurrencyCode()]; + } + + return null; + } + + private function getLocale(): string + { + if (null === $request = $this->requestStack->getMainRequest()) { + return $this->defaultLocale; + } + + return $request->getLocale(); + } +} diff --git a/Service/MoneyHelper.php b/Service/MoneyHelper.php deleted file mode 100644 index 7b54993..0000000 --- a/Service/MoneyHelper.php +++ /dev/null @@ -1,29 +0,0 @@ -config = $config; - } - - public function createMoney($amount, Currency $currency): Money - { - return new Money(bcmul($amount, pow(10, $this->config['scale']), 0), $currency); - } - - public function getAmount(Money $money, ?int $scale = null): string - { - return bcdiv( - $money->getAmount(), - pow(10, $this->config['scale']), $scale === null ? $this->config['scale'] : $scale - ); - } -} diff --git a/Twig/Extension/MoneyExtension.php b/Twig/Extension/MoneyExtension.php new file mode 100644 index 0000000..e2bff9f --- /dev/null +++ b/Twig/Extension/MoneyExtension.php @@ -0,0 +1,52 @@ +formatter->format(...)), + new TwigFilter('money_amount', $this->formatter->amount(...)), + new TwigFilter('money_currency', $this->formatter->currency(...)), + new TwigFilter('money_convert', $this->converter->convert(...)), + new TwigFilter( + 'currency', + function ($currency): ?string { + if ($currency === null) { + return null; + } + + if (!$currency instanceof Currency) { + $currency = Currency::of($currency); + } + + return Currencies::getSymbol($currency->getCurrencyCode()); + } + ), + ]; + } + + public function getFunctions(): array + { + return [ + new TwigFunction('money', fn(BigNumber|int|float|string $amount, Currency|string|int $currency): Money => Money::of($amount, $currency)), + new TwigFunction('currency', fn(string|int $code): Currency => Currency::of($code)), + ]; + } +} diff --git a/composer.json b/composer.json index f467218..341e1c2 100644 --- a/composer.json +++ b/composer.json @@ -3,12 +3,16 @@ "description": "Symfony bundle for https://github.com/moneyphp/money", "type": "symfony-bundle", "require": { - "php": "^7.4 || ^8.0", + "php": "^8.4", "ext-bcmath": "*", - "symfony/framework-bundle": "^4.0 || ^5.0 || ^6.0", - "moneyphp/money": "^3.1", - "doctrine/orm": "^2.5", - "doctrine/doctrine-bundle": "^1.5 || ^2.0" + "symfony/framework-bundle": "^6.0 || ^7.0 || ^8.0", + "brick/money": "^0.9", + "doctrine/orm": "^3.0", + "doctrine/doctrine-bundle": "^3.0", + "doctrine/dbal": "^4.0" + }, + "conflict": { + "doctrine/persistence": "3.4.2 || 4.1.1" }, "require-dev": { "phpunit/phpunit": "^7.2"