diff --git a/UPGRADE_GUIDE.md b/UPGRADE_GUIDE.md
index 78277f13e..b5a704637 100644
--- a/UPGRADE_GUIDE.md
+++ b/UPGRADE_GUIDE.md
@@ -6,6 +6,7 @@ This guide helps you upgrade your TorrentPier installation to the latest version
- [Configuration System Migration](#configuration-system-migration)
- [Censor System Migration](#censor-system-migration)
+- [Development System Migration](#development-system-migration)
- [Breaking Changes](#breaking-changes)
- [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
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
### Deprecated Functions
@@ -170,6 +262,8 @@ When you update censored words in the admin panel, the system now automatically:
### Deprecated Patterns
- `new TorrentPier\Censor()` → Use `censor()` global function
- Direct `$wordCensor` access → Use `censor()` methods
+- `new TorrentPier\Dev()` → Use `dev()` global function
+- Static `Dev::` methods → Use `dev()` instance methods
### File Structure Changes
- New `/src/` directory for modern PHP classes
@@ -213,6 +307,25 @@ function processUserInput(string $text): string {
}
// ✅ 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 {
public function getDisplayText(): string {
return censor()->censorString($this->text);
diff --git a/common.php b/common.php
index 256dc8a3a..5da013a31 100644
--- a/common.php
+++ b/common.php
@@ -110,6 +110,16 @@ function censor(): \TorrentPier\Censor
return \TorrentPier\Censor::getInstance();
}
+/**
+ * Get the Dev instance
+ *
+ * @return \TorrentPier\Dev
+ */
+function dev(): \TorrentPier\Dev
+{
+ return \TorrentPier\Dev::getInstance();
+}
+
/**
* Initialize debug
*/
@@ -119,7 +129,7 @@ if (APP_ENV === 'local') {
} else {
define('DBG_USER', isset($_COOKIE[COOKIE_DBG]));
}
-(new \TorrentPier\Dev());
+(\TorrentPier\Dev::init());
/**
* Server variables initialize
diff --git a/library/includes/page_footer_dev.php b/library/includes/page_footer_dev.php
index d7ddd49eb..5f972d735 100644
--- a/library/includes/page_footer_dev.php
+++ b/library/includes/page_footer_dev.php
@@ -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) {
echo '
' . $sql_log . '
';
diff --git a/src/Ajax.php b/src/Ajax.php
index 2debc0a20..374ee6664 100644
--- a/src/Ajax.php
+++ b/src/Ajax.php
@@ -196,8 +196,8 @@ class Ajax
];
}
- if (Dev::sqlDebugAllowed()) {
- $this->response['sql_log'] = Dev::getSqlLog();
+ if (dev()->checkSqlDebugAllowed()) {
+ $this->response['sql_log'] = dev()->getSqlDebugLog();
}
// sending output will be handled by $this->ob_handler()
diff --git a/src/Dev.php b/src/Dev.php
index 0ad193880..e3661a11d 100644
--- a/src/Dev.php
+++ b/src/Dev.php
@@ -26,11 +26,15 @@ use jacklul\MonologTelegramHandler\TelegramFormatter;
use Exception;
/**
- * Class Dev
- * @package TorrentPier
+ * Development and Debugging System
+ *
+ * Singleton class that provides development and debugging functionality
+ * including error handling, SQL logging, and debugging utilities.
*/
class Dev
{
+ private static ?Dev $instance = null;
+
/**
* Whoops instance
*
@@ -39,9 +43,9 @@ class Dev
private Run $whoops;
/**
- * Constructor
+ * Initialize debugging system
*/
- public function __construct()
+ private function __construct()
{
$this->whoops = new Run;
@@ -60,6 +64,25 @@ class Dev
$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
*
@@ -164,44 +187,44 @@ class Dev
}
/**
- * Get SQL debug log
+ * Get SQL debug log (instance method)
*
* @return string
* @throws Exception
*/
- public static function getSqlLog(): string
+ public function getSqlLogInstance(): string
{
global $DBS, $CACHES, $datastore;
$log = '';
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) {
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)) {
- $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)) {
- $log .= self::getSqlLogHtml($datastore->db, "cache: datastore [{$datastore->db->engine}]");
+ $log .= $this->getSqlLogHtml($datastore->db, "cache: datastore [{$datastore->db->engine}]");
} elseif (!empty($datastore->dbg)) {
- $log .= self::getSqlLogHtml($datastore, "cache: datastore [{$datastore->engine}]");
+ $log .= $this->getSqlLogHtml($datastore, "cache: datastore [{$datastore->engine}]");
}
return $log;
}
/**
- * Sql debug status
+ * Sql debug status (instance method)
*
* @return bool
*/
- public static function sqlDebugAllowed(): bool
+ public function sqlDebugAllowedInstance(): bool
{
return (SQL_DEBUG && DBG_USER && !empty($_COOKIE['sql_log']));
}
@@ -215,13 +238,13 @@ class Dev
* @return string
* @throws Exception
*/
- private static function getSqlLogHtml(object $db_obj, string $log_name): string
+ private function getSqlLogHtml(object $db_obj, string $log_name): string
{
$log = '';
foreach ($db_obj->dbg as $i => $dbg) {
$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']);
$perc = '[' . round($dbg['time'] * 100 / $db_obj->sql_timetotal) . '%]';
$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 bool $esc_html
* @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;
$sql = str_compact($sql);
@@ -257,4 +280,120 @@ class Dev
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.");
+ }
}
diff --git a/src/Legacy/Cache/APCu.php b/src/Legacy/Cache/APCu.php
index d7e4eb8ed..732e6b5e7 100644
--- a/src/Legacy/Cache/APCu.php
+++ b/src/Legacy/Cache/APCu.php
@@ -61,7 +61,7 @@ class APCu extends Common
}
$this->apcu = new Apc();
$this->prefix = $prefix;
- $this->dbg_enabled = Dev::sqlDebugAllowed();
+ $this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
diff --git a/src/Legacy/Cache/Common.php b/src/Legacy/Cache/Common.php
index 4fb32eec9..b6fdbe85a 100644
--- a/src/Legacy/Cache/Common.php
+++ b/src/Legacy/Cache/Common.php
@@ -82,7 +82,7 @@ class Common
switch ($mode) {
case 'start':
$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['file'] = $this->debug_find_source('file');
$dbg['line'] = $this->debug_find_source('line');
diff --git a/src/Legacy/Cache/File.php b/src/Legacy/Cache/File.php
index fea4d0ea5..fc64fb068 100644
--- a/src/Legacy/Cache/File.php
+++ b/src/Legacy/Cache/File.php
@@ -61,7 +61,7 @@ class File extends Common
$filesystem = new Filesystem($adapter);
$this->file = new Flysystem($filesystem);
$this->prefix = $prefix;
- $this->dbg_enabled = Dev::sqlDebugAllowed();
+ $this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
diff --git a/src/Legacy/Cache/Memcached.php b/src/Legacy/Cache/Memcached.php
index 4c5c5225b..27aec1942 100644
--- a/src/Legacy/Cache/Memcached.php
+++ b/src/Legacy/Cache/Memcached.php
@@ -85,7 +85,7 @@ class Memcached extends Common
$this->client = new MemcachedClient();
$this->cfg = $cfg;
$this->prefix = $prefix;
- $this->dbg_enabled = Dev::sqlDebugAllowed();
+ $this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
diff --git a/src/Legacy/Cache/Redis.php b/src/Legacy/Cache/Redis.php
index 08a6a16ab..df7528420 100644
--- a/src/Legacy/Cache/Redis.php
+++ b/src/Legacy/Cache/Redis.php
@@ -85,7 +85,7 @@ class Redis extends Common
$this->client = new RedisClient();
$this->cfg = $cfg;
$this->prefix = $prefix;
- $this->dbg_enabled = Dev::sqlDebugAllowed();
+ $this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
diff --git a/src/Legacy/Cache/Sqlite.php b/src/Legacy/Cache/Sqlite.php
index ae38a7699..c8079b5e5 100644
--- a/src/Legacy/Cache/Sqlite.php
+++ b/src/Legacy/Cache/Sqlite.php
@@ -71,7 +71,7 @@ class Sqlite extends Common
$client = new PDO('sqlite:' . $dir . $this->dbExtension);
$this->sqlite = new SQLiteCache($client);
$this->prefix = $prefix;
- $this->dbg_enabled = Dev::sqlDebugAllowed();
+ $this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
diff --git a/src/Legacy/Datastore/APCu.php b/src/Legacy/Datastore/APCu.php
index c99e1c333..76ca886c7 100644
--- a/src/Legacy/Datastore/APCu.php
+++ b/src/Legacy/Datastore/APCu.php
@@ -54,7 +54,7 @@ class APCu extends Common
}
$this->apcu = new Apc();
$this->prefix = $prefix;
- $this->dbg_enabled = Dev::sqlDebugAllowed();
+ $this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
diff --git a/src/Legacy/Datastore/Common.php b/src/Legacy/Datastore/Common.php
index 53f148d6e..89490737a 100644
--- a/src/Legacy/Datastore/Common.php
+++ b/src/Legacy/Datastore/Common.php
@@ -157,7 +157,7 @@ class Common
switch ($mode) {
case 'start':
$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['file'] = $this->debug_find_source('file');
$dbg['line'] = $this->debug_find_source('line');
diff --git a/src/Legacy/Datastore/File.php b/src/Legacy/Datastore/File.php
index a802724ba..d6aef20f8 100644
--- a/src/Legacy/Datastore/File.php
+++ b/src/Legacy/Datastore/File.php
@@ -54,7 +54,7 @@ class File extends Common
$filesystem = new Filesystem($adapter);
$this->file = new Flysystem($filesystem);
$this->prefix = $prefix;
- $this->dbg_enabled = Dev::sqlDebugAllowed();
+ $this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
diff --git a/src/Legacy/Datastore/Memcached.php b/src/Legacy/Datastore/Memcached.php
index 65fb92c94..87c1c97f0 100644
--- a/src/Legacy/Datastore/Memcached.php
+++ b/src/Legacy/Datastore/Memcached.php
@@ -78,7 +78,7 @@ class Memcached extends Common
$this->client = new MemcachedClient();
$this->cfg = $cfg;
$this->prefix = $prefix;
- $this->dbg_enabled = Dev::sqlDebugAllowed();
+ $this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
diff --git a/src/Legacy/Datastore/Redis.php b/src/Legacy/Datastore/Redis.php
index 20a843589..71603b229 100644
--- a/src/Legacy/Datastore/Redis.php
+++ b/src/Legacy/Datastore/Redis.php
@@ -78,7 +78,7 @@ class Redis extends Common
$this->client = new RedisClient();
$this->cfg = $cfg;
$this->prefix = $prefix;
- $this->dbg_enabled = Dev::sqlDebugAllowed();
+ $this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
diff --git a/src/Legacy/Datastore/Sqlite.php b/src/Legacy/Datastore/Sqlite.php
index 9d032a4f5..b15ce1625 100644
--- a/src/Legacy/Datastore/Sqlite.php
+++ b/src/Legacy/Datastore/Sqlite.php
@@ -64,7 +64,7 @@ class Sqlite extends Common
$client = new PDO('sqlite:' . $dir . $this->dbExtension);
$this->sqlite = new SQLiteCache($client);
$this->prefix = $prefix;
- $this->dbg_enabled = Dev::sqlDebugAllowed();
+ $this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
diff --git a/src/Legacy/SqlDb.php b/src/Legacy/SqlDb.php
index 366108923..6394e8725 100644
--- a/src/Legacy/SqlDb.php
+++ b/src/Legacy/SqlDb.php
@@ -60,7 +60,7 @@ class SqlDb
global $DBS;
$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->slow_time = SQL_SLOW_QUERY_TIME;
@@ -839,7 +839,7 @@ class SqlDb
$msg[] = sprintf('%-6s', $q_time);
$msg[] = sprintf('%05d', getmypid());
$msg[] = $this->db_server;
- $msg[] = Dev::shortQuery($this->cur_query);
+ $msg[] = dev()->formatShortQuery($this->cur_query);
$msg = implode(LOG_SEPR, $msg);
$msg .= ($info = $this->query_info()) ? ' # ' . $info : '';
$msg .= ' # ' . $this->debug_find_source() . ' ';
@@ -948,7 +948,7 @@ class SqlDb
' . $this->explain_hold . ' |
- ' . Dev::shortQuery($dbg['sql'], true) . '
+ ' . dev()->formatShortQuery($dbg['sql'], true) . '
';
break;