Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Decimal128 type #2128

Merged
merged 4 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@
"symfony/var-dumper": "^3.4|^4.1|^5.0"
},
"require-dev": {
"ext-bcmath": "*",
"doctrine/coding-standard": "^6.0",
"jmikola/geojson": "^1.0",
"phpbench/phpbench": "^0.13.0",
"phpstan/phpstan": "^0.10.3",
"phpunit/phpunit": "^8.2",
"squizlabs/php_codesniffer": "^3.5, <3.5.5"
},
"suggest": {
"ext-bcmath": "Decimal128 type support"
},
"autoload": {
"psr-4": { "Doctrine\\ODM\\MongoDB\\": "lib/Doctrine/ODM/MongoDB" }
},
Expand Down
2 changes: 1 addition & 1 deletion docs/en/reference/annotations-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,7 @@ Alias of `@Index`_, with the ``unique`` option set by default.
--------

The annotated instance variable will be used to store version information for :ref:`optimistic locking <transactions_and_concurrency_optimistic_locking>`.
This is only compatible with ``int``, ``date``, and ``date_immutable`` field types, and cannot be combined with `@Id`_.
This is only compatible with ``int``, ``decimal128``, ``date``, and ``date_immutable`` field types, and cannot be combined with `@Id`_.

.. code-block:: php

Expand Down
2 changes: 2 additions & 0 deletions docs/en/reference/basic-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ Here is a quick overview of the built-in mapping types:
- ``custom_id``
- ``date``
- ``date_immutable``
- ``decimal128``
- ``file``
- ``float``
- ``hash``
Expand Down Expand Up @@ -182,6 +183,7 @@ This list explains some of the less obvious mapping types:
- ``collection``: numerically indexed array to MongoDB array
- ``date``: DateTime to ``MongoDB\BSON\UTCDateTime``
- ``date_immutable``: DateTimeImmutable to ``MongoDB\BSON\UTCDateTime``
- ``decimal128``: string to ``MongoDB\BSON\Decimal128``, requires ``ext-bcmath``
- ``hash``: associative array to MongoDB object
- ``id``: string to ObjectId by default, but other formats are possible
- ``timestamp``: string to ``MongoDB\BSON\Timestamp``
Expand Down
6 changes: 4 additions & 2 deletions docs/en/reference/storage-strategies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ increment
---------

The ``increment`` strategy does not apply to collections but can be used for
``int`` and ``float`` fields. When using the ``increment`` strategy, the field
value will be updated using the `$inc`_ operator.
``int``, ``float``, ``decimal128``, and any custom type implementing the
``\Doctrine\ODM\MongoDB\Types\Incrementable`` interface. When using the
``increment`` strategy, the field value will be updated using the `$inc`_
operator.

addToSet
--------
Expand Down
18 changes: 16 additions & 2 deletions docs/en/reference/transactions-and-concurrency.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Doctrine has integrated support for automatic optimistic locking
via a ``version`` field. Any document that should be
protected against concurrent modifications during long-running
business transactions gets a ``version`` field that is either a simple
number (mapping type: ``int``) or a date (mapping type: ``date`` or ``date_immutable``).
number (mapping type: ``int`` or ``decimal128``) or a date (mapping type: ``date`` or ``date_immutable``).
When changes to the document are persisted,
the expected version and version increment are incorporated into the update criteria and modifiers, respectively.
If this results in no document being modified by the update (i.e. expected version did not match),
Expand All @@ -69,6 +69,20 @@ The following example designates a version field using the ``int`` type:

<field field-name="version" version="true" type="int" />

Or with ``decimal128`` type:

.. configuration-block::

.. code-block:: php

<?php
/** @Version @Field(type="decimal128") */
private $version;

.. code-block:: xml

<field field-name="version" version="true" type="decimal128" />

Alternatively, the ``date`` type may be used:

.. configuration-block::
Expand Down Expand Up @@ -101,7 +115,7 @@ Choosing the Field Type
"""""""""""""""""""""""

When using the date-based type in a high-concurrency environment, it is still possible to create multiple documents
with the same version and cause a conflict. This can be avoided by using the ``int`` type.
with the same version and cause a conflict. This can be avoided by using the ``int`` or ``decimal128`` type.

Usage
"""""
Expand Down
2 changes: 1 addition & 1 deletion lib/Doctrine/ODM/MongoDB/LockException.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ public static function invalidLockFieldType(string $type) : self

