dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxDatabaseMigrationService.class.php
Go to the documentation of this file.
1<?php
2
3declare(strict_types=1);
4
5namespace dbx\dbxAdmin;
6
7use RuntimeException;
8use Throwable;
9
18{
19 public const LEDGER_DD = 'dbxAdmin|dbxMigration';
20
21 private object $db;
22 private object $dd;
23
24 public function __construct(?object $db = null, ?object $dd = null)
25 {
26 $this->db = $db ?? dbx()->get_system_obj('dbxDB');
27 $this->dd = $dd ?? dbx()->get_system_obj('dbxDD');
28 if (!is_object($this->db) || !is_object($this->dd)) {
29 throw new RuntimeException('dbxDB/dbxDD stehen fuer Migrationen nicht zur Verfuegung.');
30 }
31 }
32
36 public function discover(string $releaseRoot, string $targetVersion = ''): array
37 {
38 $root = realpath($releaseRoot);
39 if ($root === false || !is_dir($root)) {
40 throw new RuntimeException('Releasewurzel fuer DB-Migrationen ist ungueltig.');
41 }
42
43 $migrations = array();
44 $pattern = rtrim($root, '\\/') . '/dbx/modules/*/migrations/*.migration.php';
45 foreach (glob($pattern) ?: array() as $file) {
46 $record = (static function (string $migrationFile): mixed {
47 return include $migrationFile;
48 })($file);
49 if (!is_array($record)) {
50 throw new RuntimeException('Migration liefert keine Definition: ' . basename($file));
51 }
52
53 $id = trim((string)($record['id'] ?? ''));
54 $version = trim((string)($record['version'] ?? ''));
55 if ($id === ''
56 || preg_match('/^[A-Za-z0-9._-]{1,128}$/', $id) !== 1
57 || $version === ''
58 || preg_match('/^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/', $version) !== 1
59 ) {
60 throw new RuntimeException('Migration besitzt ID oder Version im ungueltigen Format: ' . basename($file));
61 }
62 if ($targetVersion !== '' && version_compare($version, $targetVersion, '>')) {
63 continue;
64 }
65 if (isset($migrations[$id])) {
66 throw new RuntimeException('Doppelte Migrations-ID: ' . $id);
67 }
68
69 $operations = is_array($record['operations'] ?? null)
70 ? $record['operations']
71 : array();
72 $affected = is_array($record['affected_dd'] ?? null)
73 ? $record['affected_dd']
74 : array();
75 foreach ($operations as $operation) {
76 if (is_array($operation)
77 && ($operation['type'] ?? '') === 'sync_dd'
78 && trim((string)($operation['dd'] ?? '')) !== ''
79 ) {
80 $affected[] = trim((string)$operation['dd']);
81 }
82 }
83
84 $record['id'] = $id;
85 $record['version'] = $version;
86 $record['module'] = $this->moduleFromMigrationFile($file);
87 $record['file'] = $file;
88 $record['checksum'] = hash_file('sha256', $file);
89 $record['operations'] = $operations;
90 $record['affected_dd'] = array_values(array_unique(array_filter(
91 array_map('strval', $affected)
92 )));
93 $migrations[$id] = $record;
94 }
95
96 uasort($migrations, static function (array $left, array $right): int {
97 return version_compare((string)$left['version'], (string)$right['version'])
98 ?: strcmp((string)$left['id'], (string)$right['id']);
99 });
100 return $migrations;
101 }
102
103 private function moduleFromMigrationFile(string $file): string
104 {
105 $normalized = str_replace('\\', '/', $file);
106 return preg_match('#/dbx/modules/([^/]+)/migrations/#', $normalized, $match) === 1
107 ? (string)$match[1]
108 : '';
109 }
110
111 private function ledgerExists(): bool
112 {
113 $server = (string)$this->db->get_dd_server(self::LEDGER_DD);
114 $table = (string)$this->db->get_dd_table(self::LEDGER_DD);
115 return $server !== ''
116 && $table !== ''
117 && (bool)$this->dd->get_table_exist($server, $table);
118 }
119
120 private function ledgerRecord(string $id): array
121 {
122 if (!$this->ledgerExists()) {
123 return array();
124 }
125 $record = $this->db->select1(
126 self::LEDGER_DD,
127 array('migration_id' => $id),
128 '*',
129 0
130 );
131 return is_array($record) ? $record : array();
132 }
133
137 public function plan(string $releaseRoot, string $targetVersion): array
138 {
139 $migrations = $this->discover($releaseRoot, $targetVersion);
140 $pending = array();
141 $applied = array();
142
143 foreach ($migrations as $id => $migration) {
144 $ledger = $this->ledgerRecord($id);
145 if (($ledger['status'] ?? '') === 'finished') {
146 if (!hash_equals(
147 strtolower((string)($ledger['checksum'] ?? '')),
148 strtolower((string)$migration['checksum'])
149 )) {
150 throw new RuntimeException('Bereits ausgefuehrte Migration wurde veraendert: ' . $id);
151 }
152 $applied[] = $id;
153 continue;
154 }
155 $pending[] = $id;
156 }
157
158 return array(
159 'migrations' => $migrations,
160 'pending' => $pending,
161 'applied' => $applied,
162 );
163 }
164
170 public function prepare(
171 string $releaseRoot,
172 string $targetVersion,
173 string $backupDirectory
174 ): array {
175 $plan = $this->plan($releaseRoot, $targetVersion);
176 $root = realpath($releaseRoot);
177 if ($root === false) {
178 throw new RuntimeException('Releasewurzel ist nicht mehr vorhanden.');
179 }
180 if ($plan['pending'] === array()) {
181 return array(
182 'schema' => 1,
183 'release_root' => $root,
184 'target_version' => $targetVersion,
185 'pending' => array(),
186 'checksums' => array(),
187 'backups' => array(),
188 );
189 }
190
191 $dbBackupDir = rtrim($backupDirectory, '\\/')
192 . DIRECTORY_SEPARATOR . 'database';
193 if (!is_dir($dbBackupDir)
194 && !mkdir($dbBackupDir, 0700, true)
195 && !is_dir($dbBackupDir)
196 ) {
197 throw new RuntimeException('DB-Backupverzeichnis konnte nicht erstellt werden.');
198 }
199
200 $checksums = array();
201 $affected = array();
202 foreach ($plan['pending'] as $id) {
203 $migration = $plan['migrations'][$id];
204 $checksums[$id] = (string)$migration['checksum'];
205 foreach ($migration['affected_dd'] as $ddRef) {
206 $affected[$ddRef] = true;
207 }
208 }
209
210 $backupsByServer = array();
211 $backups = array();
212 foreach (array_keys($affected) as $ddRef) {
213 $binding = $this->db->get_dd_server_binding_info($ddRef);
214 if (($binding['source'] ?? '') === 'missing-dd') {
215 $backups[] = array(
216 'dd' => $ddRef,
217 'existed' => false,
218 'server' => '',
219 'table' => '',
220 'file' => '',
221 );
222 continue;
223 }
224 if (empty($binding['valid']) || ($binding['resolved_server'] ?? '') === '') {
225 throw new RuntimeException('Ungueltige Serverbindung fuer Migration: ' . $ddRef);
226 }
227
228 $this->assertStagedServerCompatible($root, $ddRef, $binding);
229 $server = (string)$binding['resolved_server'];
230 $table = (string)$this->db->get_dd_table($ddRef);
231 $entry = array(
232 'dd' => $ddRef,
233 'existed' => false,
234 'server' => $server,
235 'table' => $table,
236 'file' => '',
237 'fields' => array(),
238 'indexes' => array(),
239 );
240 if ($table === '' || !$this->dd->get_table_exist($server, $table)) {
241 $backups[] = $entry;
242 continue;
243 }
244
245 $entry['existed'] = true;
246 $entry['fields'] = $this->dd->get_db_fields($server, $table);
247 $entry['indexes'] = $this->dd->get_db_indexes($server, $table);
248 $entry['file'] = $dbBackupDir . DIRECTORY_SEPARATOR
249 . preg_replace('/[^A-Za-z0-9._-]+/', '-', $ddRef) . '.ddb.zip';
250 $backups[] = $entry;
251 $backupsByServer[$server][] = count($backups) - 1;
252 }
253
254 foreach ($backupsByServer as $indexes) {
255 $representative = $backups[$indexes[0]]['dd'];
256 if ($this->db->begin($representative) !== 1) {
257 throw new RuntimeException('DB-Backuptransaktion konnte nicht gestartet werden: ' . $representative);
258 }
259 try {
260 foreach ($indexes as $index) {
261 $entry = $backups[$index];
262 $state = array();
263 for ($step = 0; $step < 100000; $step++) {
264 $state = $this->dd->backup(
265 $entry['server'],
266 $entry['table'],
267 $entry['file'],
268 1
269 );
270 if (in_array(
271 (string)($state['status'] ?? ''),
272 array('finished', 'error', 'cancelled'),
273 true
274 )) {
275 break;
276 }
277 }
278 if (($state['status'] ?? '') !== 'finished') {
279 throw new RuntimeException('Tabellenbackup fehlgeschlagen: ' . $entry['dd']);
280 }
281 }
282 if ($this->db->commit($representative) !== 1) {
283 throw new RuntimeException('DB-Backuptransaktion konnte nicht abgeschlossen werden.');
284 }
285 } catch (Throwable $exception) {
286 $this->db->rollback($representative);
287 throw $exception;
288 }
289 }
290
291 return array(
292 'schema' => 1,
293 'release_root' => $root,
294 'target_version' => $targetVersion,
295 'pending' => $plan['pending'],
296 'checksums' => $checksums,
297 'backups' => $backups,
298 );
299 }
300
305 private function assertStagedServerCompatible(
306 string $releaseRoot,
307 string $ddRef,
308 array $binding
309 ): void {
310 if (($binding['source'] ?? '') === 'local-binding') {
311 return;
312 }
313 $parts = explode('|', $ddRef, 2);
314 if (count($parts) !== 2) {
315 return;
316 }
317 $file = rtrim($releaseRoot, '\\/')
318 . '/dbx/modules/' . $parts[0] . '/dd/' . $parts[1] . '.dd.php';
319 if (!is_file($file)) {
320 return;
321 }
322
323 $stagedTable = (static function (string $ddFile): array {
324 $table = array();
325 $fields = array();
326 $indexes = array();
327 include $ddFile;
328 return is_array($table) ? $table : array();
329 })($file);
330 $stagedServer = trim((string)($stagedTable['server'] ?? ''));
331 $declared = trim((string)($binding['declared_server'] ?? ''));
332 $module = trim((string)$parts[0]);
333 if ($stagedServer !== ''
334 && $declared !== ''
335 && $this->canonicalServerReference($stagedServer, $module)
336 !== $this->canonicalServerReference($declared, $module)
337 ) {
338 throw new RuntimeException(
339 'DD-Serverwechsel benoetigt eine explizite lokale Bindung: '
340 . $ddRef . ' (' . $declared . ' -> ' . $stagedServer . ')'
341 );
342 }
343 }
344
351 private function canonicalServerReference(string $server, string $module): string
352 {
353 $server = trim($server);
354 if ($server === '' || $module === '') {
355 return $server;
356 }
357 if (strpos($server, '|') !== false) {
358 return $server;
359 }
360 if (preg_match('/\.(?:db3|sqlite|sqlite3)$/i', $server)) {
361 return $module . '|' . $server;
362 }
363 return $server;
364 }
365
366 public function apply(array $state): array
367 {
368 $releaseRoot = (string)($state['release_root'] ?? '');
369 $targetVersion = (string)($state['target_version'] ?? '');
370 $pending = is_array($state['pending'] ?? null) ? $state['pending'] : array();
371 if ($pending === array()) {
372 return array('applied' => array());
373 }
374
375 $migrations = $this->discover($releaseRoot, $targetVersion);
376 if (!$this->dd->create_db_tab(self::LEDGER_DD)) {
377 throw new RuntimeException('Migration-Ledger konnte nicht erstellt werden.');
378 }
379
380 $applied = array();
381 foreach ($pending as $id) {
382 if (!isset($migrations[$id])) {
383 throw new RuntimeException('Vorbereitete Migration fehlt: ' . $id);
384 }
385 $migration = $migrations[$id];
386 $expected = strtolower((string)($state['checksums'][$id] ?? ''));
387 if ($expected === ''
388 || !hash_equals($expected, strtolower((string)$migration['checksum']))
389 ) {
390 throw new RuntimeException('Migration wurde nach der Vorbereitung veraendert: ' . $id);
391 }
392
393 $this->writeLedger($migration, 'running', '', $state);
394 try {
395 $this->executeMigration($migration);
396 $this->writeLedger($migration, 'finished', '', $state);
397 $applied[] = $id;
398 } catch (Throwable $exception) {
399 $this->writeLedger($migration, 'error', $exception->getMessage(), $state);
400 throw new RuntimeException(
401 'DB-Migration fehlgeschlagen (' . $id . '): ' . $exception->getMessage(),
402 0,
403 $exception
404 );
405 }
406 }
407
408 return array('applied' => $applied);
409 }
410
419 private function activateReleaseDdDefinitions(array $migration): array
420 {
421 $migrationFile = (string)($migration['file'] ?? '');
422 $releaseRoot = $migrationFile !== '' ? dirname($migrationFile, 5) : '';
423 if ($releaseRoot === '' || !is_dir($releaseRoot)) {
424 throw new RuntimeException('Releasewurzel fuer staged DDs fehlt.');
425 }
426
427 if (!isset($_SESSION) || !is_array($_SESSION)) {
428 $_SESSION = array();
429 }
430 if (!isset($_SESSION['dbx']['cache']['dd'])
431 || !is_array($_SESSION['dbx']['cache']['dd'])
432 ) {
433 $_SESSION['dbx']['cache']['dd'] = array();
434 }
435
436 $saved = array();
437 $activated = array();
438 foreach ((array)($migration['operations'] ?? array()) as $operation) {
439 if (!is_array($operation) || ($operation['type'] ?? '') !== 'sync_dd') {
440 continue;
441 }
442 $parts = explode('|', trim((string)($operation['dd'] ?? '')), 2);
443 if (count($parts) !== 2) {
444 continue;
445 }
446 [$module, $ddName] = $parts;
447 $cacheKey = $module . '|' . $ddName;
448 if (isset($activated[$cacheKey])) {
449 continue;
450 }
451
452 $ddFile = $releaseRoot . '/dbx/modules/' . $module . '/dd/' . $ddName . '.dd.php';
453 if (!is_file($ddFile)) {
454 $module = 'dbx';
455 $ddFile = $releaseRoot . '/dbx/modules/dbx/dd/' . $ddName . '.dd.php';
456 }
457 if (!is_file($ddFile)) {
458 throw new RuntimeException('Staged DD fehlt: ' . $cacheKey);
459 }
460
461 $definition = (static function (string $file): array {
462 $table = array();
463 $fields = array();
464 $indexes = array();
465 include $file;
466 return array(
467 'table' => is_array($table) ? $table : array(),
468 'fields' => is_array($fields) ? $fields : array(),
469 'indexes' => is_array($indexes) ? $indexes : array(),
470 );
471 })($ddFile);
472 if ($definition['table'] === array()) {
473 throw new RuntimeException('Staged DD ist ungueltig: ' . $cacheKey);
474 }
475
476 $languageDynamic = (($definition['table']['language'] ?? '') === '*');
477 if ($languageDynamic) {
478 $language = function_exists('dbx_lng_current')
479 ? strtolower(trim((string)dbx_lng_current()))
480 : strtolower(trim((string)dbx()->get_system_var('dbx_lng', 'de')));
481 if ($language === '' || preg_match('/^[a-z]{2,3}$/', $language) !== 1) {
482 $language = 'de';
483 }
484 $tableBase = trim((string)($definition['table']['table'] ?? ''));
485 $languageFile = $tableBase !== ''
486 ? dirname($ddFile) . '/' . $tableBase . '_' . $language . '.dd.php'
487 : '';
488 if ($languageFile !== '' && is_file($languageFile)) {
489 $definition = (static function (string $file): array {
490 $table = array();
491 $fields = array();
492 $indexes = array();
493 include $file;
494 return array(
495 'table' => is_array($table) ? $table : array(),
496 'fields' => is_array($fields) ? $fields : array(),
497 'indexes' => is_array($indexes) ? $indexes : array(),
498 );
499 })($languageFile);
500 $ddFile = $languageFile;
501 } elseif ($tableBase !== '') {
502 $definition['table']['table'] = $tableBase . '_' . $language;
503 $definition['table']['datadic'] = $tableBase . '_' . $language;
504 $definition['table']['language'] = $language;
505 }
506 }
507
508 $existed = isset($_SESSION['dbx']['cache']['dd'][$module])
509 && array_key_exists($ddName, $_SESSION['dbx']['cache']['dd'][$module]);
510 $saved[] = array(
511 'module' => $module,
512 'dd' => $ddName,
513 'existed' => $existed,
514 'value' => $existed
515 ? $_SESSION['dbx']['cache']['dd'][$module][$ddName]
516 : null,
517 );
518 $_SESSION['dbx']['cache']['dd'][$module][$ddName] = array(
519 'table' => $definition['table'],
520 'fields' => $definition['fields'],
521 'indexes' => $definition['indexes'],
522 'file' => $ddFile,
523 'language_dynamic' => $languageDynamic,
524 'declared_server' => (string)($definition['table']['server'] ?? 'default'),
525 );
526 $activated[$cacheKey] = true;
527 }
528 return $saved;
529 }
530
531 private function restoreDdDefinitions(array $saved): void
532 {
533 foreach (array_reverse($saved) as $entry) {
534 $module = (string)($entry['module'] ?? '');
535 $ddName = (string)($entry['dd'] ?? '');
536 if ($module === '' || $ddName === '') {
537 continue;
538 }
539 if (!empty($entry['existed'])) {
540 $_SESSION['dbx']['cache']['dd'][$module][$ddName] = $entry['value'];
541 } else {
542 unset($_SESSION['dbx']['cache']['dd'][$module][$ddName]);
543 }
544 }
545 }
546
547 private function executeMigration(array $migration): void
548 {
549 $savedDdDefinitions = $this->activateReleaseDdDefinitions($migration);
550 try {
551 foreach ($migration['operations'] as $operation) {
552 if (!is_array($operation)) {
553 throw new RuntimeException('Ungueltige Migrationsoperation.');
554 }
555 $type = (string)($operation['type'] ?? '');
556 if ($type === 'sync_dd') {
557 $ddRef = trim((string)($operation['dd'] ?? ''));
558 $syncMode = strtolower(trim((string)(
559 $operation['mode'] ?? 'apply'
560 )));
561 $parts = explode('|', $ddRef, 2);
562 if (count($parts) !== 2) {
563 throw new RuntimeException('sync_dd benoetigt modul|dd.');
564 }
565 if (!in_array($syncMode, array('apply', 'rebuild'), true)) {
566 throw new RuntimeException(
567 'sync_dd erlaubt nur apply oder rebuild: ' . $ddRef
568 );
569 }
570 $this->dd->sync_dd_to_db($parts[0], $parts[1], 'reset');
571 $sync = array();
572 for ($step = 0; $step < 1000; $step++) {
573 $sync = $this->dd->sync_dd_to_db(
574 $parts[0],
575 $parts[1],
576 $syncMode
577 );
578 if (in_array(
579 (string)($sync['status'] ?? ''),
580 array('finished', 'error', 'cancelled'),
581 true
582 )) {
583 break;
584 }
585 }
586 if (($sync['status'] ?? '') !== 'finished') {
587 throw new RuntimeException(
588 $ddRef . ': ' . (string)($sync['message'] ?? 'DD-Sync fehlgeschlagen')
589 );
590 }
591 continue;
592 }
593 if ($type === 'seed_core') {
594 dbx()->get_include_obj('dbxInstallationService', 'dbxSetup');
595 $installerClass = '\\dbx\\dbxSetup\\dbxInstallationService';
596 $installer = new $installerClass($this->db, $this->dd);
597 $installer->seedCoreGroups();
598 continue;
599 }
600 throw new RuntimeException('Nicht erlaubter Migrationstyp: ' . $type);
601 }
602
603 if (isset($migration['up'])) {
604 if (!is_callable($migration['up'])) {
605 throw new RuntimeException('Migration-up ist nicht aufrufbar.');
606 }
607 ($migration['up'])($this->db, $this->dd);
608 }
609 } finally {
610 $this->restoreDdDefinitions($savedDdDefinitions);
611 }
612 }
613
614 private function writeLedger(
615 array $migration,
616 string $status,
617 string $error,
618 array $state
619 ): void {
620 $existing = $this->ledgerRecord((string)$migration['id']);
621 $values = array(
622 'migration_id' => (string)$migration['id'],
623 'release_version' => (string)$migration['version'],
624 'module' => (string)$migration['module'],
625 'checksum' => (string)$migration['checksum'],
626 'status' => $status,
627 'phase' => 'apply',
628 'affected_servers' => json_encode(
629 array_values(array_unique(array_map(
630 static fn(array $entry): string => (string)($entry['server'] ?? ''),
631 is_array($state['backups'] ?? null) ? $state['backups'] : array()
632 ))),
633 JSON_UNESCAPED_SLASHES
634 ),
635 'backup_reference' => json_encode(
636 array_values(array_filter(array_map(
637 static fn(array $entry): string => (string)($entry['file'] ?? ''),
638 is_array($state['backups'] ?? null) ? $state['backups'] : array()
639 ))),
640 JSON_UNESCAPED_SLASHES
641 ),
642 'error_text' => $error,
643 );
644 if ($status === 'running') {
645 $values['started_at'] = date('Y-m-d H:i:s.u');
646 $values['finished_at'] = '';
647 } else {
648 $values['finished_at'] = date('Y-m-d H:i:s.u');
649 }
650
651 if ((int)($existing['id'] ?? 0) > 0) {
652 $ok = $this->db->update(
653 self::LEDGER_DD,
654 $values,
655 (int)$existing['id'],
656 0,
657 1,
658 1,
659 0
660 );
661 } else {
662 $ok = $this->db->insert(
663 self::LEDGER_DD,
664 $values,
665 0,
666 1,
667 1,
668 0
669 );
670 }
671 if ((int)$ok < 0 || $ok === false) {
672 throw new RuntimeException('Migration-Ledger konnte nicht geschrieben werden.');
673 }
674 }
675
679 public function rollback(array $state): array
680 {
681 $backups = is_array($state['backups'] ?? null) ? $state['backups'] : array();
682 foreach (array_reverse($backups) as $entry) {
683 $ddRef = (string)($entry['dd'] ?? '');
684 $server = (string)($entry['server'] ?? '');
685 $table = (string)($entry['table'] ?? '');
686
687 if ($server === '' || $table === '') {
688 if ($ddRef !== '') {
689 $server = (string)$this->db->get_dd_server($ddRef);
690 $table = (string)$this->db->get_dd_table($ddRef);
691 }
692 }
693 if ($server === '' || $table === '') {
694 continue;
695 }
696
697 if (empty($entry['existed'])) {
698 $this->dd->drop_db_tab($server, $table);
699 continue;
700 }
701
702 $this->dd->drop_db_tab($server, $table);
703 if (!$this->dd->create_db_tab_from_fields(
704 $server,
705 $table,
706 is_array($entry['fields'] ?? null) ? $entry['fields'] : array(),
707 is_array($entry['indexes'] ?? null) ? $entry['indexes'] : array()
708 )) {
709 throw new RuntimeException('Rollback-Schema konnte nicht erstellt werden: ' . $ddRef);
710 }
711
712 $restore = array();
713 for ($step = 0; $step < 100000; $step++) {
714 $restore = $this->dd->restore(
715 $server,
716 $table,
717 (string)$entry['file'],
718 array(),
719 1
720 );
721 if (in_array(
722 (string)($restore['status'] ?? ''),
723 array('finished', 'error', 'cancelled'),
724 true
725 )) {
726 break;
727 }
728 }
729 if (($restore['status'] ?? '') !== 'finished') {
730 throw new RuntimeException('Rollback-Daten konnten nicht wiederhergestellt werden: ' . $ddRef);
731 }
732 }
733
734 if ($this->ledgerExists()) {
735 foreach ((array)($state['pending'] ?? array()) as $id) {
736 $record = $this->ledgerRecord((string)$id);
737 if ((int)($record['id'] ?? 0) > 0) {
738 $this->db->update(
739 self::LEDGER_DD,
740 array(
741 'status' => 'rolled_back',
742 'finished_at' => date('Y-m-d H:i:s.u'),
743 ),
744 (int)$record['id'],
745 0,
746 1,
747 1,
748 0
749 );
750 }
751 }
752 }
753
754 return array('rolled_back' => array_values((array)($state['pending'] ?? array())));
755 }
756}
$table['server']
Definition .dd.php:6
$indexes[]
Definition .dd.php:167
Engine-unabhaengiger, DD-basierter Datenbank-Migrationsrunner.
plan(string $releaseRoot, string $targetVersion)
discover(string $releaseRoot, string $targetVersion='')
rollback(array $state)
Stellt Schema und Daten aller vorbereiteten Tabellen wieder her.
prepare(string $releaseRoot, string $targetVersion, string $backupDirectory)
Erstellt konsistente Tabellenbackups fuer alle ausstehenden Migrationen.
if($failed !==array()) $installer
if((int)( $admin[ 'id'] ?? 0)<=0) if(!dbx() ->patch_local_config('dbxLogin', array('register'=> '0'))) $binding
dbx_lng_current()
Lädt eine Klasse aus dem Cache oder erstellt eine neue Instanz der Klasse.
Definition dbxApi.php:3038
foreach( $iterator as $file) if(! $groups) $expected
if(!defined( 'IMG_WEBP')) define( 'IMG_WEBP'
DBX schema administration.