diff --git a/src/Framework/Common.php b/src/Framework/Common.php index b2f2aa6..833db00 100644 --- a/src/Framework/Common.php +++ b/src/Framework/Common.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace IOcornerstone\Framework; use IOcornerstone\Framework\{ + Configure, Console, Logger, String\StringFacade as F, @@ -203,6 +204,31 @@ final class Common } } + private static function allowDump(): bool + { + $hasShowDumps = Configure::has('security', 'show_dumps'); + if (! $hasShowDumps) { + return false; + } + $b_show_dumps = Configure::get('security', 'show_dumps'); + + if ($b_show_dumps === false || $b_show_dumps === null) { + return false; + } + + $hasShowDumps = Configure::has('debugger', 'allow_show_dumps'); + if (! $hasShowDumps) { + return false; + } + $b_allow_show_dumps = Configure::get('debugger', 'allow_show_dumps'); + + if ($b_allow_show_dumps === false || $b_allow_show_dumps === null) { + return false; + } + + return true; + } + /** * Variable Dump and exit * Configure of security for show_dumps must be true for debugging. @@ -214,7 +240,7 @@ final class Common endDump $end = endDump::EXIT_AND_STOP ): void { - if (\IOcornerstone\Framework\Configure::get('security', 'show_dumps') !== true) { + if (! self::allowDump()) { return; } $isConsole = Console::isConsole(); diff --git a/src/Framework/Database/BuildSQL.php b/src/Framework/Database/BuildSQL.php index 049fd79..f16c71c 100644 --- a/src/Framework/Database/BuildSQL.php +++ b/src/Framework/Database/BuildSQL.php @@ -85,4 +85,151 @@ class BuildSQL . "\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; } + public function generateSelectAll(\PDO $pdo, string $table): string + { + $sql = " + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + ORDER BY ORDINAL_POSITION + "; + + $stmt = $pdo->prepare($sql); + $stmt->execute([$table]); + + $columns = []; + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $line = '`' . $row['COLUMN_NAME'] . '`'; + $columns[] = $line; + } + + $out = "\$query = \"SELECT " . implode(", ", $columns) . " FROM `{$table}`\";" . PHP_EOL; + $out .= "\${$table}Stmt = \$this->pdo->query(\$query); + while (\${$table}Row = \${$table}Stmt->fetch(\\PDO::FETCH_ASSOC)) { + \$a[] = \"\";"; + foreach($columns as $col) { + $sCol = trim($col, "`"); + $out .= "\$a['{$sCol}'] = \${$table}Row['{$sCol}'] ?? \"\";" . PHP_EOL; + } + return $out; + } + + public function generateSelect(\PDO $pdo, string $table): string + { + $sql = " + SELECT COLUMN_NAME, COLUMN_KEY + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + ORDER BY ORDINAL_POSITION + "; + + $stmt = $pdo->prepare($sql); + $stmt->execute([$table]); + + $columns = []; + $primary = []; + $where = ""; + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $line = '`' . $row['COLUMN_NAME'] . '`'; + + $columns[] = $line; + + if ($row['COLUMN_KEY'] === 'PRI') { + $primary[] = '`' . $row['COLUMN_NAME'] . '`'; + } + } + + if (!empty($primary)) { + $where = " WHERE {$primary} = \$id LIMIT 1"; + } + + return "SELECT " . implode(", ", $columns) . " FROM `{$table}` $where"; + } + + public function generateInserts(\PDO $pdo, string $table): string + { + $sql = " + SELECT + COLUMN_NAME, + IS_NULLABLE, + COLUMN_DEFAULT, + COLUMN_KEY + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + ORDER BY ORDINAL_POSITION + "; + + $stmt = $pdo->prepare($sql); + $stmt->execute([$table]); + + $columns = []; + $cleanColumns = []; + $extras = []; + $primary = []; + $pKey = ""; + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $cleanColumns[] = $row['COLUMN_NAME']; + $line = '`' . $row['COLUMN_NAME'] . '`'; + + if ($row['IS_NULLABLE'] === 'NO') { + $extra[$row['COLUMN_NAME']] = ' NOT NULL'; + } + + if ($row['COLUMN_DEFAULT'] !== null) { + $default = $row['COLUMN_DEFAULT']; + + if (strtoupper($default) === 'CURRENT_TIMESTAMP') { + $extra[$row['COLUMN_NAME']] = ' DEFAULT CURRENT_TIMESTAMP'; + } else { + $extra[$row['COLUMN_NAME']] = ' DEFAULT ' . $pdo->quote($default); + } + } + + $columns[] = $line; + + if ($row['COLUMN_KEY'] === 'PRI') { + $primary[] = '`' . $row['COLUMN_NAME'] . '`'; + } + } + + if (!empty($primary)) { + $pKey = '// PRIMARY KEY (' . implode(', ', $primary) . ')'; + } + + $data = ""; + foreach($extra as $key => $value) { + $data .= '//' . $key . ' ' . $value . PHP_EOL; + } + + return $data . "INSERT INTO `{$table}` (\n " + . implode(",\n ", $columns) + . "\n) VALUES (\n" + . implode(",\n :", $cleanColumns) + . "\n)\n $pKey" . $this->bindInsert($table, $columns); + } + + private function bindInsert(string $table, array $columns): string + { + $out = "\n private function {$table}Inserts(\$p, array \$a): bool\n\t{\t\t try {\n"; + + foreach($columns as $col) { + $sCol = trim($col, "`"); + $out .= "\$p->bindParam(':{$sCol}', \$a['{$sCol}'])". PHP_EOL; + } + + $out .= 'return $p->execute(); + } catch (\PDOException $e) { + echo $e->getMessage(); + return false; + } + }'; + + return $out; + } } diff --git a/src/Framework/Database/Grammar/Grammar.php b/src/Framework/Database/Grammar/Grammar.php new file mode 100644 index 0000000..7113908 --- /dev/null +++ b/src/Framework/Database/Grammar/Grammar.php @@ -0,0 +1,85 @@ +select; + + $sql .= ' FROM '; + $sql .= $this->wrap($query->table); + + $sql .= $this->compileJoins($query); + + $sql .= $this->compileWhere($query); + + $sql .= $this->compileGroupBy($query); + + $sql .= $this->compileHaving($query); + + $sql .= $this->compileOrderBy($query); + + $sql .= $this->compileLimit($query); + + $sql .= $this->compileOffset($query); + + return $sql; + } + + + public function compileLimit(?int $limit): string + { + return $limit !== null ? " LIMIT {$limit}" : ''; + } + + public function compileOffset(?int $offset): string + { + return $offset !== null ? " OFFSET {$offset}" : ''; + } + + protected function compileOrderBy($query): string + { + if (!$query->orders) + return ''; + + $parts = []; + + foreach ($query->orders as $order) { + + $parts[] = + $this->wrap($order['column']) + .' ' + .$order['direction']; + + } + + return ' ORDER BY '.implode(',', $parts); + } + + abstract public function wrap(string $identifier): string; + abstract public function compileDate(string $column): string; + abstract public function compileTime(string $column): string; + abstract public function compileYear(string $column): string; + abstract public function compileMonth(string $column): string; + abstract public function compileDay(string $column): string; + abstract public function compileJsonContains(string $column): string; +} \ No newline at end of file diff --git a/src/Framework/Database/Grammar/MySQLGrammar.php b/src/Framework/Database/Grammar/MySQLGrammar.php new file mode 100644 index 0000000..63de94e --- /dev/null +++ b/src/Framework/Database/Grammar/MySQLGrammar.php @@ -0,0 +1,54 @@ +wrap($column)})"; + } + + public function compileMonth(string $column): string + { + return "MONTH({$this->wrap($column)})"; + } + + public function compileDay(string $column): string + { + return "DAY({$this->wrap($column)})"; + } + + public function compileDate(string $column): string + { + return "DATE({$this->wrap($column)})"; + } + + public function compileTime(string $column): string + { + return "TIME({$this->wrap($column)})"; + } + + public function compileJsonContains(string $column): string + { + return "JSON_CONTAINS({$this->wrap($column)}, ?)"; + } +} \ No newline at end of file diff --git a/src/Framework/Database/Grammar/SQLiteGrammar.php b/src/Framework/Database/Grammar/SQLiteGrammar.php new file mode 100644 index 0000000..bf50003 --- /dev/null +++ b/src/Framework/Database/Grammar/SQLiteGrammar.php @@ -0,0 +1,58 @@ +wrap($column).")"; + } + + public function compileMonth(string $column): string + { + return "strftime('%m',".$this->wrap($column).")"; + } + + public function compileDay(string $column): string + { + return "strftime('%d',".$this->wrap($column).")"; + } + + public function compileDate(string $column): string + { + return "date(".$this->wrap($column).")"; + } + + public function compileTime(string $column): string + { + return "time(".$this->wrap($column).")"; + } + + public function compileJsonContains(string $column): string + { + return "EXISTS ( + SELECT 1 + FROM json_each(".$this->wrap($column).") + WHERE value=? + )"; + } +} \ No newline at end of file diff --git a/src/Framework/Database/Model.php b/src/Framework/Database/Model.php index 36dcab9..980c2d1 100644 --- a/src/Framework/Database/Model.php +++ b/src/Framework/Database/Model.php @@ -22,11 +22,11 @@ class Model { use RunSql; use Validation; - const SUCCESSFUL_SAVE = 1; - const DUPLICATE_FOUND = 2; - const VALIDATION_ERROR = 3; - const PRE_SAVE_FAILED = 4; - const POST_SAVE_FAILED = 5; + public const int SUCCESSFUL_SAVE = 1; + public const int DUPLICATE_FOUND = 2; + public const int VALIDATION_ERROR = 3; + public const int PRE_SAVE_FAILED = 4; + public const int POST_SAVE_FAILED = 5; private $members = []; private $validationMembers = []; @@ -72,6 +72,23 @@ class Model { return false; } + public function beginTransaction() + { + $this->pdo->beginTransaction(); + } + + public function commit() + { + $this->pdo->commit(); + } + + public function rollBack() + { + if ($this->pdo->inTransaction()) { + $this->pdo->rollBack(); + } + } + private function doDump(array $data) { echo "
";
         print_r($data);
@@ -96,7 +113,8 @@ class Model {
                 continue;
             }
             if (in_array($key, $onlyThese) || !count($onlyThese)) {
-                $this->members[$key] = (! empty($data)) ? $data : "";
+                $saferKey = $this->sanitizeFieldName($key);
+                $this->members[$saferKey] = (! empty($data)) ? $data : "";
             }
         }
     }
@@ -114,7 +132,8 @@ class Model {
     }
     
     public function setMember(string $member, string $data): void {
-        $this->members[$member] = $data;
+        $saferMember = $this->sanitizeFieldName($member);
+        $this->members[$saferMember] = $data;
     }
 
     public function hasMember($member): bool {
@@ -150,6 +169,30 @@ class Model {
     public function clearValidationMember(string $key): void {
         unset($this->validationMembers[$key]);
     }
+    
+    public function createFromArray(array $data): object
+    {
+        $model = new \stdClass();
+        foreach($data as $key => $value) {
+            $model->$key = $value;
+        }
+        return $model;
+    }
+    
+    public function getColumns(array $data): string
+    {
+        return implode(', ', array_keys($data));
+    }
+    
+    public function getPlaceHolders(array $data): string
+    {
+        return implode(', ', array_fill(0, count($data), '?'));
+    }
+    
+    public function getValues(array $data): array
+    {
+        return array_values($data);
+    }
 
     private function cleanup($bind) {
         if (!is_array($bind)) {
@@ -184,12 +227,48 @@ class Model {
     }
 
     
-    public function load(int $id): bool{
+    public function sanitizeFieldName(string $field): string {
+        // Remove dangerous characters
+        $field = preg_replace('/[^a-zA-Z0-9_.]/', '', $field);
+    
+        // Remove SQL keywords
+        $field = preg_replace('/\b(SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE|UNION|WHERE|FROM)\b/i', '', $field);
+    
+        return $field;
+    }
+    
+    private function saferFields(string $fields): string
+    {
+        $safeFields = array_map([$this, 'sanitizeFieldName'], explode(',', $fields));
+        $safeFields = array_filter($safeFields); // Remove empty entries
+
+        if (empty($safeFields)) {
+            throw new Exception('Invalid field names');
+        }
+        return implode(', ', $safeFields);
+    }
+    
+    private function getSafeSelect(array|string $fields): string
+    {
+        if ($fields === "*") {
+            return "*";
+        }
+        if (is_array($fields) && Common::isArrayWithData($fields)) {
+            if (Common::hasKeys($fields)) {
+                $fields = implode(', ', $this->filter($this->table, $fields));
+            } else {
+                $fields = implode(', ', $fields);
+            }
+        }
+        return $this->saferFields($fields);
+    }
+    
+    public function load(int $id, array|string $fields = "*"): bool{
         if (method_exists($this, 'pre_load')) {
             $this->pre_load();
         }
-
-        $sql = "SELECT * FROM {$this->table} WHERE {$this->primaryKey} = ? LIMIT 1";
+                
+        $sql = "SELECT {$this->getSafeSelect($fields)} FROM {$this->table} WHERE {$this->primaryKey} = ? LIMIT 1";
         $query = $this->pdo->prepare($sql);
         $query->execute(array($id));
         if ($query === false) {
@@ -206,55 +285,6 @@ class Model {
         }
         return true;
     }
-    
-    public function insert($table, $info) {
-        $fields = $this->filter($table, $info);
-        
-        if (strrpos($table, "`") === false) {
-            $table = "`{$table}`";
-        }
-
-        $sql = "INSERT INTO {$table} (" . implode(", ", $fields) . ") VALUES (:" . implode(", :", $fields) . ");";
-        $bind = array();
-        foreach ($fields as $field) {
-            $bind[":$field"] = $info[$field];
-        }
-        return $this->run($sql, $bind);
-    }
-
-    public function update($table, $info, $where, $bind = "") {
-        $bind = $this->cleanup($bind);
-        $fields = $this->filter($table, $info);
-
-        if (strrpos($table, "`") === false) {
-            $table = "`{$table}`";
-        }
-
-        $sql = "UPDATE {$table} SET ";
-        $f = 0;
-
-        foreach ($fields as $key => $value) {
-            if ($f > 0) {
-                $sql .= ", ";
-            }
-            $f++;
-
-            $value = trim($value);
-
-            if (strrpos($value, "`") === false) {
-                $cf = '`' . $value . '`';
-            } else {
-                $cf = $value;
-            }
-            
-            $sql .= $cf . " = :update_" . $value;
-            $bind[":update_$value"] = $info[$value];
-        }
-
-        $sql .= " WHERE " . $where . ";";
-
-        return $this->run($sql, $bind);
-    }
 
     public static function makeDbTimeStamp(): string {
       return TimeZones::convertTimeZone(['format' => 'database', 'timezone' => 'UTC']);
@@ -281,8 +311,8 @@ class Model {
             }
         }
 
-        if ($this->hasMember('modified')) {
-            $this->setMember('modified', self::makeDbTimeStamp());
+        if ($this->hasMember('updated_at')) {
+            $this->setMember('updated_at', self::makeDbTimeStamp());
         }
 
         if (empty($this->members[$this->primaryKey])) {
@@ -293,8 +323,8 @@ class Model {
                 }
             }
 
-            if ($this->hasMember('created')) {
-                $this->setMember('created', self::makeDbTimeStamp());
+            if ($this->hasMember('created_at')) {
+                $this->setMember('created_at', self::makeDbTimeStamp());
             }
             
             $this->members[$this->primaryKey] = $this->insert($this->table, $this->members);
@@ -315,6 +345,56 @@ class Model {
         return self::SUCCESSFUL_SAVE;
     }
 
+    private function insert($table, $info) {
+        $fields = $this->filter($table, $info);
+        
+        if (strrpos($table, "`") === false) {
+            $table = "`{$table}`";
+        }
+
+        $sql = "INSERT INTO {$table} (" . implode(", ", $fields) . ") VALUES (:" . implode(", :", $fields) . ");";
+        $bind = array();
+        foreach ($fields as $field) {
+            $bind[":$field"] = $info[$field];
+        }
+        return $this->run($sql, $bind);
+    }
+
+    private function update($table, $info, $where, $bind = "") {
+        $bind = $this->cleanup($bind);
+        $fields = $this->filter($table, $info);
+
+        if (strrpos($table, "`") === false) {
+            $table = "`{$table}`";
+        }
+
+        $sql = "UPDATE {$table} SET ";
+        $f = 0;
+
+        foreach ($fields as $key => $value) {
+            if ($f > 0) {
+                $sql .= ", ";
+            }
+            $f++;
+
+            $value = trim($value);
+
+            if (strrpos($value, "`") === false) {
+                $cf = '`' . $value . '`';
+            } else {
+                $cf = $value;
+            }
+            
+            $sql .= $cf . " = :update_" . $value;
+            $bind[":update_$value"] = $info[$value];
+        }
+
+        $sql .= " WHERE " . $where . ";";
+
+        return $this->run($sql, $bind);
+    }
+    
+    
    /**
    * This FN requires SSL connection to the database.
    * So, the KEY does not get exposed!!!!
diff --git a/src/Framework/Database/Paginate.php b/src/Framework/Database/Paginate.php
index ccb5d2b..839b0b3 100644
--- a/src/Framework/Database/Paginate.php
+++ b/src/Framework/Database/Paginate.php
@@ -117,7 +117,13 @@ class Paginate
 
     private function do_href(int $limit, int $page): string
     {
-        return 'href="' . $this->_url . $this->_url_limit . $limit . $this->_url_page . $page . '"';
+        $queryParmas = $_GET;
+        unset($queryParmas['limit']);
+        unset($queryParmas['page']);
+        $q = http_build_query($queryParmas);
+        $querySeperator = (str_contains($this->_url_limit, "?")) ? "&" : "?";
+        $querySeperator = (empty($q)) ? "" : $querySeperator;
+        return 'href="' . $this->_url . $this->_url_limit . $limit . $this->_url_page . $page . $querySeperator . $q . '"';
     }
 
     public function createLinks(int $links = 7, string $list_class = "ui pagination menu", string $item = "item"): string
diff --git a/src/Framework/ErrorHandler.php b/src/Framework/ErrorHandler.php
index d6eebb0..f775cec 100644
--- a/src/Framework/ErrorHandler.php
+++ b/src/Framework/ErrorHandler.php
@@ -310,7 +310,7 @@ final class ErrorHandler
         $out .= "On Line # " . $e->getLine() . PHP_EOL;
         $out .= $e->getTraceAsString() . PHP_EOL;
         
-        echo $color . $out . PHP_EOL; 
+        fwrite(STDERR, $color . $out . PHP_EOL); 
     }
 
     private function getCleanHTML(string $in): string
diff --git a/src/Framework/SaferOutput.php b/src/Framework/SaferOutput.php
index ad039b4..40b24e8 100644
--- a/src/Framework/SaferOutput.php
+++ b/src/Framework/SaferOutput.php
@@ -118,13 +118,18 @@ class SaferOutput
         return ($object instanceof \JsonException);
     }
 
-    // Escape URL output
+    // Escape URL output by Encoding it
     public static function u(string $string): string
     {
         return urlencode($string);
     }
 
-    /*
+    public static function urlDecode(string $string): string
+    {
+        return urldecode($string);
+    }
+
+     /*
      * Encode HTML kindof... The problem with htmlentities() is that it is not 
      * very powerful, in fact, it does not escape single quotes, cannot detect 
      * the character set and does not validate HTML as well.
@@ -140,7 +145,7 @@ class SaferOutput
     {
         return html_entity_decode($data);
     }
-
+    
     /**
      * As PHP uses the underlying C functions for file system related operations, 
      * it may handle null bytes in a quite unexpected way. 
diff --git a/src/Framework/Services/Sessions/FileSessionHandler.php b/src/Framework/Services/Sessions/FileSessionHandler.php
index 9212254..cb8a4ba 100644
--- a/src/Framework/Services/Sessions/FileSessionHandler.php
+++ b/src/Framework/Services/Sessions/FileSessionHandler.php
@@ -3,7 +3,7 @@
 declare(strict_types=1);
 
 /** 
- * @author Robert Strutts 
+ * @author Robert Strutts
  * @copyright (c) 2025, Robert Strutts
  * @license MIT
  */
