doStuff(); * * ...but this works fine: * * id(new Thing())->doStuff(); * * @template T * @param T $x Anything * @return T Unmodified argument. */ function id($x) { return $x; } /** * Access an array index, retrieving the value stored there if it exists or * a default if it does not. This function allows you to concisely access an * index which may or may not exist without raising a warning. * * @param array $array Array to access. * @param scalar $key Index to access in the array. * @param wild $default (optional) Default value to return if the key is * not present in the array. * @return wild If `$array[$key]` exists, that value is returned. If not, * $default is returned without raising a warning. */ function idx(array $array, $key, $default = null) { // isset() is a micro-optimization - it is fast but fails for null values. if (isset($array[$key])) { return $array[$key]; } // Comparing $default is also a micro-optimization. if ($default === null || array_key_exists($key, $array)) { return null; } return $default; } /** * Access a sequence of array indexes, retrieving a deeply nested value if * it exists or a default if it does not. * * For example, `idxv($dict, array('a', 'b', 'c'))` accesses the key at * `$dict['a']['b']['c']`, if it exists. If it does not, or any intermediate * value is not itself an array, it returns the defualt value. * * @param array $map Array to access. * @param list $path List of keys to access, in sequence. * @param wild $default (optional) Default value to return. * @return wild Accessed value, or default if the value is not accessible. */ function idxv(array $map, array $path, $default = null) { if (!$path) { return $default; } $last = last($path); $path = array_slice($path, 0, -1); $cursor = $map; foreach ($path as $key) { $cursor = idx($cursor, $key); if (!is_array($cursor)) { return $default; } } return idx($cursor, $last, $default); } /** * Call a method on a list of objects. Short for "method pull", this function * works just like @{function:ipull}, except that it operates on a list of * objects instead of a list of arrays. This function simplifies a common type * of mapping operation: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $key => $object) { * $names[$key] = $object->getName(); * } * * You can express this more concisely with mpull(): * * $names = mpull($objects, 'getName'); * * mpull() takes a third argument, which allows you to do the same but for * the array's keys: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $object) { * $names[$object->getID()] = $object->getName(); * } * * This is the mpull version(): * * $names = mpull($objects, 'getName', 'getID'); * * If you pass ##null## as the second argument, the objects will be preserved: * * COUNTEREXAMPLE * $id_map = array(); * foreach ($objects as $object) { * $id_map[$object->getID()] = $object; * } * * With mpull(): * * $id_map = mpull($objects, null, 'getID'); * * See also @{function:ipull}, which works similarly but accesses array indexes * instead of calling methods. * * @param list $list Some list of objects. * @param string|null $method Determines which **values** will appear in * the result array. Use a string like 'getName' to * store the value of calling the named method in each * value, or ##null## to preserve the original objects. * @param string|null $key_method (optional) Determines how **keys** will * be assigned in the result array. * Use a string like 'getID' to use the result * of calling the named method as each object's key, or * `null` to preserve the original keys. * @return dict A dictionary with keys and values derived according * to whatever you passed as `$method` and `$key_method`. */ function mpull(array $list, $method, $key_method = null) { $result = array(); foreach ($list as $key => $object) { if ($key_method !== null) { $key = $object->$key_method(); } if ($method !== null) { $value = $object->$method(); } else { $value = $object; } $result[$key] = $value; } return $result; } /** * Access a property on a list of objects. Short for "property pull", this * function works just like @{function:mpull}, except that it accesses object * properties instead of methods. This function simplifies a common type of * mapping operation: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $key => $object) { * $names[$key] = $object->name; * } * * You can express this more concisely with ppull(): * * $names = ppull($objects, 'name'); * * ppull() takes a third argument, which allows you to do the same but for * the array's keys: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $object) { * $names[$object->id] = $object->name; * } * * This is the ppull version(): * * $names = ppull($objects, 'name', 'id'); * * If you pass ##null## as the second argument, the objects will be preserved: * * COUNTEREXAMPLE * $id_map = array(); * foreach ($objects as $object) { * $id_map[$object->id] = $object; * } * * With ppull(): * * $id_map = ppull($objects, null, 'id'); * * See also @{function:mpull}, which works similarly but calls object methods * instead of accessing object properties. * * @param list $list Some list of objects. * @param string|null $property Determines which **values** will appear in * the result array. Use a string like 'name' to store * the value of accessing the named property in each * value, or `null` to preserve the original objects. * @param string|null $key_property (optional) Determines how **keys** will * be assigned in the result array. Use a string like * 'id' to use the result of accessing the named property * as each object's key, or `null` to preserve the * original keys. * @return dict A dictionary with keys and values derived according * to whatever you passed as `$property` and * `$key_property`. */ function ppull(array $list, $property, $key_property = null) { $result = array(); foreach ($list as $key => $object) { if ($key_property !== null) { $key = $object->$key_property; } if ($property !== null) { $value = $object->$property; } else { $value = $object; } $result[$key] = $value; } return $result; } /** * Choose an index from a list of arrays. Short for "index pull", this function * works just like @{function:mpull}, except that it operates on a list of * arrays and selects an index from them instead of operating on a list of * objects and calling a method on them. * * This function simplifies a common type of mapping operation: * * COUNTEREXAMPLE * $names = array(); * foreach ($list as $key => $dict) { * $names[$key] = $dict['name']; * } * * With ipull(): * * $names = ipull($list, 'name'); * * See @{function:mpull} for more usage examples. * * @param list $list Some list of arrays. * @param scalar|null $index Determines which **values** will appear in the * result array. Use a scalar to select that index from * each array, or null to preserve the arrays unmodified * as values. * @param scalar|null $key_index (optional) Determines which **keys** will * appear in the result array. Use a scalar to select * that index from each array, or null to preserve the * array keys. * @return dict A dictionary with keys and values derived according * to whatever you passed for `$index` and `$key_index`. */ function ipull(array $list, $index, $key_index = null) { $result = array(); foreach ($list as $key => $array) { if ($key_index !== null) { $key = $array[$key_index]; } if ($index !== null) { $value = $array[$index]; } else { $value = $array; } $result[$key] = $value; } return $result; } /** * Group a list of objects by the result of some method, similar to how * GROUP BY works in an SQL query. This function simplifies grouping objects * by some property: * * COUNTEREXAMPLE * $animals_by_species = array(); * foreach ($animals as $animal) { * $animals_by_species[$animal->getSpecies()][] = $animal; * } * * This can be expressed more tersely with mgroup(): * * $animals_by_species = mgroup($animals, 'getSpecies'); * * In either case, the result is a dictionary which maps species (e.g., like * "dog") to lists of animals with that property, so all the dogs are grouped * together and all the cats are grouped together, or whatever super * businessesey thing is actually happening in your problem domain. * * See also @{function:igroup}, which works the same way but operates on * array indexes. * * @param list $list List of objects to group by some property. * @param string $by Name of a method, like 'getType', to call on each * object in order to determine which group it should be * placed into. * @param ... (optional) Zero or more additional method names, to * subgroup the groups. * @return dict Dictionary mapping distinct method returns to lists of * all objects which returned that value. */ function mgroup(array $list, $by /* , ... */) { $map = mpull($list, $by); $groups = array(); foreach ($map as $group) { // Can't array_fill_keys() here because 'false' gets encoded wrong. $groups[$group] = array(); } foreach ($map as $key => $group) { $groups[$group][$key] = $list[$key]; } $args = func_get_args(); $args = array_slice($args, 2); if ($args) { array_unshift($args, null); foreach ($groups as $group_key => $grouped) { $args[0] = $grouped; $groups[$group_key] = call_user_func_array('mgroup', $args); } } return $groups; } /** * Group a list of arrays by the value of some index. This function is the same * as @{function:mgroup}, except it operates on the values of array indexes * rather than the return values of method calls. * * @param list $list List of arrays to group by some index value. * @param string $by Name of an index to select from each array in order to * determine which group it should be placed into. * @param ... (optional) Zero or more additional indexes names, to * subgroup the groups. * @return dict Dictionary mapping distinct index values to lists of * all objects which had that value at the index. */ function igroup(array $list, $by /* , ... */) { $map = ipull($list, $by); $groups = array(); foreach ($map as $group) { $groups[$group] = array(); } foreach ($map as $key => $group) { $groups[$group][$key] = $list[$key]; } $args = func_get_args(); $args = array_slice($args, 2); if ($args) { array_unshift($args, null); foreach ($groups as $group_key => $grouped) { $args[0] = $grouped; $groups[$group_key] = call_user_func_array('igroup', $args); } } return $groups; } /** * Sort a list of objects by the return value of some method. In PHP, this is * often vastly more efficient than `usort()` and similar. * * // Sort a list of Duck objects by name. * $sorted = msort($ducks, 'getName'); * * It is usually significantly more efficient to define an ordering method * on objects and call `msort()` than to write a comparator. It is often more * convenient, as well. * * NOTE: This method does not take the list by reference; it returns a new list. * * @param list $list List of objects to sort by some property. * @param string $method Name of a method to call on each object; the return * values will be used to sort the list. * @return list Objects ordered by the return values of the method calls. */ function msort(array $list, $method) { $surrogate = mpull($list, $method); // See T13303. A "PhutilSortVector" is technically a sortable object, so // a method which returns a "PhutilSortVector" is suitable for use with // "msort()". However, it's almost certain that the caller intended to use // "msortv()", not "msort()", and forgot to add a "v". Treat this as an error. if ($surrogate) { $item = head($surrogate); if ($item instanceof PhutilSortVector) { throw new Exception( pht( 'msort() was passed a method ("%s") which returns '. '"PhutilSortVector" objects. Use "msortv()", not "msort()", to '. 'sort a list which produces vectors.', $method)); } } asort($surrogate); $result = array(); foreach ($surrogate as $key => $value) { $result[$key] = $list[$key]; } return $result; } /** * Sort a list of objects by a sort vector. * * This sort is stable, well-behaved, and more efficient than `usort()`. * * @param list $list List of objects to sort. * @param string $method Name of a method to call on each object. The method * must return a @{class:PhutilSortVector}. * @return list Objects ordered by the vectors. */ function msortv(array $list, $method) { return msortv_internal($list, $method, SORT_STRING); } function msortv_natural(array $list, $method) { return msortv_internal($list, $method, SORT_NATURAL | SORT_FLAG_CASE); } function msortv_internal(array $list, $method, $flags) { $surrogate = mpull($list, $method); $index = 0; foreach ($surrogate as $key => $value) { if (!($value instanceof PhutilSortVector)) { throw new Exception( pht( 'Objects passed to "%s" must return sort vectors (objects of '. 'class "%s") from the specified method ("%s"). One object (with '. 'key "%s") did not.', 'msortv()', 'PhutilSortVector', $method, $key)); } // Add the original index to keep the sort stable. $value->addInt($index++); $surrogate[$key] = (string)$value; } asort($surrogate, $flags); $result = array(); foreach ($surrogate as $key => $value) { $result[$key] = $list[$key]; } return $result; } /** * Sort a list of arrays by the value of some index. This method is identical to * @{function:msort}, but operates on a list of arrays instead of a list of * objects. * * @param list $list List of arrays to sort by some index value. * @param string $index Index to access on each object; the return values * will be used to sort the list. * @return list Arrays ordered by the index values. */ function isort(array $list, $index) { $surrogate = ipull($list, $index); asort($surrogate); $result = array(); foreach ($surrogate as $key => $value) { $result[$key] = $list[$key]; } return $result; } /** * Filter a list of objects by executing a method across all the objects and * filter out the ones with empty() results. this function works just like * @{function:ifilter}, except that it operates on a list of objects instead * of a list of arrays. * * For example, to remove all objects with no children from a list, where * 'hasChildren' is a method name, do this: * * mfilter($list, 'hasChildren'); * * The optional third parameter allows you to negate the operation and filter * out nonempty objects. To remove all objects that DO have children, do this: * * mfilter($list, 'hasChildren', true); * * @param array $list List of objects to filter. * @param string $method A method name. * @param bool $negate (optional) Pass true to drop objects which pass * the filter instead of keeping them. * @return array List of objects which pass the filter. */ function mfilter(array $list, $method, $negate = false) { if (!is_string($method)) { throw new InvalidArgumentException(pht('Argument method is not a string.')); } $result = array(); foreach ($list as $key => $object) { $value = $object->$method(); if (!$negate) { if (!empty($value)) { $result[$key] = $object; } } else { if (empty($value)) { $result[$key] = $object; } } } return $result; } /** * Filter a list of arrays by removing the ones with an empty() value for some * index. This function works just like @{function:mfilter}, except that it * operates on a list of arrays instead of a list of objects. * * For example, to remove all arrays without value for key 'username', do this: * * ifilter($list, 'username'); * * The optional third parameter allows you to negate the operation and filter * out nonempty arrays. To remove all arrays that DO have value for key * 'username', do this: * * ifilter($list, 'username', true); * * @param array $list List of arrays to filter. * @param scalar $index The index. * @param bool $negate (optional) Pass true to drop arrays which pass * the filter instead of keeping them. * @return array List of arrays which pass the filter. */ function ifilter(array $list, $index, $negate = false) { if (!is_scalar($index)) { throw new InvalidArgumentException(pht('Argument index is not a scalar.')); } $result = array(); if (!$negate) { foreach ($list as $key => $array) { if (!empty($array[$index])) { $result[$key] = $array; } } } else { foreach ($list as $key => $array) { if (empty($array[$index])) { $result[$key] = $array; } } } return $result; } /** * Selects a list of keys from an array, returning a new array with only the * key-value pairs identified by the selected keys, in the specified order. * * Note that since this function orders keys in the result according to the * order they appear in the list of keys, there are effectively two common * uses: either reducing a large dictionary to a smaller one, or changing the * key order on an existing dictionary. * * @param dict $dict Dictionary of key-value pairs to select from. * @param list $keys List of keys to select. * @return dict Dictionary of only those key-value pairs where the key was * present in the list of keys to select. Ordering is * determined by the list order. */ function array_select_keys(array $dict, array $keys) { $result = array(); foreach ($keys as $key) { if (array_key_exists($key, $dict)) { $result[$key] = $dict[$key]; } } return $result; } /** * Checks if all values of array are instances of the passed class. Throws * `InvalidArgumentException` if it isn't true for any value. * * @param array $arr * @param string $class Name of the class or 'array' to check arrays. * @return array Returns passed array. */ function assert_instances_of(array $arr, $class) { $is_array = !strcasecmp($class, 'array'); foreach ($arr as $key => $object) { if ($is_array) { if (!is_array($object)) { $given = gettype($object); throw new InvalidArgumentException( pht( "Array item with key '%s' must be of type array, %s given.", $key, $given)); } } else if (!($object instanceof $class)) { $given = gettype($object); if (is_object($object)) { $given = pht('instance of %s', get_class($object)); } throw new InvalidArgumentException( pht( "Array item with key '%s' must be an instance of %s, %s given.", $key, $class, $given)); } } return $arr; } /** * Assert that two arrays have the exact same keys, in any order. * * @param map $expect Array with expected keys. * @param map $actual Array with actual keys. * @return void */ function assert_same_keys(array $expect, array $actual) { foreach ($expect as $key => $value) { if (isset($actual[$key]) || array_key_exists($key, $actual)) { continue; } throw new InvalidArgumentException( pht( 'Expected to find key "%s", but it is not present.', $key)); } foreach ($actual as $key => $value) { if (isset($expect[$key]) || array_key_exists($key, $expect)) { continue; } throw new InvalidArgumentException( pht( 'Found unexpected surplus key "%s" where no such key was expected.', $key)); } } /** * Assert that passed data can be converted to string. * * @param string $parameter Assert that this data is valid. * @return void * * @task assert */ function assert_stringlike($parameter) { switch (gettype($parameter)) { case 'string': case 'NULL': case 'boolean': case 'double': case 'integer': return; case 'object': if (method_exists($parameter, '__toString')) { return; } break; case 'array': case 'resource': case 'unknown type': default: break; } throw new InvalidArgumentException( pht( 'Argument must be scalar or object which implements %s!', '__toString()')); } /** * Returns the first argument which is not strictly null, or `null` if there * are no such arguments. Identical to the MySQL function of the same name. * * @param ... Zero or more arguments of any type. * @return mixed First non-`null` arg, or null if no such arg exists. */ function coalesce(/* ... */) { $args = func_get_args(); foreach ($args as $arg) { if ($arg !== null) { return $arg; } } return null; } /** * Similar to @{function:coalesce}, but less strict: returns the first * non-`empty()` argument, instead of the first argument that is strictly * non-`null`. If no argument is nonempty, it returns the last argument. This * is useful idiomatically for setting defaults: * * $display_name = nonempty($user_name, $full_name, "Anonymous"); * * @param ... Zero or more arguments of any type. * @return mixed First non-`empty()` arg, or last arg if no such arg * exists, or null if you passed in zero args. */ function nonempty(/* ... */) { $args = func_get_args(); $result = null; foreach ($args as $arg) { $result = $arg; if ($arg) { break; } } return $result; } /** * Invokes the "new" operator with a vector of arguments. There is no way to * `call_user_func_array()` on a class constructor, so you can instead use this * function: * * $obj = newv($class_name, $argv); * * That is, these two statements are equivalent: * * $pancake = new Pancake('Blueberry', 'Maple Syrup', true); * $pancake = newv('Pancake', array('Blueberry', 'Maple Syrup', true)); * * DO NOT solve this problem in other, more creative ways! Three popular * alternatives are: * * - Build a fake serialized object and unserialize it. * - Invoke the constructor twice. * - just use `eval()` lol * * These are really bad solutions to the problem because they can have side * effects (e.g., __wakeup()) and give you an object in an otherwise impossible * state. Please endeavor to keep your objects in possible states. * * If you own the classes you're doing this for, you should consider whether * or not restructuring your code (for instance, by creating static * construction methods) might make it cleaner before using `newv()`. Static * constructors can be invoked with `call_user_func_array()`, and may give your * class a cleaner and more descriptive API. * * @param string $class_name The name of a class. * @param list $argv Array of arguments to pass to its constructor. * @return obj A new object of the specified class, constructed by passing * the argument vector to its constructor. */ function newv($class_name, array $argv) { $reflector = new ReflectionClass($class_name); if ($argv) { return $reflector->newInstanceArgs($argv); } else { return $reflector->newInstance(); } } /** * Returns the first element of an array. Exactly like reset(), but doesn't * choke if you pass it some non-referenceable value like the return value of * a function. * * @param array $arr Array to retrieve the first element from. * @return wild The first value of the array. */ function head(array $arr) { return reset($arr); } /** * Returns the last element of an array. This is exactly like `end()` except * that it won't warn you if you pass some non-referencable array to * it -- e.g., the result of some other array operation. * * @param array $arr Array to retrieve the last element from. * @return wild The last value of the array. */ function last(array $arr) { return end($arr); } /** * Returns the first key of an array. * * @param array $arr Array to retrieve the first key from. * @return int|string The first key of the array. */ function head_key(array $arr) { reset($arr); return key($arr); } /** * Returns the last key of an array. * * @param array $arr Array to retrieve the last key from. * @return int|string The last key of the array. */ function last_key(array $arr) { end($arr); return key($arr); } /** * Merge a vector of arrays performantly. This has the same semantics as * array_merge(), so these calls are equivalent: * * array_merge($a, $b, $c); * array_mergev(array($a, $b, $c)); * * However, when you have a vector of arrays, it is vastly more performant to * merge them with this function than by calling array_merge() in a loop, * because using a loop generates an intermediary array on each iteration. * * @param list $arrayv Vector of arrays to merge. * @return list Arrays, merged with array_merge() semantics. */ function array_mergev(array $arrayv) { if (!$arrayv) { return array(); } foreach ($arrayv as $key => $item) { if (!is_array($item)) { throw new InvalidArgumentException( pht( 'Expected all items passed to "array_mergev()" to be arrays, but '. 'argument with key "%s" has type "%s".', $key, gettype($item))); } } // See T13588. In PHP8, "call_user_func_array()" will attempt to use // "unnatural" array keys as named parameters, and then fail because // "array_merge()" does not accept named parameters . Guarantee the list is // a "natural" list to avoid this. $arrayv = array_values($arrayv); return call_user_func_array('array_merge', $arrayv); } /** * Split a corpus of text into lines. This function splits on "\n", "\r\n", or * a mixture of any of them. * * NOTE: This function does not treat "\r" on its own as a newline because none * of SVN, Git or Mercurial do on any OS. * * @param string|PhutilSafeHTML $corpus Block of text to be split into lines. * @param bool $retain_endings (optional) If true, retain line endings in * result strings. * @return list List of lines. * * @phutil-external-symbol class PhutilSafeHTML * @phutil-external-symbol function phutil_safe_html */ function phutil_split_lines($corpus, $retain_endings = true) { if (!phutil_nonempty_stringlike($corpus)) { return array(''); } // Split on "\r\n" or "\n". if ($retain_endings) { $lines = preg_split('/(?<=\n)/', $corpus); } else { $lines = preg_split('/\r?\n/', $corpus); } // If the text ends with "\n" or similar, we'll end up with an empty string // at the end; discard it. if (end($lines) == '') { array_pop($lines); } if ($corpus instanceof PhutilSafeHTML) { foreach ($lines as $key => $line) { $lines[$key] = phutil_safe_html($line); } return $lines; } return $lines; } /** * Simplifies a common use of `array_combine()`. Specifically, this: * * COUNTEREXAMPLE: * if ($list) { * $result = array_combine($list, $list); * } else { * // Prior to PHP 5.4, array_combine() failed if given empty arrays. * $result = array(); * } * * ...is equivalent to this: * * $result = array_fuse($list); * * @param list $list (optional) List of scalars. * @return dict Dictionary with inputs mapped to themselves. */ function array_fuse(array $list = null) { if ($list) { return array_combine($list, $list); } return array(); } /** * Add an element between every two elements of some array. That is, given a * list `A, B, C, D`, and some element to interleave, `x`, this function returns * `A, x, B, x, C, x, D`. This works like `implode()`, but does not concatenate * the list into a string. In particular: * * implode('', array_interleave($x, $list)); * * ...is equivalent to: * * implode($x, $list); * * This function does not preserve keys. * * @param wild $interleave Element to interleave. * @param list $array List of elements to be interleaved. * @return list Original list with the new element interleaved. */ function array_interleave($interleave, array $array) { $result = array(); foreach ($array as $item) { $result[] = $item; $result[] = $interleave; } array_pop($result); return $result; } function phutil_is_windows() { // We can also use PHP_OS, but that's kind of sketchy because it returns // "WINNT" for Windows 7 and "Darwin" for Mac OS X. Practically, testing for // DIRECTORY_SEPARATOR is more straightforward. return (DIRECTORY_SEPARATOR != '/'); } function phutil_is_hiphop_runtime() { return (array_key_exists('HPHP', $_ENV) && $_ENV['HPHP'] === 1); } /** * Converts a string to a loggable one, with unprintables and newlines escaped. * * @param string $string Any string. * @return string String with control and newline characters escaped, suitable * for printing on a single log line. */ function phutil_loggable_string($string) { if (preg_match('/^[\x20-\x7E]+$/', $string)) { return $string; } $result = ''; static $c_map = array( '\\' => '\\\\', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t', ); $len = strlen($string); for ($ii = 0; $ii < $len; $ii++) { $c = $string[$ii]; if (isset($c_map[$c])) { $result .= $c_map[$c]; } else { $o = ord($c); if ($o < 0x20 || $o >= 0x7F) { $result .= '\\x'.sprintf('%02X', $o); } else { $result .= $c; } } } return $result; } /** * Perform an `fwrite()` which distinguishes between EAGAIN and EPIPE. * * PHP's `fwrite()` is broken, and never returns `false` for writes to broken * nonblocking pipes: it always returns 0, and provides no straightforward * mechanism for distinguishing between EAGAIN (buffer is full, can't write any * more right now) and EPIPE or similar (no write will ever succeed). * * See: https://bugs.php.net/bug.php?id=39598 * * If you call this method instead of `fwrite()`, it will attempt to detect * when a zero-length write is caused by EAGAIN and return `0` only if the * write really should be retried. * * @param resource $stream Socket or pipe stream. * @param string $bytes Bytes to write. * @return bool|int Number of bytes written, or `false` on any error (including * errors which `fwrite()` can not detect, like a broken pipe). */ function phutil_fwrite_nonblocking_stream($stream, $bytes) { if (!strlen($bytes)) { return 0; } $result = @fwrite($stream, $bytes); if ($result !== 0) { // In cases where some bytes are witten (`$result > 0`) or // an error occurs (`$result === false`), the behavior of fwrite() is // correct. We can return the value as-is. return $result; } // If we make it here, we performed a 0-length write. Try to distinguish // between EAGAIN and EPIPE. To do this, we're going to `stream_select()` // the stream, write to it again if PHP claims that it's writable, and // consider the pipe broken if the write fails. // (Signals received during the "fwrite()" do not appear to affect anything, // see D20083.) $read = array(); $write = array($stream); $except = array(); $result = @stream_select($read, $write, $except, 0); if ($result === false) { // See T13243. If the select is interrupted by a signal, it may return // "false" indicating an underlying EINTR condition. In this case, the // results (notably, "$write") are not usable because "stream_select()" // didn't update them. // In this case, treat this stream as blocked and tell the caller to // retry, since EINTR is the only condition we're currently aware of that // can cause "fwrite()" to return "0" and "stream_select()" to return // "false" on the same stream. return 0; } if (!$write) { // The stream isn't writable, so we conclude that it probably really is // blocked and the underlying error was EAGAIN. Return 0 to indicate that // no data could be written yet. return 0; } // If we make it here, PHP **just** claimed that this stream is writable, so // perform a write. If the write also fails, conclude that these failures are // EPIPE or some other permanent failure. $result = @fwrite($stream, $bytes); if ($result !== 0) { // The write worked or failed explicitly. This value is fine to return. return $result; } // We performed a 0-length write, were told that the stream was writable, and // then immediately performed another 0-length write. Conclude that the pipe // is broken and return `false`. return false; } /** * Convert a human-readable unit description into a numeric one. This function * allows you to replace this: * * COUNTEREXAMPLE * $ttl = (60 * 60 * 24 * 30); // 30 days * * ...with this: * * $ttl = phutil_units('30 days in seconds'); * * ...which is self-documenting and difficult to make a mistake with. * * @param string $description Human readable description of a unit quantity. * @return int Quantity of specified unit. */ function phutil_units($description) { $matches = null; if (!preg_match('/^(\d+) (\w+) in (\w+)$/', $description, $matches)) { throw new InvalidArgumentException( pht( 'Unable to parse unit specification (expected a specification in the '. 'form "%s"): %s', '5 days in seconds', $description)); } $quantity = (int)$matches[1]; $src_unit = $matches[2]; $dst_unit = $matches[3]; $is_divisor = false; switch ($dst_unit) { case 'seconds': switch ($src_unit) { case 'second': case 'seconds': $factor = 1; break; case 'minute': case 'minutes': $factor = 60; break; case 'hour': case 'hours': $factor = 60 * 60; break; case 'day': case 'days': $factor = 60 * 60 * 24; break; default: throw new InvalidArgumentException( pht( 'This function can not convert from the unit "%s".', $src_unit)); } break; case 'bytes': switch ($src_unit) { case 'byte': case 'bytes': $factor = 1; break; case 'bit': case 'bits': $factor = 8; $is_divisor = true; break; default: throw new InvalidArgumentException( pht( 'This function can not convert from the unit "%s".', $src_unit)); } break; case 'milliseconds': switch ($src_unit) { case 'second': case 'seconds': $factor = 1000; break; case 'minute': case 'minutes': $factor = 1000 * 60; break; case 'hour': case 'hours': $factor = 1000 * 60 * 60; break; case 'day': case 'days': $factor = 1000 * 60 * 60 * 24; break; default: throw new InvalidArgumentException( pht( 'This function can not convert from the unit "%s".', $src_unit)); } break; case 'microseconds': switch ($src_unit) { case 'second': case 'seconds': $factor = 1000000; break; case 'minute': case 'minutes': $factor = 1000000 * 60; break; case 'hour': case 'hours': $factor = 1000000 * 60 * 60; break; case 'day': case 'days': $factor = 1000000 * 60 * 60 * 24; break; default: throw new InvalidArgumentException( pht( 'This function can not convert from the unit "%s".', $src_unit)); } break; default: throw new InvalidArgumentException( pht( 'This function can not convert into the unit "%s".', $dst_unit)); } if ($is_divisor) { if ($quantity % $factor) { throw new InvalidArgumentException( pht( '"%s" is not an exact quantity.', $description)); } return (int)($quantity / $factor); } else { return $quantity * $factor; } } /** * Compute the number of microseconds that have elapsed since an earlier * timestamp (from `microtime(true)`). * * @param double $timestamp Microsecond-precision timestamp, from * `microtime(true)`. * @return int Elapsed microseconds. */ function phutil_microseconds_since($timestamp) { if (!is_float($timestamp)) { throw new Exception( pht( 'Argument to "phutil_microseconds_since(...)" should be a value '. 'returned from "microtime(true)".')); } $delta = (microtime(true) - $timestamp); $delta = 1000000 * $delta; $delta = (int)$delta; return $delta; } /** * Decode a JSON dictionary. * * @param string $string A string which ostensibly contains a JSON-encoded * list or dictionary. * @return mixed Decoded list/dictionary. */ function phutil_json_decode($string) { $result = @json_decode($string, true); if (!is_array($result)) { // Failed to decode the JSON. Try to use @{class:PhutilJSONParser} instead. // This will probably fail, but will throw a useful exception. $parser = new PhutilJSONParser(); $result = $parser->parse($string); } return $result; } /** * Encode a value in JSON, raising an exception if it can not be encoded. * * @param wild $value A value to encode. * @return string JSON representation of the value. */ function phutil_json_encode($value) { $result = @json_encode($value); if ($result === false) { $reason = phutil_validate_json($value); if (function_exists('json_last_error')) { $err = json_last_error(); if (function_exists('json_last_error_msg')) { $msg = json_last_error_msg(); $extra = pht('#%d: %s', $err, $msg); } else { $extra = pht('#%d', $err); } } else { $extra = null; } if ($extra) { $message = pht( 'Failed to JSON encode value (%s): %s.', $extra, $reason); } else { $message = pht( 'Failed to JSON encode value: %s.', $reason); } throw new Exception($message); } return $result; } /** * Produce a human-readable explanation why a value can not be JSON-encoded. * * @param wild $value Value to validate. * @param string $path (optional) Path within the object to provide context. * @return string|null Explanation of why it can't be encoded, or null. */ function phutil_validate_json($value, $path = '') { if ($value === null) { return; } if ($value === true) { return; } if ($value === false) { return; } if (is_int($value)) { return; } if (is_float($value)) { return; } if (is_array($value)) { foreach ($value as $key => $subvalue) { if (strlen($path)) { $full_key = $path.' > '; } else { $full_key = ''; } if (!phutil_is_utf8($key)) { $full_key = $full_key.phutil_utf8ize($key); return pht( 'Dictionary key "%s" is not valid UTF8, and cannot be JSON encoded.', $full_key); } $full_key .= $key; $result = phutil_validate_json($subvalue, $full_key); if ($result !== null) { return $result; } } } if (is_string($value)) { if (!phutil_is_utf8($value)) { $display = substr($value, 0, 256); $display = phutil_utf8ize($display); if (!strlen($path)) { return pht( 'String value is not valid UTF8, and can not be JSON encoded: %s', $display); } else { return pht( 'Dictionary value at key "%s" is not valid UTF8, and cannot be '. 'JSON encoded: %s', $path, $display); } } } return; } /** * Decode an INI string. * * @param string $string * @return mixed */ function phutil_ini_decode($string) { $results = null; $trap = new PhutilErrorTrap(); try { $results = @parse_ini_string($string, true, INI_SCANNER_RAW); if ($results === false) { throw new PhutilINIParserException(trim($trap->getErrorsAsString())); } foreach ($results as $section => $result) { if (!is_array($result)) { // We JSON decode the value in ordering to perform the following // conversions: // // - `'true'` => `true` // - `'false'` => `false` // - `'123'` => `123` // - `'1.234'` => `1.234` // $result = json_decode($result, true); if ($result !== null && !is_array($result)) { $results[$section] = $result; } continue; } foreach ($result as $key => $value) { $value = json_decode($value, true); if ($value !== null && !is_array($value)) { $results[$section][$key] = $value; } } } } catch (Exception $ex) { $trap->destroy(); throw $ex; } $trap->destroy(); return $results; } /** * Attempt to censor any plaintext credentials from a string. * * The major use case here is to censor usernames and passwords from command * output. For example, when `git fetch` fails, the output includes credentials * for authenticated HTTP remotes. * * @param string $string Some block of text. * @return string A similar block of text, but with credentials that could * be identified censored. */ function phutil_censor_credentials($string) { return preg_replace(',(?<=://)([^/@\s]+)(?=@|$),', '********', $string); } /** * Returns a parsable string representation of a variable. * * This function is intended to behave similarly to PHP's `var_export` function, * but the output is intended to follow our style conventions. * * @param wild $var The variable you want to export. * @return string */ function phutil_var_export($var) { // `var_export(null, true)` returns `"NULL"` (in uppercase). if ($var === null) { return 'null'; } // PHP's `var_export` doesn't format arrays very nicely. In particular: // // - An empty array is split over two lines (`"array (\n)"`). // - A space separates "array" and the first opening brace. // - Non-associative arrays are returned as associative arrays with an // integer key. // if (is_array($var)) { if (count($var) === 0) { return 'array()'; } // Don't show keys for non-associative arrays. $show_keys = !phutil_is_natural_list($var); $output = array(); $output[] = 'array('; foreach ($var as $key => $value) { // Adjust the indentation of the value. $value = str_replace("\n", "\n ", phutil_var_export($value)); $output[] = ' '. ($show_keys ? var_export($key, true).' => ' : ''). $value.','; } $output[] = ')'; return implode("\n", $output); } // Let PHP handle everything else. return var_export($var, true); } /** * An improved version of `fnmatch`. * * @param string $glob A glob pattern. * @param string $path A path. * @return bool */ function phutil_fnmatch($glob, $path) { // Modify the glob to allow `**/` to match files in the root directory. $glob = preg_replace('@(?:(? $parameters Dictionary of parameters. * @return string HTTP query string. */ function phutil_build_http_querystring(array $parameters) { $pairs = array(); foreach ($parameters as $key => $value) { $pairs[] = array($key, $value); } return phutil_build_http_querystring_from_pairs($pairs); } /** * Build a query string from a list of parameter pairs. * * @param list> $pairs List of pairs. * @return string HTTP query string. */ function phutil_build_http_querystring_from_pairs(array $pairs) { // We want to encode in RFC3986 mode, but "http_build_query()" did not get // a flag for that mode until PHP 5.4.0. This is equivalent to calling // "http_build_query()" with the "PHP_QUERY_RFC3986" flag. $query = array(); foreach ($pairs as $pair_key => $pair) { if (!is_array($pair) || (count($pair) !== 2)) { throw new Exception( pht( 'HTTP parameter pair (with key "%s") is not valid: each pair must '. 'be an array with exactly two elements.', $pair_key)); } list($key, $value) = $pair; list($key, $value) = phutil_http_parameter_pair($key, $value); $query[] = rawurlencode($key).'='.rawurlencode($value); } $query = implode('&', $query); return $query; } /** * Typecheck and cast an HTTP key-value parameter pair. * * Scalar values are converted to strings. Nonscalar values raise exceptions. * * @param scalar $key HTTP parameter key. * @param scalar $value HTTP parameter value. * @return pair Key and value as strings. */ function phutil_http_parameter_pair($key, $value) { try { assert_stringlike($key); } catch (InvalidArgumentException $ex) { throw new PhutilProxyException( pht('HTTP query parameter key must be a scalar.'), $ex); } $key = phutil_string_cast($key); try { assert_stringlike($value); } catch (InvalidArgumentException $ex) { throw new PhutilProxyException( pht( 'HTTP query parameter value (for key "%s") must be a scalar.', $key), $ex); } $value = phutil_string_cast($value); return array($key, $value); } function phutil_decode_mime_header($header) { if (function_exists('iconv_mime_decode')) { return iconv_mime_decode($header, 0, 'UTF-8'); } if (function_exists('mb_decode_mimeheader')) { return mb_decode_mimeheader($header); } throw new Exception( pht( 'Unable to decode MIME header: install "iconv" or "mbstring" '. 'extension.')); } /** * Perform a "(string)" cast without disabling standard exception behavior. * * When PHP invokes "__toString()" automatically, it fatals if the method * raises an exception. In older versions of PHP (until PHP 7.1), this fatal is * fairly opaque and does not give you any information about the exception * itself, although newer versions of PHP at least include the exception * message. * * This is documented on the "__toString()" manual page: * * Warning * You cannot throw an exception from within a __toString() method. Doing * so will result in a fatal error. * * However, this only applies to implicit invocation by the language runtime. * Application code can safely call `__toString()` directly without any effect * on exception handling behavior. Very cool. * * We also reject arrays. PHP casts them to the string "Array". This behavior * is, charitably, evil. * * @param wild $value Any value which aspires to be represented as a string. * @return string String representation of the provided value. */ function phutil_string_cast($value) { if (is_array($value)) { throw new Exception( pht( 'Value passed to "phutil_string_cast()" is an array; arrays can '. 'not be sensibly cast to strings.')); } if (is_object($value)) { $string = $value->__toString(); if (!is_string($string)) { throw new Exception( pht( 'Object (of class "%s") did not return a string from "__toString()".', get_class($value))); } return $string; } return (string)$value; } /** * Return a short, human-readable description of an object's type. * * This is mostly useful for raising errors like "expected x() to return a Y, * but it returned a Z". * * This is similar to "get_type()", but describes objects and arrays in more * detail. * * @param wild $value Anything. * @return string Human-readable description of the value's type. */ function phutil_describe_type($value) { return PhutilTypeSpec::getTypeOf($value); } /** * Test if a list has the natural numbers (1, 2, 3, and so on) as keys, in * order. * * @return bool True if the list is a natural list. */ function phutil_is_natural_list(array $list) { $expect = 0; foreach ($list as $key => $item) { if ($key !== $expect) { return false; } $expect++; } return true; } /** * Escape text for inclusion in a URI or a query parameter. Note that this * method does NOT escape '/', because "%2F" is invalid in paths and Apache * will automatically 404 the page if it's present. This will produce correct * (the URIs will work) and desirable (the URIs will be readable) behavior in * these cases: * * '/path/?param='.phutil_escape_uri($string); # OK: Query Parameter * '/path/to/'.phutil_escape_uri($string); # OK: URI Suffix * * It will potentially produce the WRONG behavior in this special case: * * COUNTEREXAMPLE * '/path/to/'.phutil_escape_uri($string).'/thing/'; # BAD: URI Infix * * In this case, any '/' characters in the string will not be escaped, so you * will not be able to distinguish between the string and the suffix (unless * you have more information, like you know the format of the suffix). For infix * URI components, use @{function:phutil_escape_uri_path_component} instead. * * @param string $string Some string. * @return string URI encoded string, except for '/'. */ function phutil_escape_uri($string) { if ($string === null) { return ''; } return str_replace('%2F', '/', rawurlencode($string)); } /** * Escape text for inclusion as an infix URI substring. See discussion at * @{function:phutil_escape_uri}. This function covers an unusual special case; * @{function:phutil_escape_uri} is usually the correct function to use. * * This function will escape a string into a format which is safe to put into * a URI path and which does not contain '/' so it can be correctly parsed when * embedded as a URI infix component. * * However, you MUST decode the string with * @{function:phutil_unescape_uri_path_component} before it can be used in the * application. * * @param string $string Some string. * @return string URI encoded string that is safe for infix composition. */ function phutil_escape_uri_path_component($string) { return rawurlencode(rawurlencode($string)); } /** * Unescape text that was escaped by * @{function:phutil_escape_uri_path_component}. See * @{function:phutil_escape_uri} for discussion. * * Note that this function is NOT the inverse of * @{function:phutil_escape_uri_path_component}! It undoes additional escaping * which is added to survive the implied unescaping performed by the webserver * when interpreting the request. * * @param string $string Some string emitted * from @{function:phutil_escape_uri_path_component} and * then accessed via a web server. * @return string Original string. */ function phutil_unescape_uri_path_component($string) { return rawurldecode($string); } function phutil_is_noninteractive() { if (function_exists('posix_isatty') && !posix_isatty(STDIN)) { return true; } return false; } function phutil_is_interactive() { if (function_exists('posix_isatty') && posix_isatty(STDIN)) { return true; } return false; } function phutil_encode_log($message) { return addcslashes($message, "\0..\37\\\177..\377"); } /** * Insert a value in between each pair of elements in a list. * * Keys in the input list are preserved. */ function phutil_glue(array $list, $glue) { if (!$list) { return $list; } $last_key = last_key($list); $keys = array(); $values = array(); $tmp = $list; foreach ($list as $key => $ignored) { $keys[] = $key; if ($key !== $last_key) { $tmp[] = $glue; $keys[] = last_key($tmp); } } return array_select_keys($tmp, $keys); } function phutil_partition(array $map) { $partitions = array(); $partition = array(); $is_first = true; $partition_value = null; foreach ($map as $key => $value) { if (!$is_first) { if ($partition_value === $value) { $partition[$key] = $value; continue; } $partitions[] = $partition; } $is_first = false; $partition = array($key => $value); $partition_value = $value; } if ($partition) { $partitions[] = $partition; } return $partitions; } function phutil_preg_match( $pattern, $subject, $flags = 0, $offset = 0) { $matches = null; $result = @preg_match($pattern, $subject, $matches, $flags, $offset); if ($result === false || $result === null) { phutil_raise_preg_exception( 'preg_match', array( $pattern, $subject, $matches, $flags, $offset, )); } return $matches; } function phutil_preg_match_all( $pattern, $subject, $flags = 0, $offset = 0) { $matches = null; $result = @preg_match_all($pattern, $subject, $matches, $flags, $offset); if ($result === false || $result === null) { phutil_raise_preg_exception( 'preg_match_all', array( $pattern, $subject, $matches, $flags, $offset, )); } return $matches; } function phutil_raise_preg_exception($function, array $argv) { $trap = new PhutilErrorTrap(); // NOTE: This ugly construction to avoid issues with reference behavior when // passing values through "call_user_func_array()". switch ($function) { case 'preg_match': @preg_match($argv[0], $argv[1], $argv[2], $argv[3], $argv[4]); break; case 'preg_match_all': @preg_match_all($argv[0], $argv[1], $argv[2], $argv[3], $argv[4]); break; } $error_message = $trap->getErrorsAsString(); $trap->destroy(); $pattern = $argv[0]; $pattern_display = sprintf( '"%s"', addcslashes($pattern, '\\\"')); $message = array(); $message[] = pht( 'Call to %s(%s, ...) failed.', $function, $pattern_display); if (strlen($error_message)) { $message[] = pht( 'Regular expression engine emitted message: %s', $error_message); } $message = implode("\n\n", $message); throw new PhutilRegexException($message); } /** * Test if a value is a nonempty string. * * The value "null" and the empty string are considered empty; all other * strings are considered nonempty. * * This method raises an exception if passed a value which is neither null * nor a string. * * @param $value Value to test. * @return bool True if the parameter is a nonempty string. */ function phutil_nonempty_string($value) { if ($value === null) { return false; } if ($value === '') { return false; } if (is_string($value)) { return true; } throw new InvalidArgumentException( pht( 'Call to phutil_nonempty_string() expected null or a string, got: %s.', phutil_describe_type($value))); } /** * Test if a value is a nonempty, stringlike value. * * The value "null", the empty string, and objects which have a "__toString()" * method which returns the empty string are empty. * * Other strings, and objects with a "__toString()" method that returns a * string other than the empty string are considered nonempty. * * This method raises an exception if passed any other value. * * @param $value Value to test. * @return bool True if the parameter is a nonempty, stringlike value. */ function phutil_nonempty_stringlike($value) { if ($value === null) { return false; } if ($value === '') { return false; } if (is_string($value)) { return true; } if (is_object($value)) { try { $string = phutil_string_cast($value); return phutil_nonempty_string($string); } catch (Exception $ex) { // Continue below. } catch (Throwable $ex) { // Continue below. } } throw new InvalidArgumentException( pht( 'Call to phutil_nonempty_stringlike() expected a string or stringlike '. 'object, got: %s.', phutil_describe_type($value))); } /** * Test if a value is a nonempty, scalar value. * * The value "null", the empty string, and objects which have a "__toString()" * method which returns the empty string are empty. * * Other strings, objects with a "__toString()" method which returns a * string other than the empty string, integers, and floats are considered * scalar. * * Note that booleans are also valid scalars, where false is considered empty, * and true is non-empty since if you cast true to string, it's non-empty. * * This method raises an exception if passed any other value. * * @param $value Value to test. * @return bool True if the parameter is a nonempty, scalar value. */ function phutil_nonempty_scalar($value) { if ($value === null) { return false; } if ($value === '') { return false; } if (is_string($value) || is_int($value) || is_float($value)) { return true; } // Booleans are also valid scalars by PHP. Inventing the opposite can be // too much esoteric and problematic. // false: empty, because casted to string becomes '' (empty) // true: non-empty, because casted to string becomes '1' (non-empty) if ($value === false || $value === true) { return $value; } if (is_object($value)) { try { $string = phutil_string_cast($value); return phutil_nonempty_string($string); } catch (Exception $ex) { // Continue below. } catch (Throwable $ex) { // Continue below. } } throw new InvalidArgumentException( pht( 'Call to phutil_nonempty_scalar() expected: a string; or stringlike '. 'object; or int; or float. Got: %s.', phutil_describe_type($value))); }