getName(), '{closure')){ //closure wraps a named function, can be done with reflection or fromCallable() //isClosure() is useless here because it just tells us if $func is reflecting a Closure object $scope = $func->getClosureScopeClass(); if($scope !== null){ //class method return $scope->getName() . ($func->getClosureThis() !== null ? "->" : "::") . $func->getName(); //name doesn't include class in this case } //non-class function return $func->getName(); } $filename = $func->getFileName(); return "closure@" . ($filename !== false ? Filesystem::cleanPath($filename) . "#L" . $func->getStartLine() : "internal" ); } /** * Returns a readable identifier for the class of the given object. Sanitizes class names for anonymous classes. * * @throws \ReflectionException */ public static function getNiceClassName(object $obj) : string{ $reflect = new \ReflectionClass($obj); if($reflect->isAnonymous()){ $filename = $reflect->getFileName(); return "anonymous@" . ($filename !== false ? Filesystem::cleanPath($filename) . "#L" . $reflect->getStartLine() : "internal" ); } return $reflect->getName(); } /** * @phpstan-return \Closure(object) : object * @deprecated */ public static function cloneCallback() : \Closure{ return static function(object $o){ return clone $o; }; } /** * @phpstan-template TKey of array-key * @phpstan-template TValue of object * * @param object[] $array * @phpstan-param array|list $array * * @return object[] * @phpstan-return ($array is list ? list : array) */ public static function cloneObjectArray(array $array) : array{ return array_map(fn(object $o) => clone $o, $array); } /** * Gets this machine / server instance unique ID * Returns a hash, the first 32 characters (or 16 if raw) * will be an identifier that won't change frequently. * The rest of the hash will change depending on other factors. * * @param string $extra optional, additional data to identify the machine */ public static function getMachineUniqueId(string $extra = "") : UuidInterface{ if(self::$serverUniqueId !== null && $extra === ""){ return self::$serverUniqueId; } $machine = php_uname("a"); $cpuinfo = @file("/proc/cpuinfo"); if($cpuinfo !== false){ $cpuinfoLines = preg_grep("/(model name|Processor|Serial)/", $cpuinfo); if($cpuinfoLines === false){ throw new AssumptionFailedError("Pattern is valid, so this shouldn't fail ..."); } $machine .= implode("", $cpuinfoLines); } $machine .= sys_get_temp_dir(); $machine .= $extra; $os = Utils::getOS(); if($os === Utils::OS_WINDOWS){ @exec("ipconfig /ALL", $mac); $mac = implode("\n", $mac); if(preg_match_all("#Physical Address[. ]{1,}: ([0-9A-F\\-]{17})#", $mac, $matches) > 0){ foreach($matches[1] as $i => $v){ if($v === "00-00-00-00-00-00"){ unset($matches[1][$i]); } } $machine .= implode(" ", $matches[1]); //Mac Addresses } }elseif($os === Utils::OS_LINUX){ if(file_exists("/etc/machine-id")){ $machine .= file_get_contents("/etc/machine-id"); }else{ @exec("ifconfig 2>/dev/null", $mac); $mac = implode("\n", $mac); if(preg_match_all("#HWaddr[ \t]{1,}([0-9a-f:]{17})#", $mac, $matches) > 0){ foreach($matches[1] as $i => $v){ if($v === "00:00:00:00:00:00"){ unset($matches[1][$i]); } } $machine .= implode(" ", $matches[1]); //Mac Addresses } } }elseif($os === Utils::OS_ANDROID){ $machine .= @file_get_contents("/system/build.prop"); }elseif($os === Utils::OS_MACOS){ $machine .= shell_exec("system_profiler SPHardwareDataType | grep UUID"); } $data = $machine . PHP_MAXPATHLEN; $data .= PHP_INT_MAX; $data .= PHP_INT_SIZE; $data .= get_current_user(); foreach(get_loaded_extensions() as $ext){ $data .= $ext . ":" . phpversion($ext); } //TODO: use of NIL as namespace is a hack; it works for now, but we should have a proper namespace UUID $uuid = Uuid::uuid3(Uuid::NIL, $data); if($extra === ""){ self::$serverUniqueId = $uuid; } return $uuid; } /** * Returns the current Operating System * Windows => win * MacOS => mac * iOS => ios * Android => android * Linux => Linux * BSD => bsd * Other => other */ public static function getOS(bool $recalculate = false) : string{ if(self::$os === null || $recalculate){ $uname = php_uname("s"); if(stripos($uname, "Darwin") !== false){ if(str_starts_with(php_uname("m"), "iP")){ self::$os = self::OS_IOS; }else{ self::$os = self::OS_MACOS; } }elseif(stripos($uname, "Win") !== false || $uname === "Msys"){ self::$os = self::OS_WINDOWS; }elseif(stripos($uname, "Linux") !== false){ if(@file_exists("/system/build.prop")){ self::$os = self::OS_ANDROID; }else{ self::$os = self::OS_LINUX; } }elseif(stripos($uname, "BSD") !== false || $uname === "DragonFly"){ self::$os = self::OS_BSD; }else{ self::$os = self::OS_UNKNOWN; } } return self::$os; } public static function getCoreCount(bool $recalculate = false) : int{ if(self::$cpuCores !== null && !$recalculate){ return self::$cpuCores; } $processors = 0; switch(Utils::getOS()){ case Utils::OS_LINUX: case Utils::OS_ANDROID: if(($cpuinfo = @file('/proc/cpuinfo')) !== false){ foreach($cpuinfo as $l){ if(preg_match('/^processor[ \t]*:[ \t]*[0-9]+$/m', $l) > 0){ ++$processors; } } }elseif(($cpuPresent = @file_get_contents("/sys/devices/system/cpu/present")) !== false){ if(preg_match("/^([0-9]+)\\-([0-9]+)$/", trim($cpuPresent), $matches) > 0){ $processors = ((int) $matches[2]) - ((int) $matches[1]); } } break; case Utils::OS_BSD: case Utils::OS_MACOS: $processors = (int) shell_exec("sysctl -n hw.ncpu"); break; case Utils::OS_WINDOWS: $processors = (int) getenv("NUMBER_OF_PROCESSORS"); break; } return self::$cpuCores = $processors; } /** * Returns a prettified hexdump */ public static function hexdump(string $bin) : string{ $output = ""; $bin = str_split($bin, 16); foreach($bin as $counter => $line){ $hex = chunk_split(chunk_split(str_pad(bin2hex($line), 32, " ", STR_PAD_RIGHT), 2, " "), 24, " "); $ascii = preg_replace('#([^\x20-\x7E])#', ".", $line); $output .= str_pad(dechex($counter << 4), 4, "0", STR_PAD_LEFT) . " " . $hex . " " . $ascii . PHP_EOL; } return $output; } /** * Returns a string that can be printed, replaces non-printable characters */ public static function printable(mixed $str) : string{ if(!is_string($str)){ return gettype($str); } return preg_replace('#([^\x20-\x7E])#', '.', $str); } public static function javaStringHash(string $string) : int{ $hash = 0; for($i = 0, $len = strlen($string); $i < $len; $i++){ $ord = ord($string[$i]); if(($ord & 0x80) !== 0){ $ord -= 0x100; } $hash = 31 * $hash + $ord; $hash &= 0xFFFFFFFF; } return $hash; } public static function getReferenceCount(object $value, bool $includeCurrent = true) : int{ ob_start(); debug_zval_dump($value); $contents = ob_get_contents(); if($contents === false) throw new AssumptionFailedError("ob_get_contents() should never return false here"); $ret = explode("\n", $contents); ob_end_clean(); if(preg_match('/^.* refcount\\(([0-9]+)\\)\\{$/', trim($ret[0]), $m) > 0){ return ((int) $m[1]) - ($includeCurrent ? 3 : 4); //$value + zval call + extra call } return -1; } private static function printableExceptionMessage(\Throwable $e) : string{ $errstr = preg_replace('/\s+/', ' ', trim($e->getMessage())); $errno = $e->getCode(); if(is_int($errno)){ try{ $errno = ErrorTypeToStringMap::get($errno); }catch(\InvalidArgumentException $ex){ //pass } } $errfile = Filesystem::cleanPath($e->getFile()); $errline = $e->getLine(); return get_class($e) . ": \"$errstr\" ($errno) in \"$errfile\" at line $errline"; } /** * @param mixed[][] $trace * @phpstan-param list>|null $trace * @return string[] */ public static function printableExceptionInfo(\Throwable $e, $trace = null) : array{ if($trace === null){ $trace = $e->getTrace(); } $lines = [self::printableExceptionMessage($e)]; $lines[] = "--- Stack trace ---"; foreach(Utils::printableTrace($trace) as $line){ $lines[] = " " . $line; } for($prev = $e->getPrevious(); $prev !== null; $prev = $prev->getPrevious()){ $lines[] = "--- Previous ---"; $lines[] = self::printableExceptionMessage($prev); foreach(Utils::printableTrace($prev->getTrace()) as $line){ $lines[] = " " . $line; } } $lines[] = "--- End of exception information ---"; return $lines; } private static function stringifyValueForTrace(mixed $value, int $maxStringLength) : string{ return match(true){ is_object($value) => "object " . self::getNiceClassName($value) . "#" . spl_object_id($value), is_array($value) => "array[" . count($value) . "]", is_string($value) => "string[" . strlen($value) . "] " . substr(Utils::printable($value), 0, $maxStringLength), is_bool($value) => $value ? "true" : "false", is_int($value) => "int " . $value, is_float($value) => "float " . $value, $value === null => "null", default => gettype($value) . " " . Utils::printable((string) $value) }; } /** * @param mixed[][] $trace * @phpstan-param list> $trace * * @return string[] * @phpstan-return list */ public static function printableTrace(array $trace, int $maxStringLength = 80) : array{ $messages = []; for($i = 0; isset($trace[$i]); ++$i){ $params = ""; if(isset($trace[$i]["args"]) || isset($trace[$i]["params"])){ if(isset($trace[$i]["args"])){ $args = $trace[$i]["args"]; }else{ $args = $trace[$i]["params"]; } /** @phpstan-var array $args */ $paramsList = []; $offset = 0; foreach($args as $argId => $value){ $paramsList[] = ($argId === $offset ? "" : "$argId: ") . self::stringifyValueForTrace($value, $maxStringLength); $offset++; } $params = implode(", ", $paramsList); } $messages[] = "#$i " . (isset($trace[$i]["file"]) ? Filesystem::cleanPath($trace[$i]["file"]) : "") . "(" . (isset($trace[$i]["line"]) ? $trace[$i]["line"] : "") . "): " . (isset($trace[$i]["class"]) ? $trace[$i]["class"] . (($trace[$i]["type"] === "dynamic" || $trace[$i]["type"] === "->") ? "->" : "::") : "" ) . $trace[$i]["function"] . "(" . Utils::printable($params) . ")"; } return $messages; } /** * Similar to {@link Utils::printableTrace()}, but associates metadata such as file and line number with each frame. * This is used to transmit thread-safe information about crash traces to the main thread when a thread crashes. * * @param mixed[][] $rawTrace * @phpstan-param list> $rawTrace * * @return ThreadCrashInfoFrame[] */ public static function printableTraceWithMetadata(array $rawTrace, int $maxStringLength = 80) : array{ $printableTrace = self::printableTrace($rawTrace, $maxStringLength); $safeTrace = []; foreach($printableTrace as $frameId => $printableFrame){ $rawFrame = $rawTrace[$frameId]; $safeTrace[$frameId] = new ThreadCrashInfoFrame( $printableFrame, $rawFrame["file"] ?? null, $rawFrame["line"] ?? 0 ); } return $safeTrace; } /** * @return mixed[][] * @phpstan-return list> */ public static function currentTrace(int $skipFrames = 0) : array{ ++$skipFrames; //omit this frame from trace, in addition to other skipped frames if(function_exists("xdebug_get_function_stack") && count($trace = @xdebug_get_function_stack()) !== 0){ $trace = array_reverse($trace); }else{ $e = new \Exception(); $trace = $e->getTrace(); } for($i = 0; $i < $skipFrames; ++$i){ unset($trace[$i]); } return array_values($trace); } /** * @return string[] */ public static function printableCurrentTrace(int $skipFrames = 0) : array{ return self::printableTrace(self::currentTrace(++$skipFrames)); } /** * Extracts one-line tags from the doc-comment * * @return string[] an array of tagName => tag value. If the tag has no value, an empty string is used as the value. */ public static function parseDocComment(string $docComment) : array{ $rawDocComment = substr($docComment, 3, -2); //remove the opening and closing markers preg_match_all('/(*ANYCRLF)^[\t ]*(?:\* )?@([a-zA-Z\-]+)(?:[\t ]+(.+?))?[\t ]*$/m', $rawDocComment, $matches); return array_combine($matches[1], $matches[2]); } /** * @phpstan-param class-string $className * @phpstan-param class-string $baseName */ public static function testValidInstance(string $className, string $baseName) : void{ $baseInterface = false; if(!class_exists($baseName)){ if(!interface_exists($baseName)){ throw new \InvalidArgumentException("Base class $baseName does not exist"); } $baseInterface = true; } if(!class_exists($className)){ throw new \InvalidArgumentException("Class $className does not exist or is not a class"); } if(!is_a($className, $baseName, true)){ throw new \InvalidArgumentException("Class $className does not " . ($baseInterface ? "implement" : "extend") . " $baseName"); } $class = new \ReflectionClass($className); if(!$class->isInstantiable()){ throw new \InvalidArgumentException("Class $className cannot be constructed"); } } /** * Verifies that the given callable is compatible with the desired signature. Throws a TypeError if they are * incompatible. * * @param callable|CallbackType $signature Dummy callable with the required parameters and return type * @param callable $subject Callable to check the signature of * @phpstan-param anyCallable|CallbackType $signature * @phpstan-param anyCallable $subject * * @throws \DaveRandom\CallbackValidator\InvalidCallbackException * @throws \TypeError */ public static function validateCallableSignature(callable|CallbackType $signature, callable $subject) : void{ if(!($signature instanceof CallbackType)){ $signature = CallbackType::createFromCallable($signature); } if(!$signature->isSatisfiedBy($subject)){ throw new \TypeError("Declaration of callable `" . CallbackType::createFromCallable($subject) . "` must be compatible with `" . $signature . "`"); } } /** * @phpstan-template TMemberType * @phpstan-param array $array * @phpstan-param \Closure(TMemberType) : void $validator */ public static function validateArrayValueType(array $array, \Closure $validator) : void{ foreach($array as $k => $v){ try{ $validator($v); }catch(\TypeError $e){ throw new \TypeError("Incorrect type of element at \"$k\": " . $e->getMessage(), 0, $e); } } } /** * Generator which forces array keys to string during iteration. * This is necessary because PHP has an anti-feature where it casts numeric string keys to integers, leading to * various crashes. * * @phpstan-template TKeyType of string * @phpstan-template TValueType * @phpstan-param array $array * @phpstan-return \Generator */ public static function stringifyKeys(array $array) : \Generator{ foreach($array as $key => $value){ // @phpstan-ignore-line - this is where we fix the stupid bullshit with array keys :) yield (string) $key => $value; } } /** * Gets rid of PHPStan BenevolentUnionType on array keys, so that wrong type errors get reported properly * Use this if you don't care what the key type is and just want proper PHPStan error reporting * * @phpstan-template TValueType * @phpstan-param array $array * @phpstan-return array */ public static function promoteKeys(array $array) : array{ return $array; } public static function checkUTF8(string $string) : void{ if(!mb_check_encoding($string, 'UTF-8')){ throw new \InvalidArgumentException("Text must be valid UTF-8"); } } /** * @phpstan-template TValue * @phpstan-param TValue|false $value * @phpstan-param string|\Closure() : string $context * @phpstan-return TValue */ public static function assumeNotFalse(mixed $value, \Closure|string $context = "This should never be false") : mixed{ if($value === false){ throw new AssumptionFailedError("Assumption failure: " . (is_string($context) ? $context : $context()) . " (THIS IS A BUG)"); } return $value; } public static function checkFloatNotInfOrNaN(string $name, float $float) : void{ if(is_nan($float)){ throw new \InvalidArgumentException("$name cannot be NaN"); } if(is_infinite($float)){ throw new \InvalidArgumentException("$name cannot be infinite"); } } public static function checkVector3NotInfOrNaN(Vector3 $vector3) : void{ if($vector3 instanceof Location){ //location could be masquerading as vector3 self::checkFloatNotInfOrNaN("yaw", $vector3->yaw); self::checkFloatNotInfOrNaN("pitch", $vector3->pitch); } self::checkFloatNotInfOrNaN("x", $vector3->x); self::checkFloatNotInfOrNaN("y", $vector3->y); self::checkFloatNotInfOrNaN("z", $vector3->z); } public static function checkLocationNotInfOrNaN(Location $location) : void{ self::checkVector3NotInfOrNaN($location); } /** * Returns an integer describing the current OPcache JIT setting. * @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.jit */ public static function getOpcacheJitMode() : ?int{ if( function_exists('opcache_get_status') && ($opcacheStatus = opcache_get_status(false)) !== false && isset($opcacheStatus["jit"]["on"]) ){ $jit = $opcacheStatus["jit"]; if($jit["on"] === true){ return (($jit["opt_flags"] >> 2) * 1000) + (($jit["opt_flags"] & 0x03) * 100) + ($jit["kind"] * 10) + $jit["opt_level"]; } //jit available, but disabled return 0; } //jit not available return null; } /** * Returns a random float between 0.0 and 1.0 * Drop-in replacement for lcg_value() */ public static function getRandomFloat() : float{ return mt_rand() / mt_getrandmax(); } }