@@ -13,18 +13,18 @@ namespace IOcornerstone\Framework\Services\Sessions;
 use IOcornerstone\Framework\GzCompression;
 use IOcornerstone\Framework\Enum\CompressionMethod as Method;
 
-class FileSessionHandler implements SessionHandlerInterface
+class FileSessionHandler implements \SessionHandlerInterface
 {
     private $savePath;
     private $filePrefix;
     private $compression;
     private $enc;
 
-    public function __construct(false|callable $enc, array $config = [])
+    public function __construct($enc, array $config = [])
     {
         
-        $savePath = $config['save_path'] ?: null;
-        $filePrefix = $config['prefix'] ?: 'sess_';
+        $savePath = $config['save_path'] ?? null;
+        $filePrefix = $config['prefix'] ?? 'sess_';
         
         $this->filePrefix = $filePrefix;
         
@@ -36,9 +36,9 @@ class FileSessionHandler implements SessionHandlerInterface
             mkdir($this->savePath, 0700, true);
         }
         
-        $method = $config['method'] ?: Method::DEFLATE;
-        $level = $config['level'] ?: 4;
-        $enabled = $config['enabled'] ?: true;
+        $method = $config['method'] ?? Method::DEFLATE;
+        $level = $config['level'] ?? 4;
+        $enabled = $config['enabled'] ?? true;
         
         $this->compression = new GzCompression($method, $level, $enabled);
         
diff --git a/src/Framework/Services/Sessions/RedisSessionHandler.php b/src/Framework/Services/Sessions/RedisSessionHandler.php
index 014527b..24164cf 100644
--- a/src/Framework/Services/Sessions/RedisSessionHandler.php
+++ b/src/Framework/Services/Sessions/RedisSessionHandler.php
@@ -3,7 +3,7 @@
 declare(strict_types=1);
 
 /** 
- * @author Robert Strutts 
+ * @author Robert Strutts
  * @copyright (c) 2025, Robert Strutts
  * @license MIT
  */
@@ -13,7 +13,7 @@ namespace IOcornerstone\Framework\Services\Sessions;
 use IOcornerstone\Framework\GzCompression;
 use IOcornerstone\Framework\Enum\CompressionMethod as Method;
 
-class RedisSessionHandler implements SessionHandlerInterface
+class RedisSessionHandler implements \SessionHandlerInterface
 {
     private $redis;
     private $prefix;
@@ -23,7 +23,7 @@ class RedisSessionHandler implements SessionHandlerInterface
     private $lockWait = 100000; // microseconds
     private $enc;
 
-    public function __construct(callable $enc, array $config = [])
+    public function __construct($enc, array $config = [])
     {
         $defaults = [
             'host' => '127.0.0.1',
@@ -50,12 +50,26 @@ class RedisSessionHandler implements SessionHandlerInterface
             $this->ttl = 1440;
         }
 
-        $this->redis = new Redis();
+        $this->redis = new \Redis();
         
         if ($config['persistent']) {
-            $this->redis->pconnect($config['host'], $config['port'], $config['timeout'], $config['persistent']);
+            $this->redis->pconnect(
+                host: $config['host'],
+                port: $config['port'],
+                timeout: $config['timeout'],
+                persistent_id: null,
+                retry_interval: $config['retry_interval'],
+                read_timeout: $config['read_timeout']
+            );
         } else {
-            $this->redis->connect($config['host'], $config['port'], $config['timeout']);
+            $this->redis->connect(
+                host: $config['host'],
+                port: $config['port'],
+                timeout: $config['timeout'],
+                persistent_id: null,
+                retry_interval: $config['retry_interval'],
+                read_timeout: $config['read_timeout']
+            );
         }
 
         if ($config['auth']) {
@@ -67,16 +81,12 @@ class RedisSessionHandler implements SessionHandlerInterface
         }
 
         if ($config['read_timeout']) {
-            $this->redis->setOption(Redis::OPT_READ_TIMEOUT, $config['read_timeout']);
-        }
-
-        if ($config['retry_interval']) {
-            $this->redis->setOption(Redis::OPT_RETRY_INTERVAL, $config['retry_interval']);
+            $this->redis->setOption(\Redis::OPT_READ_TIMEOUT, $config['read_timeout']);
         }
         
-        $method = $config['method'] ?: Method::DEFLATE;
-        $level = $config['level'] ?: 4;
-        $enabled = $config['enabled'] ?: true;
+        $method = $config['method'] ?? Method::DEFLATE;
+        $level = $config['level'] ?? 4;
+        $enabled = $config['enabled'] ?? true;
         
         $this->compression = new GzCompression($method, $level, $enabled);
         
@@ -135,7 +145,7 @@ class RedisSessionHandler implements SessionHandlerInterface
                 
                 $data = $this->redis->get($key);
                 $this->redis->del($key . '.lock');
-                return $this->readHelper($data) ?: '';
+                return $this->readHelper($data) ?? '';
             }
             
             usleep($this->lockWait);
@@ -147,7 +157,7 @@ class RedisSessionHandler implements SessionHandlerInterface
 
     public function write($sessionId, $data): bool
     {
-        $data = write_helper($data);
+        $data = $this->writeHelper($data);
         $key = $this->prefix . $sessionId;
         
         // Only write if the session has changed
diff --git a/src/Framework/SessionManagement.php b/src/Framework/SessionManagement.php
index 3d499e7..16f41b0 100644
--- a/src/Framework/SessionManagement.php
+++ b/src/Framework/SessionManagement.php
@@ -15,7 +15,6 @@ use IOcornerstone\Framework\Services\Sessions\{
     CookieSessionHandler as CookieSes
 };
 use IOcornerstone\Framework\{
-    Common,
     Configure,
     Registry
 };
diff --git a/src/Framework/Trait/Database/QueryBuilder.php b/src/Framework/Trait/Database/QueryBuilder.php
new file mode 100644
index 0000000..fa172ac
--- /dev/null
+++ b/src/Framework/Trait/Database/QueryBuilder.php
@@ -0,0 +1,745 @@
+select = implode(',', $columns);
+
+        return $this;
+    }
+
+    public function where(
+            string $column,
+            string $operator,
+            mixed $value
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Basic',
+            'column' => $column,
+            'operator' => $operator,
+            'value' => $value
+        ];
+
+        $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function orWhere(
+            string $column,
+            string $operator,
+            mixed $value
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'OR',
+            'type' => 'Basic',
+            'column' => $column,
+            'operator' => $operator,
+            'value' => $value
+        ];
+
+        $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function whereNull(string $column): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Null',
+            'column' => $column
+        ];
+
+        return $this;
+    }
+
+    public function orWhereNull(string $column): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'OR',
+            'type' => 'Null',
+            'column' => $column
+        ];
+
+        return $this;
+    }
+
+    public function whereNotNull(string $column): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'NotNull',
+            'column' => $column
+        ];
+
+        return $this;
+    }
+
+    public function orWhereNotNull(string $column): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'OR',
+            'type' => 'NotNull',
+            'column' => $column
+        ];
+
+        return $this;
+    }
+
+    public function whereIn(
+            string $column,
+            array $values
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'In',
+            'column' => $column,
+            'count' => count($values)
+        ];
+
+        foreach ($values as $value)
+            $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function orWhereIn(
+            string $column,
+            array $values
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'OR',
+            'type' => 'In',
+            'column' => $column,
+            'count' => count($values)
+        ];
+
+        foreach ($values as $value)
+            $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function whereNotIn(
+            string $column,
+            array $values
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'NotIn',
+            'column' => $column,
+            'count' => count($values)
+        ];
+
+        foreach ($values as $value)
+            $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function whereBetween(
+            string $column,
+            mixed $from,
+            mixed $to
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Between',
+            'column' => $column
+        ];
+
+        $this->bindings[] = $from;
+        $this->bindings[] = $to;
+
+        return $this;
+    }
+
+    public function whereNotBetween(
+            string $column,
+            mixed $from,
+            mixed $to
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'NotBetween',
+            'column' => $column
+        ];
+
+        $this->bindings[] = $from;
+        $this->bindings[] = $to;
+
+        return $this;
+    }
+
+    public function whereLike(
+            string $column,
+            string $value
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Like',
+            'column' => $column
+        ];
+
+        $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function whereNotLike(
+            string $column,
+            string $value
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'NotLike',
+            'column' => $column
+        ];
+
+        $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function whereColumn(
+            string $left,
+            string $operator,
+            string $right
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Column',
+            'left' => $left,
+            'operator' => $operator,
+            'right' => $right
+        ];
+
+        return $this;
+    }
+
+    public function whereRaw(
+            string $sql,
+            array $bindings = []
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Raw',
+            'sql' => $sql
+        ];
+
+        foreach ($bindings as $binding) {
+            $this->bindings[] = $binding;
+        }
+        return $this;
+    }
+
+    public function whereDate(
+            string $column,
+            string $operator,
+            string $value
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Date',
+            'column' => $column,
+            'operator' => $operator,
+        ];
+
+        $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function whereTime(
+            string $column,
+            string $operator,
+            string $value
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Time',
+            'column' => $column,
+            'operator' => $operator
+        ];
+
+        $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function whereYear(
+            string $column,
+            string $operator,
+            int $year
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Year',
+            'column' => $column,
+            'operator' => $operator
+        ];
+
+        $this->bindings[] = $year;
+
+        return $this;
+    }
+
+    public function whereMonth(
+            string $column,
+            string $operator,
+            int $month
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Month',
+            'column' => $column,
+            'operator' => $operator
+        ];
+
+        $this->bindings[] = $month;
+
+        return $this;
+    }
+
+    public function whereDay(
+            string $column,
+            string $operator,
+            int $day
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'Day',
+            'column' => $column,
+            'operator' => $operator
+        ];
+
+        $this->bindings[] = $day;
+
+        return $this;
+    }
+
+    public function orWhereBetween(
+            string $column,
+            mixed $from,
+            mixed $to
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'OR',
+            'type' => 'Between',
+            'column' => $column
+        ];
+
+        $this->bindings[] = $from;
+        $this->bindings[] = $to;
+
+        return $this;
+    }
+
+    public function orWhereNotBetween(
+            string $column,
+            mixed $from,
+            mixed $to
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'OR',
+            'type' => 'NotBetween',
+            'column' => $column
+        ];
+
+        $this->bindings[] = $from;
+        $this->bindings[] = $to;
+
+        return $this;
+    }
+
+    public function orWhereLike(
+            string $column,
+            string $value
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'OR',
+            'type' => 'Like',
+            'column' => $column
+        ];
+
+        $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function orWhereNotLike(
+            string $column,
+            string $value
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'OR',
+            'type' => 'NotLike',
+            'column' => $column
+        ];
+
+        $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function whereJsonContains(
+            string $column,
+            mixed $value
+    ): static
+    {
+        $this->wheres[] = [
+            'boolean' => 'AND',
+            'type' => 'JsonContains',
+            'column' => $column
+        ];
+
+        $this->bindings[] = json_encode($value);
+
+        return $this;
+    }
+
+    public function orderBy(
+            string $column,
+            string $direction = 'ASC'
+    ): static
+    {
+        $direction = strtoupper($direction);
+
+        $this->orderBy = "{$column} {$direction}";
+
+        return $this;
+    }
+
+    public function limit(int $limit): static
+    {
+        $this->limit = $limit;
+
+        return $this;
+    }
+
+    public function leftJoin(
+            string $table,
+            string $left,
+            string $operator,
+            string $right
+    ): static
+    {
+        $this->joins[] = "LEFT JOIN {$table}
+             ON {$left} {$operator} {$right}";
+
+        return $this;
+    }
+
+    public function join(
+            string $table,
+            string $left,
+            string $operator,
+            string $right
+    ): static
+    {
+        $this->joins[] = "INNER JOIN {$table} ON {$left} {$operator} {$right}";
+
+        return $this;
+    }
+
+    private function buildWhere(string $sql): string
+    {
+        foreach ($this->wheres as $index => $where) {
+
+            if ($index > 0) {
+                $sql .= ' ' . $where['boolean'] . ' ';
+            }
+
+            switch ($where['type']) {
+                case 'Null':
+                    $sql .= "{$where['column']} IS NULL";
+                    break;
+
+                case 'NotNull':
+                    $sql .= "{$where['column']} IS NOT NULL";
+                    break;
+
+                case 'Like':
+                    $sql .= "{$where['column']} LIKE ?";
+                    break;
+
+                case 'NotLike':
+                    $sql .= "{$where['column']} NOT LIKE ?";
+                    break;
+
+                case 'Between':
+                    $sql .= "{$where['column']} BETWEEN ? AND ?";
+                    break;
+
+                case 'NotBetween':
+                    $sql .= "{$where['column']} NOT BETWEEN ? AND ?";
+                    break;
+
+                case 'In':
+                    $sql .= "{$where['column']} IN (" .
+                            implode(',', array_fill(0, $where['count'], '?')) . ")";
+                    break;
+
+                case 'NotIn':
+                    $sql .= "{$where['column']} NOT IN (" .
+                            implode(',', array_fill(0, $where['count'], '?')) . ")";
+                    break;
+
+                case 'Column':
+                    $sql .= "{$where['left']} {$where['operator']} {$where['right']}";
+                    break;
+
+                case 'Raw':
+                    $sql .= $where['sql'];
+                    break;
+                
+                case 'Basic':
+
+                    $sql .= $this->grammar->wrap($where['column']);
+
+                    $sql .= " {$where['operator']} ?";
+
+                    break;
+
+                case 'Year':
+
+                    $sql .= $this->grammar->compileYear(
+                            $where['column']
+                    );
+
+                    $sql .= " {$where['operator']} ?";
+
+                    break;
+
+                case 'Month':
+
+                    $sql .= $this->grammar->compileMonth(
+                            $where['column']
+                    );
+
+                    $sql .= " {$where['operator']} ?";
+
+                    break;
+
+                case 'Day':
+
+                    $sql .= $this->grammar->compileDay(
+                            $where['column']
+                    );
+
+                    $sql .= " {$where['operator']} ?";
+
+                    break;
+
+                case 'Date':
+
+                    $sql .= $this->grammar->compileDate(
+                            $where['column']
+                    );
+
+                    $sql .= " {$where['operator']} ?";
+
+                    break;
+
+                case 'Time':
+
+                    $sql .= $this->grammar->compileTime(
+                            $where['column']
+                    );
+
+                    $sql .= " {$where['operator']} ?";
+
+                    break;
+
+                case 'JsonContains':
+
+                    $sql .= $this->grammar->compileJsonContains(
+                            $where['column']
+                    );
+
+                    break;
+            }
+        }
+        return $sql;
+    }
+
+    protected function buildSql(): string
+    {
+        if ($this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'mysql') {
+            $this->grammar = new MySQLGrammar();
+        } else {
+            $this->grammar = new SQLiteGrammar();
+        }
+
+        $sql = "SELECT {$this->select} FROM {$this->table}";
+
+        if ($this->joins) {
+            $sql .= ' ' . implode(' ', $this->joins);
+        }
+
+        if ($this->wheres) {
+            $sql = $this->buildWhere($sql . " WHERE ");
+        }
+
+        if ($this->orderBy) {
+            $sql .= ' ORDER BY ' . $this->orderBy;
+        }
+
+        if ($this->limit) {
+            $sql .= ' LIMIT ' . $this->limit;
+        }
+
+        return $sql;
+    }
+
+    public function groupBy(string ...$columns): static
+    {
+        foreach ($columns as $column) {
+            $this->groups[] = $column;
+        }
+        return $this;
+    }
+
+    public function having(
+            string $column,
+            string $operator,
+            mixed $value
+    ): static
+    {
+        $this->havings[] = "{$column} {$operator} ?";
+
+        $this->bindings[] = $value;
+
+        return $this;
+    }
+
+    public function offset(int $offset): static
+    {
+        $this->offset = $offset;
+
+        return $this;
+    }
+
+    private function getRow(): array
+    {
+        $stmt = $this->pdo->prepare(
+                $this->buildSql()
+        );
+
+        $stmt->execute($this->bindings);
+
+        return $stmt->fetch(\PDO::FETCH_ASSOC);
+    }
+
+    public function get()
+    {
+        $stmt = $this->pdo->prepare(
+                $this->buildSql()
+        );
+
+        $stmt->execute($this->bindings);
+        return $stmt;
+    }
+
+    public function first(): ?array
+    {
+        $this->limit(1);
+
+        $rows = $this->getRow();
+
+        return $rows ?? null;
+    }
+
+    public function count(): int
+    {
+        $this->select = 'COUNT(' . $this->primaryKey . ')';
+
+        $stmt = $this->pdo->prepare(
+                $this->buildSql()
+        );
+
+        $stmt->execute($this->bindings);
+
+        return (int) $stmt->fetchColumn();
+    }
+
+    public function sum(string $column): int
+    {
+        $this->select = "SUM({$column})";
+
+        $stmt = $this->pdo->prepare(
+                $this->buildSql()
+        );
+
+        $stmt->execute($this->bindings);
+
+        return (int) $stmt->fetchColumn();
+    }
+
+    public function exists(): bool
+    {
+        $this->limit(1);
+
+        return $this->count() > 0;
+    }
+
+    public function toSql(): string
+    {
+        return $this->buildSql();
+    }
+}
diff --git a/src/Framework/Trait/Database/Validation.php b/src/Framework/Trait/Database/Validation.php
index b1986f0..8833ae3 100644
--- a/src/Framework/Trait/Database/Validation.php
+++ b/src/Framework/Trait/Database/Validation.php
@@ -12,7 +12,6 @@ namespace IOCornerstone\Framework\Trait\Database;
 use IOcornerstone\Framework\{
     Configure,
     Common,
-    Registry,
     Logger,
 };
 
