Refactored UUID generation: use pseudo unique named UUID for schedules

This commit is contained in:
Igor Scheller 2022-12-31 16:25:16 +01:00
parent 23424830e7
commit 2be8e565bf
8 changed files with 229 additions and 34 deletions

View File

@ -5,6 +5,7 @@ use Engelsystem\Helpers\Carbon;
use Engelsystem\Http\Exceptions\HttpForbidden; use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Models\AngelType; use Engelsystem\Models\AngelType;
use Engelsystem\Models\Room; use Engelsystem\Models\Room;
use Engelsystem\Models\Shifts\Schedule;
use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftType; use Engelsystem\Models\Shifts\ShiftType;
use Engelsystem\Models\User\User; use Engelsystem\Models\User\User;
@ -208,7 +209,7 @@ function admin_shifts()
'description' => $description, 'description' => $description,
]; ];
} elseif ($mode == 'multi') { } elseif ($mode == 'multi') {
$shift_start = $start; $shift_start = $start;
do { do {
$shift_end = (clone $shift_start)->addSeconds((int) $length * 60); $shift_end = (clone $shift_start)->addSeconds((int) $length * 60);
@ -589,22 +590,26 @@ function admin_shifts_history(): string
throw_redirect(page_link_to('admin_shifts_history')); throw_redirect(page_link_to('admin_shifts_history'));
} }
$schedules = Schedule::all()->pluck('name', 'id')->toArray();
$shiftsData = Db::select(' $shiftsData = Db::select('
SELECT SELECT
transaction_id, s.transaction_id,
title, s.title,
COUNT(id) AS count, schedule_shift.schedule_id,
MIN(start) AS start, COUNT(s.id) AS count,
MAX(end) AS end, MIN(s.start) AS start,
created_by AS user_id, MAX(s.end) AS end,
MAX(created_at) AS created_at s.created_by AS user_id,
FROM shifts MAX(s.created_at) AS created_at
WHERE transaction_id IS NOT NULL FROM shifts AS s
GROUP BY transaction_id LEFT JOIN schedule_shift on schedule_shift.shift_id = s.id
WHERE s.transaction_id IS NOT NULL
GROUP BY s.transaction_id
ORDER BY created_at DESC ORDER BY created_at DESC
'); ');
foreach ($shiftsData as &$shiftData) { foreach ($shiftsData as &$shiftData) {
$shiftData['title'] = $shiftData['schedule_id'] ? __('shifts_history.schedule', [$schedules[$shiftData['schedule_id']]]) : $shiftData['title'];
$shiftData['user'] = User_Nick_render(User::find($shiftData['user_id'])); $shiftData['user'] = User_Nick_render(User::find($shiftData['user_id']));
$shiftData['start'] = Carbon::make($shiftData['start'])->format(__('Y-m-d H:i')); $shiftData['start'] = Carbon::make($shiftData['start'])->format(__('Y-m-d H:i'));
$shiftData['end'] = Carbon::make($shiftData['end'])->format(__('Y-m-d H:i')); $shiftData['end'] = Carbon::make($shiftData['end'])->format(__('Y-m-d H:i'));

View File

@ -12,6 +12,7 @@ use Engelsystem\Helpers\Schedule\Event;
use Engelsystem\Helpers\Schedule\Room; use Engelsystem\Helpers\Schedule\Room;
use Engelsystem\Helpers\Schedule\Schedule; use Engelsystem\Helpers\Schedule\Schedule;
use Engelsystem\Helpers\Schedule\XmlParser; use Engelsystem\Helpers\Schedule\XmlParser;
use Engelsystem\Helpers\Uuid;
use Engelsystem\Http\Request; use Engelsystem\Http\Request;
use Engelsystem\Http\Response; use Engelsystem\Http\Response;
use Engelsystem\Models\Room as RoomModel; use Engelsystem\Models\Room as RoomModel;
@ -304,6 +305,7 @@ class ImportSchedule extends BaseController
$shift->end = $event->getEndDate()->copy()->timezone($eventTimeZone); $shift->end = $event->getEndDate()->copy()->timezone($eventTimeZone);
$shift->room()->associate($room); $shift->room()->associate($room);
$shift->url = $event->getUrl() ?? ''; $shift->url = $event->getUrl() ?? '';
$shift->transaction_id = Uuid::uuidBy($scheduleUrl->id, '5c4ed01e');
$shift->createdBy()->associate($user); $shift->createdBy()->associate($user);
$shift->save(); $shift->save();

View File

@ -2868,6 +2868,9 @@ msgstr "Titel"
msgid "schedule.import.shift.room" msgid "schedule.import.shift.room"
msgstr "Raum" msgstr "Raum"
msgid "shifts_history.schedule"
msgstr "Programm: %s"
msgid "news.title" msgid "news.title"
msgstr "News" msgstr "News"

View File

@ -145,6 +145,9 @@ msgstr "Title"
msgid "schedule.import.shift.room" msgid "schedule.import.shift.room"
msgstr "Room" msgstr "Room"
msgid "shifts_history.schedule"
msgstr "Schedule: %s"
msgid "news.title" msgid "news.title"
msgstr "News" msgstr "News"

62
src/Helpers/Uuid.php Normal file
View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Helpers;
use InvalidArgumentException;
use Illuminate\Support\Str;
use Stringable;
class Uuid
{
/**
* Generate a v4 UUID
*/
public static function uuid(): string
{
return sprintf(
'%08x-%04x-%04x-%04x-%012x',
mt_rand(0, 0xffffffff),
mt_rand(0, 0xffff),
// first bit is the uuid version, here 4
mt_rand(0, 0x0fff) | 0x4000,
// variant
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffffffffffff)
);
}
/**
* Generate a dependent v4 UUID
* @var string|int|float|Stringable $value any value that can be converted to string
*/
public static function uuidBy(mixed $value, string $name = null): string
{
if (!is_null($name)) {
if (!preg_match('/^[\da-f]+$/i', $name)) {
throw new InvalidArgumentException('$name must be a hex string');
}
if (Str::length($name) > 20) {
throw new InvalidArgumentException('$name is longer than 20 characters');
}
$name = Str::lower($name);
}
$value = $name . md5((string) $value);
return sprintf(
'%08s-%04s-%04s-%04s-%012s',
Str::substr($value, 0, 8),
Str::substr($value, 8, 4),
// first bit is the uuid version, here 4
'4' . Str::substr($value, 13, 3),
// first bit is the variant (0x8-0xb)
dechex(8 + (hexdec(Str::substr($value, 16, 1)) % 4))
. Str::substr($value, 17, 3),
Str::substr($value, 20, 12)
);
}
}

View File

@ -12,23 +12,6 @@ class UuidServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
Str::createUuidsUsing([$this, 'uuid']); Str::createUuidsUsing(Uuid::class . '::uuid');
}
/**
* Generate a v4 UUID
*/
public function uuid(): string
{
return sprintf(
'%08x-%04x-%04x-%04x-%012x',
mt_rand(0, 0xffffffff),
mt_rand(0, 0xffff),
// first bit is the uuid version, here 4
mt_rand(0, 0x0fff) | 0x4000,
// variant
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffffffffffff)
);
} }
} }

View File

@ -3,15 +3,16 @@
namespace Engelsystem\Test\Unit\Helpers; namespace Engelsystem\Test\Unit\Helpers;
use Engelsystem\Application; use Engelsystem\Application;
use Engelsystem\Helpers\Uuid;
use Engelsystem\Helpers\UuidServiceProvider; use Engelsystem\Helpers\UuidServiceProvider;
use Engelsystem\Test\Unit\ServiceProviderTest; use Engelsystem\Test\Unit\ServiceProviderTest;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use ReflectionProperty;
class UuidServiceProviderTest extends ServiceProviderTest class UuidServiceProviderTest extends ServiceProviderTest
{ {
/** /**
* @covers \Engelsystem\Helpers\UuidServiceProvider::register * @covers \Engelsystem\Helpers\UuidServiceProvider::register
* @covers \Engelsystem\Helpers\UuidServiceProvider::uuid
*/ */
public function testRegister(): void public function testRegister(): void
{ {
@ -20,9 +21,13 @@ class UuidServiceProviderTest extends ServiceProviderTest
$serviceProvider = new UuidServiceProvider($app); $serviceProvider = new UuidServiceProvider($app);
$serviceProvider->register(); $serviceProvider->register();
$this->assertStringMatchesFormat( $uuidFactoryReference = (new ReflectionProperty(Str::class, 'uuidFactory'))
'%x%x%x%x%x%x%x%x-%x%x%x%x-4%x%x%x-%x%x%x%x-%x%x%x%x%x%x%x%x%x%x%x%x', ->getValue();
Str::uuid()
); $this->assertIsCallable($uuidFactoryReference);
$this->assertIsString($uuidFactoryReference);
$this->assertEquals(Uuid::class . '::uuid', $uuidFactoryReference);
$this->assertTrue(Str::isUuid(Str::uuid()), 'Is a UUID');
} }
} }

