formattedId($alphabet, $length); } function parseDateFromQuery(array $query, string $dateName): Chronos|null { return normalizeOptionalDate(empty($query[$dateName] ?? null) ? null : Chronos::parse($query[$dateName])); } function parseDateRangeFromQuery(array $query, string $startDateName, string $endDateName): DateRange { $startDate = parseDateFromQuery($query, $startDateName); $endDate = parseDateFromQuery($query, $endDateName); return buildDateRange($startDate, $endDate); } function dateRangeToHumanFriendly(DateRange|null $dateRange): string { $startDate = $dateRange?->startDate; $endDate = $dateRange?->endDate; return match (true) { $startDate !== null && $endDate !== null => sprintf( 'Between %s and %s', $startDate->toDateTimeString(), $endDate->toDateTimeString(), ), $startDate !== null => sprintf('Since %s', $startDate->toDateTimeString()), $endDate !== null => sprintf('Until %s', $endDate->toDateTimeString()), default => 'All time', }; } function normalizeLocale(string $locale): string { return trim(strtolower(str_replace('_', '-', $locale))); } /** * Parse an accept-language-like pattern into a list of locales, optionally filtering out those which do not match a * minimum quality * * @param non-empty-string $acceptLanguage * @return iterable */ function acceptLanguageToLocales(string $acceptLanguage, float $minQuality = 0): iterable { /** @var array{string, float|null}[] $acceptLanguagesList */ $acceptLanguagesList = map(explode(',', $acceptLanguage), static function (string $lang): array { // Split locale/language and quality (en-US;q=0.7) -> [en-US, q=0.7] [$lang, $qualityString] = array_pad(explode(';', $lang), length: 2, value: ''); $normalizedLang = normalizeLocale($lang); $quality = Query::parse(trim($qualityString))['q'] ?? 1; return [$normalizedLang, (float) $quality]; }); foreach ($acceptLanguagesList as [$lang, $quality]) { if ($lang !== '*' && $quality >= $minQuality) { yield $lang; } } } /** * Splits a locale into its corresponding language and country codes. * The country code will be null if not present * 'es-AR' -> ['es', 'AR'] * 'fr-FR' -> ['fr', 'FR'] * 'en' -> ['en', null] * * @return array{string, string|null} */ function splitLocale(string $locale): array { /** @var string $lang */ [$lang, $countryCode] = array_pad(explode('-', $locale), length: 2, value: null); return [$lang, $countryCode]; } function arrayToString(array $array, int $indentSize = 4): string { $indent = str_repeat(' ', $indentSize); $names = array_keys($array); $index = 0; return array_reduce( $names, static function (string $acc, string $name) use (&$index, $indent, $array) { $index++; $messages = $array[$name]; return sprintf( "%s%s%s'%s' => %s", $acc, $index === 1 ? '' : "\n", $indent, $name, is_array($messages) ? print_r($messages, true) : $messages, ); }, '', ); } function isCrawler(string $userAgent): bool { static $detector = new CrawlerDetect(); return $detector->isCrawler($userAgent); } function parseUserAgent(string $userAgent): UserAgent { static $uaParser = new UserAgentParser(); return $uaParser->parse($userAgent); } function fieldWithUtf8Charset(FieldBuilder $field, array $emConfig, string $collation = 'unicode_ci'): FieldBuilder { return match ($emConfig['connection']['driver'] ?? null) { 'pdo_mysql' => $field->option('charset', 'utf8mb4') ->option('collation', 'utf8mb4_' . $collation), default => $field, }; } function camelCaseToSnakeCase(string $value): string { // Handle cases like "HTTPServerError" -> "http_server_error" $value = preg_replace('/([A-Z])([A-Z][a-z])/u', '$1_$2', $value); // Handle cases like "myVariable" -> "my_variable" // @phpstan-ignore argument.type $value = preg_replace('/([a-z0-9])([A-Z])/u', '$1_$2', $value); // @phpstan-ignore argument.type return mb_strtolower($value); } function toProblemDetailsType(string $errorCode): string { return sprintf('https://shlink.io/api/error/%s', $errorCode); } /** * @param class-string $enum * @return non-empty-list */ function enumValues(string $enum): array { return enumSide($enum, 'value'); } /** * @param class-string $enum * @return non-empty-list */ function enumNames(string $enum): array { return enumSide($enum, 'name'); } /** * @param class-string $enum * @param 'name'|'value' $type * @return non-empty-list */ function enumSide(string $enum, string $type): array { static $cache; if ($cache === null) { $cache = []; } return ( $cache[$type][$enum] ?? ($cache[$type][$enum] = array_map( static fn (BackedEnum $entry) => (string) ($type === 'name' ? $entry->name : $entry->value), $enum::cases(), )) ); } /** * @param class-string $enum */ function enumToString(string $enum): string { return sprintf('["%s"]', implode('", "', enumValues($enum))); } /** * Split provided string by comma and return a list of the results. * An empty array is returned if provided value is empty */ function splitByComma(string|null $value): array { if ($value === null || trim($value) === '') { return []; } return array_map(trim(...), explode(',', $value)); } function ipAddressFromRequest(ServerRequestInterface $request): string|null { return $request->getAttribute(IP_ADDRESS_REQUEST_ATTRIBUTE); } function geolocationFromRequest(ServerRequestInterface $request): Location|null { $geolocation = $request->getAttribute(Location::class); if ($geolocation !== null && !$geolocation instanceof Location) { // TODO Throw exception } return $geolocation; }