Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
Database
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 20
1122
0.00% covered (danger)
0.00%
0 / 1
 connect
n/a
0 / 0
n/a
0 / 0
0
 escapeName
n/a
0 / 0
n/a
0 / 0
0
 escapeLike
n/a
0 / 0
n/a
0 / 0
0
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 connected
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setConnected
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 query
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 replaceClassHashNamesWithTableNames
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getParametersString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 configValue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 fetch
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 fetchAll
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 setFetchMode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 fetchColumn
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 fetchOne
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 lastInsertId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 insert
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 update
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getInConditionAndParams
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 beginTransaction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 commit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rollBack
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 runInTransaction
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace Dynart\Micro\Entities;
4
5use Dynart\Micro\ConfigInterface;
6use Dynart\Micro\LoggerInterface;
7use PDO;
8use PDOException;
9use PDOStatement;
10use Psr\Log\LogLevel;
11use RuntimeException;
12
13abstract class Database
14{
15    protected string $configName = 'default';
16    protected bool $connected = false;
17    protected ?PDO $pdo = null;
18
19    abstract protected function connect(): void;
20    abstract public function escapeName(string $name): string;
21    abstract public function escapeLike(string $string): string;
22
23    public function __construct(
24        protected ConfigInterface $config,
25        protected LoggerInterface $logger,
26        protected PdoBuilder $pdoBuilder,
27    ) {}
28
29    public function connected(): bool {
30        return $this->connected;
31    }
32
33    protected function setConnected(bool $value): void {
34        $this->connected = $value;
35    }
36
37    public function query(string $query, array $params = [], bool $closeCursor = false): PDOStatement {
38        try {
39            $this->connect();
40            $query = $this->replaceClassHashNamesWithTableNames($query);
41            $stmt = $this->pdo->prepare($query);
42            $stmt->execute($params);
43            if ($this->logger->level() == LogLevel::DEBUG) {
44                $this->logger->debug("Query: $query" . $this->getParametersString($params));
45            }
46        } catch (PDOException $e) {
47            $this->logger->error("Error in query: $query" . $this->getParametersString($params));
48            throw $e;
49        }
50        if ($closeCursor) {
51            $stmt->closeCursor();
52        }
53        return $stmt;
54    }
55
56    protected function replaceClassHashNamesWithTableNames(string $query): string {
57        return preg_replace_callback(
58            '/(\'[^\'"#]*\')|(#[A-Za-z0-9_]+(?=[\s\n\r\.`]|$))/',
59            function ($matches) {
60                if ($matches[1]) {
61                    return $matches[1]; // Keep content within single quotes unchanged
62                } else {
63                    return $this->configValue('table_prefix').strtolower(substr($matches[0], 1));
64                }
65            },
66            $query
67        );
68    }
69
70    protected function getParametersString(array $params): string {
71        return $params ? "\nParameters: " . json_encode($params) : "";
72    }
73
74    public function configValue(string $name): mixed {
75        return $this->config->get("database.{$this->configName}.$name", "db_{$name}_missing");
76    }
77
78    public function fetch(string $query, array $params = [], string $className = ''): mixed {
79        $stmt = $this->query($query, $params);
80        $this->setFetchMode($stmt, $className);
81        $result = $stmt->fetch();
82        $stmt->closeCursor();
83        return $result;
84    }
85
86    public function fetchAll(string $query, array $params = [], string $className = ''): array {
87        $stmt = $this->query($query, $params);
88        $this->setFetchMode($stmt, $className);
89        $result = $stmt->fetchAll();
90        $stmt->closeCursor();
91        return $result;
92    }
93
94    protected function setFetchMode(PDOStatement $stmt, string $className): void {
95        if ($className) {
96            $stmt->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, $className);
97        } else {
98            $stmt->setFetchMode(PDO::FETCH_ASSOC);
99        }
100    }
101
102    public function fetchColumn(string $query, array $params = []): array {
103        $stmt = $this->query($query, $params);
104        $rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
105        $stmt->closeCursor();
106        $result = [];
107        foreach ($rows as $row) {
108            $result[] = $row;
109        }
110        return $result;
111    }
112
113    public function fetchOne(string $query, array $params = []): mixed {
114        $stmt = $this->query($query, $params);
115        $result = $stmt->fetchColumn(0);
116        $stmt = null;
117        return $result;
118    }
119
120    public function lastInsertId(?string $name = null): string|false {
121        return $this->pdo->lastInsertId($name);
122    }
123
124    public function insert(string $tableName, array $data): void {
125        $tableName = $this->escapeName($tableName);
126        $params = [];
127        $names = [];
128        foreach ($data as $name => $value) {
129            $names[] = $this->escapeName($name);
130            $params[':' . $name] = $value;
131        }
132        $namesString = join(', ', $names);
133        $paramsString = join(', ', array_keys($params));
134        $sql = "insert into $tableName ($namesString) values ($paramsString)";
135        $this->query($sql, $params, true);
136    }
137
138    public function update(string $tableName, array $data, string $condition = '', array $conditionParams = []): void {
139        $tableName = $this->escapeName($tableName);
140        $params = [];
141        $pairs = [];
142        foreach ($data as $name => $value) {
143            $pairs[] = $this->escapeName($name) . ' = :' . $name;
144            $params[':' . $name] = $value;
145        }
146        $params = array_merge($params, $conditionParams);
147        $pairsString = join(', ', $pairs);
148        $where = $condition ? ' where ' . $condition : '';
149        $sql = "update $tableName set $pairsString$where";
150        $this->query($sql, $params, true);
151    }
152
153    public function getInConditionAndParams(array $values, string $paramNamePrefix = 'in'): array {
154        $params = [];
155        $in = "";
156        foreach ($values as $i => $item) {
157            $key = ":" . $paramNamePrefix . $i;
158            $in .= "$key,";
159            $params[$key] = $item;
160        }
161        $condition = rtrim($in, ",");
162        return [$condition, $params];
163    }
164
165    public function beginTransaction(): bool {
166        return $this->pdo->beginTransaction();
167    }
168
169    public function commit(): bool {
170        return $this->pdo->commit();
171    }
172
173    public function rollBack(): bool {
174        return $this->pdo->rollBack();
175    }
176
177    public function runInTransaction(callable $callable): void {
178        $this->beginTransaction();
179        try {
180            call_user_func($callable); // here the CREATE/DROP table can COMMIT implicitly
181            $this->commit(); // here it drops an exception because of that
182        } catch (RuntimeException $e) {
183            // ignore "There is no active transaction"
184            if ($e->getMessage() == "There is no active transaction") {
185                return;
186            }
187            $this->rollBack();
188            throw $e;
189        }
190    }
191}