forked from MetaTunes/ProcessDbMigrate
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDbMigrationPage.class.php
4511 lines (4262 loc) · 185 KB
/
DbMigrationPage.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
namespace ProcessWire;
/*
* Need to allow for possibility of using DefaultPage (if it exists) as the base class so that any user-added methods
* are kept
*/
use Exception;
if(wireClassExists('DefaultPage')) {
class DummyMigrationPage extends DefaultPage {
}
} else {
class DummyMigrationPage extends Page {
}
}
/**
* Class DbMigrationPage
* @package ProcessWire
*
* @property object $migrations The parent page for migration pages
* @property object $migrationTemplate The template for migration pages
* @property string $migrationsPath Path to the folder holding the migrations .json files
* @property string $adminPath Path to the admin root (page id = 2)
* @property object $configData Process module settings
* @property boolean $ready To indicate tha ready() has run
* @property object $dbMigrate ProcessDbMigrate module instance
* @property string $dbName database name set in module config
* @property string $title Title
* @property mixed $dbMigrateRuntimeControl Page status
* @property mixed $dbMigrateRuntimeActions Migration actions
* @property int $dbMigrateLogChanges Toggle: 1 = Log changes, 0 = Sort on save, 2 = Manual
* @property string $dbMigrateFieldTracking Selector for fields to be tracked if 'log changes' is on
* @property string $dbMigrateTemplateTracking Selector for templates to be tracked if 'log changes' is on
* @property string $dbMigratePageTracking Selector for pages to be tracked if 'log changes' is on
* @property string $dbMigrateSummary Summary
* @property string $dbMigrateAdditionalDetails Additional details
* @property RepeaterDbMigrateItemPage $dbMigrateItem Migration item
* @property string $dbMigrateRestrictFields Restrict fields
* @property RepeaterDbMigrateSnippetsPage $dbMigrateSnippets Snippets
* @property mixed $dbMigrateRuntimeReady Hooks etc
*
*/
class DbMigrationPage extends DummyMigrationPage {
// Module constants
/*
* Fields which affect the migration - i.e. they contain key data determining the migration, rather than just information
*
*/
const KEY_DATA_FIELDS = array('dbMigrateItem', 'dbMigrateRestrictFields');
const INFO_ONLY_FIELDS = array('dbMigrateSummary', 'dbMigrateAdditionalDetails', 'dbMigrateSnippets');
/*
* ALL OTHER CONSTANTS ARE SET IN CLASS ProcessDbMigrate
*/
/**
* Create a new DbMigration page in memory.
*
* @param Template $tpl Template object this page should use.
*
*/
public function __construct(Template $tpl = null) {
if(is_null($tpl)) $tpl = $this->templates->get('DbMigration');
parent::__construct($tpl);
}
/**
* Get the data for a migration item
* @return void
*/
public function init() {
$this->dbMigrate->bd('INIT MIGRATION');
}
/**
* Better to put hooks here rather than in ready.php
* This is called from ready() in ProcessDbMigrate.module as that is autoloaded
*
* @throws WireException
*/
public function ready() {
$this->set('adminPath', wire('pages')->get(2)->path);
$this->set('migrations', wire('pages')->get($this->adminPath . ProcessDbMigrate::MIGRATION_PARENT));
$this->set('migrationTemplate', wire('templates')->get(ProcessDbMigrate::MIGRATION_TEMPLATE));
$this->set('migrationsPath', wire('config')->paths->templates . ProcessDbMigrate::MIGRATION_PATH);
$this->set('configData', wire('modules')->getConfig('ProcessDbMigrate'));
$dbMigrate = wire('modules')->get('ProcessDbMigrate');
/* @var $dbMigrate ProcessDbMigrate */
$this->set('dbMigrate', $dbMigrate);
$this->set('dbName', $dbMigrate->dbName());
if(isset($this->configData['suppress_hooks']) && $this->configData['suppress_hooks']) $this->wire()->error("Hook suppression is on - migrations will not work correctly - unset in the module settings.");
// Fix for PW versions < 3.0.152, but left in place regardless of version, in case custom page classes are not enabled
if($this->migrationTemplate->pageClass != __CLASS__) {
$this->migrationTemplate->pageClass = __CLASS__;
$this->migrationTemplate->save();
}
// Omit hooks if $this is null page (e.g. dummy-bootstrap)
if($this->id) {
$this->addHookAfter("Pages::saved(template=$this->migrationTemplate)", $this, 'afterSaved');
$this->addHookBefore("Pages::save(template=$this->migrationTemplate)", $this, 'beforeSaveThis');
$this->addHookBefore("Pages::trash(template=$this->migrationTemplate)", $this, 'beforeTrashThis');
$this->addHookAfter("Pages::trashed(template=$this->migrationTemplate)", $this, 'afterTrashedThis');
$this->addHookAfter("InputfieldFieldset::render", $this, 'afterFieldsetRender');
$readyFile = $this->migrationsPath . '/' . $this->name . '/ready.php';
if(file_exists($readyFile)) include_once $readyFile;
}
$this->dbMigrate->bd($this, 'Migration Page ready');
$this->set('ready', true);
}
/**************************************************
*********** EXPORT SECTION ***********************
*************************************************/
/**
* Export migration data to json files and compare differences
*
* This is run in the 'source' database to export the migration data ($newOld = 'new')
* It is also run in the target database on first installation of a migration to capture the pre-installation state ($newOld = 'old')
* Running with $newOld = 'compare' creates cache files ('new-data.json' and 'old-data.json') for the current state,
* to compare against data.json files in 'new' and 'old' directories
* json files are also created representing the migration page itself
*
* Return an array of items detailing the status and the differences
*
* @param $newOld
* @return array|void|null
* @throws WireException
* @throws WirePermissionException
*
*/
public function exportData($newOld) {
if(!$this->ready) $this->ready();
if(!$this->id) return;
// NB Inputfield::exportConfigData sometimes returns columnwidth = 100 even if it is not set. This hook (removed at the end of the method) tries to fix that
// ToDo A more fundamental fix would be better
$exportHookId = $this->addHookAfter("Inputfield::exportConfigData", function($event) {
$dataIn = $event->arguments(0);
$this->dbMigrate->bd($dataIn, 'dataIn');
$dataOut = $event->return;
if(!isset($dataIn['columnWidth'])
and isset($dataOut['columnWidth']) and $dataOut['columnWidth'] == 100) $dataOut['columnWidth'] = '';
$event->return = $dataOut;
$this->dbMigrate->bd($dataOut, 'dataOut');
});
$this->dbMigrate->bd($this->meta('draft'), 'meta draft');
$directory = $this->migrationsPath;
$this->dbMigrate->bd($directory);
/*
* INITIAL PROCESSING
*/
$this->dbMigrate->bd($this, 'In exportData with newOld = ' . $newOld);
$excludeFields = (isset($this->configData['exclude_fieldnames']))
? str_replace(' ', '', $this->configData['exclude_fieldnames']) : '';
$excludeFields = $this->wire()->sanitizer->array(str_replace(' ', '', $excludeFields), 'fieldName');
$excludeTypes = (isset($this->configData['exclude_fieldtypes']))
? str_replace(' ', '', $this->configData['exclude_fieldtypes']) : '';
$excludeTypes = $this->wire()->sanitizer->array(str_replace(' ', '', $excludeTypes), 'fieldName');
$excludeTypes = array_merge($excludeTypes, ProcessDbMigrate::EXCLUDE_TYPES);
$excludeFieldsForTypes = $this->excludeFieldsForTypes($excludeTypes);
$excludeFields = array_merge($excludeFields, $excludeFieldsForTypes);
$this->dbMigrate->bd($excludeFields, 'excludeFields');
$excludeFieldsBasic = $this->excludeFieldsForTypes(ProcessDbMigrate::EXCLUDE_TYPES);
$this->dbMigrate->bd($this->configData, 'configData in exportData');
$excludeAttributes = (isset($configData['exclude_attributes']))
? str_replace(' ', '', $configData['exclude_attributes']) : '';
$excludeAttributes = $this->wire()->sanitizer->array(str_replace(' ', '', $excludeAttributes));
$excludeAttributes = array_merge($excludeAttributes, ProcessDbMigrate::EXCLUDE_ATTRIBUTES);
$excludeAttributesBasic = ProcessDbMigrate::EXCLUDE_ATTRIBUTES;
$result = null;
$migrationPath = $directory . $this->name . '/';
$migrationPathNewOld = $migrationPath . $newOld . '/';
if($newOld != 'compare') {
if($newOld == 'old' and is_dir($migrationPathNewOld)) return; // Don't over-write old directory once created
if(!is_dir($migrationPathNewOld)) if(!wireMkdir($migrationPathNewOld, true, "0777")) { // wireMkDir recursive
throw new WireException("Unable to create migration directory: $migrationPathNewOld");
}
if(!is_dir($migrationPathNewOld . 'files/')) if(!wireMkdir($migrationPathNewOld . 'files/', true, "0777")) {
throw new WireException("Unable to create migration files directory: {$migrationPathNewOld}bootstrap/");
}
}
/*
* GET DATA FOR THE MIGRATION PAGE ITSELF AND SAVE IN JSON
*/
$item = [];
$item['type'] = 'pages';
$item['action'] = 'changed';
$item['name'] = $this->path;
$item['oldName'] = '';
$migrationData = $this->getMigrationItemData(null, $item, $excludeAttributesBasic, $excludeFieldsBasic, $newOld, 'new')['data'];
$this->dbMigrate->bd($migrationData, 'migrationData');
$this->dbMigrate->bd($this->meta('sourceDb'), 'sourceDb');
if($this->meta('draft') and $this->meta('sourceDb')) {
$migrationData['sourceDb'] = $this->meta('sourceDb');
} else if($this->dbName) {
$migrationData['sourceDb'] = $this->dbName;
}
// Include an item for the site url and admin url as these may be different in the target
$migrationData['sourceSiteUrl'] = $this->wire()->config->urls->site;
$migrationData['sourceAdminUrl'] = $this->wire()->config->urls->admin;
$migrationObjectJson = $this->modifiedJsonEncode($migrationData);
if($newOld != 'compare') {
file_put_contents($migrationPathNewOld . 'migration.json', $migrationObjectJson);
$this->wire()->session->message($this->_('Exported migration definition as ') . $migrationPathNewOld . 'migration.json');
}
/*
* NOW CREATE THE MAIN JSON DATA FILES
*/
if(!$this->meta('draft') or $newOld != 'new') { // meta('draft') denotes draft migration prepared from comparison
/*
* GET DATA FROM PAGE AND SAVE IN JSON
*/
//$itemRepeater = $this->getFormatted('dbMigrateItem'); //getFormatted to get only published items
$itemRepeater = $this->dbMigrateItem->find("status=1");
$this->dbMigrate->bd($itemRepeater, $itemRepeater);
if($newOld == 'new' || $newOld == 'compare') {
$items = $this->cycleItems($itemRepeater, $excludeAttributes, $excludeFields, $newOld, 'new');
$data = $items['data'];
$this->dbMigrate->bd($data, 'data for json');
$files['new'] = $items['files'];
$objectJson['new'] = $this->modifiedJsonEncode($data);
$this->dbMigrate->bd($objectJson['new'], 'New json created');
}
if($newOld == 'old' || $newOld == 'compare') {
$reverseItems = $this->cycleItems($itemRepeater, $excludeAttributes, $excludeFields, $newOld, 'old'); // cycleItems will reverse order for uninstall
$reverseData = $reverseItems['data'];
$files['old'] = $reverseItems['files'];
$objectJson['old'] = $this->modifiedJsonEncode($reverseData);
}
$this->dbMigrate->bd($files, 'files in export data');
$this->dbMigrate->bd($objectJson, '$objectJson ($newOld = ' . $newOld . ')');
if($newOld != 'compare') {
file_put_contents($migrationPathNewOld . 'data.json', $objectJson[$newOld]);
$this->wire()->session->message($this->_('Exported object data as') . ' ' . $migrationPathNewOld . 'data.json');
$this->dbMigrate->bd($files[$newOld], '$files[$newOld]');
foreach($files[$newOld] as $fileArray) {
foreach($fileArray as $id => $baseNames) {
$filesPath = $this->wire('config')->paths->files . $id . '/';
if(!is_dir($migrationPathNewOld . 'files/' . $id . '/')) {
if(!wireMkdir($migrationPathNewOld . 'files/' . $id . '/', true, "0777")) {
throw new WireException("Unable to create migration files directory: {$migrationPathNewOld}files/{$id}/");
}
}
if(is_dir($filesPath)) {
$copyFiles = [];
foreach($baseNames as $baseName) {
$this->dbMigrate->bd($baseName, 'Base name for id ' . $id);
if(is_string($baseName)) {
$copyFiles[] = $filesPath . $baseName;
} else if(is_array($baseName)) {
$copyFiles = array_merge($copyFiles, $baseName);
}
}
$this->dbMigrate->bd($copyFiles, 'copyfiles');
foreach($copyFiles as $copyFile) {
if(file_exists($copyFile)) {
$this->wire()->files->copy($copyFile, $migrationPathNewOld . 'files/' . $id . '/');
$this->message(sprintf($this->_('Copied file %s to '), $copyFile) . $migrationPathNewOld . 'files/' . $id . '/');
} else {
$installType = ($newOld == 'new') ? 'Install' : 'Uninstall'; // $newOld is 'new' or 'old' in this context
$this->error(sprintf($this->_('File %1$s does not exist in this environment. %2$s will not be usable.'), $copyFile, $installType));
}
}
}
}
}
}
/*
* ON CREATION OF OLD JSON FILE, MAKE A COPY OF THE ORIGINAL NEW JSON FILE FOR LATER COMPARISON
* (introduced in version 0.1.0 - migrations created under earlier versions will not have done this and therefore will have more limited scope change checking)
*/
$newFileExists = (file_exists($migrationPath . 'new/data.json'));
if($newFileExists and $newOld == 'old') {
$this->wire()->files->copy($migrationPath . 'new/data.json', $migrationPath . 'old/orig-new-data.json');
}
/*
* COMPARE CURRENT STATE WITH NEW / OLD STATES
*/
if($newOld == 'compare') {
$cachePath = $this->wire()->config->paths->assets . 'cache/dbMigrate/';
if(!is_dir($cachePath)) if(!wireMkdir($cachePath, true, "0777")) { // wireMkDir recursive
throw new WireException("Unable to create cache migration directory: $cachePath");
}
if($data and $objectJson) {
$this->dbMigrate->bd($migrationPath, 'migrationPath');
/*
* Get file data
*/
$newFile = (file_exists($migrationPath . 'new/data.json'))
? file_get_contents($migrationPath . 'new/data.json') : null;
$oldFile = (file_exists($migrationPath . 'old/data.json'))
? file_get_contents($migrationPath . 'old/data.json') : null;
file_put_contents($cachePath . 'old-data.json', $objectJson['old']);
file_put_contents($cachePath . 'new-data.json', $objectJson['new']);
$this->dbMigrate->bd($newFile, 'New file');
$newArray = $this->compactArray(wireDecodeJSON($newFile));
$this->dbMigrate->bd($newArray, 'newArray');
$oldArrayFull = wireDecodeJSON($oldFile);
$oldArray = $this->compactArray($oldArrayFull);
$this->dbMigrate->bd('New compare');
$cmpArray['new'] = $this->compactArray(wireDecodeJSON($objectJson['new']));
$this->dbMigrate->bd($cmpArray['new'], 'cmpArray');
$cmpArrayFull['old'] = wireDecodeJSON($objectJson['old']);
$cmpArray['old'] = $this->compactArray($cmpArrayFull['old']);
/*
* Compare 'new' data
*/
$this->dbMigrate->bd('new data');
$R = $this->array_compare($newArray, $cmpArray['new']);
$R = $this->pruneImageFields($R, 'new');
$this->dbMigrate->bd($R, ' array compare new->cmp');
$this->dbMigrate->bd($this->modifiedJsonEncode($R), ' array compare json new->cmp');
$installedData = (!$R);
$installedDataDiffs = $R;
/*
* Compare 'old' data
*/
$this->dbMigrate->bd('old data');
$R2 = $this->array_compare($oldArray, $cmpArray['old']);
$R2 = $this->pruneImageFields($R2, 'old');
$this->dbMigrate->bd($R2, ' array compare old->cmp');
$this->dbMigrate->bd($this->modifiedJsonEncode($R2), ' array compare json old->cmp');
$uninstalledData = (!$R2);
$uninstalledDataDiffs = $R2;
/*
* Finally compare the total difference between old and new files if both files are present
*/
$this->dbMigrate->bd('total data');
if($newFile and $oldFile) {
$R3 = $this->array_compare($newArray, $oldArray);
$R3 = $this->pruneImageFields($R3, 'both');
$reviewedDataDiffs = $R3;
} else {
$reviewedDataDiffs = [];
}
/*
* DETECT ANY SCOPE CHANGES AS EVIDENCED IN THE NEW/DATA.JSON FILE
*/
$origNewFile = (file_exists($migrationPath . 'old/orig-new-data.json'))
? file_get_contents($migrationPath . 'old/orig-new-data.json') : null;
$origNewArray = $this->compactArray(wireDecodeJSON($origNewFile));
$scopeDiffs = [];
$scopeChange = false;
/*
* For migrations set using v0.1.0 or later, an 'orig-new-data' json file should have been copied at installation time
* Check the current 'new' data.json to see if the scope generated is different form the scope on initial installation
*/
if($origNewFile and $newFile) {
$this->dbMigrate->bd(['new' => $newArray, 'orig' => $origNewArray], 'new and orig');
$scopeDiffs = array_diff_key($newArray, $origNewArray); // array
$scopeChange = (count($scopeDiffs) > 0); // Boolean
}
} else {
$installedData = true;
$uninstalledData = true;
$installedDataDiffs = [];
$uninstalledDataDiffs = [];
$reviewedDataDiffs = [];
$scopeDiffs = [];
$scopeChange = false;
}
/*
* MIGRATION COMPARISON
*/
// migration.json is just a single page, so json differs from data.json by missing square brackets.
// Add the square brackets to the migration.json so that we can use the same compactArray() method as we use for data.json
if($this->data) {
$newMigFile = (file_exists($migrationPath . 'new/migration.json'))
? '[' . file_get_contents($migrationPath . 'new/migration.json') . ']' : null;
$oldMigFile = (file_exists($migrationPath . 'old/migration.json'))
? '[' . file_get_contents($migrationPath . 'old/migration.json') . ']' : null;
if($migrationObjectJson) {
file_put_contents($cachePath . 'migration.json', $migrationObjectJson);
} else {
if(is_dir($cachePath) and file_exists($cachePath . 'migration.json'))
unlink($cachePath . 'migration.json');
}
$cmpMigFile = (file_exists($cachePath . 'migration.json'))
? '[' . file_get_contents($cachePath . 'migration.json') . ']' : null;
$this->dbMigrate->bd('new migration');
$R = $this->array_compare($this->compactArray(wireDecodeJSON($newMigFile)), $this->compactArray(wireDecodeJSON($cmpMigFile)));
$R = $this->pruneImageFields($R, 'new');
$installedMigration = (!$R);
$installedMigrationDiffs = $R;
$this->dbMigrate->bd('old migration');
$R2 = $this->array_compare($this->compactArray(wireDecodeJSON($oldMigFile)), $this->compactArray(wireDecodeJSON($cmpMigFile)));
$R2 = $this->pruneImageFields($R2, 'old');
$uninstalledMigration = (!$R2);
$uninstalledMigrationDiffs = $R2;
// This comparison only looks at the migration elements that affect the database
if($oldMigFile) {
$this->dbMigrate->bd('migration key only');
$R3 = $this->array_compare($this->compactArray(wireDecodeJSON($oldMigFile), true),
$this->compactArray(wireDecodeJSON($cmpMigFile), true));
$R3 = $this->pruneImageFields($R3, 'old');
$uninstalledMigrationKey = (!$R3);
$uninstalledMigrationKeyDiffs = $R3;
} else {
$uninstalledMigrationKey = true;
$uninstalledMigrationKeyDiffs = [];
}
} else {
$installedMigration = true;
$uninstalledMigration = true;
$installedMigrationDiffs = [];
$uninstalledMigrationDiffs = [];
$uninstalledMigrationKey = true;
$uninstalledMigrationKeyDiffs = [];
}
/*
* REPORT THE STATUS
*/
$installed = ($installedData and $installedMigration);
$uninstalled = ($uninstalledData and $uninstalledMigration);
$locked = ($this->meta('locked'));
if($this->meta('installable')) {
if($installed) {
if($uninstalled) {
$status = 'void';
} else {
$status = 'installed';
}
} else if($uninstalled) {
$status = 'uninstalled';
} else if($locked) {
$status = 'superseded';
} else {
$status = 'indeterminate';
}
} else {
if($installed) {
$status = 'exported';
} else if($locked) {
$status = 'superseded';
} else {
$status = 'pending';
}
}
$result = [
'status' => $status,
'scopeChange' => $scopeChange,
'scopeDiffs' => $scopeDiffs,
'installed' => $installed,
'uninstalled' => $uninstalled,
'installedData' => $installedData,
'uninstalledData' => $uninstalledData,
'installedDataDiffs' => $installedDataDiffs,
'uninstalledDataDiffs' => $uninstalledDataDiffs,
'installedMigration' => $installedMigration,
'installedMigrationDiffs' => $installedMigrationDiffs,
'uninstalledMigration' => $uninstalledMigration,
'uninstalledMigrationDiffs' => $uninstalledMigrationDiffs,
'uninstalledMigrationKey' => $uninstalledMigrationKey,
'uninstalledMigrationKeyDiffs' => $uninstalledMigrationKeyDiffs,
'reviewedDataDiffs' => $reviewedDataDiffs,
'timestamp' => $this->wire()->datetime->date()
];
$this->dbMigrate->bd($result, 'result in exportData');
}
$this->meta('installedStatus', $result);
if(!$this->meta('installable') and $newOld == 'new')
$this->wire()->pages->___save($this, array('noHooks' => true, 'quiet' => true)); // to ensure reload after export
// Remove the hook that was added at the start of this method
$this->removeHook($exportHookId);
return $result;
}
}
public function modifiedJsonEncode($data) {
$json = wireEncodeJSON($data, true, true);
$json = str_replace('\t', ' ', $json);
$this->dbMigrate->bd($json, 'modified json');
return $json;
}
/**
* Cycle through items in a migration and get the data for each
* If $compareType is 'old' then reverse the order and swap 'new' and 'removed' actions
*
* @param $itemRepeater // The migration item
* @param $excludeAttributes
* @param $excludeFields
* @param $newOld // 'new'. 'old', or 'compare'
* @param $compareType // 'new' to create install data; 'old' to create uninstall data
* @return array|array[]
* @throws WireException
* @throws WirePermissionException
*
*/
protected function cycleItems($itemRepeater, $excludeAttributes, $excludeFields, $newOld, $compareType) {
$data = [];
$count = 0;
$item = [];
$files = [];
if(!$itemRepeater || $itemRepeater->isUnpublished) return ['data' => '', 'files' => ''];
if($compareType == 'old') $itemRepeater = $itemRepeater->reverse();
foreach($itemRepeater as $repeaterItem) {
/* @var $repeaterItem RepeaterDbMigrateItemPage */
$item = $this->populateItem($repeaterItem, ($compareType == 'old')); // swap new and removed if compareType is 'old'
$this->dbMigrate->bd($item, 'item');
$count++;
$migrationItem = $this->getMigrationItemData($count, $item, $excludeAttributes, $excludeFields, $newOld, $compareType);
$data[] = $migrationItem['data'];
$this->dbMigrate->bd($migrationItem['files'], 'migrationItem files');
$files = array_merge_recursive($files, $migrationItem['files']);
$this->dbMigrate->bd($files, 'files at end of cycleItems');
}
$this->dbMigrate->bd($data, 'data returned by cycleItems for ' . $newOld);
return ['data' => $data, 'files' => $files];
}
/**
* Get the migration data for an individual item
*
* @param $k // The sequence number (starting at 0) of the migration item within the migration
* @param $item
* @param $excludeAttributes
* @param $excludeFields
* @param $newOld // 'new'. 'old', or 'compare'
* @param $compareType // 'new' to create install data; 'old' to create uninstall data
* @return array
* @throws WireException
* @throws WirePermissionException
*
*/
protected function getMigrationItemData($k, $item, $excludeAttributes, $excludeFields, $newOld, $compareType) {
/*
* Initial checks
*/
$files = [];
$empty = ['data' => [], 'files' => []];
$item['action'] = (isset($item['action'])) ? $item['action'] : 'changed';
if(!$item['type'] or !$item['name']) {
if($newOld == 'new' and $compareType == 'new') {
$this->wire()->session->warning($this->_('Missing values for item ') . $k);
$this->dbMigrate->bd($item, 'missing values in item');
}
return $empty;
}
if(!$this->id) return $empty;
$itemName = $item['name']; // This will be the name in the source environment
$this->dbMigrate->bd($itemName, 'itemName');
/*
* Convert selectors into individual items where they exist in the current database
*/
$expanded = $this->expandItem($item); // $expanded['items'] is the list of items derived from the selector
$selector = ($expanded['selector']); // true if original item was a selector
$old = $expanded['old']; // Not currently used
if($selector and $item['oldName']) {
$this->wire()->session->warning($this->_('Old name is not applicable when a selector is used. It will be treated as being blank.'));
$item['oldName'] = '';
}
/*
* Check old names (where name is not a selector)
*/
if(!$selector) {
if($item['oldName']) {
$isOld = $this->wire($item['type'])->get($item['oldName']);
$isNew = $this->wire($item['type'])->get($item['name']);
if($isNew and $isNew->id and $isOld and $isOld->id) {
$this->wire()->session->warning(sprintf($this->_('Both new name (%1$s) and old name (%2$s) exist in the database. Please use unique names.'), $item['name'], $item['oldName']));
return $empty;
}
if($isOld and $isOld->id) {
$itemName = $item['oldName'];
$this->dbMigrate->bd($item['oldName'], 'using old name');
} else if(!$isNew or !$isNew->id) {
$this->wire()->session->warning(sprintf($this->_('Neither new name (%1$s) nor old name (%2$s) exist in the database.'), $item['name'], $item['oldName']));
return $empty;
}
}
}
/*
* Should anything have been found and was it?
*/
$expandedItems = $expanded['items']; // the list of items derived from the selector (or just from the name/path)
$noFind = (count($expandedItems) == 0); // name/path or selector yields no results
$shouldExist = $this->shouldExist($item['action'], $compareType); //should the item exist as a database object in this context?
$this->dbMigrate->bd(['item' => $item, 'compareType' => $compareType, 'shouldExist' => $shouldExist, 'noFind' => $noFind, 'OK' => ($shouldExist xor $noFind)], "Test existence");
if(!$this->meta('installable')) { // i.e. we are in the source database
if($noFind and $shouldExist) $this->wire()->session->warning($this->name . ': ' .
sprintf($this->_('No %s object for '), $item['type']) . $itemName);
if(!$noFind and !$shouldExist) $this->wire()->session->warning("{$this->name}: " .
sprintf($this->_('There is already a %1s object for %2s but none should exist'), $item['type'], $itemName));
}
/*
* Where there is nothing in this database for the item, just return the array keys with no values
*/
if($noFind and ($item['action'] == 'removed' or $item['action'] == 'new')) {
$data = [$item['type'] => [$item['action'] => [$itemName => []]]];
$this->dbMigrate->bd($data, 'Returning data for new/removed which do not exist in current db');
// $flag indicates which actions should have associated item in the current database
if($this->meta('installable')) { // target database
$flag = ($compareType == 'new') ? 'removed' : 'new';
} else { // source database
$flag = 'new';
}
$this->dbMigrate->bd(['action' => $item['action'], 'newOld' => $newOld, 'compareType' => $compareType, 'flag' => $flag]);
if(isset($flag) && $item['action'] == $flag) {
$this->dbMigrate->bd(['action' => $item['action'], 'flag' => $flag], 'Reporting exception');
$this->dbMigrate->bd(debug::backtrace(), 'backtrace');
$this->wire()->session->warning(sprintf($this->_('Selector "%s" did not select any items'), $item['name']));
}
return ['data' => $data, 'files' => []];
}
if($noFind) {
// 'changed' items should exist in all contexts
$this->dbMigrate->bd('No object for ' . $itemName . '.');
$this->wire()->session->warning($this->name . ': ' . sprintf($this->_('No %s object for '), $item['type']) . $itemName);
return $empty;
}
/*
* If we got this far, then we should have found some matching objects in the current database
* So get the export data for them
*/
$objectData = [];
foreach($expandedItems as $expandedItem) {
$this->dbMigrate->bd($expandedItem, 'expandedItem');
$object = $expandedItem['object'];
// (For non-draft migrations) check object existence if migration is not exported/installed (draft migrations are created from comparisons)
if(!$this->meta('draft') and (!$this->meta('installedStatus') or !$this->meta('installedStatus')['installed'])) {
if(!$object or !$object->id or $object->id == 0 and $newOld != 'compare') {
if($shouldExist) $this->wire()->session->warning($this->name . ': ' .
sprintf($this->_('No %s object for '), $item['type']) . $itemName);
$data = [$item['type'] => [$item['action'] => [$itemName => []]]];
return ['data' => $data, 'files' => []];
} else if(!$shouldExist and $newOld == 'new') { // 2nd condition is to avoid double reporting for new and old
$this->wire()->session->warning("{$this->name}: " .
sprintf($this->_('There is already a %1s object for %2s but none should exist'), $item['type'], $itemName));
}
}
/*
* GET DATA FOR ITEMS IN THE DATABASE
*/
$name = $expandedItem['name'];
$oldName = $expandedItem['oldName'];
$key = ($oldName) ? $name . '|' . $oldName : $name;
if($item['type'] == 'pages') {
$exportObjects = $this->getExportPageData($k, $key, $object, $excludeFields);
} else {
$exportObjects = $this->getExportStructureData($k, $key, $item, $object, $excludeAttributes, $newOld, $compareType);
}
$objectData = array_merge($objectData, $exportObjects['data']);
$this->dbMigrate->bd($exportObjects['files'], '$exportObjects[files]');
$files = array_merge($files, $exportObjects['files']);
$this->dbMigrate->bd($objectData, 'object data');
}
/*
* Return the result
*/
$data = [$item['type'] => [$item['action'] => $objectData]];
return ['data' => $data, 'files' => $files];
}
/**
* Return a list of all fields of the given types
* (used to convert excluded types into excluded names)
*
* @param array $types
* @return array
* @throws WireException
*
*/
protected function excludeFieldsForTypes(array $types) {
$fullTypes = [];
foreach($types as $type) {
$fullTypes[] = (!strpos($type, 'Fieldtype')) ? 'Fieldtype' . $type : $type;
}
$exclude = [];
$fields = $this->wire()->fields->getAll();
foreach($fields as $field) {
if(in_array($field->type->name, $fullTypes)) $exclude[] = $field->name;
if(!is_object($field)) throw new WireException("bad field $field");
}
return $exclude;
}
/**
* Take migration items (provided as an array of attributes) and expand any which are selectors
* Return the (expanded) item object(s) and name(s) in an array in format
* ['selector' => true/false, 'old' => true/false, 'items' => ['object' => object, 'name' => name, 'oldName' => oldName]]
* where 'selector' denotes that the name was a selector and 'old' denotes that the 'oldName' has been necessary to find the object
*
* @param $itemArray
* @return array
* @throws WireException
*
*/
public function expandItem($itemArray) {
/* @var $item RepeaterDbMigrateItemPage */
$this->dbMigrate->bd($itemArray, 'In expandItem with ' . $itemArray['name']);
$type = $itemArray['type'];
$result = [];
$result['selector'] = false;
$result['old'] = false;
$empty = ['selector' => false, 'old' => false, 'items' => []];
if($type == 'pages') {
$testName = $this->wire()->sanitizer->path($itemArray['name']);
$nameType = 'path';
} else {
$testName = $this->wire()->sanitizer->validate($itemArray['name'], 'name');
$nameType = 'name';
}
if(!$testName) {
$this->dbMigrate->bd($itemArray['name'], 'Selector provided instead of path/name');
// we have a selector
try {
if($type == 'pages') {
$objects = $this->wire($type)->find($itemArray['name'] . ", include=all"); // $itemArray['name'] is a selector
} else {
$objects = $this->wire($type)->find($itemArray['name']);
}
// for pages, sort by path if required
$this->dbMigrate->bd($itemArray['name'], 'Name for sort path test');
$objects = $objects->getArray(); // convert to plain array
$this->dbMigrate->bd($objects);
if($type == 'pages' and strpos($itemArray['name'], 'sort=path')) {
usort($objects, function($a, $b) {
return strnatcmp($a->path, $b->path);
});
}
$result['items'] = [];
foreach($objects as $object) {
if($object->id) $result['items'][] = ['object' => $object, 'name' => $object->$nameType, 'oldName' => ''];
}
$result['selector'] = true;
} catch(WireException $e) {
$this->dbMigrate->bd($itemArray, 'invalid selector');
$this->wire()->session->error($this->_('Invalid selector: ') . $itemArray['name']);
return $empty;
}
} else {
try {
$result['items'] = [];
$oldName = ($itemArray['oldName']) ?: $itemArray['name'];
$object = $this->wire($type)->get($itemArray['name']);
if(!$object or !$object->id) {
/*
* Hack introduced to deal with failure to get bootstrap in earlier PW versions
* (see https://processwire.com/talk/topic/25940-issue-with-pages-getpathname-in-pw30148/?tab=comments#comment-216359)
*/
$s1 = basename($itemArray['name']);
$s2 = dirname($itemArray['name']);
$object = $this->wire()->pages->get("name=$s1, parent=$s2");
}
if(!$object or !$object->id) {
$object = $this->wire($type)->get($oldName);
$result['old'] = true;
}
if($object and $object->id)
$result['items'] = [['object' => $object, 'name' => $itemArray['name'], 'oldName' => $itemArray['oldName']]];
} catch(WireException $e) {
$this->dbMigrate->bd($itemArray, 'invalid name/path');
$this->wire()->session->error($this->_('Invalid name/path: ') . $itemArray['name']);
return $empty;
}
}
$this->dbMigrate->bd($result, 'expansion result');
return $result;
}
/**
* Determine whether or not an item should exist in the current database
* * Changed items should exist in source and target
* * New items should exist in the source but not the target
* * Removed items should exist in the target but not the source
*
* Note that if $this is 'installable' ($this->meta('installable) ) then we are in its target database, else we are in its source database
*
* If $compareType is 'new' then we are using the migration items as defined in the page
* If $compareType is 'old' then we are using the mirror terms (reverse order and 'new' and 'removed' swapped)
*
* @param $compareType
* @param $action
* @return boolean
*
*/
protected function shouldExist($action, $compareType) {
if($action == 'changed') return true;
$actInd = ($action == 'new') ? 1 : 0;
$newInd = ($compareType == 'new') ? 1 : 0;
$sourceInd = (!$this->meta('installable')) ? 1 : 0;
$ind = $actInd + $newInd + $sourceInd;
return ($ind & 1); // test if odd by bit checking.
}
/**
* Get array of page data, with some limitations to prevent unnecessary mismatching
*
* @param $k
* @param $key
* @param $exportPage
* @param $excludeFields
* @return array[]|null[]
* @throws WireException
*
*/
protected function getExportPageData($k, $key, $exportPage, $excludeFields) {
if($k !== null) { // $k=null for migration page itself. Restrict fields must not operate on migration page.
$restrictFields = $this->restrictFields();
} else {
$restrictFields = [];
}
$data = array();
$files = array();
$oldPage = '';
$repeaterPages = array();
$this->dbMigrate->bd($exportPage, 'exportpage');
if(!$exportPage
or !is_a($exportPage, 'Processwire\Page')
or !$exportPage->id)
return ['data' => [], 'files' => [], 'repeaterPages' => []];
// Now we are sure we have a page object we can continue
$attrib = [];
$attrib['template'] = $exportPage->template->name;
$attrib['parent'] = ($exportPage->parent->path) ?: $exportPage->parent->id; // id needed in case page is root (will be 0)
$attrib['status'] = $exportPage->status;
$attrib['name'] = $exportPage->name;
$attrib['id'] = $exportPage->id;
$this->getAllFieldData($exportPage, $restrictFields, $excludeFields, $attrib, $files);
foreach($excludeFields as $excludeField) {
unset($attrib[$excludeField]);
}
$data[$key] = $attrib;
$this->dbMigrate->bd($data, 'returning data');
return ['data' => $data, 'files' => $files, 'repeaterPages' => $repeaterPages];
}
/**
* Get array of restricted fields - i.e. only these fields to be considered in migrating pages
*
* @return array
* @throws WireException
*/
public function restrictFields() {
if(!$this->dbMigrateRestrictFields) return [];
$restrictFields = array_filter($this->wire()->sanitizer->array(
str_replace(' ', '', $this->dbMigrateRestrictFields),
'fieldName',
['delimiter' => ','])
);
return $restrictFields;
}
/**
* @param $exportPage Page
* @param $restrictFields array
* @param $excludeFields array
* @param $attrib array
* @param $files array
* @param $fresh boolean Use fresh pages from DB throughout
* @return void
*/
public function getAllFieldData($exportPage, $restrictFields, $excludeFields, &$attrib, &$files, $fresh = false) {
if(!$exportPage || !$exportPage->id) return;
foreach($exportPage->getFields() as $field) {
$name = $field->name;
$this->dbMigrate->bd($restrictFields, '$restrictFields');
if((count($restrictFields) > 0 && !in_array($name, $restrictFields)) || in_array($name, $excludeFields)) continue;
$exportPageDetails = $this->getFieldData($exportPage, $field, $restrictFields, $excludeFields, $fresh);
$attrib = array_merge_recursive($attrib, $exportPageDetails['attrib']);
$this->dbMigrate->bd([$exportPage, $field, $attrib], 'exportPage, field, attrib in getAllFieldData');
$files[] = $exportPageDetails['files'];
$this->dbMigrate->bd($files, 'files in getAllFieldData');
}
}
/**
* Get field data (as an array) for a page field
* NB Repeater fields cause this method to be called recursively
*
* @param $page
* @param $field
* @param array $restrictFields
* @param array $excludeFields
* @param bool $fresh Use fresh pages from DB throughout
* @return array
*
*/
public function getFieldData($page, $field, $restrictFields = [], $excludeFields = [], $fresh = false) {
$attrib = [];
$files = [];
$name = $field->name;
if($fresh) $page = $this->wire()->pages->getFresh($page->id);
if(!$page->data($name)) $page->set($name, $page->$field);
// $page = $this->pages()->findJoin("id={$page->id}", "$name")->first();
if(!$page->hasField($name)) { // NB changed from (1) if(!$page->data($name) and then from (2) $page->data($name) === null. Review options if this causes probs, but remember need to return empty values if there is no item
// NB (2) Must be === otherwise items with value=0 get discarded as if they had no value and cause mismatch errors in target
$this->dbMigrate->bd([$page, $name], 'returning empty');
return ['attrib' => $attrib, 'files' => $files];
}
switch($field->type) {
case 'FieldtypePage' :
// case 'FieldtypePageTable' :
$attrib[$name] = $this->getPageRef($page->$field);
break;
case 'FieldtypePageTable' : // NB Not sure why I replaced the above with this, which returns name and parent path separately
$contents = [];
foreach($page->$field as $items) {
$this->dbMigrate->bd($item, 'pagetable item');
$contents['items'] = [];
$items = $page->$field->getArray();
foreach($items as $item) {
$contents['items'][] = ['name' => $item['name'], 'parent' => $item['parent']->path];
}
$attrib[$name] = $contents;
}
break;
case 'FieldtypeFields':
$contents = [];
foreach($page->$field as $fId) {
$f = $this->wire->fields->get($fId);
$contents[] = $f->name;
}
$attrib[$name] = $contents;
break;
case 'FieldtypeTemplates' :
$contents = [];
foreach($page->$field as $tId) {
$t = $this->wire->templates->get($tId);
$contents[] = $t->name;
}
$attrib[$name] = $contents;
break;
case 'FieldtypeImage' :
case 'FieldtypeFile' :
$contents = [];
$contents['url'] = ($page->$field && $page->$field->url) ? $page->$field->url : '';
$contents['path'] = ($page->$field && $page->$field->path) ? $page->$field->path : '';
$contents['items'] = [];
$contents['custom_fields'] = [];
if($page->$field && (get_class($page->$field) == "ProcessWire\Pageimage" || get_class($page->$field) == "ProcessWire\Pagefile")) {
$items = [$page->$field]; // need it to be an array if only singular image or file
} else {
$items = ($page->$field) ? $page->$field->getArray() : [];
}
$files[$page->id] = [];
foreach($items as $item) {
$itemArray = $item->getArray();
// don't want these in item as they won't necessarily match in target system
unset($itemArray['modified']);
unset($itemArray['created']);
unset($itemArray['modified_users_id']);
unset($itemArray['created_users_id']);
unset($itemArray['formatted']);
// If there are custom fields, capture these separately
$imageTemplate = $this->wire()->templates->get('field-' . $name);
if($imageTemplate) {
$imageTemplateName = $imageTemplate->name;
$itemArray['custom_fields']['template'] = $imageTemplateName;
$templateItems = $imageTemplate->fieldgroup;
foreach($templateItems as $templateItem) {
$templateItemName = $templateItem->name;
$itemArray['custom_fields']['items'][$templateItemName] = $item->$templateItemName;
}
}
$contents['items'][] = $itemArray; // sets unremoved items - basename, description, tags, filesize, as well as any custom fields
//
if($field->type == 'FieldtypeImage') {
$files[$page->id] = array_merge($files[$page->id], $item->getVariations(['info' => true, 'verbose' => false]));
}