Skip to content

Instantly share code, notes, and snippets.

@leemcd56
Last active November 21, 2023 21:10
Show Gist options
  • Save leemcd56/73c89faf9fce30286517f08cc3000bf6 to your computer and use it in GitHub Desktop.
Save leemcd56/73c89faf9fce30286517f08cc3000bf6 to your computer and use it in GitHub Desktop.
Common Helpers
<?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;
}
}
<?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);
}
}
<?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);
}
}
<?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);
}
}
<?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);
}
}
<?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);
}
}
<?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();
}
}
<?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;
}
}
<?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));
}
}
<?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),
)
)
)
);
}
}
<?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();
}
}
<?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