View File

@ -0,0 +1,132 @@
<?php
namespace Engelsystem\Test\Unit\Helpers;
use InvalidArgumentException;
use Engelsystem\Helpers\Uuid;
use Engelsystem\Test\Unit\TestCase;
use Illuminate\Support\Str;
class UuidTest extends TestCase
{
/**
* @covers \Engelsystem\Helpers\Uuid::uuid
*/
public function testUuid(): void
{
$uuid = new Uuid();
$result = $uuid->uuid();
$this->checkUuid4Format($result);
}
public function generateUuidBy(): array
{
return [
[42, 'a1d0c6e8-3f02-4327-9846-1063f4ac58a6'],
['42', 'a1d0c6e8-3f02-4327-9846-1063f4ac58a6'],
[1.23, '579c4c7f-58e4-45e8-8cfc-73e08903a08c'],
['1.23', '579c4c7f-58e4-45e8-8cfc-73e08903a08c'],
['test', '098f6bcd-4621-4373-8ade-4e832627b4f6']
];
}
/**
* @covers \Engelsystem\Helpers\Uuid::uuidBy
* @dataProvider generateUuidBy
*/
public function testUuidBy(mixed $value, string $expected): void
{
$uuid = new Uuid();
$result = $uuid->uuidBy($value);
$this->checkUuid4Format($result);
$this->assertEquals($expected, $result);
}
public function generateUuidByNumbers(): array
{
$numbers = [];
foreach (range(0, 10) as $number) {
$numbers[] = [$number];
}
return $numbers;
}
/**
* @covers \Engelsystem\Helpers\Uuid::uuidBy
* @dataProvider generateUuidByNumbers
*/
public function testUuidByNumbers(mixed $value): void
{
$uuid = new Uuid();
$result = $uuid->uuidBy($value);
$this->checkUuid4Format($result);
}
public function generateUuidByNamed(): array
{
return [
['42', 42, '42a1d0c6-e83f-4273-a7d8-461063f4ac58'],
['123', 1.23, '123579c4-c7f5-4e48-9e8c-cfc73e08903a'],
['7e57', 'test', '7e57098f-6bcd-4621-9373-cade4e832627'],
['5c4ed01eda7a1490751', 'schedule data', '5c4ed01e-da7a-4490-b514-33ffb998ffe8'],
['00001000010000100001', '20 characters', '00001000-0100-4010-8001-3e8c8aa31133'],
['ABC0DEF', 'lowercase', 'abc0deff-8241-4ecc-87fb-74bf40ccfe96'],
['0123456789000abc0def', 'change nothing', '01234567-8900-4abc-8def-4a28d7089c93'],
];
}
/**
* @covers \Engelsystem\Helpers\Uuid::uuidBy
* @dataProvider generateUuidByNamed
*/
public function testUuidByNamed(string $name, mixed $value, string $expected): void
{
$uuid = new Uuid();
$result = $uuid->uuidBy($value, $name);
$this->checkUuid4Format($result);
$this->assertEquals($expected, $result);
}
/**
* @covers \Engelsystem\Helpers\Uuid::uuidBy
*/
public function testUuidByNamedTooLong(): void
{
$uuid = new Uuid();
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessageMatches('/\$name.+20.+/');
$uuid->uuidBy('', '111111111111111111111');
}
/**
* @covers \Engelsystem\Helpers\Uuid::uuidBy
*/
public function testUuidByNamedNotHex(): void
{
$uuid = new Uuid();
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessageMatches('/\$name.+hex.+/');
$uuid->uuidBy('', 'not a hex name');
}
protected function checkUuid4Format(string $uuid): void
{
$version = Str::substr($uuid, 14, 1);
$variant = Str::substr($uuid, 19, 1);
$this->assertTrue(Str::isUuid($uuid), 'Is a UUID');
$this->assertEquals(4, $version, 'Version');
$this->assertStringStartsWith(
'10',
sprintf('%04b', hexdec($variant)),
'Variant is 0x8-0xb (RFC 4122, DCE 1.1, ISO/IEC 11578:1996)'
);
}
}