Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.73% covered (warning)
72.73%
56 / 77
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
PDODriver
72.73% covered (warning)
72.73%
56 / 77
70.00% covered (warning)
70.00%
7 / 10
50.49
0.00% covered (danger)
0.00%
0 / 1
 __construct
78.57% covered (warning)
78.57%
22 / 28
0.00% covered (danger)
0.00%
0 / 1
9.80
 getDriverName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 lastInsertId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 statement
55.56% covered (warning)
55.56%
10 / 18
0.00% covered (danger)
0.00%
0 / 1
9.16
 insert
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 select
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 transaction
50.00% covered (danger)
50.00%
7 / 14
0.00% covered (danger)
0.00%
0 / 1
8.12
 closeDriver
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Lucent\Database\Drivers;
4
5use Lucent\Database;
6use Lucent\Database\DatabaseInterface;
7use Lucent\Database\Schema;
8use Lucent\Facades\FileSystem;
9use Lucent\Filesystem\File;
10use PDO;
11
12class PDODriver extends DatabaseInterface
13{
14
15    private ?PDO $connection;
16
17    public static array $map = [
18        "mysql" => [
19            "types" => [
20                "binary" => "binary",
21                "tinyint" => "tinyint",
22                "decimal" => "decimal",
23                "int" => "int",
24                "bigint" => "bigint",
25                "json" => "json",
26                "timestamp" => "timestamp",
27                "enum" => "enum",
28                "date" => "date",
29                "text" => "text",
30                "varchar" => "varchar",
31                "bool" => "tinyint",
32                "float" => "float",
33                "double" => "double",
34                "char" => "char",
35                "longtext" => "longtext",
36                "mediumtext" => "mediumtext",
37                "uuid" => "char"
38            ],
39            "functions" => [
40                "foreign_key_checks" => [
41                    "disable" => "SET FOREIGN_KEY_CHECKS=0",
42                    "enable" => "SET FOREIGN_KEY_CHECKS=1"
43                ],
44                "column_exists" => "SELECT COUNT(*) > 0 FROM information_schema.COLUMNS 
45                WHERE TABLE_SCHEMA = DATABASE() 
46                AND TABLE_NAME = `{table}` 
47                AND COLUMN_NAME = ?",
48                "list_tables" => "SHOW TABLES",
49                "drop_table" => "DROP TABLE IF EXISTS `{table}`",
50                "table_exists" => "SELECT COUNT(*) > 0 FROM information_schema.TABLES 
51                          WHERE TABLE_SCHEMA = DATABASE() 
52                          AND TABLE_NAME = ?"
53            ]
54        ],
55        "sqlite" => [
56            "types" => [
57                "binary" => "BLOB",
58                "tinyint" => "INTEGER",
59                "decimal" => "REAL",
60                "int" => "INTEGER",
61                "bigint" => "INTEGER",
62                "json" => "TEXT",
63                "timestamp" => "DATETIME",
64                "enum" => "TEXT",
65                "date" => "DATE",
66                "text" => "TEXT",
67                "varchar" => "TEXT",
68                "bool" => "INTEGER",
69                "float" => "REAL",
70                "double" => "REAL",
71                "char" => "TEXT",
72                "longtext" => "TEXT",
73                "mediumtext" => "TEXT",
74                "uuid" => "TEXT"
75            ],
76            "functions" => [
77                "foreign_key_checks" => [
78                    "disable" => "PRAGMA foreign_keys = OFF",
79                    "enable" => "PRAGMA foreign_keys = ON"
80                ],
81                "column_exists" => "SELECT COUNT(*) > 0 FROM pragma_table_info('{table}') WHERE name = ?",
82                "list_tables" => "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';",
83                "drop_table" => "DROP TABLE IF EXISTS `{table}`",
84                "table_exists" => "SELECT name FROM sqlite_master WHERE type='table' AND name=?"
85            ]
86        ]
87    ];
88
89    public function __construct()
90    {
91        // Check if database name is set
92        if (empty(Database::env("DB_DRIVER"))) {
93            Database::log("critical","[PDODriver] DB_DRIVER environment variable is not set or empty");
94            throw new \Exception("[PDODriver] DB_DRIVER environment variable is not set or empty");
95        }
96
97        $driver_name = Database::env("DB_DRIVER");
98
99        switch ($driver_name) {
100            case "sqlite":
101                $path = Database::env('DB_DATABASE');
102
103                if(str_starts_with($path, DIRECTORY_SEPARATOR)) {
104                    $path = ltrim($path, DIRECTORY_SEPARATOR);
105                }
106
107                $path = rtrim(FileSystem::rootPath(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $path;
108
109                if (!file_exists($path)) {
110                    touch($path);
111                    chmod($path, 0666);
112                }
113
114                // Verify file is writable
115                if (!is_writable($path)) {
116                    Database::log("critical","[PDODriver] SQLite database file is not writable: $path");
117                    throw new \RuntimeException("[PDODriver] SQLite database file is not writable: $path");
118                }
119
120                $this->connection = new PDO("sqlite:$path");
121                break;
122            case "mysql":
123                $host = Database::env("DB_HOST");
124                $database = Database::env("DB_DATABASE");
125                $username = Database::env("DB_USERNAME");
126                $password = Database::env("DB_PASSWORD");
127                $port = Database::env("DB_PORT") ?: "3306";
128
129                $dsn = "mysql:host={$host};port={$port};dbname={$database}";
130                $this->connection = new PDO($dsn, $username, $password);
131                break;
132            default:
133                Database::log("critical","[PDODriver] Unknown driver type provided: $driver_name");
134                throw new \RuntimeException("[PDODriver] Unknown driver type provided: $driver_name");
135        }
136
137    }
138
139    public function getDriverName(): string
140    {
141        return $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME);
142    }
143
144    public function lastInsertId(): string|int
145    {
146        return $this->connection->lastInsertId();
147    }
148
149    public function statement(string $query, array $params = []): bool
150    {
151        Database::log("info","[PDODriver] Executing statement: $query");
152
153        try {
154            if (count($params) > 0) {
155                $stmt = $this->connection->prepare($query);
156
157                if (!$stmt) {
158                    Database::log("critical","[PDODriver] Failed to prepare statement: $query");
159                    return false;
160                }
161
162                $result = $stmt->execute($params);
163
164                if (!$result) {
165                    Database::log("critical","[PDODriver] Failed to execute statement: \n$query\nError:".print_r($stmt->errorInfo(),true)."\nParams:".print_r($params,true));
166                    return false;
167                }
168
169                // For INSERT/UPDATE/DELETE, we want to know if it succeeded
170                // rowCount() can be 0 for successful insert in some cases
171                return true;
172            }
173
174            $result = $this->connection->exec($query);
175
176
177            if ($result === false) {
178                Database::log("info","[PDODriver] Failed to execute statement: \n$query\nParams:".print_r($params,true));
179            }
180
181            return $result !== false;
182        } catch (\PDOException $e) {
183
184            Database::log("info","[PDODriver] Failed to execute statement: \n$query\nError:".print_r($e->getMessage(),true)."\nParams:".print_r($params,true));
185            return false;
186        }
187    }
188
189    public function insert(string $query, array $params = []): bool
190    {
191        return $this->statement($query, $params);
192    }
193
194    public function delete(string $query, array $params = []): bool
195    {
196        return $this->statement($query, $params);
197    }
198
199    public function update(string $query, array $params = []): bool
200    {
201        return $this->statement($query, $params);
202    }
203
204    public function select(string $query, bool $fetchAll = true, array $params = []): ?array
205    {
206        Database::log("info","[PDODriver] Executing select: $query");
207
208        if (count($params) > 0) {
209            $stmt = $this->connection->prepare($query);
210            $stmt->execute($params);
211        } else {
212            $stmt = $this->connection->query($query);
213        }
214
215
216        if ($fetchAll) {
217            $results = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
218        } else {
219            $results = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
220        }
221
222        $stmt->closeCursor();
223        return $results;
224    }
225
226    public function transaction(callable $callback, ...$args): bool
227    {
228        if (!$this->connection->beginTransaction()) {
229            return false;
230        }
231        try {
232            $result = call_user_func_array($callback, $args);
233            if ($result === false) {
234                $this->connection->rollBack();
235                return false;
236            }
237        } catch (\Exception $e) {
238            $this->connection->rollBack();
239            Database::log("error","[PDODriver] Failed to execute transaction\n"."Error:".print_r($e->getMessage(),true));
240            throw $e;
241        }
242        $result = $this->connection->commit();
243
244        if (!$result) {
245            $this->connection->rollBack();
246        }
247        return $result;
248    }
249
250    public function closeDriver(): bool
251    {
252        $this->connection = null;
253        return true;
254    }
255}