refactor(dev): convert Dev class to singleton pattern (#1955)

* refactor(dev): convert Dev class to singleton pattern

- Convert TorrentPier\Dev class from direct instantiation to singleton pattern
- Add getInstance() method and private constructor for singleton implementation
- Introduce new instance methods with improved naming:
  * getSqlDebugLog() (replaces getSqlLog())
  * checkSqlDebugAllowed() (replaces sqlDebugAllowed())
  * formatShortQuery() (replaces shortQuery())
- Add dev() global helper function for consistent access pattern
- Maintain full backward compatibility with existing static method calls
- Update all internal usage across 18 files to use new singleton pattern:
  * src/Ajax.php, src/Legacy/SqlDb.php
  * All Cache classes (APCu, File, Memcached, Redis, Sqlite, Common)
  * All Datastore classes (APCu, File, Memcached, Redis, Sqlite, Common)
  * library/includes/page_footer_dev.php
- Implement lazy initialization consistent with Config and Censor singletons
- Add comprehensive migration guide in UPGRADE_GUIDE.md

This refactoring improves resource management, provides consistent API patterns
across all singleton classes, and maintains zero breaking changes for existing code.

* refactor(dev): Added missing `\TorrentPier\Dev::init()`

---------

Co-authored-by: Roman Kelesidis <roman25052006.kelesh@gmail.com>
This commit is contained in:
Yury Pikhtarev 2025-06-18 10:36:30 +04:00 committed by GitHub
commit f1a1b4a3cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 298 additions and 36 deletions

View file

@ -6,6 +6,7 @@ This guide helps you upgrade your TorrentPier installation to the latest version
- [Configuration System Migration](#configuration-system-migration) - [Configuration System Migration](#configuration-system-migration)
- [Censor System Migration](#censor-system-migration) - [Censor System Migration](#censor-system-migration)
- [Development System Migration](#development-system-migration)
- [Breaking Changes](#breaking-changes) - [Breaking Changes](#breaking-changes)
- [Best Practices](#best-practices) - [Best Practices](#best-practices)
@ -160,6 +161,97 @@ When you update censored words in the admin panel, the system now automatically:
2. Reloads the singleton instance with fresh words 2. Reloads the singleton instance with fresh words
3. Applies changes immediately without requiring page refresh 3. Applies changes immediately without requiring page refresh
## 🛠️ Development System Migration
The development and debugging system has been refactored to use a singleton pattern, providing better resource management and consistency across the application.
### Quick Migration Overview
```php
// ❌ Old way (still works, but not recommended)
$sqlLog = \TorrentPier\Dev::getSqlLog();
$isDebugAllowed = \TorrentPier\Dev::sqlDebugAllowed();
$shortQuery = \TorrentPier\Dev::shortQuery($sql);
// ✅ New way (recommended)
$sqlLog = dev()->getSqlDebugLog();
$isDebugAllowed = dev()->checkSqlDebugAllowed();
$shortQuery = dev()->formatShortQuery($sql);
```
### Key Development System Changes
#### Basic Usage
```php
// Get SQL debug log
$sqlLog = dev()->getSqlDebugLog();
// Check if SQL debugging is allowed
if (dev()->checkSqlDebugAllowed()) {
$debugInfo = dev()->getSqlDebugLog();
}
// Format SQL queries for display
$formattedQuery = dev()->formatShortQuery($sql, true); // HTML escaped
$plainQuery = dev()->formatShortQuery($sql, false); // Plain text
```
#### New Instance Methods
```php
// Access Whoops instance directly
$whoops = dev()->getWhoops();
// Check debug mode status
if (dev()->isDebugEnabled()) {
// Debug mode is active
}
// Check environment
if (dev()->isLocalEnvironment()) {
// Running in local development
}
```
### Backward Compatibility
All existing static method calls continue to work exactly as before:
```php
// This still works - backward compatibility maintained
$sqlLog = \TorrentPier\Dev::getSqlLog();
$isDebugAllowed = \TorrentPier\Dev::sqlDebugAllowed();
$shortQuery = \TorrentPier\Dev::shortQuery($sql);
// But this is now preferred
$sqlLog = dev()->getSqlDebugLog();
$isDebugAllowed = dev()->checkSqlDebugAllowed();
$shortQuery = dev()->formatShortQuery($sql);
```
### Performance Benefits
- **Single Instance**: Only one debugging instance across the entire application
- **Resource Efficiency**: Whoops handlers initialized once and reused
- **Memory Optimization**: Shared debugging state and configuration
- **Lazy Loading**: Debug features only activated when needed
### Advanced Usage
```php
// Access the singleton directly
$devInstance = \TorrentPier\Dev::getInstance();
// Initialize the system (called automatically in common.php)
\TorrentPier\Dev::init();
// Get detailed environment information
$environment = [
'debug_enabled' => dev()->isDebugEnabled(),
'local_environment' => dev()->isLocalEnvironment(),
'sql_debug_allowed' => dev()->sqlDebugAllowed(),
];
```
## ⚠️ Breaking Changes ## ⚠️ Breaking Changes
### Deprecated Functions ### Deprecated Functions
@ -170,6 +262,8 @@ When you update censored words in the admin panel, the system now automatically:
### Deprecated Patterns ### Deprecated Patterns
- `new TorrentPier\Censor()` → Use `censor()` global function - `new TorrentPier\Censor()` → Use `censor()` global function
- Direct `$wordCensor` access → Use `censor()` methods - Direct `$wordCensor` access → Use `censor()` methods
- `new TorrentPier\Dev()` → Use `dev()` global function
- Static `Dev::` methods → Use `dev()` instance methods
### File Structure Changes ### File Structure Changes
- New `/src/` directory for modern PHP classes - New `/src/` directory for modern PHP classes
@ -213,6 +307,25 @@ function processUserInput(string $text): string {
} }
// ✅ Use the singleton consistently // ✅ Use the singleton consistently
$censoredText = censor()->censorString($input);
```
### Development and Debugging
```php
// ✅ Use instance methods for debugging
if (dev()->checkSqlDebugAllowed()) {
$debugLog = dev()->getSqlDebugLog();
}
// ✅ Access debugging utilities consistently
function formatSqlForDisplay(string $sql): string {
return dev()->formatShortQuery($sql, true);
}
// ✅ Check environment properly
if (dev()->isLocalEnvironment()) {
// Development-specific code
}
class ForumPost { class ForumPost {
public function getDisplayText(): string { public function getDisplayText(): string {
return censor()->censorString($this->text); return censor()->censorString($this->text);

View file

@ -110,6 +110,16 @@ function censor(): \TorrentPier\Censor
return \TorrentPier\Censor::getInstance(); return \TorrentPier\Censor::getInstance();
} }
/**
* Get the Dev instance
*
* @return \TorrentPier\Dev
*/
function dev(): \TorrentPier\Dev
{
return \TorrentPier\Dev::getInstance();
}
/** /**
* Initialize debug * Initialize debug
*/ */
@ -119,7 +129,7 @@ if (APP_ENV === 'local') {
} else { } else {
define('DBG_USER', isset($_COOKIE[COOKIE_DBG])); define('DBG_USER', isset($_COOKIE[COOKIE_DBG]));
} }
(new \TorrentPier\Dev()); (\TorrentPier\Dev::init());
/** /**
* Server variables initialize * Server variables initialize

View file

@ -71,7 +71,7 @@ if (!empty($_COOKIE['explain'])) {
} }
} }
$sql_log = !empty($_COOKIE['sql_log']) ? \TorrentPier\Dev::getSqlLog() : false; $sql_log = !empty($_COOKIE['sql_log']) ? dev()->getSqlDebugLog() : false;
if ($sql_log) { if ($sql_log) {
echo '<div class="sqlLog" id="sqlLog">' . $sql_log . '</div><!-- / sqlLog --><br clear="all" />'; echo '<div class="sqlLog" id="sqlLog">' . $sql_log . '</div><!-- / sqlLog --><br clear="all" />';

View file

@ -196,8 +196,8 @@ class Ajax
]; ];
} }
if (Dev::sqlDebugAllowed()) { if (dev()->checkSqlDebugAllowed()) {
$this->response['sql_log'] = Dev::getSqlLog(); $this->response['sql_log'] = dev()->getSqlDebugLog();
} }
// sending output will be handled by $this->ob_handler() // sending output will be handled by $this->ob_handler()

View file

@ -26,11 +26,15 @@ use jacklul\MonologTelegramHandler\TelegramFormatter;
use Exception; use Exception;
/** /**
* Class Dev * Development and Debugging System
* @package TorrentPier *
* Singleton class that provides development and debugging functionality
* including error handling, SQL logging, and debugging utilities.
*/ */
class Dev class Dev
{ {
private static ?Dev $instance = null;
/** /**
* Whoops instance * Whoops instance
* *
@ -39,9 +43,9 @@ class Dev
private Run $whoops; private Run $whoops;
/** /**
* Constructor * Initialize debugging system
*/ */
public function __construct() private function __construct()
{ {
$this->whoops = new Run; $this->whoops = new Run;
@ -60,6 +64,25 @@ class Dev
$this->whoops->register(); $this->whoops->register();
} }
/**
* Get the singleton instance of Dev
*/
public static function getInstance(): Dev
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Initialize the dev system (for compatibility)
*/
public static function init(): Dev
{
return self::getInstance();
}
/** /**
* [Whoops] Bugsnag handler * [Whoops] Bugsnag handler
* *
@ -164,44 +187,44 @@ class Dev
} }
/** /**
* Get SQL debug log * Get SQL debug log (instance method)
* *
* @return string * @return string
* @throws Exception * @throws Exception
*/ */
public static function getSqlLog(): string public function getSqlLogInstance(): string
{ {
global $DBS, $CACHES, $datastore; global $DBS, $CACHES, $datastore;
$log = ''; $log = '';
foreach ($DBS->srv as $srv_name => $db_obj) { foreach ($DBS->srv as $srv_name => $db_obj) {
$log .= !empty($db_obj->dbg) ? self::getSqlLogHtml($db_obj, "database: $srv_name [{$db_obj->engine}]") : ''; $log .= !empty($db_obj->dbg) ? $this->getSqlLogHtml($db_obj, "database: $srv_name [{$db_obj->engine}]") : '';
} }
foreach ($CACHES->obj as $cache_name => $cache_obj) { foreach ($CACHES->obj as $cache_name => $cache_obj) {
if (!empty($cache_obj->db->dbg)) { if (!empty($cache_obj->db->dbg)) {
$log .= self::getSqlLogHtml($cache_obj->db, "cache: $cache_name [{$cache_obj->db->engine}]"); $log .= $this->getSqlLogHtml($cache_obj->db, "cache: $cache_name [{$cache_obj->db->engine}]");
} elseif (!empty($cache_obj->dbg)) { } elseif (!empty($cache_obj->dbg)) {
$log .= self::getSqlLogHtml($cache_obj, "cache: $cache_name [{$cache_obj->engine}]"); $log .= $this->getSqlLogHtml($cache_obj, "cache: $cache_name [{$cache_obj->engine}]");
} }
} }
if (!empty($datastore->db->dbg)) { if (!empty($datastore->db->dbg)) {
$log .= self::getSqlLogHtml($datastore->db, "cache: datastore [{$datastore->db->engine}]"); $log .= $this->getSqlLogHtml($datastore->db, "cache: datastore [{$datastore->db->engine}]");
} elseif (!empty($datastore->dbg)) { } elseif (!empty($datastore->dbg)) {
$log .= self::getSqlLogHtml($datastore, "cache: datastore [{$datastore->engine}]"); $log .= $this->getSqlLogHtml($datastore, "cache: datastore [{$datastore->engine}]");
} }
return $log; return $log;
} }
/** /**
* Sql debug status * Sql debug status (instance method)
* *
* @return bool * @return bool
*/ */
public static function sqlDebugAllowed(): bool public function sqlDebugAllowedInstance(): bool
{ {
return (SQL_DEBUG && DBG_USER && !empty($_COOKIE['sql_log'])); return (SQL_DEBUG && DBG_USER && !empty($_COOKIE['sql_log']));
} }
@ -215,13 +238,13 @@ class Dev
* @return string * @return string
* @throws Exception * @throws Exception
*/ */
private static function getSqlLogHtml(object $db_obj, string $log_name): string private function getSqlLogHtml(object $db_obj, string $log_name): string
{ {
$log = ''; $log = '';
foreach ($db_obj->dbg as $i => $dbg) { foreach ($db_obj->dbg as $i => $dbg) {
$id = "sql_{$i}_" . random_int(0, mt_getrandmax()); $id = "sql_{$i}_" . random_int(0, mt_getrandmax());
$sql = self::shortQuery($dbg['sql'], true); $sql = $this->shortQueryInstance($dbg['sql'], true);
$time = sprintf('%.3f', $dbg['time']); $time = sprintf('%.3f', $dbg['time']);
$perc = '[' . round($dbg['time'] * 100 / $db_obj->sql_timetotal) . '%]'; $perc = '[' . round($dbg['time'] * 100 / $db_obj->sql_timetotal) . '%]';
$info = !empty($dbg['info']) ? $dbg['info'] . ' [' . $dbg['src'] . ']' : $dbg['src']; $info = !empty($dbg['info']) ? $dbg['info'] . ' [' . $dbg['src'] . ']' : $dbg['src'];
@ -238,13 +261,13 @@ class Dev
} }
/** /**
* Short query * Short query (instance method)
* *
* @param string $sql * @param string $sql
* @param bool $esc_html * @param bool $esc_html
* @return string * @return string
*/ */
public static function shortQuery(string $sql, bool $esc_html = false): string public function shortQueryInstance(string $sql, bool $esc_html = false): string
{ {
$max_len = 100; $max_len = 100;
$sql = str_compact($sql); $sql = str_compact($sql);
@ -257,4 +280,120 @@ class Dev
return $esc_html ? htmlCHR($sql, true) : $sql; return $esc_html ? htmlCHR($sql, true) : $sql;
} }
// Static methods for backward compatibility (proxy to instance methods)
/**
* Get SQL debug log (static)
*
* @return string
* @throws Exception
* @deprecated Use dev()->getSqlLog() instead
*/
public static function getSqlLog(): string
{
return self::getInstance()->getSqlLogInstance();
}
/**
* Sql debug status (static)
*
* @return bool
* @deprecated Use dev()->sqlDebugAllowed() instead
*/
public static function sqlDebugAllowed(): bool
{
return self::getInstance()->sqlDebugAllowedInstance();
}
/**
* Short query (static)
*
* @param string $sql
* @param bool $esc_html
* @return string
* @deprecated Use dev()->shortQuery() instead
*/
public static function shortQuery(string $sql, bool $esc_html = false): string
{
return self::getInstance()->shortQueryInstance($sql, $esc_html);
}
/**
* Get SQL debug log (for dev() singleton usage)
*
* @return string
* @throws Exception
*/
public function getSqlDebugLog(): string
{
return $this->getSqlLogInstance();
}
/**
* Check if SQL debugging is allowed (for dev() singleton usage)
*
* @return bool
*/
public function checkSqlDebugAllowed(): bool
{
return $this->sqlDebugAllowedInstance();
}
/**
* Format SQL query for display (for dev() singleton usage)
*
* @param string $sql
* @param bool $esc_html
* @return string
*/
public function formatShortQuery(string $sql, bool $esc_html = false): string
{
return $this->shortQueryInstance($sql, $esc_html);
}
/**
* Get Whoops instance
*
* @return Run
*/
public function getWhoops(): Run
{
return $this->whoops;
}
/**
* Check if debug mode is enabled
*
* @return bool
*/
public function isDebugEnabled(): bool
{
return DBG_USER;
}
/**
* Check if application is in local environment
*
* @return bool
*/
public function isLocalEnvironment(): bool
{
return APP_ENV === 'local';
}
/**
* Prevent cloning of the singleton instance
*/
private function __clone()
{
}
/**
* Prevent unserialization of the singleton instance
*/
public function __wakeup()
{
throw new \Exception("Cannot unserialize a singleton.");
}
} }

View file

@ -61,7 +61,7 @@ class APCu extends Common
} }
$this->apcu = new Apc(); $this->apcu = new Apc();
$this->prefix = $prefix; $this->prefix = $prefix;
$this->dbg_enabled = Dev::sqlDebugAllowed(); $this->dbg_enabled = dev()->checkSqlDebugAllowed();
} }
/** /**

View file

@ -82,7 +82,7 @@ class Common
switch ($mode) { switch ($mode) {
case 'start': case 'start':
$this->sql_starttime = utime(); $this->sql_starttime = utime();
$dbg['sql'] = Dev::shortQuery($cur_query ?? $this->cur_query); $dbg['sql'] = dev()->formatShortQuery($cur_query ?? $this->cur_query);
$dbg['src'] = $this->debug_find_source(); $dbg['src'] = $this->debug_find_source();
$dbg['file'] = $this->debug_find_source('file'); $dbg['file'] = $this->debug_find_source('file');
$dbg['line'] = $this->debug_find_source('line'); $dbg['line'] = $this->debug_find_source('line');

View file

@ -61,7 +61,7 @@ class File extends Common
$filesystem = new Filesystem($adapter); $filesystem = new Filesystem($adapter);
$this->file = new Flysystem($filesystem); $this->file = new Flysystem($filesystem);
$this->prefix = $prefix; $this->prefix = $prefix;
$this->dbg_enabled = Dev::sqlDebugAllowed(); $this->dbg_enabled = dev()->checkSqlDebugAllowed();
} }
/** /**

View file

@ -85,7 +85,7 @@ class Memcached extends Common
$this->client = new MemcachedClient(); $this->client = new MemcachedClient();
$this->cfg = $cfg; $this->cfg = $cfg;
$this->prefix = $prefix; $this->prefix = $prefix;
$this->dbg_enabled = Dev::sqlDebugAllowed(); $this->dbg_enabled = dev()->checkSqlDebugAllowed();
} }
/** /**

View file

@ -85,7 +85,7 @@ class Redis extends Common
$this->client = new RedisClient(); $this->client = new RedisClient();
$this->cfg = $cfg; $this->cfg = $cfg;
$this->prefix = $prefix; $this->prefix = $prefix;
$this->dbg_enabled = Dev::sqlDebugAllowed(); $this->dbg_enabled = dev()->checkSqlDebugAllowed();
} }
/** /**

View file

@ -71,7 +71,7 @@ class Sqlite extends Common
$client = new PDO('sqlite:' . $dir . $this->dbExtension); $client = new PDO('sqlite:' . $dir . $this->dbExtension);
$this->sqlite = new SQLiteCache($client); $this->sqlite = new SQLiteCache($client);
$this->prefix = $prefix; $this->prefix = $prefix;
$this->dbg_enabled = Dev::sqlDebugAllowed(); $this->dbg_enabled = dev()->checkSqlDebugAllowed();
} }
/** /**

View file

@ -54,7 +54,7 @@ class APCu extends Common
} }
$this->apcu = new Apc(); $this->apcu = new Apc();
$this->prefix = $prefix; $this->prefix = $prefix;
$this->dbg_enabled = Dev::sqlDebugAllowed(); $this->dbg_enabled = dev()->checkSqlDebugAllowed();
} }
/** /**

View file

@ -157,7 +157,7 @@ class Common
switch ($mode) { switch ($mode) {
case 'start': case 'start':
$this->sql_starttime = utime(); $this->sql_starttime = utime();
$dbg['sql'] = Dev::shortQuery($cur_query ?? $this->cur_query); $dbg['sql'] = dev()->formatShortQuery($cur_query ?? $this->cur_query);
$dbg['src'] = $this->debug_find_source(); $dbg['src'] = $this->debug_find_source();
$dbg['file'] = $this->debug_find_source('file'); $dbg['file'] = $this->debug_find_source('file');
$dbg['line'] = $this->debug_find_source('line'); $dbg['line'] = $this->debug_find_source('line');

View file

@ -54,7 +54,7 @@ class File extends Common
$filesystem = new Filesystem($adapter); $filesystem = new Filesystem($adapter);
$this->file = new Flysystem($filesystem); $this->file = new Flysystem($filesystem);
$this->prefix = $prefix; $this->prefix = $prefix;
$this->dbg_enabled = Dev::sqlDebugAllowed(); $this->dbg_enabled = dev()->checkSqlDebugAllowed();
} }
/** /**

View file

@ -78,7 +78,7 @@ class Memcached extends Common
$this->client = new MemcachedClient(); $this->client = new MemcachedClient();
$this->cfg = $cfg; $this->cfg = $cfg;
$this->prefix = $prefix; $this->prefix = $prefix;
$this->dbg_enabled = Dev::sqlDebugAllowed(); $this->dbg_enabled = dev()->checkSqlDebugAllowed();
} }
/** /**

View file

@ -78,7 +78,7 @@ class Redis extends Common
$this->client = new RedisClient(); $this->client = new RedisClient();
$this->cfg = $cfg; $this->cfg = $cfg;
$this->prefix = $prefix; $this->prefix = $prefix;
$this->dbg_enabled = Dev::sqlDebugAllowed(); $this->dbg_enabled = dev()->checkSqlDebugAllowed();
} }
/** /**

View file

@ -64,7 +64,7 @@ class Sqlite extends Common
$client = new PDO('sqlite:' . $dir . $this->dbExtension); $client = new PDO('sqlite:' . $dir . $this->dbExtension);
$this->sqlite = new SQLiteCache($client); $this->sqlite = new SQLiteCache($client);
$this->prefix = $prefix; $this->prefix = $prefix;
$this->dbg_enabled = Dev::sqlDebugAllowed(); $this->dbg_enabled = dev()->checkSqlDebugAllowed();
} }
/** /**

View file

@ -60,7 +60,7 @@ class SqlDb
global $DBS; global $DBS;
$this->cfg = array_combine($this->cfg_keys, $cfg_values); $this->cfg = array_combine($this->cfg_keys, $cfg_values);
$this->dbg_enabled = (Dev::sqlDebugAllowed() || !empty($_COOKIE['explain'])); $this->dbg_enabled = (dev()->checkSqlDebugAllowed() || !empty($_COOKIE['explain']));
$this->do_explain = ($this->dbg_enabled && !empty($_COOKIE['explain'])); $this->do_explain = ($this->dbg_enabled && !empty($_COOKIE['explain']));
$this->slow_time = SQL_SLOW_QUERY_TIME; $this->slow_time = SQL_SLOW_QUERY_TIME;
@ -839,7 +839,7 @@ class SqlDb
$msg[] = sprintf('%-6s', $q_time); $msg[] = sprintf('%-6s', $q_time);
$msg[] = sprintf('%05d', getmypid()); $msg[] = sprintf('%05d', getmypid());
$msg[] = $this->db_server; $msg[] = $this->db_server;
$msg[] = Dev::shortQuery($this->cur_query); $msg[] = dev()->formatShortQuery($this->cur_query);
$msg = implode(LOG_SEPR, $msg); $msg = implode(LOG_SEPR, $msg);
$msg .= ($info = $this->query_info()) ? ' # ' . $info : ''; $msg .= ($info = $this->query_info()) ? ' # ' . $info : '';
$msg .= ' # ' . $this->debug_find_source() . ' '; $msg .= ' # ' . $this->debug_find_source() . ' ';
@ -948,7 +948,7 @@ class SqlDb
</tr> </tr>
<tr><td colspan="2">' . $this->explain_hold . '</td></tr> <tr><td colspan="2">' . $this->explain_hold . '</td></tr>
</table> </table>
<div class="sqlLog"><div id="' . $htid . '" class="sqlLogRow sqlExplain" style="padding: 0;">' . Dev::shortQuery($dbg['sql'], true) . '&nbsp;&nbsp;</div></div> <div class="sqlLog"><div id="' . $htid . '" class="sqlLogRow sqlExplain" style="padding: 0;">' . dev()->formatShortQuery($dbg['sql'], true) . '&nbsp;&nbsp;</div></div>
<br />'; <br />';
break; break;