diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6ba93ed --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: Testing +on: [push, pull_request] +jobs: + tests: + strategy: + matrix: + operating-system: [ubuntu-latest] + php-versions: ["8.1", "8.2", "8.3"] + runs-on: ${{ matrix.operating-system }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, intl, pdo_sqlite, pdo_mysql + coverage: pcov + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + # Use composer.json for key, if composer.lock is not committed. + # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Composer dependencies + run: composer install --no-progress + + - name: Test with phpunit + run: vendor/bin/phpunit --configuration phpunit.xml --coverage-text + + - name: Make code coverage badge + uses: coverallsapp/github-action@v2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b42440 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +vendor +build +composer.lock +.phpunit.cache +theme +index.php +coverage.xml +clover.xml diff --git a/README.md b/README.md index 7f2293d..a318b42 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Debench +[![Test Debench](https://github.com/myaaghubi/Debench/actions/workflows/ci.yml/badge.svg)](https://github.com/myaaghubi/Debench/actions/workflows/ci.yml) [![Debench Coverage Status](https://coveralls.io/repos/github/myaaghubi/Debench/badge.svg?branch=main)](https://coveralls.io/github/myaaghubi/Debench?branch=main) ![Debench release (latest by date)](https://img.shields.io/github/v/release/myaaghubi/Debench) ![Debench License](https://img.shields.io/github/license/myaaghubi/Debench) A small debug/benchmark helper for PHP @@ -19,34 +20,38 @@ require __DIR__ . '/vendor/autoload.php'; // call it from your index.php after autoload // then check the webpage with your browser -// $debench = new Debench(); -Debench::getInstance(); +// $debench = new Debench(true, 'theme'); +Debench::getInstance(true, 'theme'); $st = str_repeat("Debench!", 10000); // step one -// $debench->newPoint("step one"); -Debench::point('step one'); +// $debench->newPoint("one"); +Debench::point('one'); $st .= str_repeat("Debench!", 10000); // step two -Debench::point("step two"); +Debench::info('step two'); +Debench::point("two"); ``` For `minimal` mode: ```php -$debench->setMinimal(true); +// it is safe and secure to use for production mode +// $debench->setMinimalOnly(true); +Debench::minimalOnly(true); ``` For `production` mode ```php -// this one is better -$debench = new Debench(false); +// it's better to do it on initializing +//$debench = new Debench(false); +Debench::getInstance(false); // or -$debench->setEnable(false); +Debench::enable(false); ``` ## License You are allowed to use this plugin under the terms of the MIT License. -Copyright (C) 2024 Mohammad Yaaghubi +Copyright (C) 2024 Mohammad Yaaghubi \ No newline at end of file diff --git a/composer.json b/composer.json index 83702dc..e516ff2 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,12 @@ ], "autoload": { "classmap": [ - "lib/" + "lib/Debench" + ] + }, + "autoload-dev": { + "classmap": [ + "lib/DebenchTest" ] }, "require-dev": { diff --git a/lib/Debench/Debench.php b/lib/Debench/Debench.php index 25c4acf..b2955c5 100644 --- a/lib/Debench/Debench.php +++ b/lib/Debench/Debench.php @@ -14,8 +14,15 @@ class Debench { - private bool $minimal; + private static bool $enable; + private static string $ui; + private static string $pathCalled; + private static string $pathUI; + private static bool $minimalOnly; + private array $checkPoints; + private array $exceptions; + private static array $messages; private int $initPointMS; private int $endPointMS; @@ -24,86 +31,118 @@ class Debench private static ?Debench $instance = null; + /** * Debench constructor * * @param bool $enable * @param string $ui - * @param string $path * @return void */ - public function __construct(private bool $enable = true, private string $ui = 'theme', private string $path = '') + public function __construct(bool $enable = null, string $ui = null) { - if (!$this->enable) { + self::$enable = $enable ?? true; + self::$ui = $ui ? rtrim($ui, '/') : 'theme'; + self::$pathCalled = dirname((Utils::getBacktrace()[0])['file']); + self::$pathUI = self::$pathCalled . '/' . self::$ui . '/debench'; + + if (!self::$enable) { return; } - $this->minimal = false; - $this->checkPoints = []; - $this->lastCheckPointInMS = 0; - $this->lastCheckPointNumber = 0; + // make sure about the theme + Template::makeUI(self::$pathUI); - $this->newPoint('debench'); + // Script initial point + $this->addScriptPoint('Script'); - $this->ui = rtrim($ui, '/'); + // Debench initial point + $this->newPoint('Debench'); - if (empty($path)) { - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); - $this->path = dirname(($backtrace[0])['file']); - } + set_exception_handler([$this, 'addException']); - // check for UI - $this->checkUI(); + set_error_handler([$this, 'addAsException']); + register_shutdown_function([$this, 'shutdownFunction']); - if (PHP_SAPI !== 'cli' && session_status() != PHP_SESSION_ACTIVE) { - @session_start(); + self::$instance = $this; + } + + + /** + * To finalize the Debench + * + * @return bool + */ + private function shutdownFunction(): bool + { + if (!self::$enable) { + return false; } - register_shutdown_function(function () { - if (!$this->enable) { - return; - } + $this->processCheckPoints(); - if (Utils::isInTestMode()) { - return; - } + $output = $this->makeOutput(); - $this->calculateExecutionTime(); - print $this->makeOutput(); - }); + print Utils::isInTestMode() ? '' : $output; - self::$instance = $this; + return !empty($output); } /** - * Copy the template from ui dir into your webroot dir if - * it doesn't exist - * + * Run session_start() + * * @return void */ - public function checkUI(): void + private function startSession(): void { - $currentPath = __DIR__; - $basePath = $this->path; + if (session_status() != PHP_SESSION_ACTIVE) { + @session_start(); + } + } - $uiPath = $basePath . '/' . $this->ui; - $uiPathFull = $uiPath . '/debench'; - // for assets - if (!is_dir($uiPath)) { - if (!is_dir($basePath) || !is_writable($basePath)) { - throw new \Exception("Directory not exists or not writable! `$basePath` ", 500); - } - - @mkdir($uiPath); + /** + * Add a new item in checkpoint[] + * + * @param int $currentTime + * @param int $memory + * @param string $path + * @param int $lineNumber + * @param string $key + * @return void + */ + private function addCheckPoint(int $currentTime, int $memory, string $path, int $lineNumber, string $key = ''): void + { + if (empty($key) || !$this->checkTag($key)) { + throw new \Exception("The `key` is empty or is not in the right format!!"); } - // for assets - if (!is_dir($uiPathFull)) { - Utils::copyDir($currentPath . '/ui', $uiPathFull); + $checkPoint = new CheckPoint($currentTime, $memory, $path, $lineNumber); + + if (!isset($this->checkPoints)) { + $this->checkPoints = []; } + + $this->checkPoints[$key] = $checkPoint; + } + + + /** + * Add the first checkpoint + * + * @param string $tag + * @return void + */ + private function addScriptPoint(string $tag = ''): void + { + $time = $this->getRequestTime(); + $path = $this->getScriptName(); + $tag = $this->makeTag($tag, $this->incrementLastCheckPointNumber(true)); + + // add a check point as preload + $this->addCheckPoint($time, 0, $path, 0, $tag); } @@ -111,11 +150,11 @@ public function checkUI(): void * Add a new checkpoint * * @param string $tag - * @return object + * @return void */ public function newPoint(string $tag = ''): void { - if (!$this->enable) { + if (!self::$enable) { return; } @@ -126,34 +165,18 @@ public function newPoint(string $tag = ''): void $ramUsage = $this->getRamUsage(); - if (empty($tag)) { - $tag = 'point ' . ($this->lastCheckPointNumber + 1); - } - - // to avoid duplicate tags(keys) - $tag .= '#' . ($this->lastCheckPointNumber + 1); + // debug_backtrace + $debugBT = Utils::getBacktrace()[0]; - $dbc = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $dbcIndex = 0; - - // specify calls from self class - while (count($dbc) > $dbcIndex && strrpos(($dbc[$dbcIndex])['file'], __FILE__) !== false) { - $dbcIndex += 1; - } + $file = $debugBT['file']; + $line = $debugBT['line']; - $file = ($dbc[$dbcIndex])['file']; - $line = ($dbc[$dbcIndex])['line']; - if (strrpos($file, __FILE__) !== false) { - $file = '-'; - $line = '-'; - } + $tag = $this->makeTag($tag, $this->incrementLastCheckPointNumber(true)); - $checkPoint = new CheckPoint($currentTime, $ramUsage, $file, $line); - $this->checkPoints[$tag] = $checkPoint; + $this->addCheckPoint($currentTime, $ramUsage, $file, $line, $tag); - $this->lastCheckPointInMS = $currentTime; - $this->lastCheckPointNumber += 1; + $this->setLastCheckPointInMS($currentTime); } @@ -162,23 +185,26 @@ public function newPoint(string $tag = ''): void * * @return void */ - private function calculateExecutionTime(): void + private function processCheckPoints(): void { - // may the below loop take some time + // depends on the the array, it may the below loop take some time to process $this->endPointMS = $this->getCurrentTime(); $prevKey = ''; $prevCP = null; - foreach ($this->checkPoints as $key => $cp) { + + foreach ($this->getCheckPoints() as $key => $cp) { if (!empty($prevKey) && $prevCP != null) { - $this->checkPoints[$prevKey]->setTimestamp($cp->getTimestamp() - $prevCP->getTimestamp()); + $diff = $cp->getTimestamp() - $prevCP->getTimestamp(); + $this->checkPoints[$prevKey]->setTimestamp($diff); } $prevKey = $key; $prevCP = $cp; } - $this->checkPoints[$prevKey]->setTimestamp($this->endPointMS - $prevCP->getTimestamp()); + $diff = $this->endPointMS - $prevCP->getTimestamp(); + $this->checkPoints[$prevKey]->setTimestamp($diff); } @@ -187,21 +213,33 @@ private function calculateExecutionTime(): void * * @return bool */ - public function isMinimal(): bool + public function isMinimalOnly(): bool + { + return self::$minimalOnly ?? false; + } + + + /** + * Set Debench to only minimal mode + * + * @param bool $minimalModeOnly + * @return void + */ + public function setMinimalOnly(bool $minimalModeOnly): void { - return $this->minimal; + self::$minimalOnly = $minimalModeOnly; } /** * Set Debench to only minimal mode * - * @param bool $minimalMode + * @param bool $minimalModeOnly * @return void */ - public function setMinimal(bool $minimalMode): void + public static function minimalOnly(bool $minimalModeOnly): void { - $this->minimal = $minimalMode; + self::$minimalOnly = $minimalModeOnly; } @@ -212,7 +250,7 @@ public function setMinimal(bool $minimalMode): void */ public function isEnable(): bool { - return $this->enable; + return self::$enable; } @@ -224,7 +262,19 @@ public function isEnable(): bool */ public function setEnable(bool $enable): void { - $this->enable = $enable; + self::$enable = $enable; + } + + + /** + * Set Debench enable + * + * @param bool $enable + * @return void + */ + public static function enable(bool $enable): void + { + self::$enable = $enable; } @@ -235,7 +285,19 @@ public function setEnable(bool $enable): void */ private function getLastCheckPointInMS(): int { - return $this->lastCheckPointInMS; + return $this->lastCheckPointInMS ?? 0; + } + + + /** + * Set the last checkpoint in milliseconds + * + * @param int $timestamp + * @return void + */ + private function setLastCheckPointInMS(int $timestamp): void + { + $this->lastCheckPointInMS = $timestamp; } @@ -246,7 +308,38 @@ private function getLastCheckPointInMS(): int */ private function getLastCheckPointNumber(): int { - return $this->lastCheckPointNumber; + return $this->lastCheckPointNumber ?? 0; + } + + + /** + * Get the last checkpoint number, and increase it + * + * @param bool $postfix + * @return int + */ + private function incrementLastCheckPointNumber(bool $postfix = true): int + { + if (!isset($this->lastCheckPointNumber)) { + $this->lastCheckPointNumber = 0; + } + + if ($postfix) { + return $this->lastCheckPointNumber++; + } + + return ++$this->lastCheckPointNumber; + } + + + /** + * Get the $pathUI + * + * @return string + */ + public function getPathUI(): string + { + return self::$pathUI ?? ''; } @@ -257,10 +350,7 @@ private function getLastCheckPointNumber(): int */ public function getCheckPoints(): array { - if (!$this->checkPoints) { - return []; - } - return $this->checkPoints; + return $this->checkPoints ?? []; } @@ -268,15 +358,16 @@ public function getCheckPoints(): array * Get the ram usage * * @param bool $formatted + * @param bool $roundUnderMB * @return int|string */ - public function getRamUsage(bool $formatted = false): int|string + public function getRamUsage(bool $formatted = false, bool $roundUnderMB = false): int|string { // true => memory_real_usage $peak = memory_get_usage(); if ($formatted) - return Utils::toFormattedBytes($peak); + return Utils::toFormattedBytes($peak, $roundUnderMB); return $peak; } @@ -286,15 +377,16 @@ public function getRamUsage(bool $formatted = false): int|string * Get the real ram usage (peak) * * @param bool $formatted + * @param bool $roundUnderMB * @return int|string */ - public function getRamUsagePeak(bool $formatted = false): int|string + public function getRamUsagePeak(bool $formatted = false, bool $roundUnderMB = false): int|string { // true => memory_real_usage $peak = memory_get_peak_usage(true); if ($formatted) - return Utils::toFormattedBytes($peak); + return Utils::toFormattedBytes($peak, $roundUnderMB); return $peak; } @@ -307,9 +399,9 @@ public function getRamUsagePeak(bool $formatted = false): int|string */ public function getExecutionTime(): int { + return $this->getCurrentTime() - $this->getRequestTime(); // what about loads before Debench such as composer !? - // return $this->getCurrentTime() - $this->getRequestTime(); - return $this->endPointMS - $this->initPointMS; + // return $this->endPointMS - $this->initPointMS; } @@ -324,6 +416,41 @@ public function getRequestTime(): int } + /** + * Get the script name + * + * @return string + */ + public function getScriptName(): string + { + return $_SERVER['SCRIPT_NAME'] ?? 'non'; + } + + + /** + * Get the $_SERVER['REQUEST_METHOD'] + * + * @return string + */ + public function getRequestMethod(): string + { + return $_SERVER['REQUEST_METHOD'] ?? 'non'; + } + + + /** + * Get the http_response_code() + * + * @return string + */ + public function getResponseCode(): int + { + $rCode = http_response_code(); + // 501: Not Implemented + return $rCode === false ? 501 : $rCode; + } + + /** * Get the current time in milliseconds * @@ -360,6 +487,122 @@ private function getTagName(string $tag = ''): string } + /** + * Make the tag + * + * @param string $tag + * @param int $id + * @return string + */ + private function makeTag(string $tag, int $id): string + { + if (empty($tag)) { + $tag = 'point ' . $id; + } + + // tags(keys) should to be unique + return $tag .= '#' . $id; + } + + + /** + * Validate the tag + * + * @param string $tag + * @return bool + */ + private function checkTag(string $tag): bool + { + $regex = "/^([a-zA-Z0-9_ -]+)#[0-9]+$/"; + + if (preg_match($regex, $tag)) { + return true; + } + + return false; + } + + + /** + * Add a message as an info + * + * @param string $message + * @return void + */ + public static function info(string $message = ''): void + { + if (!isset(self::$messages)) { + self::$messages = []; + } + + $lastBT = Utils::getBacktrace()[0]; + $path = $lastBT['file']; + $line = $lastBT['line']; + + $messageObject = new Message($message, $path, $line); + + self::$messages[] = $messageObject; + } + + + /** + * Get the exceptions array + * + * @return array + */ + public static function messages(): array + { + return self::$messages ?? []; + } + + + /** + * Add an exception to exceptions array + * + * @param Throwable $exception + * @return void + */ + public function addException(\Throwable $exception): void + { + if (!isset($this->exceptions)) { + $this->exceptions = []; + } + + $this->exceptions[] = $exception; + } + + + /** + * Get the exceptions array + * + * @return array + */ + private function getExceptions(): array + { + return $this->exceptions ?? []; + } + + + /** + * Throw errors as exceptions + * + * @param int $level + * @param string $message + * @param string $file + * @param int $line + * @param array $context + * @return void + */ + public function addAsException(int $level, string $message, string $file = '', int $line = 0, array $context = []): void + { + if (!isset($this->exceptions)) { + $this->exceptions = []; + } + + $this->exceptions[] = new \ErrorException($message, 0, $level, $file, $line); + } + + /** * Make formatted output * @@ -370,26 +613,29 @@ private function makeOutput(): string $eTime = $this->getExecutionTime(); // ------- the minimal widget - if ($this->minimal) { - return Template::render($this->path . '/' . $this->ui . '/debench/widget.minimal.htm', [ - 'base' => $this->ui, - 'ramUsage' => $this->getRamUsage(true), - 'includedFilesCount' => $this->getLoadedFilesCount(), + if ($this->isMinimalOnly()) { + return Template::render(self::$pathUI . '/widget.minimal.htm', [ + 'base' => self::$ui, + 'ramUsage' => $this->getRamUsage(true, true), + 'requestInfo' => $this->getRequestMethod() . ' ' . $this->getResponseCode(), 'fullExecTime' => $eTime ]); } + // ------- infoLog - $infoLog = Template::render($this->path . '/' . $this->ui . '/debench/widget.log.info.htm', [ + $infoLog = Template::render(self::$pathUI . '/widget.log.info.htm', [ "phpVersion" => SystemInfo::getPHPVersion(), - "opcache" => SystemInfo::getOPCacheStatus() ? 'On' : 'Off', + "opcache" => SystemInfo::getOPCacheStatus(), "systemAPI" => SystemInfo::getSystemAPI(), ]); + // ------- timeLog $timeLog = ''; - foreach ($this->checkPoints as $key => $cp) { - $timeLog .= Template::render($this->path . '/' . $this->ui . '/debench/widget.log.checkpoint.htm', [ + + foreach ($this->getCheckPoints() as $key => $cp) { + $timeLog .= Template::render(self::$pathUI . '/widget.log.checkpoint.htm', [ "name" => $this->getTagName($key), "path" => $cp->getPath(), "fileName" => basename($cp->getPath()), @@ -400,54 +646,126 @@ private function makeOutput(): string ]); } - // ------- logRequest - $logRequest = ''; - foreach ($_REQUEST as $key => $value) { - $logRequest .= Template::render($this->path . '/' . $this->ui . '/debench/widget.log.request.htm', [ - "key" => $key, - "value" => $value - ]); - } - if (!$_REQUEST) { - $logRequest = 'No $_REQUEST Yet!'; + // ------- logPost + $logPost = $this->makeOutputLoop(self::$pathUI . '/widget.log.request.post.htm', $_POST, false); + + + // ------- logGet + $logGet = $this->makeOutputLoop(self::$pathUI . '/widget.log.request.get.htm', $_GET, false); + + + // ------- logCookie + $logCookie = $this->makeOutputLoop(self::$pathUI . '/widget.log.request.cookie.htm', $_COOKIE, false); + + + if (empty($logPost . $logGet . $logCookie)) { + $logPost = 'Nothing Yet!'; } + // ------- logSession - if (PHP_SAPI === 'cli') { - $_SESSION = array(); + $session = isset($_SESSION) ? $_SESSION : null; + + $logSession = $this->makeOutputLoop(self::$pathUI . '/widget.log.request.session.htm', $session); + + if (!isset($session)) { + $logSession = '_SESSION is not available!'; } - $logSession = ''; - foreach ($_SESSION as $key => $value) { - $logSession .= Template::render($this->path . '/' . $this->ui . '/debench/widget.log.request.htm', [ - "key" => $key, - "value" => $value + + // ------- logMessages + $logMessage = ''; + + foreach (self::messages() as $message) { + $file = basename($message->getPath()); + $path = str_replace($file, "$file", $message->getPath()); + + $logMessage .= Template::render(self::$pathUI . '/widget.log.message.htm', [ + // "code" => $exception->getCode(), + "message" => $message->getMessage(), + "path" => $path, + "line" => $message->getLineNumber(), ]); } - if (!$_SESSION) { - $logSession = 'No $_SESSION Yet!'; + if (empty($logMessage)) { + $logMessage = 'Nothing Yet!'; } + + // ------- logException + $logException = ''; + + foreach ($this->getExceptions() as $exception) { + $file = basename($exception->getFile()); + $path = str_replace($file, "$file", $exception->getFile()); + + $logException .= Template::render(self::$pathUI . '/widget.log.exception.htm', [ + // "code" => $exception->getCode(), + "message" => $exception->getMessage(), + "path" => $path, + "line" => $exception->getLine(), + ]); + } + + if (empty($logException)) { + $logException = 'Nothing Yet!'; + } + + // ------- the main widget - return Template::render($this->path . '/' . $this->ui . '/debench/widget.htm', [ - 'base' => $this->ui, - 'ramUsagePeak' => $this->getRamUsagePeak(true), - 'ramUsage' => $this->getRamUsage(true), - 'includedFilesCount' => $this->getLoadedFilesCount(), + return Template::render(self::$pathUI . '/widget.htm', [ + 'base' => self::$ui, + 'ramUsagePeak' => $this->getRamUsagePeak(true, true), + 'ramUsage' => $this->getRamUsage(true, true), + // 'includedFilesCount' => $this->getLoadedFilesCount(), 'preloadTime' => $this->initPointMS - $this->getRequestTime(), - 'request' => count($_REQUEST ?? []), - 'requestLog' => $logRequest, + 'request' => count($_POST) + count($_GET) + count($_COOKIE), + 'logPost' => $logPost, + 'logGet' => $logGet, + 'logCookie' => $logCookie, 'session' => count($_SESSION ?? []), 'sessionLog' => $logSession, 'infoLog' => $infoLog, 'timeLog' => $timeLog, + 'logMessage' => $logMessage, + 'message' => count(self::messages()), + 'logException' => $logException, + 'exception' => count($this->getExceptions()), + 'requestInfo' => $this->getRequestMethod() . ' ' . $this->getResponseCode(), 'fullExecTime' => $eTime ]); } + /** + * Make formatted output in loop, $key => $value + * + * @return string + */ + private function makeOutputLoop(string $theme, array|null $data, string|false $message = ''): string + { + if (empty($data)) { + if ($message === false) { + return ''; + } + return empty($message) || !is_string($message) ? 'Nothing Yet!' : $message; + } + + $output = ''; + + foreach ($data as $key => $value) { + $output .= Template::render($theme, [ + "key" => $key, + "value" => $value + ]); + } + + return $output; + } + + /** * Add a new checkpoint * @@ -467,13 +785,10 @@ public static function point(string $tag = ''): void * @param string $ui * @return Debench */ - public static function getInstance($enable = true, string $ui = 'theme'): Debench + public static function getInstance(bool $enable = null, string $ui = null): Debench { if (self::$instance === null) { - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); - $path = dirname(($backtrace[0])['file']); - - self::$instance = new self($enable, $ui, $path); + self::$instance = new self($enable, $ui); } return self::$instance; diff --git a/lib/Debench/Message.php b/lib/Debench/Message.php new file mode 100644 index 0000000..cf2fba9 --- /dev/null +++ b/lib/Debench/Message.php @@ -0,0 +1,97 @@ + + * @copyright Copyright (c) 2024, Mohammad Yaaghubi + * @license MIT License + */ + +namespace DEBENCH; + +class Message +{ + /** + * Message Constructor + * + * @return void + */ + public function __construct( + private string $message, + private string $path, + private int $lineNumber + ) { + } + + + /** + * Get the message + * + * @return string + */ + public function getMessage(): string + { + return $this->message; + } + + + /** + * Set the message + * + * @param string $message + * @return void + */ + public function setMessage(string $message): void + { + $this->message = $message; + } + + + /** + * Get the path + * + * @return string + */ + public function getPath(): string + { + return $this->path; + } + + + /** + * Set the path + * + * @param string $path + * @return void + */ + public function setPath(string $path): void + { + $this->path = $path; + } + + + /** + * Get the line number + * + * @return int + */ + public function getLineNumber(): int + { + return $this->lineNumber; + } + + + /** + * Set the line number + * + * @param int $lineNumber + * @return void + */ + public function setLineNumber(int $lineNumber): void + { + $this->lineNumber = $lineNumber; + } +} diff --git a/lib/Debench/SystemInfo.php b/lib/Debench/SystemInfo.php index 6f25010..65ddac6 100644 --- a/lib/Debench/SystemInfo.php +++ b/lib/Debench/SystemInfo.php @@ -32,7 +32,18 @@ public static function getPHPVersion(): string */ public static function getSystemAPI(): string { - return php_sapi_name(); + return PHP_SAPI; + } + + + /** + * Is in the cli mode + * + * @return bool + */ + public static function isCLI(): bool + { + return strtolower(self::getSystemAPI()) == 'cli'; } @@ -41,8 +52,23 @@ public static function getSystemAPI(): string * * @return string */ - public static function getOPCacheStatus(): bool + public static function getOPCacheStatus(): string { - return function_exists('opcache_get_status') && is_array(opcache_get_status()) && opcache_get_status()['opcache_enabled'] == true; + if ( + !function_exists('opcache_get_status') && + (!extension_loaded("opcache") && !extension_loaded("Zend OPcache")) + ) { + return 'Off (Not Loaded)'; + } + + if ( + is_array(opcache_get_status()) && + isset(opcache_get_status()['opcache_enabled']) && + opcache_get_status()['opcache_enabled'] == 1 + ) { + return 'On'; + } + + return 'Off'; } -} \ No newline at end of file +} diff --git a/lib/Debench/Template.php b/lib/Debench/Template.php index d8881ff..939abfc 100644 --- a/lib/Debench/Template.php +++ b/lib/Debench/Template.php @@ -14,18 +14,50 @@ class Template { + private static array $paths; + + + /** + * Make suer to have the UI dir + * + * @param string $targetPath + * @return void + */ + public static function makeUI(string $targetPath): void + { + // for ui/assets + @mkdir($targetPath, 0777, true); + + // for path + if (is_dir($targetPath)) { + // Copy the templates files from ui dir into your webroot dir if files don't match + Utils::copyDir(__DIR__ . '/ui', $targetPath); + } + } + + /** - * Render the theme by params + * Render .htm files by params * + * @param string $themePath + * @param array $params * @return string */ public static function render(string $themePath, array $params): string { - if (!file_exists($themePath)) { - throw new \Exception("File '$themePath` doesn't exists!"); + if (!isset(self::$paths)) { + self::$paths = []; + } + + if (empty(self::$paths[$themePath])) { + if (!file_exists($themePath)) { + throw new \Exception("File '$themePath` doesn't exists!"); + return ''; + } + self::$paths[$themePath] = file_get_contents($themePath); } - $theme = file_get_contents($themePath); + $theme = self::$paths[$themePath]; foreach ($params as $key => $value) { $theme = str_replace("{{@$key}}", "$value", $theme); @@ -33,4 +65,4 @@ public static function render(string $themePath, array $params): string return $theme; } -} \ No newline at end of file +} diff --git a/lib/Debench/Utils.php b/lib/Debench/Utils.php index 9437359..9bc45d4 100644 --- a/lib/Debench/Utils.php +++ b/lib/Debench/Utils.php @@ -14,11 +14,33 @@ class Utils { + /** + * Get the backtrace, make sure to call it directly + * + * @return array + */ + public static function getBacktrace(): array + { + $debugBTOut = []; + + $debugBT = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 0); + + foreach ($debugBT as $btItem) { + if (strrpos($btItem['file'], __DIR__) === false) { + $debugBTOut[] = $btItem; + } + } + + return $debugBTOut; + } + + /** * Copy folder * * @param string $from * @param string $to + * @param bool $checkForError * @return void */ public static function copyDir(string $from, string $to, bool $checkForError = true): void @@ -27,25 +49,25 @@ public static function copyDir(string $from, string $to, bool $checkForError = t if (!file_exists($from)) { throw new \Exception("File/Dir source '$from` doesn't exists!"); } - $toBack = dirname($to, 1); - if (!is_writable($toBack)) { - throw new \Exception("Dir dest '$toBack' is not writable!"); - } } // open the source directory $dir = opendir($from); - mkdir($to); + @mkdir($to); // Loop through the files in source directory while ($file = readdir($dir)) { if (($file != '.') && ($file != '..')) { - if (is_dir($from . DIRECTORY_SEPARATOR . $file)) { + $fileFrom = $from . DIRECTORY_SEPARATOR . $file; + $fileTo = $to . DIRECTORY_SEPARATOR . $file; + if (is_dir($fileFrom)) { // for sub directory - Utils::copyDir($from . DIRECTORY_SEPARATOR . $file, $to . DIRECTORY_SEPARATOR . $file, false); + Utils::copyDir($fileFrom, $fileTo, false); } else { - copy($from . DIRECTORY_SEPARATOR . $file, $to . DIRECTORY_SEPARATOR . $file); + if (!file_exists($fileTo) || filesize($fileFrom) != filesize($fileTo)) { + copy($fileFrom, $fileTo); + } } } } @@ -54,22 +76,49 @@ public static function copyDir(string $from, string $to, bool $checkForError = t } + + /** + * Delete a folder recursively + * + * @param string $dir + * @return void + */ + public static function deleteDir(string $dir): void + { + $glob = glob($dir); + foreach ($glob as $file) { + if (is_dir($file)) { + self::deleteDir("$file/*"); + rmdir($file); + } else { + unlink($file); + } + } + } + + /** * Format bytes with B, KB, MB, 'GB', 'TB' etc. * * @param int $size + * @param bool $roundUnderMB * @return string */ - public static function toFormattedBytes(int $size = 0): string + public static function toFormattedBytes(int $size = 0, bool $roundUnderMB = false): string { if ($size == 0) { return '0 B'; } + $suffixes = ['B', 'KB', 'MB', 'GB', 'TB']; $base = log($size, 1024); - $suffixes = array('B', 'KB', 'MB', 'GB', 'TB'); - return round(pow(1024, $base - floor($base))) . ' ' . $suffixes[floor($base)]; + $round = 1; + if ($roundUnderMB && floor($base) < array_search('MB', $suffixes)) { + $round = 0; + } + + return round(pow(1024, $base - floor($base)), $round) . ' ' . $suffixes[floor($base)]; } @@ -80,18 +129,14 @@ public static function toFormattedBytes(int $size = 0): string */ public static function isInTestMode(): bool { - if (PHP_SAPI != 'cli') { - return false; - } - - if (defined('PHPUNIT_COMPOSER_INSTALL') && defined('__PHPUNIT_PHAR__')) { + if (SystemInfo::isCLI() && defined('PHPUNIT_COMPOSER_INSTALL') && defined('__PHPUNIT_PHAR__')) { return true; } - if (strpos($_SERVER['argv'][0], 'phpunit') !== false) { + if (SystemInfo::isCLI() && strpos($_SERVER['argv'][0], 'phpunit') !== false) { return true; } return false; } -} \ No newline at end of file +} diff --git a/lib/Debench/ui/assets/css/debench.css b/lib/Debench/ui/assets/css/debench.css index c80df2c..d05f33d 100644 --- a/lib/Debench/ui/assets/css/debench.css +++ b/lib/Debench/ui/assets/css/debench.css @@ -1,54 +1,71 @@ debench { - margin:0 0 -1px 0; - position: fixed!important; - z-index: 1000000!important; - bottom: 0px!important; - left: 0px!important; + margin: 0 0 -1px 0; + position: fixed !important; + z-index: 1000000 !important; + bottom: 0px !important; + left: 0px !important; } debench * { - font-family: tahoma!important; - direction: ltr!important; - text-align: left!important; - font-size: 13px!important; + font-family: tahoma !important; + direction: ltr !important; + text-align: left !important; + font-size: 13px !important; } debench ul.debench-main-block { - margin: 0px!important; - padding: 0!important; - list-style-type: none!important; + margin: 0px !important; + padding: 0 !important; + list-style-type: none !important; } debench ul.debench-main-block > li:not(:last-child) { - padding:0px 0 0 0; - margin:0 0 -1px 0; - line-height: 0%!important; + padding: 0px 0 0 0; + margin: 0 0 -1px 0; + line-height: 0% !important; } debench ul.debench-main-block > li:not(:last-child) { display: none; } -debench .debench-panel { - border: 1px solid #dee2e6!important; - border-width: 1px 1px 0 0!important; - border-radius: 0 5px 0 0!important; - background: #f8f9fa!important; - color: #555!important; +debench .debench-panel { + border: 1px solid #d9e6eb !important; + border-width: 1px 1px 0 0 !important; + border-radius: 0 3px 0 0 !important; + background: #f8f9fa !important; + color: #555 !important; padding: 5px 7px; - line-height: 150%!important; + line-height: 150% !important; display: inline-block; - overflow: hidden; + max-height: 200px; + overflow: auto; +} +debench .debench-panel table { + border-collapse: separate!important; + border-spacing: 1px!important; +} +debench .debench-panel td { + padding: 0 1px; + color: #555 !important; + line-height: 150% !important; } debench .debench-panel td { - color: #555!important; - line-height: 150%!important; + padding:1px 10px 1px 3px !important; +} +debench .debench-panel tr:nth-child(odd) td { + background: #e2ecf0!important; +} +debench .debench-panel tr:nth-child(even) td { + /* background: #eeeff0!important; */ } debench .debench-panel span { - font-style:italic!important; - text-decoration:none!important; + font-style: italic !important; + text-decoration: none !important; } -debench .debench-panel span[title], debench .debench-panel td[title] { - text-decoration: underline!important; +debench .debench-panel span[title], +debench .debench-panel td[title] { + text-decoration: underline !important; } -debench .debench-panel-title span, debench .debench-panel-title td { - font-style:italic!important; - text-decoration: underline!important; +debench .debench-panel-title span, +debench .debench-panel-title td { + font-style: italic !important; + text-decoration: underline !important; } debench .debench-main-panel-minimal { display: block; @@ -56,26 +73,50 @@ debench .debench-main-panel-minimal { debench .debench-main-panel { display: none; } -debench .debench-main-panel, debench .debench-main-panel-minimal { +debench .debench-main-panel, +debench .debench-main-panel-minimal { padding: 0px; } -debench .debench-main-panel ul, debench .debench-main-panel-minimal ul { - float:left; +debench .debench-main-panel ul, +debench .debench-main-panel-minimal ul { + float: left; padding: 0; - margin: 0px!important; - list-style-type: none!important; - line-height: 0%!important; + margin: 0px !important; + list-style-type: none !important; + line-height: 0% !important; } -debench .debench-main-panel ul li, debench .debench-main-panel-minimal ul li { +debench .debench-main-panel ul li, +debench .debench-main-panel-minimal ul li { float: left; padding: 5px 7px; - line-height: 150%!important; + line-height: 150% !important; } -debench .debench-main-panel ul li[data-target]:hover, debench .debench-main-panel-minimal ul li[data-target]:hover { - background: #efefef; +debench .debench-main-panel ul li.active, +debench .debench-main-panel ul li[data-target]:hover, +debench .debench-main-panel-minimal ul li[data-target]:hover { + background: #e2ecf0; cursor: pointer; - user-select: none; + user-select: none; +} +debench .debench-main-panel ul li[data-target="debench-info-block"] { + background-color: #77aabb; + border-radius: 1px; +} +debench .debench-main-panel ul li[data-target="debench-info-block"]:hover { + background-color: #77aabb; } debench b { color: #333; +} +debench .debench-panel::-webkit-scrollbar { + width: 6px; +} +debench .debench-panel::-webkit-scrollbar-track { + background: #f1f1f1; +} +debench .debench-panel::-webkit-scrollbar-thumb { + background: #555; +} +debench .debench-panel::-webkit-scrollbar-thumb:hover { + background: #555; } \ No newline at end of file diff --git a/lib/Debench/ui/assets/img/info.png b/lib/Debench/ui/assets/img/info.png index 8bd2e3c..34a2298 100644 Binary files a/lib/Debench/ui/assets/img/info.png and b/lib/Debench/ui/assets/img/info.png differ diff --git a/lib/Debench/ui/assets/js/debench.js b/lib/Debench/ui/assets/js/debench.js index 95b308b..d9ee13b 100644 --- a/lib/Debench/ui/assets/js/debench.js +++ b/lib/Debench/ui/assets/js/debench.js @@ -1,28 +1,53 @@ -$(window).on("load", function () { - if (!debenchPanelMain) { - $("debench .debench-main-panel").toggle(); - $("debench .debench-main-panel-minimal").toggle(); - if (debenchPanelLast != "") - $("debench ul.debench-main-block > li." + debenchPanelLast).toggle(); +$(window).ready(function () { + debenchPanelLast = debenchGetCookie("debench_panel"); + + if (debenchPanelLast != "") { + debenchTogglePannels(); } $("debench ul li[data-target]").on("click", function () { - var panel = $(this).data("target"); - if (panel == "debench-panel-toggle") { - $("debench .debench-main-panel").toggle(); - $("debench .debench-main-panel-minimal").toggle(); + $("debench ul li[data-target]").removeClass("active"); + $(this).addClass("active"); - if (debenchPanelLast != "") - $("debench ul.debench-main-block > li." + debenchPanelLast).toggle(); + let panel = $(this).data("target"); + if (panel == "debench-panel-toggle") { + debenchTogglePannels(); } else { $("debench ul.debench-main-block > li:not(:last-child)").hide(); if (panel == debenchPanelLast) { $("." + panel).hide(); + $(this).removeClass("active"); panel = ""; } else { $("." + panel).show(); + $(this).addClass("active"); } debenchPanelLast = panel; } + + let isVisible = $("debench .debench-main-panel").is(":visible"); + let panelToKeep = debenchPanelLast + ? debenchPanelLast + : "debench-panel-toggle"; + debenchSetCookie("debench_panel", isVisible ? panelToKeep : ""); }); }); + +function debenchTogglePannels() { + $("debench .debench-main-panel").toggle(); + $("debench .debench-main-panel-minimal").toggle(); + if (debenchPanelLast != "") { + $("debench ul.debench-main-block > li." + debenchPanelLast).toggle(); + } +} + +function debenchGetCookie(key) { + return $.cookie(key); +} + +function debenchSetCookie(key, value) { + $.cookie(key, value, 1, { + expires: 10, + path: "/", + }); +} diff --git a/lib/Debench/ui/assets/js/jquery.cookie-1.4.1.min.js b/lib/Debench/ui/assets/js/jquery.cookie-1.4.1.min.js new file mode 100644 index 0000000..c0f19d8 --- /dev/null +++ b/lib/Debench/ui/assets/js/jquery.cookie-1.4.1.min.js @@ -0,0 +1,2 @@ +/*! jquery.cookie v1.4.1 | MIT */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?a(require("jquery")):a(jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0===a.cookie(b)?!1:(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}}); \ No newline at end of file diff --git a/lib/Debench/ui/widget.htm b/lib/Debench/ui/widget.htm index 7e17fb1..9b3ae50 100644 --- a/lib/Debench/ui/widget.htm +++ b/lib/Debench/ui/widget.htm @@ -5,20 +5,24 @@
  • - {{@infoLog}} + {{@infoLog}}
  • -
  • +
  • - {{@timeLog}} + {{@timeLog}}
  • - {{@requestLog}} + + {{@logPost}} + {{@logGet}} + {{@logCookie}} +
  • @@ -26,39 +30,54 @@ {{@sessionLog}}
  • +
  • +
    + + {{@logException}} +
    +
    +
  • +
  • +
    + + {{@logMessage}} +
    +
    +
  • - - -