UuidExtension::assignNewUuid()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 13
c 1
b 0
f 0
dl 0
loc 18
rs 9.8333
cc 3
nc 2
nop 1
1
<?php
2
3
namespace LeKoala\Uuid;
4
5
use Ramsey\Uuid\Uuid;
6
use SilverStripe\ORM\DB;
7
use InvalidArgumentException;
8
use SilverStripe\Core\Extension;
9
use SilverStripe\Forms\FieldList;
10
use SilverStripe\Forms\ReadonlyField;
11
use Tuupola\Base62Proxy as Base62;
12
use SilverStripe\ORM\DataObjectSchema;
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\DataObjectSchema was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
14
class UuidExtension extends Extension
15
{
16
    public const UUID_FIELD = 'Uuid';
17
18
    public const UUID_BINARY_FORMAT = 'binary';
19
20
    public const UUID_STRING_FORMAT = 'string';
21
22
    public const UUID_BASE62_FORMAT = 'base62';
23
24
    /**
25
     * @var array<string,string>
26
     */
27
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
28
        self::UUID_FIELD => DBUuid::class,
29
    ];
30
31
    /**
32
     * @var array<string,mixed>
33
     */
34
    private static $indexes = [
0 ignored issues
show
introduced by
The private property $indexes is not used, and could be removed.
Loading history...
35
        self::UUID_FIELD => true,
36
    ];
37
38
    protected function getBaseTableName(): string
39
    {
40
        $schema = DataObjectSchema::create();
41
        $table = $schema->tableForField(
42
            get_class($this->getOwner()),
43
            self::UUID_FIELD
44
        );
45
46
        return $table;
47
    }
48
49
    /**
50
     * Assign a new uuid to this record. This will overwrite any existing uuid.
51
     *
52
     * @param bool $check Check if the uuid is already taken
53
     * @return string The new uuid
54
     */
55
    public function assignNewUuid($check = true)
56
    {
57
        $uuid = Uuid::uuid4();
58
        if ($check) {
59
            $table = $this->getBaseTableName();
60
            do {
61
                $this->owner->Uuid = $uuid->getBytes();
62
                // If we have something, keep checking
63
                $check = DB::prepared_query(
64
                    'SELECT count(ID) FROM ' . $table . ' WHERE Uuid = ?',
65
                    [$this->owner->Uuid]
66
                )->value() > 0;
67
            } while ($check);
68
        } else {
69
            $this->owner->Uuid = $uuid->getBytes();
70
        }
71
72
        return $this->owner->Uuid;
73
    }
74
75
    /**
76
     * Get a record by its uuid
77
     *
78
     * @template T
79
     * @param class-string<T> $class The class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
80
     * @param string $value The uuid value
81
     * @param string $format Any UUID_XXXX_FORMAT constant or string
82
     * @return T|null The DataObject or null if no record is found or format invalid
83
     */
84
    public static function getByUuid($class, $value, $format = null)
85
    {
86
        // Guess format from value
87
        if ($format === null) {
88
            try {
89
                $format = self::getUuidFormat($value);
90
            } catch (InvalidArgumentException $ex) {
91
                $format = null;
92
            }
93
        }
94
        // Convert format to bytes for query
95
        switch ($format) {
96
            case self::UUID_BASE62_FORMAT:
97
                try {
98
                    $decodedValue = Base62::decode($value);
99
                } catch (InvalidArgumentException $ex) {
100
                    // Invalid arguments should not return anything
101
                    return null;
102
                }
103
                $uuid = Uuid::fromBytes($decodedValue);
104
                break;
105
            case self::UUID_STRING_FORMAT:
106
                $uuid = Uuid::fromString($value);
107
                break;
108
            case self::UUID_BINARY_FORMAT:
109
                $uuid = Uuid::fromBytes($value);
110
                break;
111
            default:
112
                return null;
113
        }
114
        // Fetch the first record and disable subsite filter in a similar way as asking by ID
115
        $q = $class::get()->filter(
116
            self::UUID_FIELD,
117
            $uuid->getBytes()
118
        )->setDataQueryParam('Subsite.filter', false);
119
        return $q->first();
120
    }
121
122
    /**
123
     * Guess uuid format based on strlen
124
     *
125
     * @param mixed $value
126
     * @return string
127
     * @throws InvalidArgumentException
128
     */
129
    public static function getUuidFormat($value)
130
    {
131
        $len = strlen((string)$value);
132
133
        if ($len == 36) {
134
            // d84560c8-134f-11e6-a1e2-34363bd26dae => 36 chars
135
            return self::UUID_STRING_FORMAT;
136
        } elseif ($len > 20 && $len < 24) {
137
            // 6a630O1jrtMjCrQDyG3D3O => 22 chars (in theory, because sometimes it's different)
138
            return self::UUID_BASE62_FORMAT;
139
        } elseif ($len == 16) {
140
            return self::UUID_BINARY_FORMAT;
141
        }
142
        throw new InvalidArgumentException("$value does not seem to be a valid uuid");
143
    }
144
145
    /**
146
     * Return a uuid suitable for an URL, like an URLSegment
147
     *
148
     * @return string
149
     */
150
    public function UuidSegment()
151
    {
152
        // assign on the fly
153
        if (!$this->owner->Uuid) {
154
            $uuid = $this->assignNewUuid();
155
            // Make a quick write without using orm
156
            if ($this->owner->ID) {
157
                $table = $this->getBaseTableName();
158
                DB::prepared_query("UPDATE $table SET Uuid = ? WHERE ID = ?", [$uuid, $this->owner->ID]);
159
            }
160
        }
161
        /** @var DBUuid $dbObject */
162
        $dbObject = $this->owner->dbObject(self::UUID_FIELD);
163
        return $dbObject->Base62();
164
    }
165
166
    /**
167
     * @param FieldList $fields
168
     * @return void
169
     */
170
    public function updateCMSFields(FieldList $fields)
171
    {
172
        if (DBUuid::config()->show_cms_field) {
173
            $firstField = $fields->dataFieldNames()[0] ?? null;
174
            /** @var DBUuid $dbObject */
175
            $dbObject = $this->owner->dbObject('Uuid');
176
            $uuidField = ReadonlyField::create('UuidNice', 'Uuid', $dbObject->Nice());
177
            $fields->addFieldToTab('Root.Main', $uuidField, $firstField);
178
        }
179
    }
180
181
    /**
182
     * @return void
183
     */
184
    public function onBeforeWrite()
185
    {
186
        $owner = $this->getOwner();
187
188
        if (!$owner->Uuid) {
189
            $this->assignNewUuid();
190
        }
191
    }
192
}
193