depage-db v1.4.0
Loading...
Searching...
No Matches
Schema.php
Go to the documentation of this file.
1<?php
2
11
12namespace Depage\Db;
13
14class Schema
15{
16 public const TABLENAME_TAG = '@tablename';
17 public const CONNECTION_TAG = '@connection';
18 public const VERSION_TAG = '@version';
19 protected $replaceFunction = [];
20 protected $updateData = [];
21 protected $dryRun;
22 protected $pdo = null;
23
24 public function __construct($pdo)
25 {
26 $this->pdo = $pdo;
27 }
28
29 public function loadGlob($path)
30 {
31 $fileNames = glob($path);
32 if (empty($fileNames)) {
33 trigger_error('No file found matching "' . $path . '".', E_USER_WARNING);
34 }
35 sort($fileNames);
36
37 foreach ($fileNames as $fileName) {
38 $this->loadFile($fileName);
39 }
40
41 return $this;
42 }
43 public function loadFile($fileName)
44 {
45 if (!is_readable($fileName)) {
46 throw new Exceptions\SchemaException('File "' . $fileName . '" doesn\'t exist or isn\'t readable.');
47 }
48
49 $parser = new SqlParser();
50 $header = true;
51 $versions = [];
52 $dictionary = [];
53 $tableName;
54
55 foreach (file($fileName) as $key => $line) {
56 $number = $key + 1;
57 $split = $parser->split($line);
58 $tag = $this->extractTag($split);
59
60 if ($tag[self::VERSION_TAG]) {
61 $versions[$tag[self::VERSION_TAG]] = $number;
62 }
63
64 if ($header) {
65 if ($tag[self::TABLENAME_TAG]) {
66 if (isset($tableName)) {
67 throw new Exceptions\SchemaException('More than one tablename tags in "' . $fileName . '".');
68 } else {
69 $tableName = $tag[self::TABLENAME_TAG];
70 $dictionary[$tableName] = $this->replace($tableName);
71 }
72 }
73
74 if ($tag[self::CONNECTION_TAG]) {
75 $dictionary[$tag[self::CONNECTION_TAG]] = $this->replace($tag[self::CONNECTION_TAG]);
76 }
77
78 if (!$parser->isEndOfStatement()) {
79 $header = false;
80 if (!isset($tableName)) {
81 throw new Exceptions\SchemaException('Tablename tag missing in "' . $fileName . '".');
82 }
83 if (empty($versions)) {
84 throw new Exceptions\SchemaException('There is code without version tags in "' . $fileName . '" at line ' . $number . '.');
85 }
86 }
87 }
88
89 $this->checkDictionary($dictionary);
90 $replaced = $this->replaceIdentifiers($dictionary, $split);
91 $statements = $parser->tidy($replaced);
92
93 if ($statements) {
94 $statementBlock[$number] = $statements;
95 }
96 }
97
98 if (!$parser->isEndOfStatement()) {
99 throw new Exceptions\SchemaException('Incomplete statement at the end of "' . $fileName . '".');
100 }
101 if (empty($versions)) {
102 throw new Exceptions\SchemaException('No version tags found in "' . $fileName . '".');
103 }
104 if (empty($tableName)) {
105 throw new Exceptions\SchemaException('No tablename tag found in "' . $fileName . '".');
106 }
107
108 $this->updateData[] = [
109 'tableName' => $this->replace($tableName),
110 'statementBlock' => $statementBlock,
111 'versions' => $versions,
112 ];
113
114 return $this;
115 }
116 public function dryRun()
117 {
118 $this->dryRun = true;
119 $this->history = [];
120 $this->run();
121 return $this->history;
122 }
123 public function update()
124 {
125 $this->dryRun = false;
126 $this->run();
127 }
128 protected function run()
129 {
130 foreach ($this->updateData as $dataSet) {
131 extract($dataSet);
132 $keys = array_keys($versions);
133
134 if ($this->tableExists($tableName)) {
135 $currentVersion = $this->currentTableVersion($tableName);
136 $search = array_search($currentVersion, $keys);
137
138 if ($search == count($keys) - 1) {
139 $startKey = false;
140 } elseif ($search === false) {
141 $startKey = false;
142 trigger_error('Current table version (' . $currentVersion . ') not in schema file.', E_USER_WARNING);
143 } else {
144 $startKey = $keys[$search + 1];
145 }
146 } else {
147 $startKey = $keys[0];
148 }
149
150 if ($startKey !== false) {
151 $startLine = $versions[$startKey];
152
153 foreach ($statementBlock as $lineNumber => $statements) {
154 if ($lineNumber >= $startLine) {
155 $this->execute($lineNumber, $statements);
156 }
157 }
158
159 $lastVersion = $keys[count($keys) - 1];
160 $this->updateTableVersion($tableName, $lastVersion);
161 }
162 }
163
164 $this->updateData = [];
165 }
166 protected function execute($number, $statements)
167 {
168 foreach ($statements as $statement) {
169 if ($this->dryRun) {
170 $this->history[] = $statement;
171 } else {
172 try {
173 $this->pdo->exec($statement);
174 } catch (\PDOException $e) {
175 if (class_exists('\ReflectionClass', false)) {
176 $PDOExceptionReflection = new \ReflectionClass('PDOException');
177 $line = $PDOExceptionReflection->getProperty('line');
178 $message = $PDOExceptionReflection->getProperty('message');
179
180 $line->setAccessible(true);
181 $line->setValue($e, $number);
182 $line->setAccessible(false);
183 $message->setAccessible(true);
184 $message->setValue($e, preg_replace('/ at line [0-9]+$/', ' at line ' . $number, $message->getValue($e)));
185 $message->setAccessible(false);
186 }
187 throw $e;
188 }
189 }
190 }
191 }
192
193 protected function tableExists($tableName)
194 {
195 $exists = false;
196
197 try {
198 $this->pdo->query('SELECT 1 FROM ' . $tableName);
199 $exists = true;
200 } catch (\PDOException $e) {
201 // only catch "table doesn't exist" exception
202 if (!preg_match("/SQLSTATE\\[42S02\\]/", $e->getMessage())) {
203 throw $e;
204 }
205 }
206
207 return $exists;
208 }
209 protected function currentTableVersion($tableName)
210 {
211 try {
212 $query = 'SELECT TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = "' . $tableName . '" AND TABLE_SCHEMA=database() LIMIT 1';
213 $statement = $this->pdo->query($query);
214 $statement->execute();
215 $row = $statement->fetch();
216
217 if ($row['TABLE_COMMENT'] == '') {
218 throw new Exceptions\SchemaException('Missing version identifier in table "' . $tableName . '".');
219 }
220
221 $version = $row['TABLE_COMMENT'];
222 } catch (\PDOException $e) {
223 $query = 'SHOW CREATE TABLE ' . $tableName;
224 $statement = $this->pdo->query($query);
225 $statement->execute();
226 $row = $statement->fetch();
227
228 if (!preg_match('/COMMENT=\'(.*)\'/', $row[1], $matches)) {
229 throw new Exceptions\SchemaException('Missing version identifier in table "' . $tableName . '".');
230 }
231
232 $version = array_pop($matches);
233 }
234
235 return $version;
236 }
237 protected function updateTableVersion($tableName, $version)
238 {
239 $statement = 'ALTER TABLE ' . $tableName . ' COMMENT \'' . $version . '\'';
240 $this->execute(null, [$statement]);
241 }
242
243 protected function extractTag($split = [])
244 {
245 $tags = [
246 self::VERSION_TAG,
247 self::TABLENAME_TAG,
248 self::CONNECTION_TAG,
249 ];
250
251 $comments = array_filter($split, function ($v) {
252 return $v['type'] == 'comment';
253 });
254 $matchedTags = [];
255
256 $values = array_values($comments);
257 $values = array_shift($values);
258 $comment = $values['string'] ?? "";
259
260 foreach ($tags as $tag) {
261 if (
262 count($comments) == 1
263 && preg_match('/' . $tag . '\s+(\S.*\S)\s*$/', $comment, $matches)
264 && count($matches) == 2
265 ) {
266 // @todo get rid of '*/' in preg_match
267 $matchedTags[$tag] = preg_replace('/\s*\*\/\s*$/', '', $matches[1]);
268 } else {
269 $matchedTags[$tag] = false;
270 }
271 }
272
273 return $matchedTags;
274 }
275 protected function checkDictionary($dictionary)
276 {
277 $tags = array_keys($dictionary);
278 while ($tags) {
279 $current = array_pop($tags);
280
281 foreach ($tags as $test) {
282 if (
283 strpos($current, $test) !== false
284 || strpos($test, $current) !== false
285 ) {
286 throw new Exceptions\SchemaException('Tags cannot be substrings of each other ("' . $current . '", "' . $test . '").');
287 }
288 }
289 }
290 }
292 {
293 $this->replaceFunction = $replaceFunction;
294
295 return $this;
296 }
297 protected function replace($tableName)
298 {
299 if (is_callable($this->replaceFunction)) {
300 $tableName = call_user_func($this->replaceFunction, $tableName);
301 }
302
303 return $tableName;
304 }
305 protected function replaceIdentifiers($dictionary, $split = [])
306 {
307 $replaced = array_map(
308 function ($v) use ($dictionary) {
309 if ($v['type'] == 'code') {
310 $element = [
311 'type' => 'code',
312 'string' => str_replace(array_keys($dictionary), $dictionary, $v['string']),
313 ];
314 } else {
315 $element = $v;
316 }
317
318 return $element;
319 },
320 $split,
321 );
322
323 return $replaced;
324 }
325}
326
327/* vim:set ft=php sw=4 sts=4 fdm=marker et : */
loadGlob($path)
Definition Schema.php:29
extractTag($split=[])
Definition Schema.php:243
checkDictionary($dictionary)
Definition Schema.php:275
replace($tableName)
Definition Schema.php:297
currentTableVersion($tableName)
Definition Schema.php:209
setReplace($replaceFunction)
Definition Schema.php:291
tableExists($tableName)
Definition Schema.php:193
execute($number, $statements)
Definition Schema.php:166
const CONNECTION_TAG
Definition Schema.php:17
replaceIdentifiers($dictionary, $split=[])
Definition Schema.php:305
const VERSION_TAG
Definition Schema.php:18
loadFile($fileName)
Definition Schema.php:43
const TABLENAME_TAG
Definition Schema.php:16
updateTableVersion($tableName, $version)
Definition Schema.php:237
__construct($pdo)
Definition Schema.php:24