-
-
Notifications
You must be signed in to change notification settings - Fork 166
/
Uuid.php
368 lines (323 loc) · 8.56 KB
/
Uuid.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
<?php
namespace Kirby\Uuid;
use Closure;
use Generator;
use Kirby\Cms\Collection;
use Kirby\Cms\File;
use Kirby\Cms\Page;
use Kirby\Cms\Site;
use Kirby\Cms\User;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Toolkit\Str;
/**
* The `Uuid` classes provide an interface to connect
* identifiable models (page, file, site, user, blocks,
* structure entries) with a dedicated UUID string.
* It also provides methods to cache these connections
* for faster lookup.
*
* ```
* // get UUID string
* $model->uuid()->toString();
*
* // get model from an UUID string
* Uuid::for('page://HhX1YtRR2ImG6h4')->model();
*
* // cache actions
* $model->uuid()->populate();
* $model->uuid()->clear();
* ```
* @since 3.8.0
*
* @package Kirby Uuid
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Uuid
{
protected const TYPE = 'uuid';
/**
* Customizable callback function for generating new ID strings instead
* of `Str::random()`. Receives length of string as parameter.
*/
public static Closure|null $generator = null;
/**
* Collection that is likely to contain the model and
* that will be checked first to speed up the lookup
*/
public Collection|null $context;
public Identifiable|null $model;
public Uri $uri;
public function __construct(
string|null $uuid = null,
Identifiable|null $model = null,
Collection|null $context = null
) {
// throw exception when globally disabled
if (Uuids::enabled() === false) {
throw new LogicException('UUIDs have been disabled via the `content.uuid` config option.');
}
$this->context = $context;
$this->model = $model;
if ($model) {
$this->uri = new Uri([
'scheme' => static::TYPE,
'host' => static::retrieveId($model)
]);
// in the rare case that both model and ID string
// got passed, make sure they match
if ($uuid && $uuid !== $this->uri->toString()) {
throw new LogicException('UUID: can\'t create new instance from both model and UUID string that do not match');
}
} elseif ($uuid) {
$this->uri = new Uri($uuid);
}
}
/**
* Removes the current UUID from cache,
* recursively including all children if needed
*/
public function clear(bool $recursive = false): bool
{
// For all models with children: if $recursive,
// also clear UUIDs from cache for all children
if ($recursive === true && $model = $this->model()) {
if (method_exists($model, 'children') === true) {
foreach ($model->children() as $child) {
$child->uuid()->clear(true);
}
}
}
return Uuids::cache()->remove($this->key());
}
/**
* Generator function for the local context
* collection, which takes priority when looking
* up the UUID/model from index
* @internal
*/
final public function context(): Generator
{
yield from $this->context ?? [];
}
/**
* Looks up UUID in cache and resolves
* to identifiable model object;
* implemented on child classes
*
* @codeCoverageIgnore
*/
protected function findByCache(): Identifiable|null
{
throw new LogicException('UUID class needs to implement the ::findByCache() method');
}
/**
* Looks up UUID in local and global index
* and returns the identifiable model object;
* implemented on child classes
*
* @codeCoverageIgnore
*/
protected function findByIndex(): Identifiable|null
{
throw new LogicException('UUID class needs to implement the ::findByIndex() method');
}
/**
* Shorthand to create instance
* by passing either UUID or model
*/
final public static function for(
string|Identifiable $seed,
Collection|null $context = null
): static|null {
// if globally disabled, return null
if (Uuids::enabled() === false) {
return null;
}
// for UUID string
if (is_string($seed) === true) {
return match (Str::before($seed, '://')) {
'page' => new PageUuid(uuid: $seed, context: $context),
'file' => new FileUuid(uuid: $seed, context: $context),
'site' => new SiteUuid(uuid: $seed, context: $context),
'user' => new UserUuid(uuid: $seed, context: $context),
// TODO: activate for uuid-block-structure-support
// 'block' => new BlockUuid(uuid: $seed, context: $context),
// 'struct' => new StructureUuid(uuid: $seed, context: $context),
default => throw new InvalidArgumentException('Invalid UUID URI: ' . $seed)
};
}
// for model object
return match (true) {
$seed instanceof Page
=> new PageUuid(model: $seed, context: $context),
$seed instanceof File
=> new FileUuid(model: $seed, context: $context),
$seed instanceof Site
=> new SiteUuid(model: $seed, context: $context),
$seed instanceof User
=> new UserUuid(model: $seed, context: $context),
// TODO: activate for uuid-block-structure-support
// $seed instanceof Block
// => new BlockUuid(model: $seed, context: $context),
// $seed instanceof StructureObject
// => new StructureUuid(model: $seed, context: $context),
default
=> throw new InvalidArgumentException('UUID not supported for: ' . get_class($seed))
};
}
/**
* Generates a new ID string
*/
final public static function generate(int $length = 16): string
{
if (static::$generator !== null) {
return (static::$generator)($length);
}
return Str::random($length, 'alphaNum');
}
/**
* Returns the UUID's id string (UUID without scheme);
* in child classes, this method must ensure that the
* model has an ID
*/
public function id(): string
{
return $this->uri->host();
}
/**
* Generator function that creates an index of
* all identifiable model objects globally;
* implemented in child classes
*/
public static function index(): Generator
{
yield from [];
}
/**
* Merges local and global index generators
* into one iterator
* @internal
*
* @return \Generator|\Kirby\Uuid\Identifiable[]
*/
final public function indexes(): Generator
{
yield from $this->context();
yield from static::index();
}
/**
* Checks if a string resembles an UUID URI,
* optionally of the given type (scheme)
*/
final public static function is(
string $string,
string|null $type = null
): bool {
// always return false when UUIDs have been disabled
if (Uuids::enabled() === false) {
return false;
}
$type ??= implode('|', Uri::$schemes);
$pattern = sprintf('!^(%s)://(.*)!', $type);
if (preg_match($pattern, $string, $matches) !== 1) {
return false;
}
if ($matches[1] === 'site') {
return strlen($matches[2]) === 0;
}
return strlen($matches[2]) > 0;
}
/**
* Checks if the UUID has already been cached
*/
public function isCached(): bool
{
return Uuids::cache()->exists($this->key());
}
/**
* Returns key for cache entry
*/
public function key(): string
{
$id = $this->id();
// for better performance when using a file-based cache,
// turn first two characters of the id into a directory
$id = Str::substr($id, 0, 2) . '/' . Str::substr($id, 2);
return static::TYPE . '/' . $id;
}
/**
* Tries to find the identifiable model in cache
* or index and returns the object
*
* @param bool $lazy If `true`, only lookup from cache
*/
public function model(bool $lazy = false): Identifiable|null
{
if ($this->model !== null) {
return $this->model;
}
if ($this->model = $this->findByCache()) {
return $this->model;
}
if ($lazy === false) {
if ($this->model = $this->findByIndex()) {
// lazily fill cache by writing to cache
// whenever looked up from index to speed
// up future lookups of the same UUID
$this->populate();
return $this->model;
}
}
return null;
}
/**
* Feeds the UUID into the cache
*
* @return bool
*/
public function populate(): bool
{
return Uuids::cache()->set($this->key(), $this->value());
}
/**
* Retrieves the existing ID string (UUID without
* scheme) for the model;
* can be overridden in child classes depending
* on how the model stores the UUID
*/
public static function retrieveId(Identifiable $model): string|null
{
return $model->id();
}
/**
* Returns the full UUID string including scheme
*/
public function toString(): string
{
// make sure id is generated if
// it doesn't exist yet
$this->id();
// make sure the id is cached
// that it can be found again
$this->populate();
return $this->uri->toString();
}
/**
* Returns value to be stored in cache
*/
public function value(): string|array
{
return $this->model()->id();
}
/**
* @see ::render
*/
public function __toString(): string
{
return $this->toString();
}
}