You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
urlhub/app/Services/KeyGeneratorService.php

180 lines
5.6 KiB

<?php
namespace App\Services;
use App\Models\Url;
class KeyGeneratorService
{
/**
* Generate a short string that can be used as a unique key for shortened long
* urls.
*
* @return string A unique string to use as the short url key
*/
public function urlKey(string $value): string
{
// Step 1
$key = $this->generateSimpleString($value);
// Step 2
// If step 1 fail (the string is used or cannot be used), then the generator
// must generate a unique random string until it finds a string that can
// be used as a key
if ($this->assertStringCanBeUsedAsKey($key) === false) {
$key = $this->generateRandomString();
}
return $key;
}
/**
* Take some characters at the end of the string and remove all characters that
* are not in the specified character set.
*/
public function generateSimpleString(string $value): string
{
$length = config('urlhub.hash_length') * -1;
$pattern = '/[^'.config('urlhub.hash_char').']/i';
return substr(preg_replace($pattern, '', $value), $length);
}
/**
* Generate a random string of specified length. The string will only contain
* characters from the specified character set.
*
* @return string The generated random string
*/
public function generateRandomString()
{
$factory = new \RandomLib\Factory;
$generator = $factory->getMediumStrengthGenerator();
$characters = config('urlhub.hash_char');
$length = config('urlhub.hash_length');
do {
$urlKey = $generator->generateString($length, $characters);
} while ($this->assertStringCanBeUsedAsKey($urlKey) === false);
return $urlKey;
}
/**
* Check if string can be used as a keyword.
*
* This function will check under several conditions:
* 1. If the string is already used in the database
* 2. If the string is used as a reserved keyword
* 3. If the string is used as a route path
*
* If any or all of the conditions are true, then the keyword cannot be used.
*/
public function assertStringCanBeUsedAsKey(string $value): bool
{
$route = \Illuminate\Routing\Route::class;
$routeCollection = \Illuminate\Support\Facades\Route::getRoutes()->get();
$routePath = array_map(fn ($route) => $route->uri, $routeCollection);
$isExistsInDb = Url::whereKeyword($value)->exists();
$isReservedKeyword = in_array($value, config('urlhub.reserved_keyword'));
$isRegisteredRoutePath = in_array($value, $routePath);
if ($isExistsInDb || $isReservedKeyword || $isRegisteredRoutePath) {
return false;
}
return true;
}
/*
|--------------------------------------------------------------------------
| Capacity calculation
|--------------------------------------------------------------------------
*/
/**
* Calculate the maximum number of unique random strings that can be
* generated
*/
public function maxCapacity(): int
{
$characters = strlen(config('urlhub.hash_char'));
$length = config('urlhub.hash_length');
// for testing purposes only
// tests\Unit\Middleware\UrlHubLinkCheckerTest.php
if ($length === 0) {
return 0;
}
return (int) pow($characters, $length);
}
/**
* The number of unique random strings that have been used as the key for
* the long url that has been shortened
*
* Formula:
* usedCapacity = randomKey + customKey
*
* The character length and set of characters of `customKey` must be the same
* as `randomKey`.
*/
public function usedCapacity(): int
{
$hashLength = (int) config('urlhub.hash_length');
$regexPattern = '['.config('urlhub.hash_char').']{'.$hashLength.'}';
$randomKey = Url::whereIsCustom(false)
->whereRaw('LENGTH(keyword) = ?', [$hashLength])
->count();
$customKey = Url::whereIsCustom(true)
->whereRaw('LENGTH(keyword) = ?', [$hashLength])
->whereRaw("keyword REGEXP '".$regexPattern."'")
->count();
return $randomKey + $customKey;
}
/**
* Calculate the number of unique random strings that can still be generated.
*/
public function idleCapacity(): int
{
$maxCapacity = $this->maxCapacity();
$usedCapacity = $this->usedCapacity();
// prevent negative values
return max($maxCapacity - $usedCapacity, 0);
}
/**
* Calculate the percentage of the remaining unique random strings that can
* be generated from the total number of unique random strings that can be
* generated (in percent) with the specified precision (in decimal places)
* and return the result as a string.
*/
public function idleCapacityInPercent(int $precision = 2): string
{
$maxCapacity = $this->maxCapacity();
$remaining = $this->idleCapacity();
$result = round(($remaining / $maxCapacity) * 100, $precision);
$lowerBoundInPercent = 1 / (10 ** $precision);
$upperBoundInPercent = 100 - $lowerBoundInPercent;
$lowerBound = $lowerBoundInPercent / 100;
$upperBound = 1 - $lowerBound;
if ($remaining > 0 && $remaining < ($maxCapacity * $lowerBound)) {
$result = $lowerBoundInPercent;
} elseif (($remaining > ($maxCapacity * $upperBound)) && ($remaining < $maxCapacity)) {
$result = $upperBoundInPercent;
}
return $result.'%';
}
}