bkdotcom/PHPDebugConsole

View on GitHub
src/Debug/Utility/Utility.php

Summary

Maintainability
A
45 mins
Test Coverage
A
96%
<?php

/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2024 Brad Kent
 * @version   v3.0
 */

namespace bdk\Debug;

use bdk\Debug;
use Exception;
use Psr\Http\Message\StreamInterface;
use RuntimeException;

/**
 * Utility methods
 */
class Utility
{
    /**
     * Emit headers queued for output directly using `header()`
     *
     * @param array<array-key, string|string[]> $headers array of headers
     *                array(
     *                   array(name, value)
     *                   name => value
     *                   name => array(value1, value2),
     *                )
     *
     * @return void
     * @throws RuntimeException if headers already sent
     */
    public static function emitHeaders(array $headers)
    {
        if (!$headers) {
            return;
        }
        $file = '';
        $line = 0;
        // phpcs:ignore SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions.NonFullyQualified
        if (headers_sent($file, $line)) {
            throw new RuntimeException('Headers already sent: ' . $file . ', line ' . $line);
        }
        foreach ($headers as $key => $val) {
            if (\is_int($key)) {
                $key = (string) $val[0];
                $val = $val[1];
            }
            self::emitHeader($key, $val);
        }
    }

    /**
     * Format duration
     *
     * @param float    $duration  duration in seconds
     * @param string   $format    DateInterval format string, or 'auto', us', 'ms', 's', or 'sec'
     * @param int|null $precision decimal precision
     *
     * @return string
     */
    public static function formatDuration($duration, $format = 'auto', $precision = 4)
    {
        $format = self::formatDurationGetFormat($duration, $format);
        if (\preg_match('/%[YyMmDdaHhIiSsFf]/', $format)) {
            return static::formatDurationDateInterval($duration, $format);
        }
        switch ($format) {
            case 'us':
                $val = $duration * 1000000;
                $unit = 'μs';
                break;
            case 'ms':
                $val = $duration * 1000;
                $unit = 'ms';
                break;
            default:
                $val = $duration;
                $unit = 'sec';
        }
        if ($precision) {
            $val = \round($val, $precision);
        }
        return $val . ' ' . $unit;
    }

    /**
     * Convert size int into "1.23 kB" or vice versa
     *
     * @param int|string $size      bytes or similar to "1.23M"
     * @param bool       $returnInt return integer?
     *
     * @return string|int|false
     */
    public static function getBytes($size, $returnInt = false)
    {
        if (\is_string($size)) {
            $size = self::parseBytes($size);
        }
        if ($size === false) {
            return false;
        }
        if ($returnInt) {
            return (int) $size;
        }
        $units = array('B', 'kB', 'MB', 'GB', 'TB', 'PB');
        $exp = (int) \floor(\log((float) $size, 1024));
        $pow = \pow(1024, $exp);
        /** @psalm-suppress RedundantCast */
        $size = (int) $pow < 1
            ? '0 B'
            : \round($size / $pow, 2) . ' ' . $units[$exp];
        return $size;
    }

    /**
     * Returns sent/pending response header values for specified header
     *
     * @param string      $key       ('Content-Type') header to return
     * @param string|null $delimiter (', ') if string, then join the header values
     *                                 if null, return array
     *
     * @return string|string[]
     *
     * @psalm-return ($delimiter is string ? string : string[])
     */
    public static function getEmittedHeader($key = 'Content-Type', $delimiter = ', ')
    {
        $headers = static::getEmittedHeaders();
        $header = isset($headers[$key])
            ? $headers[$key]
            : array();
        return \is_string($delimiter)
            ? \implode($delimiter, $header)
            : $header;
    }

    /**
     * Returns sent/pending response headers
     *
     * The keys represent the header name as it will be sent over the wire, and
     * each value is an array of strings associated with the header.
     *
     * @return array<string, list<string>>
     */
    public static function getEmittedHeaders()
    {
        // phpcs:ignore SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions.NonFullyQualified
        $list = headers_list();
        $headers = array();
        foreach ($list as $header) {
            list($key, $value) = \array_replace(array('', ''), \explode(': ', $header, 2));
            $headers[$key][] = $value;
        }
        return $headers;
    }

    /**
     * Get stream contents without affecting pointer
     *
     * @param StreamInterface $stream StreamInteface
     *
     * @return string
     */
    public static function getStreamContents(StreamInterface $stream)
    {
        try {
            $pos = $stream->tell();
            $body = (string) $stream; // __toString() is like getContents(), but without throwing exceptions
            $stream->seek($pos);
            return $body;
            // @codeCoverageIgnoreStart
        } catch (Exception $e) {
            return '';
            // @codeCoverageIgnoreEnd
        }
    }