public static function invalidVersionFieldType(string $type) : self
{
return new self('Invalid version field type ' . $type . '. Version field must be int, integer, date or date_immutable.');
return new self('Invalid version field type ' . $type . '. Version field must be int, integer, date, date_immutable, or decimal128.');
}
}
27 changes: 20 additions & 7 deletions lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Doctrine\Instantiator\InstantiatorInterface;
use Doctrine\ODM\MongoDB\Id\AbstractIdGenerator;
use Doctrine\ODM\MongoDB\LockException;
use Doctrine\ODM\MongoDB\Types\Incrementable;
use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
use Doctrine\Persistence\Mapping\ClassMetadata as BaseClassMetadata;
Expand All @@ -25,6 +26,7 @@
use function class_exists;
use function constant;
use function count;
use function extension_loaded;
use function get_class;
use function in_array;
use function is_array;
Expand Down Expand Up @@ -1192,12 +1194,6 @@ private function applyStorageStrategy(array &$mapping) : void
}

switch (true) {
case $mapping['type'] === 'int':
case $mapping['type'] === 'float':
$defaultStrategy = self::STORAGE_STRATEGY_SET;
$allowedStrategies = [self::STORAGE_STRATEGY_SET, self::STORAGE_STRATEGY_INCREMENT];
break;

case $mapping['type'] === 'many':
$defaultStrategy = CollectionHelper::DEFAULT_STRATEGY;
$allowedStrategies = [
Expand All @@ -1210,9 +1206,18 @@ private function applyStorageStrategy(array &$mapping) : void
];
break;

case $mapping['type'] === 'one':
alcaeus marked this conversation as resolved.
Show resolved Hide resolved
$defaultStrategy = self::STORAGE_STRATEGY_SET;
$allowedStrategies = [self::STORAGE_STRATEGY_SET];
break;

default:
$defaultStrategy = self::STORAGE_STRATEGY_SET;
$allowedStrategies = [self::STORAGE_STRATEGY_SET];
$type = Type::getType($mapping['type']);
if ($type instanceof Incrementable) {
$allowedStrategies[] = self::STORAGE_STRATEGY_INCREMENT;
}
}

if (! isset($mapping['strategy'])) {
Expand Down Expand Up @@ -1667,7 +1672,7 @@ public function isIdGeneratorNone() : bool
*/
public function setVersionMapping(array &$mapping) : void
{
if (! in_array($mapping['type'], [Type::INT, Type::INTEGER, Type::DATE, Type::DATE_IMMUTABLE], true)) {
if (! in_array($mapping['type'], [Type::INT, Type::INTEGER, Type::DATE, Type::DATE_IMMUTABLE, Type::DECIMAL128], true)) {
throw LockException::invalidVersionFieldType($mapping['type']);
}

Expand Down Expand Up @@ -1974,6 +1979,7 @@ public function mapField(array $mapping) : array

$this->applyStorageStrategy($mapping);
$this->checkDuplicateMapping($mapping);
$this->typeRequirementsAreMet($mapping);

$this->fieldMappings[$mapping['fieldName']] = $mapping;
if (isset($mapping['association'])) {
Expand Down Expand Up @@ -2118,6 +2124,13 @@ private function isAllowedGridFSField(string $name) : bool
return in_array($name, self::ALLOWED_GRIDFS_FIELDS, true);
}

private function typeRequirementsAreMet(array $mapping) : void
{
if ($mapping['type'] === Type::DECIMAL128 && ! extension_loaded('bcmath')) {
throw MappingException::typeRequirementsNotFulfilled($this->name, $mapping['fieldName'], Type::DECIMAL128, 'ext-bcmath is missing');
}
}

private function checkDuplicateMapping(array $mapping) : void
{
if ($mapping['notSaved'] ?? false) {
Expand Down
5 changes: 5 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public static function typeNotFound(string $name) : self
return new self(sprintf('Type to be overwritten %s does not exist.', $name));
}

public static function typeRequirementsNotFulfilled(string $className, string $fieldName, string $type, string $reason) : self
{
return new self(sprintf("Can not use '%s' type for field '%s' in class '%s' as its requirements are not met: %s.", $fieldName, $className, $type, $reason));
}

public static function mappingNotFound(string $className, string $fieldName) : self
{
return new self(sprintf("No mapping found for field '%s' in class '%s'.", $fieldName, $className));
Expand Down
16 changes: 16 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
use function array_slice;
use function array_values;
use function assert;
use function bcadd;
use function bccomp;
use function count;
use function explode;
use function get_class;
Expand Down Expand Up @@ -219,6 +221,10 @@ public function executeInserts(array $options = []) : void
$nextVersionDateTime = $versionMapping['type'] === Type::DATE ? new DateTime() : new DateTimeImmutable();
$nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
$this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
} elseif ($versionMapping['type'] === Type::DECIMAL128) {
$current = (string) $this->class->reflFields[$this->class->versionField]->getValue($document);
$nextVersion = bccomp('1', $current) === 1 ? '1' : $current;
$this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
}
$data[$versionMapping['name']] = $nextVersion;
}
Expand Down Expand Up @@ -294,6 +300,10 @@ private function executeUpsert(object $document, array $options) : void
$nextVersionDateTime = $versionMapping['type'] === Type::DATE ? new DateTime() : new DateTimeImmutable();
$nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
$this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
} elseif ($versionMapping['type'] === Type::DECIMAL128) {
$current = (string) $this->class->reflFields[$this->class->versionField]->getValue($document);
$nextVersion = bccomp('1', $current) === 1 ? '1' : $current;
$this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
}
$data['$set'][$versionMapping['name']] = $nextVersion;
}
Expand Down Expand Up @@ -380,6 +390,12 @@ public function update(object $document, array $options = []) : void
$nextVersion = $versionMapping['type'] === Type::DATE ? new DateTime() : new DateTimeImmutable();
$update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
$query[$versionMapping['name']] = Type::convertPHPToDatabaseValue($currentVersion);
} elseif ($versionMapping['type'] === Type::DECIMAL128) {
$current = $this->class->reflFields[$this->class->versionField]->getValue($document);
$nextVersion = bcadd($current, '1');
$type = Type::getType(Type::DECIMAL128);
$update['$set'][$versionMapping['name']] = $type->convertPHPToDatabaseValue($nextVersion);
$query[$versionMapping['name']] = $type->convertPHPToDatabaseValue($currentVersion);
}
}