diff --git a/src/Framework/Trait/Security/CsrfTokenFunctions.php b/src/Framework/Trait/Security/CsrfTokenFunctions.php
index 3025fbe..6fcab67 100644
--- a/src/Framework/Trait/Security/CsrfTokenFunctions.php
+++ b/src/Framework/Trait/Security/CsrfTokenFunctions.php
@@ -18,7 +18,8 @@ use IOcornerstone\Framework\{
 
 trait CsrfTokenFunctions
 {
-    const CSRF_TOKEN_POOL_SIZE_LIMIT = 15; // Total Sessions to Keep alive
+    private const int CSRF_TOKEN_POOL_SIZE_LIMIT = 15; // Total Sessions to Keep alive
+    public const string CSRF_TOKEN_FIELD_NAME = "csrf_token";
     /**
      * Set Session to use CSRF Token
      * Useful for JSON data...
@@ -41,13 +42,13 @@ trait CsrfTokenFunctions
     public static function csrfTokenTag(): string
     {
         $token = self::createCsrfToken();
-        return "";
+        return "";
     }
 
     public static function csrfTokenStillValid(string $csrfTokenKeyName = ""): bool
     {
         if (empty($csrfTokenKeyName)) {
-            $csrfTokenKeyName = $_POST['csrf_token'] ?? "";
+            $csrfTokenKeyName = $_POST[self::CSRF_TOKEN_FIELD_NAME] ?? "";
         }
         
         $validTimeStamp = self::csrfTokenIsValid($csrfTokenKeyName);
@@ -109,7 +110,7 @@ trait CsrfTokenFunctions
     private static function csrfTokenIsValid(string $csrfTokenKeyName = ""): false|int
     {
         if (empty($csrfTokenKeyName)) {
-            $csrfTokenKeyName = $_POST['csrf_token'] ?? "";
+            $csrfTokenKeyName = $_POST[self::CSRF_TOKEN_FIELD_NAME] ?? "";
         }
 
         if (!isset($_SESSION['csrf_pool'][$csrfTokenKeyName])) {
@@ -142,7 +143,7 @@ trait CsrfTokenFunctions
     private static function destroyCsrfToken(string $csrfTokenKeyName = ""): bool
     {
         if (empty($csrfTokenKeyName)) {
-            $csrfTokenKeyName = $_POST['csrf_token'] ?? "";
+            $csrfTokenKeyName = $_POST[self::CSRF_TOKEN_FIELD_NAME] ?? "";
         }
         if (!isset($_SESSION['csrf_pool'][$csrfTokenKeyName])) {
             return false;