    /**
     * Get current git branch for specified directory
     *
     * @param string $dir (optional) defaults to current working dir
     *
     * @return string|null
     */
    public static function gitBranch($dir = null)
    {
        // exec('git branch') may fail due due to permissions / rights
        // navigate up until we find the ./git/HEAD file
        $dir = $dir ?: \getcwd();
        $docRoot = Debug::getInstance()->getServerParam('DOCUMENT_ROOT');
        $docRootFound = false;
        $parts = \explode(DIRECTORY_SEPARATOR, $dir);
        for ($i = \count($parts); $i > 0; $i--) {
            $dirParts = \array_slice($parts, 0, $i);
            $gitHeadFilepath = \implode(DIRECTORY_SEPARATOR, \array_merge(
                $dirParts,
                array('.git', 'HEAD')
            ));
            if (\file_exists($gitHeadFilepath)) {
                $filelines = \file($gitHeadFilepath);
                $parts = \explode('/', $filelines[0], 3);
                return isset($parts[2])
                    ? \trim($parts[2])
                    : null;
            }
            if ($docRootFound) {
                break;
            }
            if (\implode(DIRECTORY_SEPARATOR, $dirParts) === $docRoot) {
                $docRootFound = true;
            }
        }
        return null;
    }

    /**
     * Does specified http method generally have a request body
     *
     * @param string $method http method (such as 'GET' or 'POST')
     *
     * @return bool
     */
    public static function httpMethodHasBody($method)
    {
        // don't expect a request body for these methods
        $noBodyMethods = array('CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'TRACE');
        return \in_array($method, $noBodyMethods, true) === false;
    }

    /**
     * "Safely" test if value is a file
     *
     * @param string $val value to test
     *
     * @return bool
     *
     * @psalm-assert-if-true string $val
     */
    public static function isFile($val)
    {
        if (\is_string($val) === false) {
            return false;
        }
        /*
            pre-test / prevent "is_file() expects parameter 1 to be a valid path, string given"
        */
        if (\preg_match('#(://|[\r\n\x00])#', $val) === 1) {
            return false;
        }
        return \is_file($val);
    }

    /**
     * Emit a header
     *
     * @param string          $name  Header name
     * @param string|string[] $value Header value(s)
     *
     * @return void
     *
     * @phpcs:disable SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions.NonFullyQualified
     */
    private static function emitHeader($name, $value)
    {
        $values = (array) $value;
        $val = \array_shift($values);
        header($name . ': ' . $val);
        foreach ($values as $val) {
            // add (vs replace) the additional values
            header($name . ': ' . $val, false);
        }
    }

    /**
     * Format a duration using a DateInterval format string
     *
     * @param float  $duration duration in seconds
     * @param string $format   DateInterval format string
     *
     * @return string
     *
     * @see https://www.php.net/manual/en/dateinterval.format.php
     */
    private static function formatDurationDateInterval($duration, $format)
    {
        // php < 7.1 DateInterval doesn't support fraction..   we'll work around that
        $hours = \floor($duration / 3600);
        $sec = $duration - $hours * 3600;
        $min = \floor($sec / 60);
        $sec = $sec - $min * 60;
        $sec = \round($sec, 6);
        if (\preg_match('/%[Ff]/', $format)) {
            $secWhole = \floor($sec);
            $secFraction = $sec - $secWhole;
            $sec = $secWhole;
            $micros = $secFraction * 1000000;
            $format = \strtr($format, array(
                '%F' => \sprintf('%06d', $micros),  // Microseconds: 6 digits with leading 0
                '%f' => $micros,                    // Microseconds: w/o leading zeros
            ));
        }
        $duration = \sprintf('PT%dH%dM%dS', (int) $hours, (int) $min, (int) $sec);
        $dateInterval = new \DateInterval($duration);
        return $dateInterval->format($format);
    }

    /**
     * Get Duration format
     *
     * @param float  $duration duration in seconds
     * @param string $format   "auto", "us", "ms", "s", or DateInterval format string
     *
     * @return string
     */
    private static function formatDurationGetFormat($duration, $format)
    {
        if ($format !== 'auto') {
            return $format;
        }
        if ($duration < 1 / 1000) {
            return 'us';
        }
        if ($duration < 1) {
            return 'ms';
        }
        if ($duration < 60) {
            return 's';
        }
        if ($duration < 3600) {
            return '%im %Ss'; // M:SS
        }
        return '%hh %Im %Ss'; // H:MM:SS
    }

    /**
     * Parse string such as 128M
     *
     * @param string $size size
     *
     * @return int|false
     */
    private static function parseBytes($size)
    {
        if (\preg_match('/^[\d,]+$/', $size)) {
            return (int) \str_replace(',', '', $size);
        }
        $matches = array();
        if (\preg_match('/^([\d,.]+)\s?([kmgtp])?b?$/i', $size, $matches)) {
            $matches = \array_replace(array('', '', ''), $matches);
            $size = (float) \str_replace(',', '', $matches[1]);
            switch (\strtolower($matches[2])) {
                case 'p':
                    $size *= 1024;
                    // no break
                case 't':
                    $size *= 1024;
                    // no break
                case 'g':
                    $size *= 1024;
                    // no break
                case 'm':
                    $size *= 1024;
                    // no break
                case 'k':
                    $size *= 1024;
            }
            return (int) $size;
        }
        return false;
    }
}