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 = array();
20 protected $updateData = array();
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 = array();
52 $dictionary = array();
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
102 $this->updateData[] = array(
103 'tableName' => $this->replace($tableName),
104 'statementBlock' => $statementBlock,
105 'versions' => $versions
106 );
107
108 return $this;
109 }
110 public function dryRun()
111 {
112 $this->dryRun = true;
113 $this->history = array();
114 $this->run();
115 return $this->history;
116 }
117 public function update()
118 {
119 $this->dryRun = false;
120 $this->run();
121 }
122 protected function run()
123 {
124 foreach ($this->updateData as $dataSet) {
125 extract($dataSet);
126 $keys = array_keys($versions);
127
128 if ($this->tableExists($tableName)) {
129 $currentVersion = $this->currentTableVersion($tableName);
130 $search = array_search($currentVersion, $keys);
131
132 if ($search == count($keys) - 1) {
133 $startKey = false;
134 } elseif ($search === false) {
135 $startKey = false;
136 trigger_error('Current table version (' . $currentVersion . ') not in schema file.', E_USER_WARNING);
137 } else {
138 $startKey = $keys[$search + 1];
139 }
140 } else {
141 $startKey = $keys[0];
142 }
143
144 if ($startKey !== false) {
145 $startLine = $versions[$startKey];
146
147 foreach ($statementBlock as $lineNumber => $statements) {
148 if ($lineNumber >= $startLine) {
149 $this->execute($lineNumber, $statements);
150 }
151 }
152
153 $lastVersion = $keys[count($keys) - 1];
154 $this->updateTableVersion($tableName, $lastVersion);
155 }
156 }
157
158 $this->updateData = array();
159 }
160 protected function execute($number, $statements)
161 {
162 foreach ($statements as $statement) {
163 if ($this->dryRun) {
164 $this->history[] = $statement;
165 } else {
166 try {
167 $this->pdo->exec($statement);
168 } catch (\PDOException $e) {
169 if (class_exists('\ReflectionClass', false)) {
170 $PDOExceptionReflection = new \ReflectionClass('PDOException');
171 $line = $PDOExceptionReflection->getProperty('line');
172 $message = $PDOExceptionReflection->getProperty('message');
173
174 $line->setAccessible(true);
175 $line->setValue($e, $number);
176 $line->setAccessible(false);
177 $message->setAccessible(true);
178 $message->setValue($e, preg_replace('/ at line [0-9]+$/', ' at line ' . $number, $message->getValue($e)));
179 $message->setAccessible(false);
180 }
181 throw $e;
182 }
183 }
184 }
185 }
186
187 protected function tableExists($tableName)
188 {
189 $exists = false;
190
191 try {
192 $this->pdo->query('SELECT 1 FROM ' . $tableName);
193 $exists = true;
194 } catch (\PDOException $e) {
195 // only catch "table doesn't exist" exception
196 if (!preg_match("/SQLSTATE\\[42S02\\]/", $e->getMessage())) {
197 throw $e;
198 }
199 }
200
201 return $exists;
202 }
203 protected function currentTableVersion($tableName)
204 {
205 try {
206 $query = 'SELECT TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = "' . $tableName . '" AND TABLE_SCHEMA=database() LIMIT 1';
207 $statement = $this->pdo->query($query);
208 $statement->execute();
209 $row = $statement->fetch();
210
211 if ($row['TABLE_COMMENT'] == '') {
212 throw new Exceptions\SchemaException('Missing version identifier in table "' . $tableName . '".');
213 }
214
215 $version = $row['TABLE_COMMENT'];
216 } catch (\PDOException $e) {
217 $query = 'SHOW CREATE TABLE ' . $tableName;
218 $statement = $this->pdo->query($query);
219 $statement->execute();
220 $row = $statement->fetch();
221
222 if (!preg_match('/COMMENT=\'(.*)\'/', $row[1], $matches)) {
223 throw new Exceptions\SchemaException('Missing version identifier in table "' . $tableName . '".');
224 }
225
226 $version = array_pop($matches);
227 }
228
229 return $version;
230 }
231 protected function updateTableVersion($tableName, $version)
232 {
233 $statement = 'ALTER TABLE ' . $tableName . ' COMMENT \'' . $version . '\'';
234 $this->execute(null, array($statement));
235 }
236
237 protected function extractTag($split = array())
238 {
239 $tags = array(
240 self::VERSION_TAG,
241 self::TABLENAME_TAG,
242 self::CONNECTION_TAG,
243 );
244
245 $comments = array_filter($split, function ($v) { return $v['type'] == 'comment'; });
246 $matchedTags = array();
247
248 $values = array_values($comments);
249 $values = array_shift($values);
250 $comment = $values['string'] ?? "";
251
252 foreach ($tags as $tag) {
253 if (
254 count($comments) == 1
255 && preg_match('/' . $tag . '\s+(\S.*\S)\s*$/', $comment, $matches)
256 && count($matches) == 2
257 ) {
258 // @todo get rid of '*/' in preg_match
259 $matchedTags[$tag] = preg_replace('/\s*\*\/\s*$/', '', $matches[1]);
260 } else {
261 $matchedTags[$tag] = false;
262 }
263 }
264
265 return $matchedTags;
266 }
267 protected function checkDictionary($dictionary)
268 {
269 $tags = array_keys($dictionary);
270 while ($tags) {
271 $current = array_pop($tags);
272
273 foreach ($tags as $test) {
274 if (
275 strpos($current, $test) !== false ||
276 strpos($test, $current) !== false
277 ) {
278 throw new Exceptions\SchemaException('Tags cannot be substrings of each other ("' . $current . '", "' . $test . '").');
279 }
280 }
281 }
282 }
284 {
285 $this->replaceFunction = $replaceFunction;
286
287 return $this;
288 }
289 protected function replace($tableName)
290 {
291 if (is_callable($this->replaceFunction)) {
292 $tableName = call_user_func($this->replaceFunction, $tableName);
293 }
294
295 return $tableName;
296 }
297 protected function replaceIdentifiers($dictionary, $split = array())
298 {
299 $replaced = array_map(
300 function ($v) use ($dictionary) {
301 if ($v['type'] == 'code') {
302 $element = array(
303 'type' => 'code',
304 'string' => str_replace(array_keys($dictionary), $dictionary, $v['string']),
305 );
306 } else {
307 $element = $v;
308 }
309
310 return $element;
311 },
312 $split
313 );
314
315 return $replaced;
316 }
317}
318
319/* vim:set ft=php sw=4 sts=4 fdm=marker et : */
loadGlob($path)
Definition Schema.php:29
checkDictionary($dictionary)
Definition Schema.php:267
extractTag($split=array())
Definition Schema.php:237
replace($tableName)
Definition Schema.php:289
replaceIdentifiers($dictionary, $split=array())
Definition Schema.php:297
currentTableVersion($tableName)
Definition Schema.php:203
setReplace($replaceFunction)
Definition Schema.php:283
tableExists($tableName)
Definition Schema.php:187
execute($number, $statements)
Definition Schema.php:160
const CONNECTION_TAG
Definition Schema.php:17
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:231
__construct($pdo)
Definition Schema.php:24