getTraceAsString(); } $starter = $seen ? 'Caused by: ' : ''; $result = []; if (!$seen) $seen = []; $trace = $e->getTrace(); $prev = $e->getPrevious(); $result[] = sprintf('%s%s: %s', $starter, get_class($e), $e->getMessage()); $file = $e->getFile(); $line = $e->getLine(); while (true) { $current = "$file:$line"; if (is_array($seen) && in_array($current, $seen)) { $result[] = sprintf(" ... %d more", count($trace) + 1); break; } $resline = sprintf(" at %s%s%s(%s%s%s)", count($trace) && array_key_exists('class', $trace[0]) ? $trace[0]['class'] : '', count($trace) && array_key_exists('class', $trace[0]) && array_key_exists('function', $trace[0]) ? '->' : '', count($trace) && array_key_exists('function', $trace[0]) ? $trace[0]['function'] : '(main)', $line === null ? $file : basename($file), $line === null ? '' : ':', $line === null ? '' : $line); $result[] = $resline; if ( false !== strpos($resline, "Illuminate\\Console\\Command->execute") || false !== strpos($resline, "Illuminate\\Routing\\ControllerDispatcher->dispatch") || false !== strpos($resline, "Unknown Source") || false !== strpos($resline, "eval(SandboxController.php") ) { $result[] = sprintf(" ... %d more", count($trace) + 1); break; } if (is_array($seen)) { $seen[] = "$file:$line"; } if (!count($trace)) break; $file = array_key_exists('file', $trace[0]) ? $trace[0]['file'] : 'Unknown Source'; $line = array_key_exists('file', $trace[0]) && array_key_exists('line', $trace[0]) && $trace[0]['line'] ? $trace[0]['line'] : null; array_shift($trace); } $result = join("\n", $result); if ($prev) { $result .= "\n" . self::getStackTraceAsString($prev, $seen); } return $result; } /** * Diff arrays of objects by one of their properties. * * TODO unit test * * @param array $array1 * @param array $array2 * @param string $prop property name (accessed using the arrow operator) * @return array of (first - second) array. */ public static function arrayDiffProp($array1, $array2, $prop) { return array_udiff($array1, $array2, function ($a, $b) use ($prop) { return $a->$prop - $b->$prop; }); } /** * Sort array by field of each item * * TODO unit test * * @param array $array the array to sort * @param string|string[] $prop property or properties */ public static function arraySortProp(&$array, $prop) { if (!is_array($prop)) $prop = [$prop]; @usort($array, function ($a, $b) use ($prop) { // compare by each property foreach ($prop as $pr) { $cmp = strcasecmp($a->$pr, $b->$pr); if ($cmp != 0) return $cmp; } return 0; }); } /** * Group an array using a callback or each element's property/index * * TODO unit test * * @param array $arr array to group * @param callable|string $grouper hashing function, or property/index of each item to group by * @return array grouped */ public static function groupBy(array $arr, $grouper) { $first_el = reset($arr); $is_array = is_array($first_el) || ($first_el instanceof \ArrayAccess); // get real grouper func $func = is_callable($grouper) ? $grouper : function ($x) use ($is_array, $grouper) { if ($is_array) { return $x[$grouper]; } else { return $x->$grouper; } }; $grouped = []; foreach ($arr as $item) { $hash = $func($item); if (!isset($grouped[$hash])) { $grouped[$hash] = []; } $grouped[$hash][] = $item; } return $grouped; } /** * Get union of two arrays, preserving order if possible. * * @param array $ar1 * @param array $ar2 * @return array merged */ public static function arrayUnion(array $ar1, array $ar2) { return array_unique(array_merge($ar1, $ar2)); } /** * Create an array with keys from given array and all the same values. * * It's array_combine() combined with array_fill() * * @param array $keys * @param mixed $fill the value for all keys * @return array */ public static function arrayKeysFill(array $keys, $fill) { return array_combine($keys, array_fill(0, count($keys), $fill)); } /** * Extract n-th bit from an int array. * * Bits are numbered from LSB to MSB, and from array index 0 upwards * * TODO unit test * * @param array $ints array of ints * @param int $bit index of a bit to retrieve * @param int $itemSize number of bits per array item, default 16 * @return int the bit, 0/1 */ public static function bitFromIntArray(array $ints, $bit, $itemSize = 16) { $nth = (int) floor($bit / $itemSize); $bit_n = $bit - ($itemSize * $nth); if ($nth > count($ints)) return 0; return (int) (0 != ($ints[$nth] & (1 << $bit_n))); } /** * Set socket timeout in MS * * @param resource $socket socket * @param int $SO_xxTIMEO timeout type constant * @param int $timeout_ms timeout (millis) */ public static function setSocketTimeout(&$socket, $SO_xxTIMEO, $timeout_ms) { $to_sec = floor($timeout_ms / 1000); $to_usec = ($timeout_ms - $to_sec * 1000) * 1000; $timeout = ['sec' => $to_sec, 'usec' => $to_usec]; socket_set_option($socket, SOL_SOCKET, $SO_xxTIMEO, $timeout); } /** * Check if value is in given range * * @param int|float $value value * @param int|float $low lower bound - included * @param int|float $high upper bound - included * @return bool is in range */ public static function inRange($value, $low, $high) { // swap bounds if reverse if ($low > $high) { $sw = $low; $low = $high; $high = $sw; } return $value >= $low && $value <= $high; } /** * Reverse the effect of array_chunk. * * @param array $array * @return array */ public static function array_unchunk($array) { return call_user_func_array('array_merge', $array); } /** * Clamp value to given bounds * * @param float $value input * @param float $min lower bound - included * @param float $max upper bound - included * @return float result */ public static function clamp($value, $min, $max) { // swap bounds if reverse if ($min > $max) { $sw = $min; $min = $max; $max = $sw; } if ($value < $min) { $value = $min; } else if ($value > $max) { $value = $max; } return $value; } /** * Get today as the nr of days since epoch * * @return int nr of days */ public static function unixDay() { $dt = new \DateTime('@' . ntime()); $epoch = new \DateTime('1970-01-01'); $diff = $dt->diff($epoch); return (int) $diff->format("%a"); } /** * Check if the value looks like a Mongo key (ObjectID). * Mongo key is a 24 chars long hex string, so it's unlikely to occur as an alias. * * TODO unit test * * @param mixed $key object to inspect (string or ObjectID) * @return bool is _id value */ public static function isMongoKey($key) { if ($key instanceof ObjectID) return true; return (is_string($key) and strlen($key) === 24 and ctype_xdigit($key)); } /** * Check if array is associative * * TODO unit test * * @param array $array * @return bool is associative (keys are not a sequence of numbers starting 0) */ public static function isAssoc(array $array) { return array_keys($array) !== range(0, count($array) - 1); } /** * Ceil to closest multiple of 0.5 * * @param float $n * @return float rounded up to 0.5 */ public static function halfCeil($n) { return ceil($n * 2) / 2; } /** * Round to closest multiple of 0.5 * * @param float $n * @return float rounded to 0.5 */ public static function halfRound($n) { return round($n * 2) / 2; } /** * Floor to closest multiple of 0.5 * * @param float $n * @return float rounded down to 0.5 */ public static function halfFloor($n) { return ceil($n * 2) / 2; } /** * A dirty way to read private or protected fields. * Use only for debugging or vendor hacking when really needed. * * @param mixed $obj * @param string $prop * @return mixed */ public static function readProtected($obj, $prop) { $reflection = new ReflectionClass($obj); $property = $reflection->getProperty($prop); $property->setAccessible(true); return $property->getValue($obj); } /** * A dirty way to write private or protected fields. * Use only for debugging or vendor hacking when really needed. * * @param mixed $obj * @param string $prop * @param mixed $value */ public static function writeProtected($obj, $prop, $value) { $reflection = new ReflectionClass($obj); $property = $reflection->getProperty($prop); $property->setAccessible(true); $property->setValue($obj, $value); } /** * A dirty way to call private or protected methods. * Use only for debugging or vendor hacking when really needed. * * @param mixed $obj * @param string $methodName * @param array ...$arguments * @return mixed */ public static function callProtected($obj, $methodName, ...$arguments) { $method = new ReflectionMethod(get_class($obj), $methodName); $method->setAccessible(true); return $method->invoke($obj, ...$arguments); } /** * Get class constants * * @param string $class f. q. class name * @return array constants */ public static function getClassConstants($class) { $reflect = new ReflectionClass($class); return $reflect->getConstants(); } /** * Start printing log messages to stdout (for debug) */ public static function logToStdout($htmlEscape = true) { $lf = new LineFormatter(); Log::listen(function ($obj) use ($htmlEscape, $lf) { /** @var MessageLogged $obj */ $message = $obj->message; $context = $obj->context; $level = $obj->level; if ($message instanceof \Exception) { $message = self::getStackTraceAsString($message); } if ($htmlEscape) $message = e($message); $time = microtime(true) - APP_START_TIME; echo sprintf("%7.3f", $time) . " [$level] $message"; if (!empty($context)) echo Utils::callProtected($lf, 'stringify', $context); echo "\n"; }); } public static function logQueries() { \DB::listen(function ($query) { $b = []; /** @var QueryExecuted $query */ foreach ($query->bindings as $i=>$binding) $b[$i] = "'$binding'"; Log::debug('SQL: ' . preg_replace_array('/\\?/', $b, $query->sql)); }); } /** * Given an array of comma and quote positions, returns all commas that aren't within a pair of quotes. * * @param int[] $commas * @param int[] $quotes * @return int[] */ public static function discardPositionsWithinPairs($commas, $quotes) { $pairs = array_chunk($quotes, 2); $goodCommas = []; foreach ($commas as $comma) { $found = false; foreach ($pairs as $pair) { if ($comma > $pair[0] && $comma < $pair[1]) { $found = true; break; } } if (!$found) { $goodCommas[] = $comma; } } return $goodCommas; } /** * Check if any item in an array is true. * * @param bool[] $arr * @return bool */ public static function anyTrue($arr) { foreach ($arr as $item) { if ($item) return true; } return false; } /** * Get all keys from an array whose values match a given set. * * @param array $arr * @param array|mixed $allowed_values * @return array */ public static function keysOfValues($arr, $allowed_values) { if (!is_array($allowed_values)) { $allowed_values = [$allowed_values]; } $matched = []; foreach ($arr as $k => $v) { if (in_array($v, $allowed_values)) { $matched[] = $k; } } return $matched; } /** * Convert value within range to a ratio of this range. * eg. 5 on 0..10 = 0.5 * * @param float $value * @param float $minValue * @param float $maxValue * @return float */ public static function valToRatio($value, $minValue, $maxValue) { $value = Utils::clamp($value, $minValue, $maxValue); return ($value - $minValue) / ($maxValue - $minValue); } /** * Apply ratio to a range & get proportional value. * eg. 0.5 on 0..10 = 5 * * @param $ratio * @param $minValue * @param $maxValue * @return mixed */ public static function ratioToVal($ratio, $minValue, $maxValue) { $ratio = Utils::clamp($ratio, 0, 1); return $minValue + ($maxValue - $minValue) * $ratio; } /** * Range transformation * * @param float $value * @param float $minInput * @param float $maxInput * @param float $minOutput * @param float $maxOutput * @return float */ public static function transform($value, $minInput, $maxInput, $minOutput, $maxOutput) { $ratio = self::valToRatio($value, $minInput, $maxInput); return self::ratioToVal($ratio, $minOutput, $maxOutput); } private static function initSortCollator() { if (self::$collator === null) { if (extension_loaded('intl')) { if (is_object($collator = collator_create('cs_CZ.UTF-8'))) { $collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON); self::$collator = $collator; } } else { Log::warning('!! Intl PHP extension not installed / enabled.'); self::$collator = false; } } } public static function mbNatCaseCompare($a, $b) { self::initSortCollator(); $a = "$a"; $b = "$b"; if (strlen($a) > 0 && $a[0] == '_') if (strlen($b) == 0 || $b[0] != '_') return 1; if (strlen($b) > 0 && $b[0] == '_') if (strlen($a) == 0 || $a[0] != '_') return -1; if ($a === "" && $b !== "") return 1; if ($a !== "" && $b === "") return -1; if ($a === "" && $b === "") return 0; if (self::$collator) return self::$collator->compare($a, $b); else return strnatcasecmp($a, $b); } public static function mbNatCaseSort(&$arr) { uasort($arr, function ($a, $b) { return self::mbNatCaseCompare($a, $b); }); } /** * Check if exception should be reported in eventlog. * * @param \Exception $e * @return bool */ public static function shouldReportException(\Exception $e) { $eh = app(\Illuminate\Contracts\Debug\ExceptionHandler::class); return Utils::callProtected($eh, 'shouldReport', $e); } const GEO_DEG = 'GEO_DEGREES'; const GEO_DEG_MIN = 'GEO_DEG_MIN'; const GEO_DEG_MIN_SEC = 'GEO_DEG_MIN_SEC'; /** * @param string|int $coord * @return float */ public static function geoToDegrees($coord) { $coord = "$coord"; // remove any whitespace $coord = preg_replace('/\s/', '', $coord); if (strlen($coord) == 0) throw new ArgumentException("\"$coord\" is not in a known GPS coordinate format"); // remove leading letter, invert if needed $invert = false; if (in_array($coord[0], ['N', 'S', 'E', 'W'])) { $invert = in_array($coord[0], ['S', 'W']); $coord = substr($coord, 1); } if (in_array($coord[strlen($coord) - 1], ['N', 'S', 'E', 'W'])) { $invert = in_array($coord[strlen($coord) - 1], ['S', 'W']); $coord = substr($coord, 0, -1); } $invert = $invert ? -1 : 1; // Degrees if (Str::maskMatch("F°?", $coord)) { return $invert * floatval(rtrim($coord, '°')); } if (Str::maskMatch("D°F'?", $coord)) { list($d, $m) = explode('°', $coord); $d = floatval($d); if ($d < 0) { $d = -$d; $invert *= -1; } $m = floatval(rtrim($m, "'")); return $invert * ($d + $m / 60); } if (Str::maskMatch("D°D'F\"?", $coord)) { list($d, $ms) = explode('°', $coord); list($m, $s) = explode("'", $ms); $d = floatval($d); if ($d < 0) { $d = -$d; $invert *= -1; } $m = floatval($m); $s = floatval(rtrim($s, '"')); return $invert * ($d + ($m + $s / 60) / 60); } throw new ArgumentException("\"$coord\" is not in a known GPS coordinate format"); } public static function convertGeoNS($coord, $target = self::GEO_DEG) { $result = self::convertGeo($coord, $target); if ($result[0] == '-') { $result = 'S ' . substr($result, 1); } else { $result = 'N ' . $result; } return $result; } public static function convertGeoEW($coord, $target = self::GEO_DEG) { $result = self::convertGeo($coord, $target); if ($result[0] == '-') { $result = 'W ' . substr($result, 1); } else { $result = 'E ' . $result; } return $result; } public static function convertGeo($coord, $target = self::GEO_DEG) { $degrees = self::geoToDegrees($coord); $invert = $degrees < 0; $degrees = abs($degrees); $invert = $invert ? -1 : 1; switch ($target) { case self::GEO_DEG: return "" . $invert * round($degrees, 5); case self::GEO_DEG_MIN: $frac = $degrees - floor($degrees); $mins = $frac * 60; return ($invert * floor($degrees)) . "° " . round($mins, 3) . "'"; case self::GEO_DEG_MIN_SEC: $frac = $degrees - floor($degrees); $mins = $frac * 60; $frac = $mins - floor($mins); $secs = $frac * 60; return ($invert * floor($degrees)) . "° " . floor($mins) . "' " . round($secs, 1) . "\""; default: throw new ArgumentException("$target is not a valid geo coord format"); } } public static function formatFileSize($bytes) { if ($bytes >= 1073741824) { $bytes = number_format($bytes / 1073741824, 2) . ' GB'; } elseif ($bytes >= 1048576) { $bytes = number_format($bytes / 1048576, 2) . ' MB'; } elseif ($bytes >= 1024) { $bytes = number_format($bytes / 1024, 2) . ' kB'; } else { $bytes = $bytes . ' B'; } return $bytes; } /** * Format date safely for y2038 * * @param string $format date format string * @param int|null $timestamp formatted timestamp, or null for current time * @return string result */ public static function fdate(string $format, $timestamp = null) { if ($timestamp === null) $timestamp = time(); $dt = new \DateTime('@' . $timestamp); // UTC $dt->setTimezone(new \DateTimeZone(date('e'))); // set local timezone return $dt->format($format); } /** * Format time as XXd XXh XXm XXs (parse-able by HumanTime) * * @param int $secs seconds * @param bool $rough get only approximate time (for estimate) * @return string result */ public static function ftime(int $secs, $rough = false) { $d = (int) ($secs / 86400); $secs -= $d * 86400; $h = (int) ($secs / 3600); $secs -= $h * 3600; $m = (int) ($secs / 60); $secs -= $m * 60; $s = $secs; $chunks = []; if ($rough) { if ($d > 5) { if ($h > 15) $d++; // round up $chunks[] = "{$d}d"; } elseif ($d >= 1) { $chunks[] = "{$d}d"; if ($h) $chunks[] = "{$h}h"; } else { // under 1 d if ($h >= 4) { if ($m > 40) $h++; // round up $chunks[] = "{$h}h"; } elseif ($h >= 1) { $chunks[] = "{$h}h"; if ($m) $chunks[] = "{$m}m"; } else { if ($m) $chunks[] = "{$m}m"; if ($s || empty($chunks)) { $chunks[] = "{$s}s"; } } } } else { // precise if ($d) $chunks[] = "{$d}d"; if ($h) $chunks[] = "{$h}h"; if ($m) $chunks[] = "{$m}m"; if ($s || empty($chunks)) { $chunks[] = "{$s}s"; } } return trim(implode(' ', $chunks)); } /** * Get time in seconds from user entered interval * * @param $time * @return int seconds */ public static function strToSeconds(string $time) { // seconds pass through if (preg_match('/^\d+$/', trim("$time"))) { return intval($time); } $time = preg_replace('/(\s*|,|and)/i', '', $time); $pieces = preg_split('/(?<=[a-z])(?=\d)/i', $time, 0, PREG_SPLIT_NO_EMPTY); return array_sum(array_map('self::strToSeconds_do', $pieces)); } /** @noinspection PhpUnusedPrivateMethodInspection */ private static function strToSeconds_do($time) { if (preg_match('/^(\d+)\s*(s|secs?|seconds?)$/', $time, $m)) { return intval($m[1]); } if (preg_match('/^(\d+)\s*(m|mins?|minutes?)$/', $time, $m)) { return intval($m[1]) * 60; } if (preg_match('/^(\d+)\s*(h|hours?)$/', $time, $m)) { return intval($m[1]) * 60*60; } if (preg_match('/^(\d+)\s*(d|days?)$/', $time, $m)) { return intval($m[1]) * 86400; } if (preg_match('/^(\d+)\s*(w|weeks?)$/', $time, $m)) { return intval($m[1]) * 86400*7; } if (preg_match('/^(\d+)\s*(M|months?)$/', $time, $m)) { return intval($m[1]) * 86400*30; } if (preg_match('/^(\d+)\s*(y|years?)$/', $time, $m)) { return intval($m[1]) * 86400*365; } throw new FormatException("Bad time interval: \"$time\""); } /** * Convert number to arbitrary alphabet, using a system similar to EXCEL column numbering. * * @param int $n * @param string[] $alphabet - array of characters to use * @return string */ public static function alphabetEncode($n, $alphabet) { $key = ''; $abcsize = count($alphabet); $i = $n; do { $mod = $i % $abcsize; $key = $alphabet[$mod] . $key; $i = (($i - $mod) / $abcsize) - 1; } while($i >= 0); return $key; } /** * @param \SplFileObject|string $data * @return array */ public static function csvToArray($data) { if ($data instanceof \SplFileObject) { $data->setFlags(\SplFileObject::SKIP_EMPTY | \SplFileObject::DROP_NEW_LINE); $lines = []; while (! $data->eof()) { $line = $data->fgetcsv(); if ($line !== null) { $lines[] = $line; } } return $lines; } else { return array_map('str_getcsv', explode("\n", $data)); } } }