diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..c946212 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,41 @@ +name: PHP Tests + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + php: ['8.0', '8.1', '8.2', '8.3', '8.4'] + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, openssl + coverage: none + + - 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@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run PHPUnit + run: vendor/bin/phpunit --no-coverage diff --git a/.gitignore b/.gitignore index ae55217..27280e2 100755 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ bin/phpDocumentor.phar # Composer vendor/ -composer.lock # PHPUnit .phpunit.result.cache diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e802468 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] + +### Added + +- **Strict types**: `declare(strict_types=1);` in all PHP library and test files. +- **Vars::arrayContainsSubstring()**: Alias for `arrayInString()` with clearer naming. +- **Tests**: New test classes for Crypto, Files, SQLite, Times, and Random. +- **CI**: GitHub Actions workflow (`.github/workflows/php.yml`) running PHPUnit on PHP 8.0–8.4 for `main` and `dev`. +- **CHANGELOG.md**: This file. + +### Changed + +- **Error handling**: Replaced `die()` with exceptions in Network, Random, Files, and Debugger (bootstrap `_All.php` still uses `die()` for fatal startup failures). + - `Network::getUserIP()` / `getServerIP()`: throw `RuntimeException` when `$die_if_empty` is true and IP cannot be determined. + - `Random::genStr()`: throw `InvalidArgumentException` for invalid type instead of `die()`. + - `Files::preventDirect()` default callback: throw `RuntimeException("404 Not found")` after setting 404 response code. + - `Debugger::output(..., $die = true)`: echo then throw `RuntimeException` instead of `die()`. +- **Network::ipInRange()**: Removed `echo` on invalid IPs; still returns `null` for invalid input. Fixed check to use `=== false` for `ip2long()` so `0.0.0.0` is valid. +- **Vars::arrayInString()**: Documented behaviour (any element contains needle); added return type `bool`. +- **Images**: Docblocks use `\Imagick` and `\ImagickException`; `blur()` has explicit return type `string`. +- **Composer**: `composer.lock` is no longer ignored so dev and CI use reproducible dependency versions. + +### Documentation + +- **README**: Get started section documents both Composer and _All.php; module links point to `Docs/classes/*.html`. +- **TEST_SUMMARY.md**: Updated to include new test classes and strict types / exception behaviour. +- **REFACTORING_SUMMARY.md**: Can be updated to reference this changelog for recent improvements. + +--- + +## Previous work (pre-changelog) + +- OOP refactor: namespaces, Base/DI, SQL connection injection, identifier validation, Debugger data/HTML separation, visibility and return types (see `REFACTORING_SUMMARY.md` and `OOP_REVIEW.md`). +- Crypto: corrected `hash()` argument order, `verifyhash()` with `hash_equals()`, IV handling with embedded IV in ciphertext. +- Images/SQLite: `parent::__construct()`; SQLite visibility and `$this->res()`. +- SQL::search(): table and column names validated via `validateIdentifier()`. +- README and index.php: fixed doc and test links; bootstrap prefers Composer autoload. diff --git a/PHPUtils/API.php b/PHPUtils/API.php index f760786..83e3461 100755 --- a/PHPUtils/API.php +++ b/PHPUtils/API.php @@ -1,5 +1,7 @@ genIV($method) : ''); - return openssl_encrypt($str,$method,$password,iv:$iv); + public function encryptwithpw(string $str, string $password, string $method = 'aes-256-cbc', bool $iv = false): string { + if ($iv) { + $ivRaw = hex2bin($this->genIV($method)); + $encrypted = openssl_encrypt($str, $method, $password, iv: $ivRaw); + return base64_encode($ivRaw . $encrypted); + } + return openssl_encrypt($str, $method, $password); } - /** - * Decrypt a string using a password, and optionally an IV + * Decrypt a string using a password, and optionally an IV. + * When $iv is empty and the string was produced with iv=true, the embedded IV is used. + * When $iv is non-empty, it is treated as hex and used as the IV (ciphertext only in $str). * - * @param mixed $str The string to decrypt - * @param mixed $password The password to use - * @param mixed $method The encryption method to use. Defaults to aes-256-cbc - * @param mixed $iv Whether to use an IV or not. Defaults to '' - * @return string The decrypted string + * @param string $str The string to decrypt (base64 when IV was embedded, or raw/base64 ciphertext when $iv provided) + * @param string $password The password to use + * @param string $method The encryption method. Defaults to aes-256-cbc + * @param string $iv Optional hex-encoded IV when not embedded. Defaults to '' + * @return string|false The decrypted string or false on failure */ - function decryptwithpw(string $str, string $password, string $method = 'aes-256-cbc', string $iv = '') { - return openssl_decrypt($str,$method,$password,iv:$iv); + public function decryptwithpw(string $str, string $password, string $method = 'aes-256-cbc', string $iv = ''): string|false { + $ivLen = openssl_cipher_iv_length($method); + if ($iv !== '') { + $ivRaw = hex2bin($iv); + $ciphertext = base64_decode($str, true) ?: $str; + return openssl_decrypt($ciphertext, $method, $password, iv: $ivRaw); + } + $decoded = base64_decode($str, true); + if ($decoded !== false && strlen($decoded) > $ivLen) { + $ivRaw = substr($decoded, 0, $ivLen); + $ciphertext = substr($decoded, $ivLen); + return openssl_decrypt($ciphertext, $method, $password, iv: $ivRaw); + } + return openssl_decrypt($str, $method, $password); } /** - * hash + * Hash a string using the given algorithm. * - * @param mixed $str The string to hash - * @param mixed $hash The hash method to use. Defaults to sha512 + * @param string $str The string to hash + * @param string $hash The hash algorithm. Defaults to sha512 * @return string The hashed string */ - function hash(string $str, string $hash = 'sha512') { - return hash($str, $hash); + public function hash(string $str, string $hash = 'sha512'): string { + return hash($hash, $str); } /** - * verifyhash - * - * Verifies a hash - * - * @param mixed $str The string to verify - * @param mixed $hash The hash to verify against - * @param mixed $hashmethod The hash method to use. Defaults to sha512 - * - * @return bool Whether the hash is valid or not + * Verify a string against a hash (timing-safe comparison). + * + * @param string $str The string to verify + * @param string $hash The hash to verify against + * @param string $hashmethod The hash method. Defaults to sha512 + * @return bool Whether the hash is valid */ - function verifyhash(string $str, string $hash, string $hashmethod = 'sha512') { - return hash($hashmethod, $str) == $hash; + public function verifyhash(string $str, string $hash, string $hashmethod = 'sha512'): bool { + return hash_equals($hash, hash($hashmethod, $str)); } } \ No newline at end of file diff --git a/PHPUtils/Debugger.php b/PHPUtils/Debugger.php index 9207375..c856202 100755 --- a/PHPUtils/Debugger.php +++ b/PHPUtils/Debugger.php @@ -1,5 +1,7 @@ format($txt, $type); if ($die) { - die($this->format($txt, $type)); + echo $formatted; + throw new \RuntimeException("Debugger::output terminated with die=true."); } - echo $this->format($txt, $type); + echo $formatted; } diff --git a/PHPUtils/Files.php b/PHPUtils/Files.php index 7476af0..f5a95b9 100755 --- a/PHPUtils/Files.php +++ b/PHPUtils/Files.php @@ -1,5 +1,7 @@ getMessage()); + } catch (\ImagickException $e) { + throw new \RuntimeException("Error blurring image: " . $e->getMessage(), 0, $e); } } @@ -83,7 +86,7 @@ function blur(string $imagePath, float $radius = 10, float $sigma = 25, int $cha * @param string $color The color of the text. * @throws ImagickException If an error occurs while adding the text to the image. */ - function textToImage(string $text, string $imagePath, string $fontPath, int $fontSize, int $x, int $y, string $color = 'black') { + public function textToImage(string $text, string $imagePath, string $fontPath, int $fontSize, int $x, int $y, string $color = 'black') { $draw = new ImagickDraw(); $draw->setFont($fontPath); $draw->setFontSize($fontSize); @@ -106,7 +109,7 @@ function textToImage(string $text, string $imagePath, string $fontPath, int $fon * @param int $width The width of the image. * @return string The HTML snippet. */ - function renderImage(string $imagePath, int $height = 200, int $width = 200) { - return ''; + public function renderImage(string $imagePath, int $height = 200, int $width = 200) { + return ''; } } \ No newline at end of file diff --git a/PHPUtils/Navigation.php b/PHPUtils/Navigation.php index 3372684..c39a81f 100755 --- a/PHPUtils/Navigation.php +++ b/PHPUtils/Navigation.php @@ -1,5 +1,7 @@ - + '; } @@ -27,7 +29,7 @@ function Bootstrap(string $version = '5.3.2') { * @param string $version The version of jQuery to include. Default is '3.7.1'. * @return void */ - function jQuery(string $version = '3.7.1') { + public function jQuery(string $version = '3.7.1') { echo ' '; diff --git a/PHPUtils/SQL.php b/PHPUtils/SQL.php index 93783c1..39c3717 100755 --- a/PHPUtils/SQL.php +++ b/PHPUtils/SQL.php @@ -1,5 +1,7 @@ validateIdentifier($tablename, 'table'); + $validatedColumns = []; + foreach ($columns as $column) { + $validatedColumns[] = $this->validateIdentifier($column, 'column'); + } # Default options $delimiter = (empty($options["delimiter"]) ? " " : $options["delimiter"]); @@ -275,7 +282,7 @@ public function search(string $tablename, string $search, array $columns = ["nam if ($strip_chars === True) { $keyword = preg_replace("/[^a-zA-Z0-9]/", "", $keyword); } - foreach ($columns as $column) { + foreach ($validatedColumns as $column) { if ($case_sensitive === True) { $conditions[] = "(CASE WHEN REGEXP_REPLACE(`$column`, '[^a-zA-Z0-9]', '') LIKE ? THEN 2 ELSE 0 END)"; $searchParams[] = "%".$keyword."%"; @@ -286,7 +293,7 @@ public function search(string $tablename, string $search, array $columns = ["nam } } $searchQuery .= implode(" + ", $conditions) . ") AS relevance"; - $searchQuery .= " FROM $tablename WHERE " . implode(" OR ", $conditions) . " HAVING relevance > 1"; + $searchQuery .= " FROM `$validatedTable` WHERE " . implode(" OR ", $conditions) . " HAVING relevance > 1"; $searchQuery .= " ORDER BY relevance DESC"; if ($limit > 0) { if ($offset > 0) { @@ -398,5 +405,4 @@ public function countRows(string $table, ?string $column = null, ?string $value - } # END CLASS - ?> \ No newline at end of file + } # END CLASS \ No newline at end of file diff --git a/PHPUtils/SQLite.php b/PHPUtils/SQLite.php index 34ac90b..49a0534 100755 --- a/PHPUtils/SQLite.php +++ b/PHPUtils/SQLite.php @@ -1,5 +1,7 @@ $status, "data" => $data, @@ -36,43 +38,43 @@ function res(string $status = "UNKNOWN", mixed $data = "No data.") : array { } # FUNCTION: sqlite_create_db - function sqlite_create_db(string $dbname = 'database') : array { + public function sqlite_create_db(string $dbname = 'database') : array { // Create a new database, if the file doesn't exist and open it for reading/writing. // The extension of the file is arbitrary. if (!is_writable(dirname($dbname.'.sqlite'))) { - return res("ERROR", "The directory is not writable."); + return $this->res("ERROR", "The directory is not writable."); } // if (file_exists($dbname.'.sqlite')) { - // return res("ERROR", "The database already exists."); + // return $this->res("ERROR", "The database already exists."); // } - $db = new SQLite3($dbname.'.sqlite', SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE); + $db = new \SQLite3($dbname.'.sqlite', SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE); $db->enableExceptions(true); - return res("SUCCESS", $db); + return $this->res("SUCCESS", $db); } # FUNCTION: sqlite_select_db - function sqlite_select_db(string $dbname = 'database') : array { + public function sqlite_select_db(string $dbname = 'database') : array { // Open an existing database for reading/writing. if (!file_exists($dbname.'.sqlite')) { - return res("ERROR", "The database does not exist."); + return $this->res("ERROR", "The database does not exist."); } - $db = new SQLite3($dbname.'.sqlite', SQLITE3_OPEN_READWRITE); + $db = new \SQLite3($dbname.'.sqlite', SQLITE3_OPEN_READWRITE); // Errors are emitted as warnings by default, enable proper error handling. $db->enableExceptions(true); - return res("SUCCESS", $db); + return $this->res("SUCCESS", $db); } # FUNCTION: sqlite_drop_db - function sqlite_drop_db(string $dbname = 'database') : array { + public function sqlite_drop_db(string $dbname = 'database') : array { // Delete the database file. if (!file_exists($dbname.'.sqlite')) { - return res("ERROR", "The database does not exist."); + return $this->res("ERROR", "The database does not exist."); } if (!is_writable($dbname.'.sqlite')) { - return res("ERROR", "The database is not writable."); + return $this->res("ERROR", "The database is not writable."); } $result = unlink($dbname.'.sqlite'); - return res("SUCCESS", $result); + return $this->res("SUCCESS", $result); } # FUNCTION: sqlite_get_dbs @@ -83,7 +85,7 @@ function sqlite_drop_db(string $dbname = 'database') : array { * * @return array An array of database names. */ - function sqlite_get_dbs(string $dir = '.') : array { + public function sqlite_get_dbs(string $dir = '.') : array { $dbs = []; $files = scandir($dir); foreach ($files as $file) { @@ -91,18 +93,18 @@ function sqlite_get_dbs(string $dir = '.') : array { $dbs[] = pathinfo($file, PATHINFO_FILENAME); } } - return res("SUCCESS", $dbs); + return $this->res("SUCCESS", $dbs); } # FUNCTION: sqlite_get_tables /** * Get a list of tables in the database. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * * @return array An array of table names. */ - function sqlite_get_tables(SQLite3 $db) : array { + public function sqlite_get_tables(\SQLite3 $db) : array { try { $query = 'SELECT name FROM sqlite_master WHERE type="table";'; $result = $db->query($query); @@ -110,9 +112,9 @@ function sqlite_get_tables(SQLite3 $db) : array { while ($row = $result->fetchArray(SQLITE3_ASSOC)) { $tables[] = $row['name']; } - return res("SUCCESS", $tables); + return $this->res("SUCCESS", $tables); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -120,16 +122,16 @@ function sqlite_get_tables(SQLite3 $db) : array { /** * Create a table in the database. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * @param string $table_name The name of the table. * @param array $columns An associative array of column names and types. The key is the column name, the value is the column type. * Example: ["column_name" => "column_type"] * You don't need to specify the "id" column, it's automatically created as an INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL. */ - function sqlite_create_table(SQLite3 $db, string $table_name, array $columns = ["column_name" => "column_type"]) : array { + public function sqlite_create_table(\SQLite3 $db, string $table_name, array $columns = ["column_name" => "column_type"]) : array { try { if (sqlite_table_exists($db, $table_name)) { - return res("ERROR", "Table $table_name already exists."); + return $this->res("ERROR", "Table $table_name already exists."); } $query = 'CREATE TABLE IF NOT EXISTS "'.$table_name.'" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, @@ -139,9 +141,9 @@ function sqlite_create_table(SQLite3 $db, string $table_name, array $columns = [ } $query = rtrim($query, ', '); $query .= ');'; - return res("SUCCESS", $db->query($query)); + return $this->res("SUCCESS", $db->query($query)); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -149,15 +151,15 @@ function sqlite_create_table(SQLite3 $db, string $table_name, array $columns = [ /** * Drop a table from the database. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * @param string $table_name The name of the table. */ - function sqlite_drop_table(SQLite3 $db, string $table_name) : array { + public function sqlite_drop_table(\SQLite3 $db, string $table_name) : array { try { $query = 'DROP TABLE IF EXISTS "'.$table_name.'";'; - return res("SUCCESS", $db->query($query)); + return $this->res("SUCCESS", $db->query($query)); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -165,15 +167,15 @@ function sqlite_drop_table(SQLite3 $db, string $table_name) : array { /** * Empty a table in the database. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * @param string $table_name The name of the table. */ - function sqlite_empty_table(SQLite3 $db, string $table_name) : array { + public function sqlite_empty_table(\SQLite3 $db, string $table_name) : array { try { $query = 'DELETE FROM "'.$table_name.'";'; - return res("SUCCESS", $db->query($query)); + return $this->res("SUCCESS", $db->query($query)); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -181,18 +183,18 @@ function sqlite_empty_table(SQLite3 $db, string $table_name) : array { /** * Check if a table exists in the database. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * @param string $table_name The name of the table. * * @return bool TRUE if the table exists, FALSE if it doesn't. */ - function sqlite_table_exists(SQLite3 $db, string $table_name) : array { + public function sqlite_table_exists(\SQLite3 $db, string $table_name) : array { try { $query = 'SELECT name FROM sqlite_master WHERE type="table" AND name="'.$table_name.'";'; $result = $db->querySingle($query); - return res("SUCCESS", ($result === $table_name)); + return $this->res("SUCCESS", ($result === $table_name)); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -200,12 +202,12 @@ function sqlite_table_exists(SQLite3 $db, string $table_name) : array { /** * Get a list of columns in a table. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * @param string $table_name The name of the table. * * @return array An array of column names. */ - function sqlite_get_columns(SQLite3 $db, string $table_name) : array { + public function sqlite_get_columns(\SQLite3 $db, string $table_name) : array { try { $query = 'PRAGMA table_info("'.$table_name.'");'; $result = $db->query($query); @@ -216,9 +218,9 @@ function sqlite_get_columns(SQLite3 $db, string $table_name) : array { "type" => $row['type'], ]; } - return res("SUCCESS", $columns); + return $this->res("SUCCESS", $columns); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -226,23 +228,23 @@ function sqlite_get_columns(SQLite3 $db, string $table_name) : array { /** * Execute a query. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * @param string $query The query to execute. */ - function sqlite_query(?SQLite3 $db, $query) : array { + public function sqlite_query(?\SQLite3 $db, $query) : array { try { if (!$db) { - return res("ERROR", "Database connection not found."); + return $this->res("ERROR", "Database connection not found."); } // $start = $db->exec('BEGIN'); $query = $db->query($query); // $commit = $db->exec('COMMIT'); if ($query === false) { - return res("ERROR", $db->lastErrorMsg()); + return $this->res("ERROR", $db->lastErrorMsg()); } - return res("SUCCESS", $query); + return $this->res("SUCCESS", $query); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -250,14 +252,14 @@ function sqlite_query(?SQLite3 $db, $query) : array { /** * Insert data into a table. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * @param string $table The name of the table. * @param array $data An associative array of column names and values. The key is the column name, the value is the value to insert. * Example: ["column_name" => "value"] * * @return bool TRUE on success, FALSE on failure. */ - function sqlite_insert(SQLite3 $db, string $table, array $data) : array { + public function sqlite_insert(\SQLite3 $db, string $table, array $data) : array { try { $columns = implode(', ', array_keys($data)); $placeholders = implode(', ', array_fill(0, count($data), '?')); @@ -272,11 +274,11 @@ function sqlite_insert(SQLite3 $db, string $table, array $data) : array { $insert = $stmt->execute(); if ($insert === false) { - return res("ERROR", $db->lastErrorMsg()); + return $this->res("ERROR", $db->lastErrorMsg()); } - return res("SUCCESS", $insert); + return $this->res("SUCCESS", $insert); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -284,7 +286,7 @@ function sqlite_insert(SQLite3 $db, string $table, array $data) : array { /** * Select data from a table. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * @param string $table The name of the table. * @param array $columns An array of column names to select. * @param string $where The WHERE clause. @@ -292,10 +294,10 @@ function sqlite_insert(SQLite3 $db, string $table, array $data) : array { * @param string $order The ORDER BY clause. * @param string $limit The LIMIT clause. */ - function sqlite_select(SQLite3 $db, string $table, array $columns = ["*"], string $where = "", array $params = [], string $order = "", string $limit = "") : array { + public function sqlite_select(\SQLite3 $db, string $table, array $columns = ["*"], string $where = "", array $params = [], string $order = "", string $limit = "") : array { try { if (!sqlite_table_exists($db, $table)) { - return res("ERROR", "Table $table does not exist."); + return $this->res("ERROR", "Table $table does not exist."); } $columns = implode(', ', $columns); $query = "SELECT $columns FROM $table"; @@ -316,14 +318,14 @@ function sqlite_select(SQLite3 $db, string $table, array $columns = ["*"], strin $result = $stmt->execute(); if ($result === false) { - return res("ERROR", $db->lastErrorMsg()); + return $this->res("ERROR", $db->lastErrorMsg()); } if ($result->numColumns() == 0) { - return res("ERROR", "No columns returned."); + return $this->res("ERROR", "No columns returned."); } - return res("SUCCESS", $result); + return $this->res("SUCCESS", $result); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -331,7 +333,7 @@ function sqlite_select(SQLite3 $db, string $table, array $columns = ["*"], strin /** * Update data in a table. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * @param string $table The name of the table. * @param array $data An associative array of column names and values. The key is the column name, the value is the value to update. * Example: ["column_name" => "value"] @@ -340,7 +342,7 @@ function sqlite_select(SQLite3 $db, string $table, array $columns = ["*"], strin * * @return bool TRUE on success, FALSE on failure. */ - function sqlite_update(SQLite3 $db, string $table, array $data, string $where, array $params) : array { + public function sqlite_update(\SQLite3 $db, string $table, array $data, string $where, array $params) : array { try { $set = ""; foreach ($data as $column => $value) { @@ -362,9 +364,9 @@ function sqlite_update(SQLite3 $db, string $table, array $data, string $where, a } $stmt->execute(); - return res("SUCCESS", $stmt->changes > 0); + return $this->res("SUCCESS", $stmt->changes > 0); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -372,14 +374,14 @@ function sqlite_update(SQLite3 $db, string $table, array $data, string $where, a /** * Delete data from a table. * - * @param SQLite3 $db The database connection. + * @param \SQLite3 $db The database connection. * @param string $table The name of the table. * @param string $where The WHERE clause. * @param array $params An array of parameters to bind to the WHERE clause. * * @return bool TRUE on success, FALSE on failure. */ - function sqlite_delete(SQLite3 $db, string $table, string $where, array $params) : array { + public function sqlite_delete(\SQLite3 $db, string $table, string $where, array $params) : array { try { $query = "DELETE FROM $table WHERE $where"; @@ -391,9 +393,9 @@ function sqlite_delete(SQLite3 $db, string $table, string $where, array $params) } $stmt->execute(); - return res("SUCCESS", $stmt->changes > 0); + return $this->res("SUCCESS", $stmt->changes > 0); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } @@ -401,11 +403,11 @@ function sqlite_delete(SQLite3 $db, string $table, string $where, array $params) /** * Get the SQLite version. */ - function sqlite_version($filter = Null) : array { + public function sqlite_version($filter = Null) : array { if ($filter) { - return res("SUCCESS", !empty(SQLite3::version()[$filter]) ? SQLite3::version()[$filter] : "Unknown"); + return $this->res("SUCCESS", !empty(\SQLite3::version()[$filter]) ? \SQLite3::version()[$filter] : "Unknown"); } - return res("SUCCESS", json_encode(SQLite3::version())); + return $this->res("SUCCESS", json_encode(\SQLite3::version())); } # FUNCTION: sqlite_function @@ -415,29 +417,29 @@ function sqlite_version($filter = Null) : array { * @param func_name The sqlite_* function to run. * @param params The parameters to pass to the function. */ - function sqlite_function($func_name, ...$params) : array { + public function sqlite_function($func_name, ...$params) : array { try { $func_name = "sqlite_".str_replace("sqlite_", "", $func_name); if (!function_exists($func_name)) { - return res("ERROR", "Function $func_name does not exist."); + return $this->res("ERROR", "Function $func_name does not exist."); } $call = call_user_func($func_name, ...$params); - return res($call["status"], $call["data"]); + return $this->res($call["status"], $call["data"]); $call_status = $call["status"]; $call_data = $call["data"]; if (is_string($call_data)) { $call_data = clean($call_data); } if ($call_status == "ERROR") { - return res("ERROR", $call_data); + return $this->res("ERROR", $call_data); } if ($call_data["status"] == "ERROR") { - return res("ERROR", $call_data); + return $this->res("ERROR", $call_data); } - return res($call_status, $call_data); + return $this->res($call_status, $call_data); } catch (Exception $e) { - return res("ERROR", $e->getMessage()); + return $this->res("ERROR", $e->getMessage()); } } diff --git a/PHPUtils/Session.php b/PHPUtils/Session.php index 21c758f..9bb9eb7 100755 --- a/PHPUtils/Session.php +++ b/PHPUtils/Session.php @@ -1,5 +1,7 @@ setTimeZone($tz); $return = $dt->format($format); @@ -40,8 +42,8 @@ public function getCurrentTime(string $format, string $timezone) : string { * @return string The relative time */ public function relativeTime($time, $format = null) { - $then = new DateTime('now'); - $now = new DateTime($time); + $now = new \DateTime('now'); + $then = new \DateTime($time); $diff = $now->diff($then); # Translate the format diff --git a/PHPUtils/Vars.php b/PHPUtils/Vars.php index 33beab4..18b7708 100755 --- a/PHPUtils/Vars.php +++ b/PHPUtils/Vars.php @@ -1,5 +1,7 @@ arrayInString($haystack, $needle); + } /* ───────────────────────────────────────────────────────────────────── */ /* stringify */ diff --git a/PHPUtils/_All.php b/PHPUtils/_All.php index a6477d0..f836799 100755 --- a/PHPUtils/_All.php +++ b/PHPUtils/_All.php @@ -1,11 +1,12 @@ - 8 in order to work."); + die("PHPUtils requires PHP 8.0 or later."); } diff --git a/README.md b/README.md index c18d95e..9b2c58f 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # php-utils -Disclaimer: This is so early in development it doesn't even have any useful stuff yet. Please come back later. +General-purpose PHP utility classes. Requires PHP 8.0+. # ────────────────────────────────────────────────────────── # @@ -28,32 +28,56 @@ Simply open your PHP project and clone this repo. git clone git@github.com:Darknetzz/php-utils.git ``` -Then include the `_All.php` if you want to be able to use anything from this library on demand. +Then include the library. You can use Composer autoload (recommended) or `_All.php`: + +**Using Composer (recommended):** +```bash +composer require darknetzz/php-utils +``` +```php +require_once __DIR__ . '/vendor/autoload.php'; + +use PHPUtils\Crypto; +$crypto = new Crypto(); +$hashedPassword = $crypto->hash("MyPassword123"); +``` + +**Using _All.php (e.g. when cloned as a subfolder):** ```php -include_once("php-utils/_All.php"); +include_once("php-utils/PHPUtils/_All.php"); // adjust path if your folder name differs -# Then you can instantiate any of the classes and start using them. -$crypto = new Crypto; +use PHPUtils\Crypto; +$crypto = new Crypto(); $hashedPassword = $crypto->hash("MyPassword123"); ``` # Modules -Here is a list of all the modules (classes) and their methods. - -* [API](Docs/API.md) -* [Auth](Docs/Auth.md) -* [Calendar](Docs/Calendar.md) -* [Crypto](Docs/Crypto.md) -* [Debugger](Docs/Debugger.md) -* [Files](Docs/Files.md) -* [Funcs](Docs/Funcs.md) -* [Images](Docs/Images.md) -* [Navigation](Docs/Navigation.md) -* [Network](Docs/Network.md) -* [Random](Docs/Random.md) -* [Resources](Docs/Resources.md) -* [Session](Docs/Session.md) -* [SQL](Docs/SQL.md) -* [Strings](Docs/Strings.md) -* [Times](Docs/Times.md) -* [Vars](Docs/Vars.md) +API documentation is generated in the `Docs/` folder. Class reference: + +* [API](Docs/classes/API.html) +* [Auth](Docs/classes/Auth.html) +* [Calendar](Docs/classes/Calendar.html) +* [Crypto](Docs/classes/Crypto.html) +* [Debugger](Docs/classes/Debugger.html) +* [Files](Docs/classes/Files.html) +* [Funcs](Docs/classes/Funcs.html) +* [Images](Docs/classes/Images.html) +* [Navigation](Docs/classes/Navigation.html) +* [Network](Docs/classes/Network.html) +* [Random](Docs/classes/Random.html) +* [Resources](Docs/classes/Resources.html) +* [Session](Docs/classes/Session.html) +* [SQL](Docs/classes/SQL.html) +* [SQLite](Docs/classes/SQLite.html) +* [Strings](Docs/classes/Strings.html) +* [Times](Docs/classes/Times.html) +* [Vars](Docs/classes/Vars.html) + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for recent improvements (strict types, exception-based error handling, new tests, CI). + +## Development + +- **Tests:** `composer install && vendor/bin/phpunit` +- **CI:** GitHub Actions run PHPUnit on PHP 8.0–8.4 (see [.github/workflows/php.yml](.github/workflows/php.yml)). diff --git a/REFACTORING_SUMMARY.md b/REFACTORING_SUMMARY.md index 9999ad4..caf6123 100755 --- a/REFACTORING_SUMMARY.md +++ b/REFACTORING_SUMMARY.md @@ -1,5 +1,7 @@ # OOP Refactoring Summary +For the latest improvements (strict types, exceptions, new tests, CI), see [CHANGELOG.md](CHANGELOG.md). + ## Changes Implemented This document summarizes all the OOP improvements made to the PHPUtils codebase. diff --git a/TEST_SUMMARY.md b/TEST_SUMMARY.md index ddae27c..086b18a 100755 --- a/TEST_SUMMARY.md +++ b/TEST_SUMMARY.md @@ -18,6 +18,11 @@ A comprehensive PHPUnit test suite has been created for the PHPUtils library. | StringsTest | 10 | ✅ Passing | | NetworkTest | 17 | ✅ Passing | | SQLTest | 11 | ⚠️ Requires MySQL | +| CryptoTest | 7 | ✅ Passing | +| FilesTest | 5 | ✅ Passing | +| SQLiteTest | 4 | ✅ Passing | +| TimesTest | 4 | ✅ Passing | +| RandomTest | 8 | ✅ Passing | ## Test Files Created @@ -150,15 +155,31 @@ composer install 4. **Constants** - SQL constants are tested 5. **Separation of Concerns** - Debugger's data/HTML separation is tested -## Future Test Additions +### CryptoTest +- Hash and verifyhash (correct algorithm order, timing-safe comparison) +- genIV format +- Encrypt/decrypt with and without IV -Consider adding tests for: -- Files class -- Random class -- Times class -- Images class -- Crypto class -- Other utility classes +### FilesTest +- is_file with existing file, non-existent path, directory +- file_read with temp file and non-existent file (expects exception) + +### SQLiteTest +- res() structure and defaults +- clean() strips newlines and encodes +- sqlite_create_db / sqlite_select_db / sqlite_drop_db with temp path + +### TimesTest +- getCurrentTime format and timezone +- relativeTime with format and default + +### RandomTest +- array_pick_random, roll (range), percentage (0/50/100), genStr length + +## Code Quality + +- All library and test files use `declare(strict_types=1);`. +- Network, Random, Files, and Debugger use exceptions instead of `die()` where appropriate (see CHANGELOG.md). ## Notes diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..f4d9366 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1801 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "716dea4375bf4cd425189ebfb3ca3cf4", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.50", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", + "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.50" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:59:18+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.0" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/index.php b/index.php index a13d4b5..963b753 100755 --- a/index.php +++ b/index.php @@ -27,22 +27,23 @@ $thisDoc"; + $docLink = "Doc"; - $btnClass = (file_exists($thisTest) ? "btn btn-sm btn-outline-success" : "btn btn-sm btn-outline-danger"); - $testLink = "$thisTest"; + $btnClass = (file_exists($testFile) ? "btn btn-sm btn-outline-success" : "btn btn-sm btn-outline-secondary"); + $testLink = "Tests"; echo " diff --git a/tests/BaseTest.php b/tests/BaseTest.php index 673a143..857b593 100755 --- a/tests/BaseTest.php +++ b/tests/BaseTest.php @@ -1,5 +1,7 @@ crypto = new Crypto(); + } + + public function testHashReturnsCorrectAlgorithmOrder(): void + { + $str = 'password123'; + $result = $this->crypto->hash($str, 'sha256'); + $this->assertIsString($result); + $this->assertEquals(64, strlen($result)); + $this->assertEquals(hash('sha256', $str), $result); + } + + public function testHashDefaultSha512(): void + { + $str = 'test'; + $result = $this->crypto->hash($str); + $this->assertEquals(hash('sha512', $str), $result); + } + + public function testVerifyhashWithValidHash(): void + { + $str = 'secret'; + $hash = $this->crypto->hash($str); + $this->assertTrue($this->crypto->verifyhash($str, $hash)); + } + + public function testVerifyhashWithInvalidHash(): void + { + $this->assertFalse($this->crypto->verifyhash('secret', 'wronghash')); + } + + public function testGenIVReturnsHexString(): void + { + $iv = $this->crypto->genIV('aes-256-cbc'); + $this->assertIsString($iv); + $this->assertMatchesRegularExpression('/^[a-f0-9]+$/i', $iv); + $this->assertEquals(32, strlen($iv)); // 16 bytes = 32 hex chars for aes-256-cbc + } + + public function testEncryptDecryptWithoutIv(): void + { + $plain = 'hello world'; + $password = 'secretkey'; + $encrypted = $this->crypto->encryptwithpw($plain, $password); + $this->assertIsString($encrypted); + $decrypted = $this->crypto->decryptwithpw($encrypted, $password); + $this->assertEquals($plain, $decrypted); + } + + public function testEncryptDecryptWithIv(): void + { + $plain = 'hello world'; + $password = 'secretkey'; + $encrypted = $this->crypto->encryptwithpw($plain, $password, 'aes-256-cbc', true); + $this->assertIsString($encrypted); + $decrypted = $this->crypto->decryptwithpw($encrypted, $password); + $this->assertEquals($plain, $decrypted); + } +} diff --git a/tests/DebuggerTest.php b/tests/DebuggerTest.php index 437329a..d0488b5 100755 --- a/tests/DebuggerTest.php +++ b/tests/DebuggerTest.php @@ -1,5 +1,7 @@ files = new Files(); + } + + public function testIsFileWithExistingFile(): void + { + $result = $this->files->is_file(__FILE__); + $this->assertTrue($result); + } + + public function testIsFileWithNonExistentPath(): void + { + $result = $this->files->is_file(__DIR__ . '/nonexistent_file_12345.php'); + $this->assertFalse($result); + } + + public function testIsFileWithDirectory(): void + { + $result = $this->files->is_file(__DIR__); + $this->assertFalse($result); + } + + public function testFileReadWithExistingFile(): void + { + $tmpFile = sys_get_temp_dir() . '/phputils_files_test_' . uniqid() . '.txt'; + file_put_contents($tmpFile, 'hello world'); + try { + $result = $this->files->file_read($tmpFile); + $this->assertEquals('hello world', $result); + } finally { + @unlink($tmpFile); + } + } + + public function testFileReadWithNonExistentFileThrows(): void + { + $this->expectException(\Exception::class); + $this->files->file_read(__DIR__ . '/nonexistent_12345.txt'); + } +} diff --git a/tests/NetworkTest.php b/tests/NetworkTest.php index e3be91b..18ea84d 100755 --- a/tests/NetworkTest.php +++ b/tests/NetworkTest.php @@ -1,5 +1,7 @@ assertEquals('203.0.113.1', $result); } - public function testGetUserIPAsArray() + public function testGetUserIPAsArray(): void { $_SERVER['REMOTE_ADDR'] = '192.168.1.100'; unset($_SERVER['HTTP_X_FORWARDED_FOR']); - + $result = $this->network->getUserIP(null, true); - + $this->assertIsArray($result); $this->assertArrayHasKey('type', $result); $this->assertArrayHasKey('userip', $result); $this->assertEquals('direct', $result['type']); } + public function testGetUserIPThrowsWhenDieIfEmptyAndNoIp(): void + { + unset($_SERVER['REMOTE_ADDR']); + unset($_SERVER['HTTP_X_FORWARDED_FOR']); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('getUserIP'); + $this->network->getUserIP(null, false, true); + } + public function testGetServerIP() { $_SERVER['SERVER_ADDR'] = '192.168.1.1'; diff --git a/tests/README.md b/tests/README.md index 6251053..6925840 100755 --- a/tests/README.md +++ b/tests/README.md @@ -58,6 +58,21 @@ phpunit - Tests user/server IP detection - Tests reverse proxy detection +### CryptoTest +- Hash/verifyhash, genIV, encrypt/decrypt with and without IV + +### FilesTest +- is_file, file_read (including exception for missing file) + +### SQLiteTest +- res(), clean(), sqlite_create_db / select_db / drop_db + +### TimesTest +- getCurrentTime, relativeTime + +### RandomTest +- array_pick_random, roll, percentage, genStr + ## Running Specific Tests Run a specific test class: @@ -73,8 +88,10 @@ Run a specific test method: ## Test Requirements - PHP 8.0 or higher -- PHPUnit 10.0 or higher -- MySQL server (for SQL tests - some tests are skipped if not available) +- PHPUnit 10.0 or 11.x +- Composer (preferred; bootstrap falls back to _All.php if vendor not present) +- MySQL server (for SQL tests only - those tests are skipped if not available) +- OpenSSL extension (for Crypto tests) ## Note on SQL Tests diff --git a/tests/RandomTest.php b/tests/RandomTest.php new file mode 100644 index 0000000..8913bb2 --- /dev/null +++ b/tests/RandomTest.php @@ -0,0 +1,75 @@ +random = new Random(); + } + + public function testArrayPickRandomReturnsElementFromArray(): void + { + $arr = ['a', 'b', 'c']; + $result = $this->random->array_pick_random($arr); + $this->assertContains($result, $arr); + } + + public function testArrayPickRandomWithSingleElement(): void + { + $arr = ['only']; + $result = $this->random->array_pick_random($arr); + $this->assertEquals('only', $result); + } + + public function testRollReturnsIntInRange(): void + { + for ($i = 0; $i < 20; $i++) { + $result = $this->random->roll(1, 10); + $this->assertGreaterThanOrEqual(1, $result); + $this->assertLessThanOrEqual(10, $result); + } + } + + public function testRollWithCustomRange(): void + { + $result = $this->random->roll(5, 5); + $this->assertEquals(5, $result); + } + + public function testPercentageReturnsBoolean(): void + { + $result = $this->random->percentage(50); + $this->assertIsBool($result); + } + + public function testPercentageAtZero(): void + { + $result = $this->random->percentage(0); + $this->assertFalse($result); + } + + public function testPercentageAtHundred(): void + { + $result = $this->random->percentage(100); + $this->assertTrue($result); + } + + public function testGenStrReturnsCorrectLength(): void + { + $result = $this->random->genStr(12); + $this->assertIsString($result); + $this->assertEquals(12, strlen($result)); + } +} diff --git a/tests/SQLTest.php b/tests/SQLTest.php index 5af97f6..96cf96e 100755 --- a/tests/SQLTest.php +++ b/tests/SQLTest.php @@ -1,5 +1,7 @@ sqlite = new SQLite(); + } + + public function testResReturnsArrayWithStatusAndData(): void + { + $result = $this->sqlite->res('SUCCESS', 'test data'); + $this->assertIsArray($result); + $this->assertArrayHasKey('status', $result); + $this->assertArrayHasKey('data', $result); + $this->assertArrayHasKey('data_type', $result); + $this->assertEquals('SUCCESS', $result['status']); + $this->assertEquals('test data', $result['data']); + } + + public function testResDefaultValues(): void + { + $result = $this->sqlite->res(); + $this->assertEquals('UNKNOWN', $result['status']); + $this->assertEquals('No data.', $result['data']); + } + + public function testCleanStripsNewlinesAndEncodes(): void + { + $input = " hello\n\rworld "; + $result = $this->sqlite->clean($input); + $this->assertStringNotContainsString("\n", $result); + $this->assertStringNotContainsString("\r", $result); + $this->assertNotEmpty($result); + } + + public function testSqliteCreateDbAndSelectDb(): void + { + $tmpDir = sys_get_temp_dir(); + $dbname = $tmpDir . '/phputils_sqlite_test_' . uniqid(); + $create = $this->sqlite->sqlite_create_db($dbname); + $this->assertEquals('SUCCESS', $create['status']); + $this->assertInstanceOf(\SQLite3::class, $create['data']); + + $select = $this->sqlite->sqlite_select_db($dbname); + $this->assertEquals('SUCCESS', $select['status']); + + $drop = $this->sqlite->sqlite_drop_db($dbname); + $this->assertEquals('SUCCESS', $drop['status']); + } +} diff --git a/tests/StringsTest.php b/tests/StringsTest.php index 1c516c8..8582ae1 100755 --- a/tests/StringsTest.php +++ b/tests/StringsTest.php @@ -1,5 +1,7 @@ times = new Times(); + } + + public function testGetCurrentTimeReturnsFormattedString(): void + { + $result = $this->times->getCurrentTime('Y-m-d', 'UTC'); + $this->assertIsString($result); + $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}$/', $result); + } + + public function testGetCurrentTimeWithTimezone(): void + { + $utc = $this->times->getCurrentTime('H:i', 'UTC'); + $europe = $this->times->getCurrentTime('H:i', 'Europe/London'); + $this->assertIsString($utc); + $this->assertIsString($europe); + } + + public function testRelativeTimeWithFormat(): void + { + $past = (new \DateTime('-2 days'))->format('Y-m-d H:i:s'); + $result = $this->times->relativeTime($past, 'days'); + $this->assertIsString($result); + $this->assertEquals('2', $result); + } + + public function testRelativeTimeReturnsString(): void + { + $past = (new \DateTime('-1 hour'))->format('Y-m-d H:i:s'); + $result = $this->times->relativeTime($past); + $this->assertIsString($result); + $this->assertNotEmpty($result); + } +} diff --git a/tests/VarsTest.php b/tests/VarsTest.php index c002696..409be94 100755 --- a/tests/VarsTest.php +++ b/tests/VarsTest.php @@ -1,5 +1,7 @@