Last active
November 21, 2023 21:10
-
-
Save leemcd56/73c89faf9fce30286517f08cc3000bf6 to your computer and use it in GitHub Desktop.
Common Helpers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
final class ArrayHelper | |
{ | |
/** | |
* Determine if two associative arrays are similar. | |
* | |
* Both arrays must have the same indexes with identical values | |
* without respect to key ordering | |
* | |
* @see https://stackoverflow.com/questions/3838288 | |
* | |
* @param array $array_1 | |
* @param array $array_2 | |
* | |
* @return bool | |
*/ | |
public static function similar($array_1, $array_2): bool | |
{ | |
// comparing the array_diff_assoc results treats arrays with the same key => value pairs in any order as identical | |
// i.e. [1 => 'One', 2 => 'Two', 3 => 'Three'] === [2 => 'Two', 3 => 'Three', 1 => 'One'] | |
return array_diff_assoc($array_1, $array_2) === array_diff_assoc($array_2, $array_1); | |
} | |
/** | |
* Converts an array to an object. | |
* | |
* @param array $array | |
* | |
* @return object | |
*/ | |
public static function toObject(array $array): object | |
{ | |
$result = json_decode(json_encode($array), false); | |
return is_object($result) ? $result : null; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
final class BooleanHelper | |
{ | |
/** | |
* Coerce into boolean `true`, `false`, or `null`. | |
* | |
* @param int|string|null $raw_value | |
* | |
* @return bool|null | |
*/ | |
public static function normalize($raw_value): ?bool | |
{ | |
if (is_null($raw_value)) { | |
return null; | |
} | |
$normalized = $raw_value; | |
if (is_string($normalized)) { | |
$normalized = mb_strtolower($raw_value); | |
match ($normalized) { | |
'true', '1', 'on', 'yes', 'y' => $normalized = true, | |
'false', '0', 'off', 'no', 'n' => $normalized = false, | |
default => $normalized = null, | |
}; | |
} | |
// Handles "on", "true", "yes", etc. | |
return filter_var($normalized, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
use Illuminate\Support\Facades\{Auth, Cache}; | |
final class CacheHelper | |
{ | |
/** | |
* Retrieve the organization cache key. | |
* | |
* @return string | |
*/ | |
public static function organizationKey(): string | |
{ | |
$user = Auth::user(); | |
$organization = session()->get($user->id . ':current_organization'); | |
return sprintf('organization.%s', $organization['id']); | |
} | |
/** | |
* Retrieve the user cache key. | |
* | |
* @return string | |
*/ | |
public static function userKey(): string | |
{ | |
$user = Auth::user(); | |
return sprintf('user.%s', Auth::user()->id); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
use Illuminate\Support\Facades\File; | |
final class CsvHelper | |
{ | |
/** | |
* Convert a CSV file to an array. | |
* | |
* @see https://github.com/andreas-glaser/php-helpers/blob/1.0/src/CsvHelper.php | |
* | |
* @param string $file The CSV file path. | |
* @param bool $isFirstLineTitle If the first line are the titles. | |
* @param int $length The length of the line. | |
* @param string $delimiter The delimiter between values. | |
* @param string $enclosure The enclosure for wrapped strings. | |
* @param string $escape The escape character. | |
* | |
* @return array | |
*/ | |
public static function fileToArray(string $file, bool $isFirstLineTitle = false, int $length = 0, string $delimiter = ',', string $enclosure = '"', string $escape = '\\'): array | |
{ | |
if (! File::exists($file) || ! File::isReadable($file)) { | |
$filename = File::name($file); | |
throw new \RuntimeException(sprintf('File "%s" cannot be read.', $filename)); | |
} | |
$result = []; | |
$titles = []; | |
$hasTitles = false; | |
$rowNumber = 0; | |
$handle = fopen($file, 'r'); | |
while (false !== ($row = fgetcsv($handle, $length, $delimiter, $enclosure, $escape))) { | |
if ($isFirstLineTitle && ! $hasTitles) { | |
foreach ($row as $index => $title) { | |
$titles[$index] = $title; | |
} | |
$hasTitles = true; | |
continue; | |
} | |
$result[$rowNumber] = []; | |
foreach ($row as $index => $cell) { | |
if ($isFirstLineTitle) { | |
$result[$rowNumber][$titles[$index]] = $cell; | |
} else { | |
$result[$rowNumber][] = $cell; | |
} | |
} | |
$rowNumber++; | |
} | |
fclose($handle); | |
return $result; | |
} | |
/** | |
* Convert an array to a CSV file. | |
* | |
* @param array $array The array to convert. | |
* @param string $file The CSV file path. | |
* @param bool $isFirstLineTitle If the first line are the titles. | |
* @param string $delimiter The delimiter between values. | |
* @param string $enclosure The enclosure for wrapped strings. | |
* @param string $escape The escape character. | |
*/ | |
public static function arrayToFile(array $array, string $file, bool $isFirstLineTitle = false, string $delimiter = ',', string $enclosure = '"', string $escape = '\\'): void | |
{ | |
if (! File::isWritable(dirname($file))) { | |
throw new \RuntimeException(sprintf('Directory "%s" cannot be written.', dirname($file))); | |
} | |
$handle = fopen($file, 'w'); | |
foreach ($array as $row) { | |
if ($isFirstLineTitle) { | |
$row = array_keys($row); | |
} | |
fputcsv($handle, $row, $delimiter, $enclosure, $escape); | |
} | |
fclose($handle); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
use Illuminate\Support\Str; | |
use Pdp\{Domain, ResolvedDomainName, Rules}; | |
final class DomainHelper | |
{ | |
/** | |
* PHP-equivalent of the regular expression for matching domain names. | |
* | |
* @var string | |
*/ | |
public const DOMAIN_NAME_REGEXP = '[\p{L}\d][\p{L}\d-]*(\.[\p{L}\d-]{1,63}){1,}'; | |
/** | |
* JavaScript-equivalent of the regular expression for matching domain names. | |
* | |
* @var string | |
*/ | |
public const DOMAIN_NAME_REGEXP_JAVASCRIPT = '^([a-zA-Z0-9-]+\\\.)*?[a-zA-Z0-9-]{2,}\\\.[a-zA-Z]{2,61}$'; | |
/** | |
* Get domain name string from url string. | |
* | |
* @param string|null $url_string | |
* | |
* @return string|null | |
*/ | |
public static function fromUrl(?string $url_string): ?string | |
{ | |
return parse_url($url_string, PHP_URL_HOST); | |
} | |
/** | |
* Parse a domain name. | |
* | |
* @param string|null $domain_or_url | |
* | |
* @return \Pdp\Domain | |
*/ | |
public static function parse(?string $domain_or_url): ResolvedDomainName | |
{ | |
$domain_str = $domain_or_url; | |
if (Str::startsWith($domain_or_url, ['http://', 'https://', '//'])) { | |
$possible_ip = self::fromUrl($domain_or_url); | |
if (filter_var($possible_ip, FILTER_VALIDATE_IP)) { | |
$real_host = gethostbyaddr($possible_ip); | |
$domain_or_url = str_replace($possible_ip, $real_host, $domain_or_url); | |
} | |
} | |
if (mb_strpos($domain_or_url, '/') || mb_strpos($domain_or_url, ':')) { | |
$domain_str = UrlHelper::parseAssumingHost($domain_or_url, PHP_URL_HOST); | |
} | |
$publicSuffixList = Rules::fromPath(resource_path('data/public_suffix_list.dat')); | |
$domain = Domain::fromIDNA2008($domain_str); | |
return $publicSuffixList->resolve($domain); | |
} | |
/** | |
* Get domain string excluding "www." from domain string. | |
* | |
* e.g: | |
* - store.ebay.com => store.ebay.com | |
* - www.amazon.com => amazon.com. | |
* | |
* @param string|null $domain | |
* | |
* @return string|null | |
*/ | |
public static function withoutWww(?string $domain): ?string | |
{ | |
return preg_replace('/^www\./iu', '', $domain); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
final class FileHelper | |
{ | |
/** | |
* Unit strings more familiar to everyday people. | |
* | |
* @var array | |
*/ | |
public const FAMILIAR_UNIT_SCALE = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; | |
/** | |
* Unit strings more familiar to data scientists. | |
* | |
* @var array | |
*/ | |
public const PEDANTIC_UNIT_SCALE = ['B', 'kiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; | |
/** | |
* Bytes to be converted. | |
* | |
* @var int | |
*/ | |
public int $bytes = 0; | |
/** | |
* Maximum number of decimals. | |
* | |
* @var int | |
*/ | |
protected int $decimals = 2; | |
/** | |
* Output format. | |
* | |
* @var string | |
*/ | |
protected string $format = '%g%s'; | |
/** | |
* Precision. | |
* | |
* @var int | |
*/ | |
protected int $precision = 1024; | |
/** | |
* Unit scale. | |
* | |
* @var array | |
*/ | |
public array $scale = self::FAMILIAR_UNIT_SCALE; | |
/** | |
* Constructor. | |
* | |
* @param int $bytes | |
*/ | |
public function __construct(int $bytes = 0) | |
{ | |
$this->bytes = $bytes; | |
} | |
/** | |
* Display for humans by converting to string. | |
* | |
* @return string | |
*/ | |
public function __toString(): string | |
{ | |
[$number, $power] = $this->scaledValueAndPower(); | |
if ($power >= count($this->scale)) { | |
throw new \DomainException('Too many bytes to convert to a reasonable unit'); | |
} | |
$units = $this->scale[$power]; | |
return sprintf($this->format, round($number, $this->decimals), $units); | |
} | |
/** | |
* You can also get the "raw" scaled value and its log-base 1024 power. | |
* | |
* @return array | |
*/ | |
public function scaledValueAndPower(): array | |
{ | |
if ($this->bytes === 0) { | |
return [0, 0]; | |
} | |
$power = (int) floor(log($this->bytes, $this->precision)); | |
$value = $this->bytes / pow($this->precision, $power); | |
return [$value, $power]; | |
} | |
/** | |
* For fluent setting of public properties. | |
* | |
* @return self | |
*/ | |
public function tap(callable $callback): self | |
{ | |
$callback($this); | |
return $this; | |
} | |
/** | |
* Convert file size in bytes to human readable format. | |
* | |
* @param int $bytes | |
* | |
* @return mixed | |
*/ | |
public static function bytesToHuman(int $bytes) | |
{ | |
if ($bytes < 0) { | |
throw new \DomainException('Cannot have negative bytes'); | |
} | |
return new static($bytes); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
final class JsonHelper | |
{ | |
/** | |
* Encode a string to JSON. | |
* | |
* @param string $json | |
* | |
* @return array | |
*/ | |
public static function decode(string $json): array | |
{ | |
return json_decode($json, true); | |
} | |
/** | |
* Decode a JSON string to an array. | |
* | |
* @param array $data | |
* | |
* @return string | |
*/ | |
public static function encode(array $data): string | |
{ | |
return json_encode($data, JSON_PRETTY_PRINT); | |
} | |
/** | |
* Encodes json string for the use in JavaScript. | |
* | |
* @param $string | |
* | |
* @return string | |
*/ | |
public static function encodeForJavaScript($string) | |
{ | |
return json_encode($string, JSON_HEX_QUOT | JSON_HEX_APOS); | |
} | |
/** | |
* Validates JSON input. | |
* | |
* @param $string | |
* | |
* @return bool | |
*/ | |
public static function isValid($string) | |
{ | |
if (is_int($string) || is_float($string)) { | |
return true; | |
} | |
json_decode($string); | |
return JSON_ERROR_NONE === json_last_error(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
final class NumberHelper | |
{ | |
/** | |
* Combine numbers with dashes and commas. | |
* | |
* @param array $numbers | |
* | |
* @return string | |
*/ | |
public static function combineConsecutiveNumbers(array $numbers): string | |
{ | |
$combined = []; | |
$current = $numbers[0]; | |
$last = $numbers[0]; | |
for ($i = 1; $i < count($numbers); $i++) { | |
if ($numbers[$i] == $last + 1) { | |
// This number is consecutive, so update the 'last' variable | |
$last = $numbers[$i]; | |
} else { | |
// Non-consecutive number encountered, add to the result | |
if ($current == $last) { | |
$combined[] = $current; | |
} else { | |
$combined[] = $current . '-' . $last; | |
} | |
$current = $numbers[$i]; | |
$last = $numbers[$i]; | |
} | |
} | |
// Add the last range to the result | |
if ($current == $last) { | |
$combined[] = $current; | |
} else { | |
$combined[] = $current . '-' . $last; | |
} | |
return implode(', ', $combined); | |
} | |
/** | |
* Format a number. | |
* | |
* @param int|string $number | |
* @param int $decimals | |
* | |
* @return string | |
*/ | |
public static function format($number, $decimals = 2): string | |
{ | |
return number_format($number, $decimals, '.', ','); | |
} | |
/** | |
* Validate that the given number uses the Luhn algorithm. | |
* | |
* @param string $number | |
* @param bool $hasChecksum | |
* | |
* @return bool | |
*/ | |
public static function luhn(string $number, bool $hasChecksum = false): bool | |
{ | |
$number = (string) $number; | |
$length = strlen($number); | |
if ($hasChecksum) { | |
$checksumDigit = (int) substr($number, -1); | |
$number = substr($number, 0, -1); | |
$length -= 1; | |
} | |
$sum = 0; | |
$parity = $length % 2; | |
for ($i = 0; $i < $length; $i++) { | |
$digit = (int) $number[$i]; | |
if ($i % 2 !== $parity) { | |
$sum += $digit; | |
} elseif ($digit > 4) { | |
$sum += 2 * $digit - 9; | |
} else { | |
$sum += 2 * $digit; | |
} | |
} | |
if ($hasChecksum) { | |
$sum += $checksumDigit; | |
} | |
return ($sum % 10) === 0; | |
} | |
/** | |
* Determine the ordinal suffix of a number. | |
* | |
* @param int|float $number The number to get the suffix for | |
* @param bool $combine If true, combines the suffix with the number | |
* | |
* @return string | |
*/ | |
public static function ordinal($number, bool $combine = false): string | |
{ | |
if ($number % 100 > 10 && $number % 100 < 14) { | |
return $combine ? sprintf('%dth', $number) : 'th'; | |
} | |
$value = $number; | |
match ($number % 10) { | |
1 => $value = 'st', | |
2 => $value = 'nd', | |
3 => $value = 'rd', | |
default => $value = 'th', | |
}; | |
return $combine ? sprintf('%d%s', $number, $value) : $value; | |
} | |
/** | |
* Generate a random float between $min and $max. | |
* | |
* @param float|int $min Minimum value | |
* @param float|int $max Maximum value | |
* @param int $precision Number of decimal places | |
* | |
* @return float|int | |
*/ | |
public static function randomFloat($min, $max, int $precision = 0) | |
{ | |
if ($min > $max) { | |
return 0.0; | |
} | |
$value = $min + (mt_rand() / mt_getrandmax()) * ($max - $min); | |
if ($precision > 0) { | |
$value = round($value, $precision); | |
} | |
return $value; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
use Illuminate\Support\Facades\Log; | |
use Propaganistas\LaravelPhone\Exceptions\NumberParseException; | |
use Propaganistas\LaravelPhone\PhoneNumber; | |
final class PhoneHelper | |
{ | |
/** | |
* Extract country code from phone number. | |
* | |
* @param string $raw_phone like "+1(201)200.0000x111" | |
* | |
* @return string like "+1" | |
*/ | |
public static function countryCode(string $raw_phone): string | |
{ | |
// Strip everything after country codes like "+1 " of "+1 201 200...". | |
return preg_replace('/ .*/iu', '', static::reformatWithoutExtension($raw_phone)); | |
} | |
/** | |
* Phone number to format. | |
* | |
* @param string $raw_phone like "+1(201)200.0000x111" | |
* | |
* @return string like "+1 201 200 0000" or null on error. | |
*/ | |
public static function reformatWithoutExtension(?string $raw_phone): ?string | |
{ | |
if (is_null($raw_phone) || mb_strlen($raw_phone) === 0) { | |
return ''; | |
} | |
// Begin with naive parse to guess country when no plus ('+') indicator. | |
// CONSIDER: Substituting upper-case letters for digit equivalents. | |
$dialable_without_extension = trim( | |
// Case sensitive to minimize collisions with letter substitutes. | |
preg_replace( | |
'/[^0-9A-Z+]/u', | |
'', | |
preg_replace('/(\s*(,|x)\s*[0-9]+)+$/iu', '', $raw_phone) | |
) | |
); | |
$country = null; | |
$first_character = mb_substr($dialable_without_extension, 0, 1); | |
if ($first_character !== '+' | |
// 10 and 11 digit plans (starting with '1') are likely US or at | |
// least North America. | |
&& (10 === mb_strlen($dialable_without_extension) | |
|| (11 === mb_strlen($dialable_without_extension) && '1' === $first_character)) | |
) { | |
$country = 'US'; | |
} | |
// CONSIDER: Falling back to country from IP or HTTP headers. | |
try { | |
// Use standard form "tel:+ ... ;ext= ..." to reliably strip country and extension. | |
$rfc3966 = PhoneNumber::make($raw_phone, $country)->formatRFC3966(); | |
} catch (NumberParseException $e) { | |
Log::error($e); | |
return null; | |
} | |
// Strip "tel:" and ";ext=" parts off beginning and end. | |
$reformatted = preg_split('/[:;]/', $rfc3966)[1] ?? ''; | |
// CONSIDER: Stripping all spaces except one after country. | |
return trim(str_replace('-', ' ', $reformatted)); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
use Illuminate\Support\{Collection, Str}; | |
final class StringHelper | |
{ | |
/** | |
* Return the content in a string between a left and right element. | |
* | |
* @param string $left | |
* @param string $right | |
* @param string $haystack | |
* | |
* @return string | |
*/ | |
public static function between(string $left, string $right, string $haystack): string | |
{ | |
preg_match_all('/' . preg_quote($left, '/') . '(.*?)' . preg_quote($right, '/') . '/s', $haystack, $matches); | |
return array_map('trim', $matches[1]); | |
} | |
/** | |
* Create a collection from a string and trim each value. | |
* | |
* @param string $delimiter like ',' or '|' | |
* @param string $string like 'foo,bar,baz' | |
* | |
* @return \Illuminate\Support\Collection | |
*/ | |
public static function explodeAndTrim(string $delimiter, string $string): Collection | |
{ | |
return collect(explode($delimiter, $string)) | |
->map(function ($item) { | |
return trim($item); | |
}); | |
} | |
/** | |
* Force integer values within a line of comma-separated values. | |
* | |
* @param string $raw like ' , 1,2 ,3"; DROP TABLES; --' | |
* | |
* @return string like '0,1,2,3' | |
*/ | |
public static function forceIntsWithinCsv(?string $raw): ?string | |
{ | |
if (is_null($raw)) { | |
return null; | |
} | |
return implode(',', array_map('intval', explode(',', $raw))); | |
} | |
/** | |
* Determine if the string is a valid email. | |
* | |
* @param string $email | |
* | |
* @return bool | |
*/ | |
public static function isEmail(string $email): bool | |
{ | |
return (filter_var($email, FILTER_VALIDATE_EMAIL) !== false); | |
} | |
/** | |
* Determine if the given string is really an integer. | |
* | |
* @param string|null $possible_int | |
* | |
* @return bool | |
*/ | |
public static function isInt(?string $possible_int): bool | |
{ | |
if (is_null($possible_int)) { | |
return false; | |
} | |
// Contains commas but not in proper groupings | |
if (Str::contains($possible_int, ',') && preg_match('/^\d{1,3}(,\d{3})*?$/', $possible_int) === 0) { | |
return false; | |
} | |
$possible_int = str_replace(',', '', $possible_int); | |
return $possible_int === (string) (int) $possible_int; | |
} | |
/** | |
* Convert a list of IDs into a collection. | |
* | |
* @param string|null $ids | |
* | |
* @return \Illuminate\Support\Collection | |
*/ | |
public static function stringIdsToCollection(?string $ids): Collection | |
{ | |
return collect(explode(',', $ids)) | |
->map(function ($raw_id) { | |
return trim($raw_id); | |
}) | |
->filter(function ($possible_id) { | |
return self::isInt($possible_id); | |
}) | |
->map(function ($string_id) { | |
// cast to int for type safe comparisons | |
return (int) $string_id; | |
}) | |
->unique(); | |
} | |
/** | |
* Replace any placeholders in a string with the given values. | |
* Use if `Str::swap` is not available. | |
* | |
* @param string $template | |
* @param array $segments | |
* | |
* @return string | |
*/ | |
public static function swap(string $template, array $segments): string | |
{ | |
foreach ($segments as $key => $value) { | |
// If the value is not printable, skip it | |
if (! is_string($value) && ! ctype_print($value)) { | |
continue; | |
} | |
$template = str_replace(":{$key}", $value, $template); | |
} | |
return $template; | |
} | |
/** | |
* Convert a comma-separated list of values into a unique array. | |
* | |
* @param string $string | |
* | |
* @return array | |
*/ | |
public static function toUniqueArray(string $string): array | |
{ | |
return array_values( | |
array_unique( | |
array_filter( | |
array_map( | |
fn ($item) => trim($item), | |
explode(',', $string), | |
) | |
) | |
) | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
use DateTimeZone; | |
final class TimeHelper | |
{ | |
/** | |
* Matches times like "1:50a", "12 pm", or "04:56:21". | |
* | |
* @var string | |
*/ | |
public const REGEX = '/^([012]?[0-9])(:[0-5][0-9])?(:[0-5][0-9])?(\s*am?|\s*pm?)?$/ui'; | |
/** | |
* Determine if the date string contains a time. | |
* | |
* @param string $datetime_str | |
* | |
* @return bool | |
*/ | |
public static function hasTime($datetime_str): bool | |
{ | |
// Time is like "12 am", "1:00PM", "T22:00:00", or "4am EST". | |
// CONSIDER: Reusing `static::REGEX`. | |
$re = '~(T|\s+|\b)' // Boundary | |
. '[0-9]{1,9}\s?(millisecond|second|minute|hour|day|week|month|year|weekday)s?\s?(ago|before|after)?|' // Relative times | |
. '[0-9]{1,2}(:[0-9]{2}(:[0-9]{2}(\.[0-9]{1,6})?)?|\s*a(\.?m\.?)?|\s*p(\.?m\.?)?)' // Time | |
. '\s*(' | |
. '[+-][0-9]{2}(:[0-9]{2})?|' // Zone offset | |
. implode('|', DateTimeZone::listIdentifiers()) // Zone names | |
. '|Central|CST|Eastern|EST|Mountain|MST|Pacific|PST' | |
. ')?\s*$~ui'; // Anchor right side to avoid misinterpreting noise | |
return (bool) preg_match($re, $datetime_str); | |
} | |
/** | |
* Create a selectable list of timezones. | |
* | |
* @return array | |
*/ | |
public static function timezoneSelectList(): array | |
{ | |
$americanTimeZones = [ | |
'America/New_York' => 'US Eastern', | |
'America/Chicago' => 'US Central', | |
'America/Denver' => 'US Mountain', | |
'America/Los_Angeles' => 'US Pacific', | |
]; | |
$regions = [ | |
DateTimeZone::AFRICA, | |
DateTimeZone::AMERICA, | |
DateTimeZone::ASIA, | |
DateTimeZone::ATLANTIC, | |
DateTimeZone::AUSTRALIA, | |
DateTimeZone::INDIAN, | |
DateTimeZone::PACIFIC, | |
]; | |
return collect($regions) | |
->mapWithKeys(function ($regionCode) use ($americanTimeZones) { | |
$regionName = null; | |
$zones = collect([]); | |
foreach (DateTimeZone::listIdentifiers($regionCode) as $item) { | |
if (in_array($item, array_keys($americanTimeZones))) { | |
continue; | |
} | |
[$regionName, $tz] = explode('/', $item, 2); | |
$zones->put($item, str_replace('_', ' ', $tz)); | |
} | |
return [$regionName => $zones]; | |
}) | |
->prepend($americanTimeZones, 'United States') | |
->toArray(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
final class UrlHelper | |
{ | |
/** | |
* Some URLs may have spaces in query params, so encode those for consistency. | |
* | |
* @param string $url like "//test.test?q=white space" | |
* | |
* @return string like "//test.test?q=white%20space" | |
*/ | |
public static function encodeSpaces(string $url): ?string | |
{ | |
if (preg_match_all('~\s~u', $url, $m)) { | |
return str_replace($m[0], array_map('urlencode', $m[0]), trim($url)); | |
} | |
return $url; | |
} | |
/** | |
* Strip leading and trailing whitespace and normalize case of insensitive parts. | |
* | |
* @param string $url_string the address being normalized | |
* | |
* @return string the normalized address | |
*/ | |
public static function normalize(string $url_string, bool $force_https = false): string | |
{ | |
$normalized = $url_string; | |
$parts = static::parseAssumingHost($url_string); | |
if (! $parts) { | |
return $normalized; | |
} | |
if (isset($parts['host'])) { | |
$parts['host'] = mb_strtolower($parts['host']); | |
} | |
if (isset($parts['scheme'])) { | |
$parts['scheme'] = mb_strtolower($parts['scheme']); | |
} | |
if ($force_https && in_array($parts['scheme'] ?? '', ['', 'http'], true)) { | |
$parts['scheme'] = 'https'; | |
} | |
// CONSIDER: Supporting schemes without double slash | |
// CONSIDER: Supporting unicode casing | |
$normalized = static::reassemble($parts); | |
return $normalized; | |
} | |
/** | |
* PHP's parse_url except with hint to assume host when scheme-less. | |
* | |
* @param string $url_string like "domain.example/with-path" | |
* @param int $component such as PHP_URL_HOST | |
* | |
* @return string|array|false like 'domain.example' or ['host' => , 'path' => , ...] | |
*/ | |
public static function parseAssumingHost(?string $url_string, int $component = -1) | |
{ | |
// Workaround parse_url() assuming scheme-less are paths, not hosts | |
// by prefixing ambiguous values with "//". | |
// CONSIDER: Checking that potential TLD is in public TLD list. | |
// CONSIDER: Blacklisting dot suffixes common to file names like ".html". | |
// CONSIDER: Not prefixing when starts with lone '/' as many naked paths do. | |
$prefix = '//' !== mb_substr($url_string, 0, 2) | |
&& 'http://' !== mb_substr($url_string, 0, 7) | |
&& 'https://' !== mb_substr($url_string, 0, 8) | |
&& preg_match('~^' . DomainHelper::DOMAIN_NAME_REGEXP . '\b~iu', $url_string) | |
? '//' : ''; | |
return parse_url($prefix . $url_string, $component); | |
} | |
/** | |
* Reassemble URL parts from `parse_url` and similar helpers. | |
* | |
* @param array $parts like ['scheme' => , 'host' => , 'port' => , … ] | |
* | |
* @return string | |
*/ | |
public static function reassemble(array $parts): string | |
{ | |
return (isset($parts['scheme']) ? $parts['scheme'] . '://' : '') | |
. (isset($parts['user']) ? $parts['user'] . ':' . $parts['pass'] . '@' : '') | |
. ($parts['host'] ?? '') | |
. (isset($parts['port']) ? ':' . $parts['port'] : '') | |
. (isset($parts['path']) ? $parts['path'] : '') | |
. (isset($parts['query']) ? '?' . $parts['query'] : '') | |
. (isset($parts['fragment']) ? '#' . $parts['fragment'] : ''); | |
} | |
/** | |
* Replace a part of the URL with another. | |
* | |
* @param string|array|null $url like '//test.test' or ['host' => ..., 'path' => ...]. | |
* @param string|array $part like 'host' or ['scheme', 'host']. | |
* @param string|array|null $with like 'example.test' or ['http', 'replaced.text']. | |
* | |
* @return string|array|null | |
*/ | |
public static function replace($url, $part, $with) | |
{ | |
$parts = is_array($url) ? $url : static::parseAssumingHost($url); | |
if (! $parts) { | |
return null; | |
} | |
if (is_array($part)) { | |
foreach ($part as $i => $p) { | |
$parts[$p] = is_array($with) ? ($with[$i] ?? null) : $with; | |
} | |
} else { | |
$parts[$part] = $with; | |
} | |
if (! is_array($url)) { | |
return static::reassemble($parts); | |
} | |
return $parts; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment