Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.61% covered (warning)
57.61%
53 / 92
58.33% covered (warning)
58.33%
7 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
PDODriver
57.61% covered (warning)
57.61%
53 / 92
58.33% covered (warning)
58.33%
7 / 12
128.32
0.00% covered (danger)
0.00%
0 / 1
 __construct
76.00% covered (warning)
76.00%
19 / 25
0.00% covered (danger)
0.00%
0 / 1
8.88
 getDriverName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createTable
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 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
 getValuesNotInArrayAsMap
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 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\DatabaseInterface;
6use Lucent\Database\Schema;
7use Lucent\Facades\App;
8use Lucent\Facades\Log;
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            ],
38            "functions" => [
39                "foreign_key_checks" => [
40                    "disable" => "SET FOREIGN_KEY_CHECKS=0",
41                    "enable" => "SET FOREIGN_KEY_CHECKS=1"
42                ],
43                "column_exists" => "SELECT COUNT(*) > 0 FROM information_schema.COLUMNS 
44                WHERE TABLE_SCHEMA = DATABASE() 
45                AND TABLE_NAME = `{table}` 
46                AND COLUMN_NAME = ?",
47                "list_tables" => "SHOW TABLES",
48                "drop_table" => "DROP TABLE IF EXISTS `{table}`",
49                "table_exists" => "SELECT COUNT(*) > 0 FROM information_schema.TABLES 
50                          WHERE TABLE_SCHEMA = DATABASE() 
51                          AND TABLE_NAME = ?"
52            ]
53        ],
54        "sqlite" => [
55            "types" => [
56                "binary" => "BLOB",
57                "tinyint" => "INTEGER",
58                "decimal" => "REAL",
59                "int" => "INTEGER",
60                "bigint" => "INTEGER",
61                "json" => "TEXT",
62                "timestamp" => "DATETIME",
63                "enum" => "TEXT",
64                "date" => "DATE",
65                "text" => "TEXT",
66                "varchar" => "TEXT",
67                "bool" => "INTEGER",
68                "float" => "REAL",
69                "double" => "REAL",
70                "char" => "TEXT",
71                "longtext" => "TEXT",
72                "mediumtext" => "TEXT"
73            ],
74            "functions" => [
75                "foreign_key_checks" => [
76                    "disable" => "PRAGMA foreign_keys = OFF",
77                    "enable" => "PRAGMA foreign_keys = ON"
78                ],
79                "column_exists" => "SELECT COUNT(*) > 0 FROM pragma_table_info('{table}') WHERE name = ?",
80                "list_tables" => "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';",
81                "drop_table" => "DROP TABLE IF EXISTS `{table}`",
82                "table_exists" => "SELECT name FROM sqlite_master WHERE type='table' AND name=?"
83            ]
84        ]
85    ];
86
87    public function __construct()
88    {
89        // Check if database name is set
90        if (empty(App::env("DB_DRIVER"))) {
91            Log::channel("lucent.db")->critical("[PDODriver] DB_DRIVER environment variable is not set or empty");
92            throw new \Exception("[PDODriver] DB_DRIVER environment variable is not set or empty");
93        }
94
95        $driver_name = App::env("DB_DRIVER");
96
97        switch ($driver_name) {
98            case "sqlite":
99                $file = new File(App::env('DB_DATABASE'));
100
101                if (!$file->exists()) {
102                    $file->create();
103                    $file->setPermissions(0666);
104                }
105
106                // Verify file is writable
107                if (!is_writable($file->path)) {
108                    Log::channel("lucent.db")->critical("[PDODriver] SQLite database file is not writable: $file->path");
109                    throw new \RuntimeException("[PDODriver] SQLite database file is not writable: $file->path");
110                }
111
112                $this->connection = new PDO("sqlite:$file->path");
113                break;
114            case "mysql":
115                $host = App::env("DB_HOST");
116                $database = App::env("DB_DATABASE");
117                $username = App::env("DB_USERNAME");
118                $password = App::env("DB_PASSWORD");
119                $port = App::env("DB_PORT") ?: "3306";
120
121                $dsn = "mysql:host={$host};port={$port};dbname={$database}";
122                $this->connection = new PDO($dsn, $username, $password);
123                break;
124            default:
125                Log::channel("lucent.db")->critical("[PDODriver] Unknown driver type provided: $driver_name");
126                throw new \RuntimeException("[PDODriver] Unknown driver type provided: $driver_name");
127        }
128
129    }
130
131    public function getDriverName(): string
132    {
133        return $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME);
134    }
135
136    public function createTable(string $name, array $columns): string
137    {
138        //Dynamically generate our table
139        return Schema::table($name, function ($table) use ($columns) {
140
141            //Foreach of our column attributes, transform them into table columns
142            foreach ($columns as $column) {
143
144                $name = $column["NAME"];
145                $type = $column["TYPE"];
146                $column["nullable"] = $column["ALLOW_NULL"];
147
148                $tableColumn = $table->$type($name);
149                $keysToExclude = ["NAME", "UNIQUE_KEY_TO", "ON_UPDATE", "TYPE", "ALLOW_NULL"];
150
151                if (!($tableColumn instanceof Schema\NumericColumn)) {
152                    $keysToExclude[] = "AUTO_INCREMENT";
153                    $keysToExclude[] = "UNSIGNED";
154                }
155
156                //Remove our excluded keys
157                $diff = $this->getValuesNotInArrayAsMap($column, $keysToExclude);
158
159                //For each of our remaining column properties, call the method
160                foreach ($diff as $key => $value) {
161
162                    //Methods need to be converted to camel case before being called.
163                    $key = lcfirst(str_replace('_', '', ucwords(strtolower($key), '_')));
164                    $tableColumn->$key($value);
165                }
166
167            }
168
169        })->toSql();
170    }
171
172    public function lastInsertId(): string|int
173    {
174        return $this->connection->lastInsertId();
175    }
176
177    public function statement(string $query, array $params = []): bool
178    {
179        Log::channel("lucent.db")->info("[PDODriver] Executing statement: $query");
180
181        try {
182            if (count($params) > 0) {
183                $stmt = $this->connection->prepare($query);
184
185                if (!$stmt) {
186                    Log::channel("lucent.db")->critical("[PDODriver] Failed to prepare statement: $query");
187                    return false;
188                }
189
190                $result = $stmt->execute($params);
191
192                if (!$result) {
193                    Log::channel("lucent.db")->info("[PDODriver] Failed to execute statement: \n$query\nError:".print_r($stmt->errorInfo(),true)."\nParams:".print_r($params,true));
194                    return false;
195                }
196
197                // For INSERT/UPDATE/DELETE, we want to know if it succeeded
198                // rowCount() can be 0 for successful insert in some cases
199                return true;
200            }
201
202            $result = $this->connection->exec($query);
203
204
205            if ($result === false) {
206                Log::channel("lucent.db")->info("[PDODriver] Failed to execute statement: \n$query\nParams:".print_r($params,true));
207            }
208
209            return $result !== false;
210        } catch (\PDOException $e) {
211
212            Log::channel("lucent.db")->info("[PDODriver] Failed to execute statement: \n$query\nError:".print_r($e->getMessage(),true)."\nParams:".print_r($params,true));
213            return false;
214        }
215    }
216
217    public function insert(string $query, array $params = []): bool
218    {
219        return $this->statement($query, $params);
220    }
221
222    public function delete(string $query, array $params = []): bool
223    {
224        return $this->statement($query, $params);
225    }
226
227    public function update(string $query, array $params = []): bool
228    {
229        return $this->statement($query, $params);
230    }
231
232    public function select(string $query, bool $fetchAll = true, array $params = []): ?array
233    {
234        Log::channel("lucent.db")->info("[PDODriver] Executing select: $query");
235
236        if (count($params) > 0) {
237            $stmt = $this->connection->prepare($query);
238            $stmt->execute($params);
239        } else {
240            $stmt = $this->connection->query($query);
241        }
242
243
244        if ($fetchAll) {
245            $results = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
246        } else {
247            $results = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
248        }
249
250        $stmt->closeCursor();
251        return $results;
252    }
253
254    public function transaction(callable $callback, ...$args): bool
255    {
256        if (!$this->connection->beginTransaction()) {
257            return false;
258        }
259        try {
260            $result = call_user_func_array($callback, $args);
261            if ($result === false) {
262                $this->connection->rollBack();
263                return false;
264            }
265        } catch (\Exception $e) {
266            $this->connection->rollBack();
267            Log::channel("lucent.db")->error("[PDODriver] Failed to execute transaction\n"."Error:".print_r($e->getMessage(),true));
268            throw $e;
269        }
270        $result = $this->connection->commit();
271
272        if (!$result) {
273            $this->connection->rollBack();
274        }
275        return $result;
276    }
277
278    private function getValuesNotInArrayAsMap(array $sourceMap, array $excludeArray): array
279    {
280        // Create a temporary array from the excludeArray where values are keys
281        // This allows for efficient key comparison with array_diff_key
282        $excludeKeys = array_flip($excludeArray);
283
284        // Get the keys from the source map that are NOT present in the excludeKeys
285        $diffKeys = array_diff_key($sourceMap, $excludeKeys);
286
287        // Use array_intersect_key to get the original key-value pairs from the source map
288        // for only the keys that were not found in the exclude array
289        return array_intersect_key($sourceMap, $diffKeys);
290    }
291
292    public function closeDriver(): bool
293    {
294        $this->connection = null;
295        return true;
296    }
297}