Skip to content

Unable to use #[Version] with custom typeΒ #2789

@alamirault

Description

@alamirault

Bug Report

Q A
BC Break no
Version 2.10.2 (and latest)

Summary

I try to use #[Version] to have optimistic locking, however it does not work with custom type.

Current behavior

My new type supports Ramsey/Uuid and it's register like this in the bundle

doctrine_mongodb:
    types:
        Ramsey\Uuid\Uuid: App\Doctrine\ODM\Types\UuidType

When I try to update the entity, I have the following error

Doctrine\ODM\MongoDB\LockException: A lock failed on a document.
vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/LockException.php:24
vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php:410
vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/UnitOfWork.php:1239
vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/UnitOfWork.php:3125
vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/UnitOfWork.php:472
vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/DocumentManager.php:585
tests/Integration/Document/FooTest.php:30

How to reproduce

I created a reproducer: https://github.com/alamirault/reproducer-version-custom-type

  • composer install
  • update mongodb_url and mongodb_database in config/services.yaml
  • php bin/phpunit

Expected behavior

#[Version] attribute must work with custom types

Proposal

I dig a bit and saw we call Type::convertPHPToDatabaseValue($value) with current version (Uuid instance) and next version (Uuid instance) which call Type::getTypeFromPHPVariable($variable).

if ($this->class->isVersioned) {
$versionMapping = $this->class->fieldMappings[$this->class->versionField];
$currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
$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);
}

This static method getTypeFromPHPVariable doesn't try to find type because variable is an object.

/**
* Get a Type instance based on the type of the passed php variable.
*
* @param mixed $variable
*
* @throws InvalidArgumentException
*/
public static function getTypeFromPHPVariable($variable): ?Type
{
if (is_object($variable)) {
if ($variable instanceof DateTimeInterface) {
return self::getType('date');
}
if ($variable instanceof ObjectId) {
return self::getType('id');
}
} else {
$type = gettype($variable);
switch ($type) {
case 'integer':
return self::getType('int');
}
}
return null;
}

Possible solutions:

Update `Type::getTypeFromPHPVariable` to try `self::getType($variable::class)` when is an object

Something like this

    public static function getTypeFromPHPVariable($variable): ?Type
    {
        if (is_object($variable)) {
            if ($variable instanceof DateTimeInterface) {
                return self::getType('date');
            }

            if ($variable instanceof ObjectId) {
                return self::getType('id');
            }

           // Try to found type from object variable as described in phpdoc "Get a Type instance based on the type of the passed php variable."
            try {
                return self::getType($variable::class);
            } catch (InvalidArgumentException $exception) {
                // no-op
            }
        } else {
            $type = gettype($variable);
            switch ($type) {
                case 'integer':
                    return self::getType('int');
            }
        }

        return null;
    }
Update DocumentPersister

At the point we already know version field type. So instead guess type from $currentVersion and $nextVersion, we can convert with retrieved type.
It's maybe be a BC Break if $type->getNextVersion doesn't return a value supported by itself

        if ($this->class->isVersioned) {
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
            $type           = Type::getType($versionMapping['type']);
            assert($type instanceof Versionable);
            $nextVersion                             = $type->getNextVersion($currentVersion);
            
            // Actual value is:
            // $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
            // $query[$versionMapping['name']]          = Type::convertPHPToDatabaseValue($currentVersion);

            // Proposal
            $update['$set'][$versionMapping['name']] = $type->convertToDatabaseValue($nextVersion);
            $query[$versionMapping['name']]          = $type->convertToDatabaseValue($currentVersion);
        }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions