Skip to content

Introduce Versionable interface #2184

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

Merged
merged 1 commit into from
May 19, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/en/reference/annotations-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,9 @@ 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``, ``decimal128``, ``date``, and ``date_immutable`` field types, and cannot be combined with `@Id`_.
This is only compatible with types implementing the ``\Doctrine\ODM\MongoDB\Types\Versionable`` interface and cannot be
combined with `@Id`_. Following ODM types can be used for versioning: ``int``, ``decimal128``, ``date``, and
``date_immutable``.

.. code-block:: php

Expand Down
8 changes: 6 additions & 2 deletions docs/en/reference/transactions-and-concurrency.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ Approach
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 ``decimal128``) or a date (mapping type: ``date`` or ``date_immutable``).
business transactions gets a ``version`` field.
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 @@ -52,6 +51,11 @@ a ``LockException`` is thrown, which indicates that the document was already mod

| Versioning can only be used on *root* (top-level) documents.

.. note::

Only types implementing the ``\Doctrine\ODM\MongoDB\Types\Versionable`` interface can be used for versioning.
Following ODM types can be used for versioning: ``int``, ``decimal128``, ``date``, and ``date_immutable``.

Document Configuration
^^^^^^^^^^^^^^^^^^^^^^

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, date_immutable, or decimal128.');
return new self('Type ' . $type . ' does not implement Versionable interface.');
}
}
3 changes: 2 additions & 1 deletion lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Doctrine\ODM\MongoDB\LockException;
use Doctrine\ODM\MongoDB\Types\Incrementable;
use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\ODM\MongoDB\Types\Versionable;
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
use Doctrine\Persistence\Mapping\ClassMetadata as BaseClassMetadata;
use InvalidArgumentException;
Expand Down Expand Up @@ -1674,7 +1675,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, Type::DECIMAL128], true)) {
if (! Type::getType($mapping['type']) instanceof Versionable) {
throw LockException::invalidVersionFieldType($mapping['type']);
}

Expand Down
62 changes: 18 additions & 44 deletions lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
namespace Doctrine\ODM\MongoDB\Persisters;

use BadMethodCallException;
use DateTime;
use DateTimeImmutable;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Hydrator\HydratorException;
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
Expand All @@ -24,6 +22,7 @@
use Doctrine\ODM\MongoDB\Query\Query;
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\ODM\MongoDB\Types\Versionable;
use Doctrine\ODM\MongoDB\UnitOfWork;
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
use Doctrine\Persistence\Mapping\MappingException;
Expand All @@ -46,8 +45,6 @@
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 All @@ -59,7 +56,6 @@
use function is_object;
use function is_scalar;
use function is_string;
use function max;
use function spl_object_hash;
use function sprintf;
use function strpos;
Expand Down Expand Up @@ -213,20 +209,14 @@ public function executeInserts(array $options = []) : void
// Set the initial version for each insert
if ($this->class->isVersioned) {
$versionMapping = $this->class->fieldMappings[$this->class->versionField];
$nextVersion = null;
if ($versionMapping['type'] === Type::INT || $versionMapping['type'] === Type::INTEGER) {
$nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
$this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
} elseif ($versionMapping['type'] === Type::DATE || $versionMapping['type'] === Type::DATE_IMMUTABLE) {
$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;
$nextVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
$type = Type::getType($versionMapping['type']);
assert($type instanceof Versionable);
if ($nextVersion === null) {
$nextVersion = $type->getNextVersion(null);
$this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
}
$data[$versionMapping['name']] = $nextVersion;
$data[$versionMapping['name']] = $type->convertPHPToDatabaseValue($nextVersion);
}

$inserts[] = $data;
Expand Down Expand Up @@ -292,20 +282,14 @@ private function executeUpsert(object $document, array $options) : void
// Set the initial version for each upsert
if ($this->class->isVersioned) {
$versionMapping = $this->class->fieldMappings[$this->class->versionField];
$nextVersion = null;
if ($versionMapping['type'] === Type::INT || $versionMapping === Type::INTEGER) {
$nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
$this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
} elseif ($versionMapping['type'] === Type::DATE || $versionMapping['type'] === Type::DATE_IMMUTABLE) {
$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;
$nextVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
$type = Type::getType($versionMapping['type']);
assert($type instanceof Versionable);
if ($nextVersion === null) {
$nextVersion = $type->getNextVersion(null);
$this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
}
$data['$set'][$versionMapping['name']] = $nextVersion;
$data['$set'][$versionMapping['name']] = $type->convertPHPToDatabaseValue($nextVersion);
}

foreach (array_keys($criteria) as $field) {
Expand Down Expand Up @@ -382,21 +366,11 @@ public function update(object $document, array $options = []) : void
if ($this->class->isVersioned) {
$versionMapping = $this->class->fieldMappings[$this->class->versionField];
$currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
if ($versionMapping['type'] === Type::INT || $versionMapping['type'] === Type::INTEGER) {
$nextVersion = $currentVersion + 1;
$update['$inc'][$versionMapping['name']] = 1;
$query[$versionMapping['name']] = $currentVersion;
} elseif ($versionMapping['type'] === Type::DATE || $versionMapping['type'] === Type::DATE_IMMUTABLE) {
$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);
}
$type = Type::getType($versionMapping['type']);
assert($type instanceof Versionable);
$nextVersion = $type->getNextVersion($currentVersion);
$update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
$query[$versionMapping['name']] = Type::convertPHPToDatabaseValue($currentVersion);
}

if (! empty($update)) {
Expand Down
5 changes: 5 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Types/DateImmutableType.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ public static function getDateTime($value) : DateTimeInterface
get_class($datetime)
));
}

public function getNextVersion($current)
{
return new DateTimeImmutable();
}
}
7 changes: 6 additions & 1 deletion lib/Doctrine/ODM/MongoDB/Types/DateType.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
/**
* The Date type.
*/
class DateType extends Type
class DateType extends Type implements Versionable
{
/**
* Converts a value to a DateTime.
Expand Down Expand Up @@ -111,4 +111,9 @@ public function closureToPHP() : string
{
return 'if ($value === null) { $return = null; } else { $return = \\' . static::class . '::getDateTime($value); }';
}

public function getNextVersion($current)
{
return new DateTime();
}
}
12 changes: 11 additions & 1 deletion lib/Doctrine/ODM/MongoDB/Types/Decimal128Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
namespace Doctrine\ODM\MongoDB\Types;

use MongoDB\BSON\Decimal128;
use function bcadd;
use function bcsub;

class Decimal128Type extends Type implements Incrementable
class Decimal128Type extends Type implements Incrementable, Versionable
{
use ClosureToPHP;

Expand All @@ -32,4 +33,13 @@ public function diff($old, $new)
{
return bcsub($new, $old);
}

public function getNextVersion($current)
{
if ($current === null) {
return '1';
}

return bcadd($current, '1');
}
}
9 changes: 8 additions & 1 deletion lib/Doctrine/ODM/MongoDB/Types/IntType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

namespace Doctrine\ODM\MongoDB\Types;

use function max;

/**
* The Int type.
*/
class IntType extends Type implements Incrementable
class IntType extends Type implements Incrementable, Versionable
{
public function convertToDatabaseValue($value)
{
Expand All @@ -33,4 +35,9 @@ public function diff($old, $new)
{
return $new - $old;
}

public function getNextVersion($current)
{
return max(1, (int) $current + 1);
}
}
20 changes: 20 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Types/Versionable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Types;

/**
* Types implementing this interface can be used for version fields.
*/
interface Versionable
{
/**
* Calculates next version.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add a hint that null will be passed if no current version exists?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put it into @param WDYT?

*
* @param mixed|null $current version currently in use, null if not versioned yet (i.e. first version)
*
* @return mixed
*/
public function getNextVersion($current);
}
2 changes: 1 addition & 1 deletion tests/Doctrine/ODM/MongoDB/Tests/Functional/LockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ public function testInvalidLockDocument()
public function testInvalidVersionDocument()
{
$this->expectException(MongoDBException::class);
$this->expectExceptionMessage('Invalid version field type string. Version field must be int, integer, date, date_immutable, or decimal128.');
$this->expectExceptionMessage('Type string does not implement Versionable interface.');
$this->dm->getClassMetadata(InvalidVersionDocument::class);
}

Expand Down