|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Simply\Database\Connection; |
| 4 | + |
| 5 | +/** |
| 6 | + * MySqlConnection. |
| 7 | + * @author Riikka Kalliomäki <[email protected]> |
| 8 | + * @copyright Copyright (c) 2018 Riikka Kalliomäki |
| 9 | + * @license http://opensource.org/licenses/mit-license.php MIT License |
| 10 | + */ |
| 11 | +class MySqlConnection implements Connection |
| 12 | +{ |
| 13 | + private $initializer; |
| 14 | + private $pdo; |
| 15 | + |
| 16 | + public function __construct($hostname, $database, $username, $password) |
| 17 | + { |
| 18 | + $this->lastId = false; |
| 19 | + $this->initializer = function () use ($hostname, $database, $username, $password) { |
| 20 | + return new \PDO($this->getDataSource($hostname, $database), $username, $password, [ |
| 21 | + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, |
| 22 | + \PDO::ATTR_EMULATE_PREPARES => false, |
| 23 | + \PDO::MYSQL_ATTR_INIT_COMMAND => sprintf("SET timezone = '%s'", date('P')), |
| 24 | + ]); |
| 25 | + }; |
| 26 | + } |
| 27 | + |
| 28 | + private function getDataSource($hostname, $database) |
| 29 | + { |
| 30 | + if (strncmp($hostname, '/', 1) === 0) { |
| 31 | + return sprintf('mysql:unix_socket=%s;dbname=%s;charset=utf8mb4', $hostname, $database); |
| 32 | + } |
| 33 | + |
| 34 | + $parts = explode(':', $hostname, 2); |
| 35 | + |
| 36 | + if (\count($parts) === 1) { |
| 37 | + return sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $hostname, $database); |
| 38 | + } |
| 39 | + |
| 40 | + return sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $parts[0], $parts[1], $database); |
| 41 | + } |
| 42 | + |
| 43 | + public function getConnection(): \PDO |
| 44 | + { |
| 45 | + if (!$this->pdo) { |
| 46 | + $this->pdo = ($this->initializer)(); |
| 47 | + } |
| 48 | + |
| 49 | + return $this->pdo; |
| 50 | + } |
| 51 | + |
| 52 | + public function insert(string $table, array $values, string & $primaryKey = null): \PDOStatement |
| 53 | + { |
| 54 | + $parameters = []; |
| 55 | + $result = $this->query($this->formatQuery([ |
| 56 | + 'INSERT INTO' => sprintf('%s (%s)', $this->formatTable($table), $this->formatFields(array_keys($values))), |
| 57 | + 'VALUES' => $this->formatParameters($values, $parameters), |
| 58 | + ]), $parameters); |
| 59 | + |
| 60 | + if ($primaryKey !== null) { |
| 61 | + $primaryKey = $this->getConnection()->lastInsertId(); |
| 62 | + } |
| 63 | + |
| 64 | + return $result; |
| 65 | + } |
| 66 | + |
| 67 | + public function select(array $fields, string $table, array $where, array $orderBy = [], int $limit = null): \PDOStatement |
| 68 | + { |
| 69 | + $parameters = []; |
| 70 | + |
| 71 | + return $this->query($this->formatQuery([ |
| 72 | + 'SELECT' => $this->formatFields($fields), |
| 73 | + 'FROM' => $this->formatTable($table), |
| 74 | + 'WHERE' => $this->formatConditions($where, $parameters), |
| 75 | + 'ORDER BY' => $this->formatOrder($orderBy), |
| 76 | + 'LIMIT' => $orderBy ? $this->formatLimit($limit, $parameters) : '', |
| 77 | + ]), $parameters); |
| 78 | + } |
| 79 | + |
| 80 | + public function update(string $table, array $values, array $where): \PDOStatement |
| 81 | + { |
| 82 | + $parameters = []; |
| 83 | + |
| 84 | + return $this->query($this->formatQuery([ |
| 85 | + 'UPDATE' => $this->formatTable($table), |
| 86 | + 'SET' => $this->formatAssignments($values, $parameters), |
| 87 | + 'WHERE' => $this->formatConditions($where, $parameters) |
| 88 | + ]), $parameters); |
| 89 | + } |
| 90 | + |
| 91 | + public function delete(string $table, array $where): \PDOStatement |
| 92 | + { |
| 93 | + $parameters = []; |
| 94 | + |
| 95 | + return $this->query($this->formatQuery([ |
| 96 | + 'DELETE FROM' => $this->formatTable($table), |
| 97 | + 'WHERE' => $this->formatConditions($where, $parameters), |
| 98 | + ]), $parameters); |
| 99 | + } |
| 100 | + |
| 101 | + private function formatFields(array $fields): string |
| 102 | + { |
| 103 | + if (!$fields) { |
| 104 | + throw new \InvalidArgumentException('No fields provided for the query'); |
| 105 | + } |
| 106 | + |
| 107 | + return implode(', ', array_map(function (string $field) { |
| 108 | + return $this->escapeIdentifier($field); |
| 109 | + }, $fields)); |
| 110 | + } |
| 111 | + |
| 112 | + private function formatTable(string $table): string |
| 113 | + { |
| 114 | + if ($table === '') { |
| 115 | + throw new \InvalidArgumentException('No table provided for the query'); |
| 116 | + } |
| 117 | + |
| 118 | + return $this->escapeIdentifier($table); |
| 119 | + } |
| 120 | + |
| 121 | + private function formatConditions(array $conditions, array & $parameters): string |
| 122 | + { |
| 123 | + if (!$conditions) { |
| 124 | + throw new \InvalidArgumentException('No conditions provided for the query'); |
| 125 | + } |
| 126 | + |
| 127 | + $clauses = []; |
| 128 | + |
| 129 | + foreach ($conditions as $field => $value) { |
| 130 | + $clauses[] = $this->formatClause($field, $value, $parameters); |
| 131 | + } |
| 132 | + |
| 133 | + return implode(' AND ', $clauses); |
| 134 | + } |
| 135 | + |
| 136 | + private function formatClause(string $field, $value, array & $parameters): string |
| 137 | + { |
| 138 | + $escaped = $this->escapeIdentifier($field); |
| 139 | + |
| 140 | + if (\is_array($value)) { |
| 141 | + if (\in_array(null, $value, true)) { |
| 142 | + $value = array_filter($value, function ($value): bool { |
| 143 | + return $value !== null; |
| 144 | + }); |
| 145 | + |
| 146 | + if ($value) { |
| 147 | + $placeholders = $this->formatParameters($value, $parameters); |
| 148 | + return "($escaped IN $placeholders OR $escaped IS NULL)"; |
| 149 | + } |
| 150 | + |
| 151 | + return "$escaped IS NULL"; |
| 152 | + } |
| 153 | + |
| 154 | + $placeholders = $this->formatParameters($value, $parameters); |
| 155 | + return "$escaped IN $placeholders"; |
| 156 | + } |
| 157 | + |
| 158 | + if ($value === null) { |
| 159 | + return "$escaped IS NULL"; |
| 160 | + } |
| 161 | + |
| 162 | + $parameters[] = $value; |
| 163 | + return "$escaped = ?"; |
| 164 | + } |
| 165 | + |
| 166 | + private function formatParameters(array $values, array & $parameters): string |
| 167 | + { |
| 168 | + array_push($parameters, ... array_values($parameters)); |
| 169 | + return sprintf('(%s)', implode(', ', array_fill(0, \count($values), '?'))); |
| 170 | + } |
| 171 | + |
| 172 | + private function formatOrder(array $order): string |
| 173 | + { |
| 174 | + $clauses = []; |
| 175 | + |
| 176 | + foreach ($order as $field => $direction) { |
| 177 | + $clauses[] = sprintf('%s %s', $this->escapeIdentifier($field), $this->formatDirection($direction)); |
| 178 | + } |
| 179 | + |
| 180 | + return implode(', ', $clauses); |
| 181 | + } |
| 182 | + |
| 183 | + private function formatDirection(int $order): string |
| 184 | + { |
| 185 | + if ($order === self::ORDER_ASCENDING) { |
| 186 | + return 'ASC'; |
| 187 | + } |
| 188 | + |
| 189 | + if ($order === self::ORDER_DESCENDING) { |
| 190 | + return 'DESC'; |
| 191 | + } |
| 192 | + |
| 193 | + throw new \InvalidArgumentException('Invalid sorting direction'); |
| 194 | + } |
| 195 | + |
| 196 | + private function formatLimit(?int $limit, array & $parameters): string |
| 197 | + { |
| 198 | + if ($limit === null) { |
| 199 | + return ''; |
| 200 | + } |
| 201 | + |
| 202 | + $parameters[] = $limit; |
| 203 | + return '?'; |
| 204 | + } |
| 205 | + |
| 206 | + private function formatAssignments(array $values, array & $parameters): string |
| 207 | + { |
| 208 | + if (!$values) { |
| 209 | + throw new \InvalidArgumentException('No values provided for the query'); |
| 210 | + } |
| 211 | + |
| 212 | + $assignments = []; |
| 213 | + |
| 214 | + foreach ($values as $field => $value) { |
| 215 | + $assignments[] = sprintf('%s = ?', $this->escapeIdentifier($field)); |
| 216 | + $parameters[] = $value; |
| 217 | + } |
| 218 | + |
| 219 | + return implode(', ', $assignments); |
| 220 | + } |
| 221 | + |
| 222 | + private function escapeIdentifier(string $identifier): string |
| 223 | + { |
| 224 | + return "`$identifier`"; |
| 225 | + } |
| 226 | + |
| 227 | + private function formatQuery(array $clauses): string |
| 228 | + { |
| 229 | + $parts = []; |
| 230 | + |
| 231 | + foreach ($clauses as $clause => $value) { |
| 232 | + if ($value === '') { |
| 233 | + continue; |
| 234 | + } |
| 235 | + |
| 236 | + $parts[] = sprintf('%s %s', $clause, $value); |
| 237 | + } |
| 238 | + |
| 239 | + return implode(' ', $parts); |
| 240 | + } |
| 241 | + |
| 242 | + public function query(string $sql, array $parameters = []): \PDOStatement |
| 243 | + { |
| 244 | + $query = $this->getConnection()->prepare($sql); |
| 245 | + |
| 246 | + foreach ($parameters as $name => $value) { |
| 247 | + $this->bindQueryParameter($query, \is_int($name) ? $name + 1 : $name, $value); |
| 248 | + } |
| 249 | + |
| 250 | + $query->execute(); |
| 251 | + |
| 252 | + return $query; |
| 253 | + } |
| 254 | + |
| 255 | + private function bindQueryParameter(\PDOStatement $query, $name, $value): bool |
| 256 | + { |
| 257 | + switch (true) { |
| 258 | + case \is_string($value): |
| 259 | + return $query->bindValue($name, $value, \PDO::PARAM_STR); |
| 260 | + case \is_float($value): |
| 261 | + return $query->bindValue($name, var_export($value, true), \PDO::PARAM_STR); |
| 262 | + case \is_int($value): |
| 263 | + return $query->bindValue($name, $value, \PDO::PARAM_INT); |
| 264 | + case \is_bool($value): |
| 265 | + return $query->bindValue($name, $value ? 1 : 0, \PDO::PARAM_INT); |
| 266 | + case $value === null: |
| 267 | + return $query->bindValue($name, null, \PDO::PARAM_NULL); |
| 268 | + default: |
| 269 | + throw new \InvalidArgumentException('Invalid parameter value type'); |
| 270 | + } |
| 271 | + } |
| 272 | +} |
0 commit comments