Expand Down
10 changes: 8 additions & 2 deletions lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Mapping\MappingException;
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
use Doctrine\ODM\MongoDB\Types\Incrementable;
use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\ODM\MongoDB\UnitOfWork;
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
use InvalidArgumentException;
use UnexpectedValueException;
use function array_search;
use function array_values;
use function assert;
use function get_class;

/**
Expand Down Expand Up @@ -145,7 +147,9 @@ public function prepareUpdateData($document)
} else {
if ($new !== null && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) {
$operator = '$inc';
$value = Type::getType($mapping['type'])->convertToDatabaseValue($new - $old);
$type = Type::getType($mapping['type']);
assert($type instanceof Incrementable);
$value = $type->convertToDatabaseValue($type->diff($old, $new));
} else {
$operator = '$set';
$value = $new === null ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new);
Expand Down Expand Up @@ -247,7 +251,9 @@ public function prepareUpsertData($document)
if ($new !== null) {
if (empty($mapping['id']) && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) {
$operator = '$inc';
$value = Type::getType($mapping['type'])->convertToDatabaseValue($new - $old);
$type = Type::getType($mapping['type']);
assert($type instanceof Incrementable);
$value = $type->convertToDatabaseValue($type->diff($old, $new));
} else {
$operator = '$set';
$value = Type::getType($mapping['type'])->convertToDatabaseValue($new);
Expand Down
35 changes: 35 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Types/Decimal128Type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Types;

use MongoDB\BSON\Decimal128;
use function bcsub;

class Decimal128Type extends Type implements Incrementable
{
use ClosureToPHP;

public function convertToDatabaseValue($value)
{
if ($value === null) {
return null;
}
if (! $value instanceof Decimal128) {
$value = new Decimal128($value);
}

return $value;
}

public function convertToPHPValue($value)
{
return $value !== null ? (string) $value : null;
}

public function diff($old, $new)
{
return bcsub($new, $old);
}
}
7 changes: 6 additions & 1 deletion lib/Doctrine/ODM/MongoDB/Types/FloatType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/**
* The Float type.
*/
class FloatType extends Type
class FloatType extends Type implements Incrementable
{
public function convertToDatabaseValue($value)
{
Expand All @@ -28,4 +28,9 @@ public function closureToPHP() : string
{
return '$return = (float) $value;';
}

public function diff($old, $new)
{
return $new - $old;
}
}
21 changes: 21 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Types/Incrementable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Types;

/**
* Types implementing this interface can have the `increment` storage strategy.
*/
interface Incrementable
{
/**
* Calculates PHP-based difference between given values.
*
* @param mixed $old
* @param mixed $new
*
* @return mixed
*/
public function diff($old, $new);
}
7 changes: 6 additions & 1 deletion lib/Doctrine/ODM/MongoDB/Types/IntType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/**
* The Int type.
*/
class IntType extends Type
class IntType extends Type implements Incrementable
{
public function convertToDatabaseValue($value)
{
Expand All @@ -28,4 +28,9 @@ public function closureToPHP() : string
{
return '$return = (int) $value;';
}

public function diff($old, $new)
{
return $new - $old;
}
}
2 changes: 2 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Types/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ abstract class Type
public const COLLECTION = 'collection';
public const OBJECTID = 'object_id';
public const RAW = 'raw';
public const DECIMAL128 = 'decimal128';

/** @var Type[] Map of already instantiated type objects. One instance per type (flyweight). */
private static $typeObjects = [];
Expand Down Expand Up @@ -77,6 +78,7 @@ abstract class Type
self::COLLECTION => Types\CollectionType::class,
self::OBJECTID => Types\ObjectIdType::class,
self::RAW => Types\RawType::class,
self::DECIMAL128 => Types\Decimal128Type::class,
];

/** Prevent instantiation and force use of the factory method. */
Expand Down