├── .editorconfig ├── RockMigration.class.php ├── LICENSE ├── examples └── FooConfig.php ├── RockAdminTools.module.php ├── README.md └── RockMigrations.module.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | 7 | [*.{php,inc,module,js,css,less,scss}] 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /RockMigration.class.php: -------------------------------------------------------------------------------- 1 | object->getMigrations(); 18 | $current = 0; 19 | foreach($migrations as $i=>$item) { 20 | if($item == $this->version) $current = $i; 21 | } 22 | return $current > 0 ? $this->object->getMigration($migrations[$current-1]) : null; 23 | } 24 | 25 | /** 26 | * Get next migration. 27 | * 28 | * @return void 29 | */ 30 | public function getNext() { 31 | $migrations = $this->object->getMigrations(); 32 | $current = 0; 33 | foreach($migrations as $i=>$item) { 34 | if($item == $this->version) $current = $i; 35 | } 36 | return $current < count($migrations)-1 ? $this->object->getMigration($migrations[$current+1]) : null; 37 | } 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bernhard Baumrock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/FooConfig.php: -------------------------------------------------------------------------------- 1 | function(RockMigrations $rm) { 4 | $rm->deletePage("/foos"); 5 | $rm->removeFieldsFromTemplate(['foo', 'images'], "bar"); 6 | }, 7 | 'fields' => [ 8 | 'foo' => [ 9 | 'type' => 'text', 10 | 'label' => 'foo field label', 11 | 'tags' => 'RMSample', 12 | ], 13 | 'bar' => [ 14 | 'type' => 'textarea', 15 | 'tags' => 'RMSample', 16 | ], 17 | ], 18 | 'templates' => [ 19 | 'foos' => [ 20 | 'childTemplates' => ['foo'], 21 | 'tags' => 'RMSample', 22 | 'icon' => 'bolt', 23 | 'parentTemplates' => ['home'], 24 | 'fields' => ['title'], 25 | ], 26 | 'bars' => [ 27 | 'childTemplates' => ['bar'], 28 | 'tags' => 'RMSample', 29 | 'icon' => 'bolt', 30 | 'parentTemplates' => ['home'], 31 | 'fields' => ['title'], 32 | ], 33 | 'foo' => [ 34 | 'label' => 'foo template', 35 | 'tags' => 'RMSample', 36 | 'icon' => 'check', 37 | 'parentTemplates' => ['foos'], 38 | 'fields' => [ 39 | 'foo' => [ 40 | 'label' => 'foo label on foo template', 41 | 'columnWidth' => 50, 42 | ], 43 | 'bar' => [ 44 | 'label' => 'bar label on foo template', 45 | 'columnWidth' => 50, 46 | ], 47 | ] 48 | ], 49 | 'bar' => [ 50 | 'label' => 'bar template', 51 | 'tags' => 'RMSample', 52 | 'parentTemplates' => ['bars'], 53 | 'fields' => [ 54 | 'bar' => [ 55 | 'label' => 'bar label on bar template', 56 | ], 57 | ] 58 | ], 59 | ], 60 | 'pages' => [ 61 | // foo pages 62 | 'foos' => [ 63 | 'title' => "foos page", 64 | 'template' => "foos", 65 | 'parent' => "/", 66 | 'status' => ['hidden', 'locked'], 67 | ], 68 | 'foo1' => [ 69 | 'template' => "foo", 70 | 'parent' => "/foos", 71 | ], 72 | 'foo2' => [ 73 | 'template' => "foo", 74 | 'parent' => "/foos", 75 | ], 76 | 77 | // bar pages 78 | 'bars' => [ 79 | 'title' => "bars page", 80 | 'template' => "bars", 81 | 'parent' => "/", 82 | 'status' => ['hidden', 'locked'], 83 | ], 84 | 'bar1' => [ 85 | 'template' => "bar", 86 | 'parent' => "/bars", 87 | ], 88 | 'bar2' => [ 89 | 'template' => "bar", 90 | 'parent' => "/bars", 91 | ], 92 | ], 93 | ]; 94 | -------------------------------------------------------------------------------- /RockAdminTools.module.php: -------------------------------------------------------------------------------- 1 | 'RockAdminTools', 14 | 'version' => '0.0.1', 15 | 'summary' => 'Tools for the PW backend', 16 | 'autoload' => true, 17 | 'singular' => true, 18 | 'icon' => 'bolt', 19 | 'requires' => [], 20 | 'installs' => [], 21 | ]; 22 | } 23 | 24 | public function init() { 25 | $this->addHook("InputfieldWrapper::fieldset", $this, "wrapFieldsIntoFieldset"); 26 | } 27 | 28 | /** 29 | * Add a fieldset to the form and add listed Inputfields 30 | * 31 | * Usage: 32 | * $form->fieldset([ 33 | * 'label' => 'My Fieldset', 34 | * 'fields' => [ 35 | * 'foo' => ['label' => 'foo field', 'columnWidth' => 33], 36 | * 'bar' => ['label' => 'bar field', 'columnWidth' => 33], 37 | * ], 38 | * ]); 39 | * 40 | * If a listed field does not exist in the form it will be skipped silently 41 | * 42 | * @return void 43 | */ 44 | public function wrapFieldsIntoFieldset(HookEvent $event) { 45 | $wrapper = $event->object; /** @var InputfieldWrapper $wrapper */ 46 | $data = $event->arguments(0); 47 | $fields = array_key_exists('fields', $data) ? $data['fields'] : []; 48 | unset($data['fields']); 49 | 50 | /** @var InputfieldFieldset $fs */ 51 | $fs = $this->wire('modules')->get('InputfieldFieldset'); 52 | foreach($data as $k=>$v) $fs->$k = $v; 53 | 54 | // where to add the fieldset? 55 | if($before = $event->arguments(1)) { 56 | $field = $wrapper->get((string)$before); 57 | if($field) $wrapper->insertBefore($fs, $field); 58 | } 59 | elseif($after = $event->arguments(2)) { 60 | $field = $wrapper->get((string)$after); 61 | if($field) $wrapper->insertAfter($fs, $field); 62 | } 63 | else $wrapper->add($fs); 64 | 65 | // add fields 66 | foreach($fields as $k=>$v) { 67 | if(is_int($k)) { 68 | // field was applied as simple string 69 | $f = $wrapper->get($v); 70 | } 71 | else { 72 | // we got a field plus custom settings 73 | // this makes it easy to adjust columnWidth, label, etc 74 | $f = $wrapper->get($k); 75 | if($f) { 76 | // set all dynamic properties 77 | foreach($v as $prop=>$val) $f->$prop = $val; 78 | } 79 | } 80 | 81 | if($f) { 82 | // add field to fieldset and remove it from the form 83 | $wrapper->remove($f); 84 | $fs->add($f); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Config inputfields 91 | * @param InputfieldWrapper $inputfields 92 | */ 93 | public function getModuleConfigInputfields($inputfields) { 94 | return $inputfields; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RockMigrations Module 2 | 3 | Support Board Link: https://processwire.com/talk/topic/21212-rockmigrations-easy-migrations-from-devstaging-to-live-server/ 4 | 5 | ## Why Migrations? 6 | 7 | Benjamin Milde wrote a great blog post about that: https://processwire.com/blog/posts/introduction-migrations-module/ 8 | 9 | ## Why another migrations module? 10 | 11 | I just didn't like the way the other module works. You need to create a file for every migration that you want to apply (like creating a new field, template, etc). With RockMigrations the goal is to make most of the necessary changes a 1-liner that you can add to any file you want (meaning that you can use RockMigrations in any of your modules). 12 | 13 | ## Example 14 | 15 | See this example of how it works and how easy it is to use. Let's start with a very simple Migration that only creates (on upgrade) or deletes (on downgrade) one field: 16 | 17 | ```php 18 | // See section DETAILS below 19 | $upgrade = function(RockMigrations $rm) { 20 | $rm->createField('yournewfield', 'text'); 21 | }; 22 | 23 | $downgrade = function(RockMigrations $rm) { 24 | $rm->deleteField('yournewfield'); 25 | }; 26 | ``` 27 | 28 | ## Migration config files 29 | 30 | Here's an example of a simple Migration using array syntax: 31 | 32 | ```php 33 | $modules->get('RockMigrations')->migrate([ 34 | 'fields' => [ 35 | 'cke' => [ 36 | 'type' => 'textarea', 37 | 'inputfieldClass' => 'InputfieldCKEditor', 38 | 'textformatters' => ['TextformatterEntities'], 39 | 'contentType' => 1, // html 40 | ], 41 | ], 42 | 'templates' => [ 43 | 'ckeditor-example-template' => [ 44 | 'icon' => 'align-left', 45 | 'noChildren' => 1, // no children allowed 46 | 'parentTemplates' => ['home'], 47 | 'fields' => ['cke'], 48 | ], 49 | ], 50 | ]); 51 | ``` 52 | 53 | You can also define a php file that returns an array: 54 | 55 | ```php 56 | $rm = $modules->get('RockMigrations'); 57 | $rm->migrate($config->paths($rm)."examples/FooConfig.php"); 58 | ``` 59 | 60 | See the shipped FooConfig.php file for what is possible. 61 | 62 | Using the latter option (`migrate()`) instead of a single file migration that defines an `upgrade` and `downgrade` function is easier as long as you do not need to **remove** data from your system. Take this example: 63 | 64 | ```php 65 | $rm->migrate([ 66 | 'fields' => [ 67 | 'foo' => ['type'=>'text'], 68 | 'bar' => ['type'=>'text'], 69 | ], 70 | ]); 71 | ``` 72 | 73 | What if you wanted to remove the `foo` field and rename your `bar` field to `foobar` instead and convert it to a textarea? You'd do this: 74 | 75 | ```php 76 | /** @var RockMigrations $rm */ 77 | $rm->deleteField('foo'); 78 | $rm->renameField('bar', 'foobar'); 79 | $rm->migrate([ 80 | 'fields' => [ 81 | 'foobar' => ['type'=>'textarea'], 82 | ], 83 | ]); 84 | ``` 85 | 86 | ## WARNING 87 | 88 | **All api functions are destructive and can completely ruin your pw installation! This is intended behaviour and therefore you have to be careful and know what you are doing!** 89 | 90 | For example deleting a template will also delete all pages having this template. Usually, when using the regular PW API, you'd need to firt check if there are any pages using this template, then delete those pages and finally also delete the corresponding fieldgroup. If the template has the system flag set, you'd also need to remove that flag before deleting the template. That's a lot of things to think of, if all you want to do is to delete a template (and of course all pages having this template). Using RockMigrations it's only one line of code: `$rm->deleteTemplate('yourtemplatename');` 91 | 92 | ## Details 93 | 94 | You can use the Migrations Demo Module from this source as Module Skeleton URL: https://github.com/BernhardBaumrock/RockMigrationsDemo/archive/master.zip 95 | 96 | All migrations are placed inside a `RockMigrations` folder in the root directory of your module. You can then create one php file for each migration that has the name of the version number that this migration belongs to. For example you could create a migration for your module that fires for version `1.2.3` by creating the file `1.2.3.php`. Migrations are sorted by version number internally. 97 | 98 | ![screenshot](https://i.imgur.com/Hw94jLq.png) 99 | 100 | If you are using a code intellisense plugin in your IDE you'll get nice and helpful suggestions of what you can do (I'm using Intelephense in VSCode): 101 | 102 | ![code completion](https://i.imgur.com/rwr6SBJ.png) 103 | 104 | This makes creating migrations really easy. 105 | 106 | ## Examples 107 | 108 | Please see the readme of the RockMigrationsDemo Repo for some examples of what you can do and how: https://github.com/BernhardBaumrock/RockMigrationsDemo 109 | 110 | ## Run Migrations 111 | 112 | You can run your migrations either manually or automatically when a module version change is detected. 113 | 114 | ### Manually (using Tracy) 115 | 116 | ```php 117 | // get the migrations module 118 | $rm = $modules->get('RockMigrations'); 119 | // set the module to execute 120 | $rm->setModule($modules->get('RockMigrationsDemo')); 121 | 122 | // execute upgrade 0.0.1 123 | $rm->execute(null, '0.0.1'); 124 | 125 | // execute downgrade 0.0.1 126 | $rm->execute('0.0.1', null); 127 | 128 | // or to test migrations while developing 129 | $rm->test('0.0.1'); 130 | ``` 131 | 132 | Using the `$rm->test($version)` method, the module will execute the DOWNgrade first and then execute the corresponding UPGRADE. For example `$rm->test('0.0.5')` would first execute the downgrade from 0.0.5 to 0.0.4 and then the upgrade from 0.0.4 to 0.0.5; It is actually a shortcut for `$rm->down('0.0.5'); $rm->up('0.0.5');` This can save you lots of time while developing the 0.0.5 migration where you might have to go back and forth from version 0.0.5 to 0.0.4 and vice versa to check if both the upgrade and downgrade are working. 133 | 134 | If there is no migration file for one version of the module, RockMigrations will not to anything. Some examples: 135 | 136 | ```php 137 | // will execute all upgrades from version 0.0.3 (!) to 0.0.5 138 | $rm->execute('0.0.2', '0.0.5'); 139 | // execute upgrade 0.0.3 140 | // execute upgrade 0.0.4 141 | // execute upgrade 0.0.5 142 | 143 | // let's say we have files 0.0.3, 0.0.5 and 0.0.7 available as migrations 144 | $rm->execute(null, '0.0.10'); 145 | // execute upgrade 0.0.3 146 | // execute upgrade 0.0.5 147 | // execute upgrade 0.0.7 148 | 149 | // same setup, other command 150 | $rm->execute(null, '0.0.3'); 151 | // execute upgrade 0.0.3 152 | 153 | // execute only the upgrade of version 0.0.7 154 | $rm->up('0.0.7'); 155 | 156 | // execute only the downgrade of version 0.0.7 157 | $rm->down('0.0.7'); 158 | ``` 159 | 160 | ### Automatically (by version number changes) 161 | 162 | By placing this code in your module you can tell RockMigrations to handle upgrades (or downgrades) of your module automatically: 163 | 164 | ```php 165 | public function ___upgrade($from, $to) { 166 | $this->modules->get('RockMigrations')->setModule($this)->executeUpgrade($from, $to); 167 | } 168 | public function ___install() { 169 | $this->modules->get('RockMigrations')->setModule($this)->executeInstall(); 170 | } 171 | public function ___uninstall() { 172 | $this->modules->get('RockMigrations')->setModule($this)->executeUninstall(); 173 | } 174 | ``` 175 | 176 | This means that whenever you change your version number of your module (eg from 0.0.2 to 0.0.3) and do a modules refresh in the backend, RockMigrations will kick in and execute all available migrations for you. This can be a great setup combined with GIT. Just push your changes, do a modules refresh and you are all done. You could even automate this process via Webhooks. 177 | 178 | ## Shared data across up and downgrades 179 | 180 | You can use the `$rm->data` property (WireData) to share data across your functions: 181 | 182 | ```php 183 | $this->data->tpl = "demoTemplate003"; 184 | $upgrade = function(RockMigrations $rm) { 185 | $rm->createTemplate($rm->data->tpl); 186 | }; 187 | $downgrade = function(RockMigrations $rm) { 188 | $rm->removeTemplate($rm->data->tpl); 189 | }; 190 | ``` 191 | 192 | If you are using RockMigrations I'm happy to hear about that: https://processwire.com/talk/topic/21212-rockmigrations-easy-migrations-from-devstaging-to-live-server/ 193 | -------------------------------------------------------------------------------- /RockMigrations.module.php: -------------------------------------------------------------------------------- 1 | 'RockMigrations', 17 | 'version' => '0.0.16', 18 | 'summary' => 'Module to handle Migrations inside your Modules easily.', 19 | 'autoload' => false, 20 | 'singular' => false, 21 | 'icon' => 'bolt', 22 | ]; 23 | } 24 | 25 | public function init() { 26 | // load the RockMigration Object Class 27 | require_once('RockMigration.class.php'); 28 | 29 | // new WireData object to store runtime data of migrations 30 | // see the demo module how to use this 31 | $this->data = new WireData(); 32 | } 33 | 34 | /** 35 | * Set module that is controlled 36 | * 37 | * @param string|Module $module 38 | * @return void 39 | */ 40 | public function setModule($module) { 41 | $module = $this->modules->get((string)$module); 42 | if(!$module instanceof Module) throw new WireException("This is not a valid Module!"); 43 | $this->module = $module; 44 | return $this; 45 | } 46 | 47 | /** 48 | * Execute the upgrade from one version to another 49 | * 50 | * Does also execute on downgrades. 51 | * If a module is set, we execute this upgrade on that module and not on the current. 52 | * 53 | * @param string $from 54 | * @param string $to 55 | * @param Module|string $module 56 | * 57 | * @return int number of migrations that where executed 58 | */ 59 | public function execute($from, $to, $module = null) { 60 | $currentModule = $this->module; 61 | if($module) { 62 | $module = $this->modules->get((string)$module); 63 | if(!$module) throw new WireException("Module not found!"); 64 | $this->module = $module; 65 | } 66 | 67 | // check if module is set 68 | if(!$this->module) throw new WireException("Module invalid or not set!"); 69 | 70 | // get migrations 71 | $migrations = $this->getMigrations(); 72 | 73 | // check mode and log request 74 | $mode = version_compare($from, $to) > 0 ? 'downgrade' : 'upgrade'; 75 | $this->log("Executing $mode $from --> $to for module " . $this->module); 76 | 77 | // early exit if no migrations 78 | $count = 0; 79 | if(!count($migrations)) return $count; 80 | 81 | // flip array and numbers for downgrades 82 | if($mode == 'downgrade') { 83 | $migrations = array_reverse($migrations); 84 | $tmp = $from; 85 | $from = $to; 86 | $to = $tmp; 87 | } 88 | 89 | // make sure we execute the migrations on the default language. 90 | // this is necessary that field values are set in the default language, 91 | // eg. when creating a new page and setting the title of a multi-lang page. 92 | $lang = $this->user->language; 93 | if($this->languages) $this->user->language = $this->languages->getDefault(); 94 | 95 | // now execute all available upgrades step by step 96 | foreach($migrations as $version) { 97 | // check if migration is part of the upgrade 98 | if(version_compare($version, $from) >= 1 99 | AND version_compare($version, $to) <= 0) { 100 | // this migration is part of the upgrade, so run it 101 | // this either calls upgrade() or downgrade() of the php file 102 | 103 | // make sure outputformatting is off for all migrations 104 | $this->pages->of(false); 105 | 106 | // execute the migrations 107 | $migration = $this->getMigration($version); 108 | $this->log("Executing $mode {$migration->file}"); 109 | $migration->{$mode}->__invoke($this); 110 | 111 | // increase count 112 | $count++; 113 | } 114 | } 115 | 116 | // change language back to original 117 | $this->user->setAndSave('language', $lang); 118 | 119 | // reset the module to it's initial state 120 | if($module) $this->module = $currentModule; 121 | 122 | return $count; 123 | } 124 | 125 | /** 126 | * for backwards compatibility 127 | */ 128 | public function executeUpgrade($from, $to, $module = null) { 129 | return $this->execute($from, $to, $module); 130 | } 131 | 132 | /** 133 | * Test upgrade for given version 134 | * 135 | * This will execute the downgrade and then the upgrade of only this version. 136 | * 137 | * @param string $version 138 | * @return void 139 | */ 140 | public function test($version) { 141 | $this->down($version); 142 | $this->modules->refresh(); 143 | $this->up($version); 144 | } 145 | 146 | /** 147 | * For backwards compatibility 148 | */ 149 | public function testUpgrade($version) { 150 | $this->test($version); 151 | } 152 | 153 | /** 154 | * Execute upgrade of given version 155 | * 156 | * @param string $version 157 | * @return void 158 | */ 159 | public function up($version) { 160 | // check if module is set 161 | if(!$this->module) throw new WireException("Please set the module first: setModule(\$yourmodule)"); 162 | 163 | // get migration 164 | $migration = $this->getMigration($version); 165 | if(!$migration) throw new WireException("Migration $version not found"); 166 | 167 | // now we execute the upgrade 168 | $prev = @$migration->getPrev()->version; 169 | $this->executeUpgrade($prev, $version); 170 | } 171 | 172 | /** 173 | * Execute downgrade of given version 174 | * 175 | * @param string $version 176 | * @return void 177 | */ 178 | public function down($version) { 179 | // check if module is set 180 | if(!$this->module) throw new WireException("Please set the module first: setModule(\$yourmodule)"); 181 | 182 | // get migration 183 | $migration = $this->getMigration($version); 184 | if(!$migration) throw new WireException("Migration $version not found"); 185 | 186 | // now we execute the upgrade 187 | $prev = @$migration->getPrev()->version; 188 | $this->executeUpgrade($version, $prev); 189 | } 190 | 191 | /** 192 | * Execute all Upgrade Scripts on Installation 193 | * 194 | * @return void 195 | */ 196 | public function executeInstall() { 197 | // check if module is set 198 | if(!$this->module) throw new WireException("Please set the module first: setModule(\$yourmodule)"); 199 | 200 | $version = $this->modules->getModuleInfo($this->module)['version']; 201 | $versionStr = $this->modules->formatVersion($version); 202 | return $this->executeUpgrade(null, $versionStr); 203 | } 204 | 205 | /** 206 | * Execute all Downgrade Scripts on Uninstallation 207 | * 208 | * @return void 209 | */ 210 | public function executeUninstall() { 211 | // check if module is set 212 | if(!$this->module) throw new WireException("Please set the module first: setModule(\$yourmodule)"); 213 | 214 | $version = $this->modules->getModuleInfo($this->module)['version']; 215 | $versionStr = $this->modules->formatVersion($version); 216 | return $this->executeUpgrade($versionStr, null); 217 | } 218 | 219 | /** 220 | * Get Migration Object from Version Number 221 | * 222 | * @param string $version 223 | * @return RockMigration 224 | */ 225 | public function getMigration($version) { 226 | $migration = new RockMigration(); 227 | $migration->version = $version; 228 | $migration->object = $this; 229 | 230 | // find according php file 231 | $file = $this->getMigrationsPath().$version.".php"; 232 | $upgrade = function(){}; 233 | $downgrade = function(){}; 234 | $migration->file = null; 235 | if(is_file($file)) { 236 | include($file); 237 | $migration->file = $file; 238 | } 239 | $migration->upgrade = $upgrade; 240 | $migration->downgrade = $downgrade; 241 | 242 | return $migration; 243 | } 244 | 245 | /** 246 | * Get all migrations of one module 247 | * 248 | * @param Module $module 249 | * @return array 250 | */ 251 | public function getMigrations() { 252 | $migrations = []; 253 | 254 | // find all files in the RockMigrations folder of the module 255 | $files = $this->files->find($this->getMigrationsPath(), [ 256 | 'extensions' => ['php'] 257 | ]); 258 | 259 | // build an array of migration 260 | foreach($files as $file) { 261 | $info = pathinfo($file); 262 | $migrations[] = $info['filename']; 263 | } 264 | 265 | // sort array according to version numbers 266 | // see https://i.imgur.com/F52wGT9.png 267 | usort($migrations, 'version_compare'); 268 | 269 | return $migrations; 270 | } 271 | 272 | /** 273 | * Get the module's migration path 274 | * 275 | * @return void 276 | */ 277 | private function getMigrationsPath() { 278 | return $this->config->paths($this->module) . $this->className() . "/"; 279 | } 280 | 281 | /* ##################### RockMigrations API Methods ##################### */ 282 | 283 | /* ##### fields ##### */ 284 | 285 | /** 286 | * Add field to template 287 | * 288 | * @param Field|string $field 289 | * @param Template|string $template 290 | * @return void 291 | */ 292 | public function addFieldToTemplate($field, $template, $afterfield = null, $beforefield = null) { 293 | $field = $this->getField($field); 294 | $template = $this->getTemplate($template); 295 | 296 | $afterfield = $this->getField($afterfield, false); 297 | $beforefield = $this->getField($beforefield, false); 298 | $fg = $template->fieldgroup; /** @var Fieldgroup $fg */ 299 | 300 | if($afterfield) $fg->insertAfter($field, $afterfield); 301 | elseif($beforefield) $fg->insertBefore($field, $beforefield); 302 | else $fg->add($field); 303 | 304 | // add end field for fieldsets 305 | if($field->type instanceof FieldtypeFieldsetOpen 306 | AND !$field->type instanceof FieldtypeFieldsetClose) { 307 | $closer = $field->type->getFieldsetCloseField($field, false); 308 | $this->addFieldToTemplate($closer, $template, $field); 309 | } 310 | 311 | $fg->save(); 312 | } 313 | 314 | /** 315 | * Add fields to template. 316 | * 317 | * Simple: 318 | * $rm->addFieldsToTemplate(['field1', 'field2'], 'yourtemplate'); 319 | * 320 | * Add fields at special positions: 321 | * $rm->addFieldsToTemplate([ 322 | * 'field1', 323 | * 'field4' => 'field3', // this will add field4 after field3 324 | * ], 'yourtemplate'); 325 | * 326 | * @param array $fields 327 | * @param string $template 328 | * @return void 329 | */ 330 | public function addFieldsToTemplate($fields, $template) { 331 | foreach($fields as $k=>$v) { 332 | // if the key is an integer, it's a simple field 333 | if(is_int($k)) $this->addFieldToTemplate((string)$v, $template); 334 | else $this->addFieldToTemplate((string)$k, $template, $v); 335 | } 336 | } 337 | 338 | /** 339 | * Add matrix item to given field 340 | * @param Field|string $field 341 | * @param string $name 342 | * @param array $data 343 | * @return Field|null 344 | */ 345 | public function addMatrixItem($field, $name, $data) { 346 | if(!$field = $this->getField($field, false)) return; 347 | 348 | // get number 349 | $n = 1; 350 | while(array_key_exists("matrix{$n}_name", $field->getArray())) $n++; 351 | $prefix = "matrix{$n}_"; 352 | 353 | $field->set($prefix."name", $name); 354 | $field->set($prefix."sort", $n); 355 | foreach($this->getMatrixDataArray($data) as $key => $val) { 356 | // eg set matrix1_label = ... 357 | $field->set($prefix.$key, $val); 358 | if($key === "fields") { 359 | $tpl = $this->getRepeaterTemplate($field); 360 | $this->addFieldsToTemplate($val, $tpl); 361 | } 362 | } 363 | 364 | $field = $this->resetMatrixRepeaterFields($field); 365 | $field->save(); 366 | return $field; 367 | } 368 | 369 | /** 370 | * Change type of field 371 | * @param Field|string $field 372 | * @param string $type 373 | * @param bool $keepSettings 374 | * @return Field 375 | */ 376 | public function changeFieldtype($field, $type, $keepSettings = true) { 377 | $field = $this->getField($field); 378 | 379 | // if type is already set, return early 380 | if($field->type == $type) return $field; 381 | 382 | // change type and save field 383 | $field->type = $type; 384 | $this->fields->changeFieldtype($field, $keepSettings); 385 | $field->save(); 386 | return $field; 387 | } 388 | 389 | /** 390 | * Create a field of the given type 391 | * 392 | * @param string $name 393 | * @param string $type 394 | * @param array $options 395 | * @return Field 396 | */ 397 | public function createField($name, $typename, $options = null) { 398 | $field = $this->getField($name, false); 399 | if(!$field) { 400 | // setup fieldtype 401 | $type = $this->modules->get($typename); 402 | if(!$type) { 403 | // shortcut types are possible, eg "text" for "FieldtypeText" 404 | $type = "Fieldtype".ucfirst($typename); 405 | $type = $this->modules->get($type); 406 | if(!$type) throw new WireException("Invalid Fieldtype"); 407 | } 408 | 409 | // create the new field 410 | if(strtolower($name) !== $name) throw new WireException("Fieldname must be lowercase!"); 411 | $name = strtolower($name); 412 | $field = $this->wire(new Field()); 413 | $field->type = $type; 414 | $field->name = $name; 415 | $field->save(); 416 | 417 | // create end field for fieldsets 418 | if($field->type instanceof FieldtypeFieldsetOpen) { 419 | $field->type->getFieldsetCloseField($field, true); 420 | } 421 | 422 | // this will auto-generate the repeater template 423 | if($field->type instanceof FieldtypeRepeater) { 424 | $field->type->getRepeaterTemplate($field); 425 | } 426 | } 427 | 428 | // set options 429 | if($options) $field = $this->setFieldData($field, $options); 430 | 431 | return $field; 432 | } 433 | 434 | /** 435 | * Delete the given field 436 | * 437 | * @param string $name 438 | * @return void 439 | */ 440 | public function deleteField($name) { 441 | $field = $this->getField($name, false); 442 | if(!$field) return; 443 | 444 | // delete _END field for fieldsets first 445 | if($field->type instanceof FieldtypeFieldsetOpen) { 446 | $closer = $field->type->getFieldsetCloseField($field, false); 447 | $this->deleteField($closer); 448 | } 449 | 450 | // make sure we can delete the field by removing all flags 451 | $field->flags = Field::flagSystemOverride; 452 | $field->flags = 0; 453 | 454 | // remove the field from all fieldgroups 455 | foreach($this->fieldgroups as $fieldgroup) { 456 | /** @var Fieldgroup $fieldgroup */ 457 | $fieldgroup->remove($field); 458 | $fieldgroup->save(); 459 | } 460 | 461 | return $this->fields->delete($field); 462 | } 463 | 464 | /** 465 | * Delete given fields 466 | * 467 | * @param array $fields 468 | * @return void 469 | */ 470 | public function deleteFields($fields) { 471 | foreach($fields as $field) $this->deleteField($field); 472 | } 473 | 474 | /** 475 | * Delete template overrides for the given field 476 | * 477 | * Example usage: 478 | * Delete custom field width for 'myfield' and 'mytemplate': 479 | * $rm->deleteFieldTemplateOverrides('myfield', [ 480 | * 'mytemplate' => ['columnWidth'], 481 | * ]); 482 | * 483 | * @param Field|string $field 484 | * @param array $templatesettings 485 | * @return void 486 | */ 487 | public function deleteFieldTemplateOverrides($field, $templatesettings) { 488 | $field = $this->getField($field); 489 | 490 | // loop data 491 | foreach($templatesettings as $tpl=>$val) { 492 | // get template 493 | $template = $this->templates->get((string)$tpl); 494 | if(!$template) throw new WireException("Template $tpl not found"); 495 | 496 | // set field data in template context 497 | $fg = $template->fieldgroup; 498 | $data = $fg->getFieldContextArray($field->id); 499 | foreach($val as $setting) unset($data[$setting]); 500 | $fg->setFieldContextArray($field->id, $data); 501 | $fg->saveContext(); 502 | } 503 | 504 | } 505 | 506 | /** 507 | * Get field by name 508 | * 509 | * @param Field|string $name 510 | * @return mixed 511 | */ 512 | public function getField($name, $exception = null) { 513 | if($name AND !is_string($name) AND !$name instanceof Field) { 514 | $func = @debug_backtrace()[1]['function']; 515 | throw new WireException("Invalid type set for field in $func"); 516 | } 517 | $field = $this->fields->get((string)$name); 518 | 519 | // return field when found or no exception 520 | if($field) return $field; 521 | if($exception === false) return; 522 | 523 | // field was not found, throw exception 524 | if(!$exception) $exception = "Field $name not found"; 525 | throw new WireException($exception); 526 | } 527 | 528 | /** 529 | * Move one field after another 530 | * 531 | * @param Field|string $field 532 | * @param Field|string $after 533 | * @param Template|string $template 534 | * @return void 535 | */ 536 | public function moveFieldAfter($field, $after, $template) { 537 | $this->addFieldToTemplate($field, $template, $after); 538 | } 539 | 540 | /** 541 | * Move one field before another 542 | * 543 | * @param Field|string $field 544 | * @param Field|string $before 545 | * @param Template|string $template 546 | * @return void 547 | */ 548 | public function moveFieldBefore($field, $before, $template) { 549 | $this->addFieldToTemplate($field, $template, null, $before); 550 | } 551 | 552 | /** 553 | * Remove Field from Template 554 | * 555 | * @param Field|string $field 556 | * @param Template|string $template 557 | * @param bool $force 558 | * @return void 559 | */ 560 | public function removeFieldFromTemplate($field, $template, $force = false) { 561 | $field = $this->getField($field, false); 562 | if(!$field) return; 563 | 564 | $template = $this->templates->get((string)$template); 565 | if(!$template) return; 566 | $fg = $template->fieldgroup; /** @var Fieldgroup $fg */ 567 | 568 | // remove global flag to force deletion 569 | if($force) $field->flags = 0; 570 | 571 | $fg->remove($field); 572 | $fg->save(); 573 | } 574 | 575 | /** 576 | * See method above 577 | */ 578 | public function removeFieldsFromTemplate($fields, $template, $force = false) { 579 | foreach($fields as $field) $this->removeFieldFromTemplate($field, $template, $force); 580 | } 581 | 582 | /** 583 | * Remove matrix item from field 584 | * @param Field|string $field 585 | * @param string $name 586 | * @return Field|null 587 | */ 588 | public function removeMatrixItem($field, $name) { 589 | if(!$field = $this->getField($field, false)) return; 590 | $info = $field->type->getMatrixTypesInfo($field, ['type'=>$name]); 591 | if(!$info) return; 592 | 593 | // reset all properties of that field 594 | foreach($field->getArray() as $prop=>$val) { 595 | if(strpos($prop, $info['prefix']) !== 0) continue; 596 | $field->set($prop, null); 597 | } 598 | 599 | $field = $this->resetMatrixRepeaterFields($field); 600 | $field->save(); 601 | return $field; 602 | } 603 | 604 | /** 605 | * Rename this field 606 | * @return Field|false 607 | */ 608 | public function renameField($oldname, $newname) { 609 | $field = $this->getField($oldname, false); 610 | if(!$field) return false; 611 | 612 | // the new field must not exist 613 | $newfield = $this->getField($newname, false); 614 | if($newfield) throw new WireException("Field $newname already exists"); 615 | 616 | // change the old field 617 | $field->name = $newname; 618 | $field->save(); 619 | } 620 | 621 | /** 622 | * Set data of a field 623 | * 624 | * If a template is provided the data is set in template context only. 625 | * You can also provide an array of templates. 626 | * 627 | * Multilang is also possible: 628 | * $rm->setFieldData('yourfield', [ 629 | * 'label' => 'foo', // default language 630 | * 'label1021' => 'bar', // other language 631 | * ]); 632 | * 633 | * @param Field|string $field 634 | * @param array $data 635 | * @param Template|array|string $template 636 | * @return void 637 | */ 638 | public function setFieldData($field, $data, $template = null) { 639 | $field = $this->getField($field); 640 | 641 | // prepare data array 642 | foreach($data as $key=>$val) { 643 | 644 | // this makes it possible to set the template via name 645 | if($key === "template_id") { 646 | $data[$key] = $this->templates->get($val)->id; 647 | } 648 | 649 | } 650 | 651 | // set data 652 | if(!$template) { 653 | // set field data directly 654 | foreach($data as $k=>$v) $field->{$k} = $v; 655 | } 656 | else { 657 | // make sure the template is set as array of strings 658 | if(!is_array($template)) $template = [(string)$template]; 659 | 660 | foreach($template as $t) { 661 | $tpl = $this->templates->get((string)$t); 662 | if(!$tpl) throw new WireException("Template $t not found"); 663 | 664 | // set field data in template context 665 | $fg = $tpl->fieldgroup; 666 | $current = $fg->getFieldContextArray($field->id); 667 | $fg->setFieldContextArray($field->id, array_merge($current, $data)); 668 | $fg->saveContext(); 669 | } 670 | } 671 | 672 | $field->save(); 673 | return $field; 674 | } 675 | 676 | /** 677 | * Set the language value of the given field 678 | * 679 | * $rm->setFieldLanguageValue("/admin/therapy", 'title', [ 680 | * 'default' => 'Therapie', 681 | * 'english' => 'Therapy', 682 | * ]); 683 | * 684 | * @param Page|string $page 685 | * @param Field|string $field 686 | * @param array $data 687 | * @return void 688 | */ 689 | public function setFieldLanguageValue($page, $field, $data) { 690 | $page = $this->pages->get((string)$page); 691 | if(!$page->id) throw new WireException("Page not found!"); 692 | $field = $this->getField($field); 693 | 694 | // set field value for all provided languages 695 | foreach($data as $lang=>$val) { 696 | $lang = $this->languages->get($lang); 697 | if(!$lang->id) continue; 698 | $page->{$field}->setLanguageValue($lang, $val); 699 | } 700 | $page->save(); 701 | } 702 | 703 | /** 704 | * Set options of an options field via string 705 | * 706 | * @param Field|string $name 707 | * @param string $options 708 | * @return void 709 | */ 710 | public function setFieldOptionsString($name, $options) { 711 | $field = $this->getField($name); 712 | 713 | $manager = $this->wire(new SelectableOptionManager()); 714 | $manager->setOptionsString($field, $options, false); 715 | $field->save(); 716 | 717 | return $field; 718 | } 719 | 720 | /** 721 | * Set field order at given template 722 | * 723 | * The first field is always the reference for all other fields. 724 | * 725 | * @param array $fields 726 | * @param Template|string $name 727 | * @return void 728 | */ 729 | public function setFieldOrder($fields, $name) { 730 | $template = $this->templates->get((string)$name); 731 | if(!$template) throw new WireException("Template $name not found"); 732 | 733 | // make sure that all fields exist 734 | foreach($fields as $i=>$field) { 735 | if(!$this->fields->get($field)) unset($fields[$i]); 736 | } 737 | $fields = array_values($fields); // reset indices 738 | 739 | foreach($fields as $i => $field) { 740 | if(!$i) continue; 741 | $this->addFieldToTemplate($field, $template, $fields[$i-1]); 742 | } 743 | } 744 | 745 | /** 746 | * Set matrix item data 747 | * @param Field|string $field 748 | * @param string $name 749 | * @param array $data 750 | * @return Field|null 751 | */ 752 | public function setMatrixItemData($field, $name, $data) { 753 | if(!$field = $this->getField($field, false)) return; 754 | $info = $field->type->getMatrixTypesInfo($field, ['type'=>$name]); 755 | if(!$info) return; 756 | foreach($this->getMatrixDataArray($data) as $key => $val) { 757 | // eg set matrix1_label = ... 758 | $field->set($info['prefix'].$key, $val); 759 | if($key === "fields") { 760 | $tpl = $this->getRepeaterTemplate($field); 761 | $this->addFieldsToTemplate($val, $tpl); 762 | } 763 | } 764 | 765 | $field = $this->resetMatrixRepeaterFields($field); 766 | $field->save(); 767 | return $field; 768 | } 769 | 770 | /** 771 | * Set items of a RepeaterMatrix field 772 | * 773 | * If wipe is set to TRUE it will wipe all existing matrix types before 774 | * setting the new ones. Otherwise it will override settings of old types 775 | * and add the type to the end of the matrix if it does not exist yet. 776 | * 777 | * CAUTION: wipe = true will also delete all field data stored in the 778 | * repeater matrix fields!! 779 | * 780 | * Usage: 781 | * $rm->setMatrixItems('your_matrix_field', [ 782 | * 'foo' => [ 783 | * 'label' => 'foo label', 784 | * 'fields' => ['field1', 'field2'], 785 | * ], 786 | * 'bar' => [ 787 | * 'label' => 'bar label', 788 | * 'fields' => ['field1', 'field3'], 789 | * ], 790 | * ], true); 791 | * 792 | * @param Field|string $field 793 | * @param array $items 794 | * @param bool $wipe 795 | * @return Field|null 796 | */ 797 | public function setMatrixItems($field, $items, $wipe = false) { 798 | if(!$this->modules->isInstalled('FieldtypeRepeaterMatrix')) return; 799 | if(!$field = $this->getField($field, false)) return; 800 | 801 | // get all matrix types of that field 802 | $types = $field->type->getMatrixTypes(); 803 | 804 | // if wipe is turned on we remove all existing items 805 | // this is great when you want to control the matrix solely by migrations 806 | if($wipe) { 807 | foreach($types as $type => $v) $this->removeMatrixItem($field, $type); 808 | } 809 | 810 | // loop all provided items 811 | foreach($items as $name => $data) { 812 | $type = $field->type->getMatrixTypeByName($name); 813 | if(!$type) $field = $this->addMatrixItem($field, $name, $data); 814 | else $this->setMatrixItemData($field, $name, $data); 815 | } 816 | 817 | return $field; 818 | } 819 | 820 | /* ##### templates ##### */ 821 | 822 | /** 823 | * Allow given child for given parent 824 | */ 825 | public function addAllowedChild($child, $parent) { 826 | $child = $this->getTemplate($child); 827 | $parent = $this->getTemplate($parent); 828 | $childs = $parent->childTemplates; 829 | $childs[] = $child; 830 | $this->setTemplateData($parent, ['childTemplates' => $childs]); 831 | } 832 | 833 | /** 834 | * Create a new ProcessWire Template 835 | * 836 | * @param string $name 837 | * @param bool $addTitlefield 838 | * @return void 839 | */ 840 | public function createTemplate($name, $addTitlefield = true) { 841 | $t = $this->templates->get((string)$name); 842 | if($t) return $t; 843 | 844 | // create new fieldgroup 845 | $fg = $this->wire(new Fieldgroup()); 846 | $fg->name = $name; 847 | $fg->save(); 848 | 849 | // create new template 850 | $t = $this->wire(new Template()); 851 | $t->name = $name; 852 | $t->fieldgroup = $fg; 853 | $t->save(); 854 | 855 | // add title field to this template 856 | if($addTitlefield) $this->addFieldToTemplate('title', $t); 857 | 858 | return $t; 859 | } 860 | 861 | /** 862 | * Delete a ProcessWire Template 863 | * 864 | * @param string $name 865 | * @return void 866 | */ 867 | public function deleteTemplate($name) { 868 | $template = $this->templates->get($name); 869 | if(!$template OR !$template->id) return; 870 | 871 | // remove all pages having this template 872 | foreach($this->pages->find("template=$template, include=all") as $p) { 873 | $this->deletePage($p); 874 | } 875 | 876 | // make sure we can delete the template by removing all flags 877 | $template->flags = Template::flagSystemOverride; 878 | $template->flags = 0; 879 | 880 | // delete the template 881 | $this->templates->delete($template); 882 | 883 | // delete the fieldgroup 884 | $fg = $this->fieldgroups->get($name); 885 | $this->fieldgroups->delete($fg); 886 | } 887 | 888 | /** 889 | * Get template by name 890 | * 891 | * @param Template|string $name 892 | * @return mixed 893 | */ 894 | public function getTemplate($name, $exception = null) { 895 | $template = $this->templates->get((string)$name); 896 | 897 | // return template when found or no exception 898 | if($template) return $template; 899 | if($exception === false) return; 900 | 901 | // template was not found, throw exception 902 | if(!$exception) $exception = "Template not found"; 903 | throw new WireException($exception); 904 | } 905 | 906 | /** 907 | * Get template of given repeater field 908 | * @param Field|string $field 909 | * @return Template 910 | */ 911 | public function getRepeaterTemplate($field) { 912 | $field = $this->getField($field); 913 | return $this->templates->get($field->template_id); 914 | } 915 | 916 | /** 917 | * This renames a template and corresponding fieldgroup 918 | * @return Template 919 | */ 920 | public function renameTemplate($oldname, $newname) { 921 | $t = $this->templates->get((string)$oldname); 922 | 923 | // if the new template already exists we return it 924 | // this is important if you run one migration multiple times 925 | // $bar = $rm->renameTemplate('foo', 'bar'); 926 | // $rm->setTemplateData($bar, [...]); 927 | $newTemplate = $this->templates->get((string)$newname); 928 | if($newTemplate) return $newTemplate; 929 | 930 | $t->name = $newname; 931 | $t->save(); 932 | 933 | $fg = $t->fieldgroup; 934 | $fg->name = $newname; 935 | $fg->save(); 936 | 937 | return $t; 938 | } 939 | 940 | /** 941 | * Set template icon 942 | */ 943 | public function setIcon($template, $icon) { 944 | $template = $this->templates->get((string)$template); 945 | $template->setIcon($icon); 946 | $template->save(); 947 | return $template; 948 | } 949 | 950 | /** 951 | * Set parent child family settings for two templates 952 | */ 953 | public function setParentChild($parent, $child) { 954 | $this->setTemplateData($child, [ 955 | 'noChildren' => 1, // may not have children 956 | 'noParents' => '', // can be used for new pages 957 | 'parentTemplates' => [(string)$parent], 958 | ]); 959 | $this->setTemplateData($parent, [ 960 | 'noChildren' => 0, // may have children 961 | 'noParents' => -1, // only one page 962 | 'childTemplates' => [(string)$child], 963 | 'childNameFormat' => 'title', 964 | ]); 965 | } 966 | 967 | /** 968 | * Set data of a template 969 | * 970 | * TODO: Set data in template context. 971 | * TODO: Wording is inconsistant! Set = Update, because it only sets 972 | * provided key value pairs and not the whole array 973 | * 974 | * Multilang is also possible: 975 | * $rm->setTemplateData('yourtemplate', [ 976 | * 'label' => 'foo', // default language 977 | * 'label1021' => 'bar', // other language 978 | * ]); 979 | * 980 | * @param Template|string $template 981 | * @param array $data 982 | * @return void 983 | */ 984 | public function setTemplateData($template, $data) { 985 | $template = $this->templates->get((string)$template); 986 | if(!$template) throw new WireException("template not found!"); 987 | foreach($data as $k=>$v) { 988 | if($k === 'fields' AND is_array($v)) { 989 | // set fields of template but dont remove non-mentioned fields 990 | $this->setTemplateFields($template, $v, false); 991 | continue; 992 | } 993 | if($k === 'fields-' AND is_array($v)) { 994 | // set fields of this template and remove all non-listed 995 | $this->setTemplateFields($template, $v, true); 996 | continue; 997 | } 998 | $template->{$k} = $v; 999 | } 1000 | $template->save(); 1001 | return $template; 1002 | } 1003 | 1004 | /** 1005 | * Set fields of template via array 1006 | * @return void 1007 | */ 1008 | public function setTemplateFields($template, $fields, $removeOthers = false) { 1009 | $template = $this->templates->get((string)$template); 1010 | $last = null; 1011 | $names = []; 1012 | foreach($fields as $name=>$data) { 1013 | if(is_int($name) AND is_int($data)) { 1014 | $name = $this->getField((string)$data)->name; 1015 | $data = []; 1016 | } 1017 | if(is_int($name)) { 1018 | $name = $data; 1019 | $data = []; 1020 | } 1021 | $names[] = $name; 1022 | $this->addFieldToTemplate($name, $template, $last); 1023 | $this->setFieldData($name, $data, $template); 1024 | $last = $name; 1025 | } 1026 | 1027 | if(!$removeOthers) return; 1028 | foreach($template->fields as $field) { 1029 | $name = (string)$field; 1030 | if(!in_array($name, $names)) { 1031 | // remove this field from the template 1032 | // global fields like the title field are also removed 1033 | $this->removeFieldFromTemplate($name, $template, true); 1034 | } 1035 | } 1036 | } 1037 | 1038 | /** 1039 | * Set data for multiple templates 1040 | * @return void 1041 | */ 1042 | public function setTemplatesData($templates, $data) { 1043 | foreach($templates as $t) $this->setTemplateData($t, $data); 1044 | } 1045 | 1046 | /* ##### pages ##### */ 1047 | 1048 | /** 1049 | * Create a new Page 1050 | * 1051 | * If the page exists it will return the existing page. 1052 | * All available languages will be set active by default for this page. 1053 | * 1054 | * @param array|string $title 1055 | * @param string $name 1056 | * @param Template|string $template 1057 | * @param Page|string $parent 1058 | * @param array $status 1059 | * @param array $data 1060 | * @return Page 1061 | */ 1062 | public function createPage($title, $name = null, $template = '', $parent = '', $status = [], $data = []) { 1063 | if(is_array($title)) return $this->createPageByArray($title); 1064 | 1065 | // create pagename from page title if it is not set 1066 | if(!$name) $name = $this->sanitizer->pageName($title); 1067 | 1068 | // make sure parent is a page and not a selector 1069 | $parent = $this->pages->get((string)$parent); 1070 | 1071 | // get page if it exists 1072 | $selector = [ 1073 | 'name' => $name, 1074 | 'template' => $template, 1075 | 'parent' => $parent, 1076 | ]; 1077 | $page = $this->pages->get($selector); 1078 | 1079 | if($page->id) { 1080 | // set status 1081 | $page->status($status); 1082 | $page->save(); 1083 | 1084 | // set page data 1085 | $this->setPageData($page, $data); 1086 | 1087 | return $page; 1088 | } 1089 | 1090 | // create a new page 1091 | $p = $this->wire(new Page()); 1092 | $p->template = $template; 1093 | $p->title = $title; 1094 | $p->name = $name; 1095 | $p->parent = $parent; 1096 | $p->status($status); 1097 | $p->save(); 1098 | 1099 | // set page data 1100 | $this->setPageData($p, $data); 1101 | 1102 | // enable all languages for this page 1103 | $this->enableAllLanguagesForPage($p); 1104 | 1105 | return $p; 1106 | } 1107 | 1108 | /** 1109 | * Create page by array 1110 | * 1111 | * This is more future proof and has more options than the old version, 1112 | * eg you can provide a callback: 1113 | * $rm->createPage([ 1114 | * 'title' => 'foo', 1115 | * 'onCreate' => function($page) { ... }, 1116 | * ]); 1117 | * 1118 | * @return Page 1119 | */ 1120 | public function createPageByArray($array) { 1121 | $data = $this->wire(new WireData()); /** @var WireData $data */ 1122 | $data->setArray($array); 1123 | 1124 | // check for necessary properties 1125 | $parent = $this->pages->get((string)$data->parent); 1126 | if(!$parent->id) throw new WireException("Invalid parent"); 1127 | $template = $this->templates->get((string)$data->template); 1128 | if(!$template instanceof Template OR !$template->id) throw new WireException("Invalid template"); 1129 | 1130 | // check name 1131 | $name = $data->name; 1132 | if(!$name) { 1133 | if(!$data->title) throw new WireException("If no name is set you need to set a title!"); 1134 | $name = $this->sanitizer->pageName($data->title); 1135 | } 1136 | 1137 | // set flag if page was created or not 1138 | $created = !$this->pages->get("parent=$parent,name=$name")->id; 1139 | 1140 | // create page 1141 | $page = $this->createPage( 1142 | $data->title, 1143 | $name, 1144 | $template, 1145 | $parent, 1146 | $data->status, 1147 | $data->pageData 1148 | ); 1149 | 1150 | // if page was created we fire the onCreate callback 1151 | if($created AND is_callable($data->onCreate)) $data->onCreate->__invoke($page); 1152 | 1153 | return $page; 1154 | } 1155 | 1156 | /** 1157 | * Set page data via array 1158 | * @param Page $page 1159 | * @param array $data 1160 | * @return void 1161 | */ 1162 | private function setPageData($page, $data) { 1163 | if(!$data) return; 1164 | foreach($data as $k=>$v) $page->setAndSave($k, $v); 1165 | } 1166 | 1167 | /** 1168 | * Enable all languages for given page 1169 | * 1170 | * @param Page|string $page 1171 | * @return void 1172 | */ 1173 | public function enableAllLanguagesForPage($page) { 1174 | $page = $this->pages->get((string)$page); 1175 | if($this->languages) { 1176 | foreach($this->languages as $lang) $page->set("status$lang", 1); 1177 | } 1178 | $page->save(); 1179 | } 1180 | 1181 | /** 1182 | * Delete the given page including all children. 1183 | * 1184 | * @param Page|string $page 1185 | * @return void 1186 | */ 1187 | public function deletePage($page) { 1188 | // make sure we got a page 1189 | $page = $this->pages->get((string)$page); 1190 | if(!$page->id) return; 1191 | 1192 | // make sure we can delete the page and delete it 1193 | // we also need to make sure that all descendants of this page are deletable 1194 | // todo: make this recursive? 1195 | $all = $this->wire(new PageArray()); 1196 | $all->add($page); 1197 | $all->add($this->pages->find("has_parent=$page")); 1198 | foreach($all as $p) { 1199 | $p->addStatus(Page::statusSystemOverride); 1200 | $p->status = 1; 1201 | $p->save(); 1202 | } 1203 | $this->pages->delete($page, true); 1204 | } 1205 | 1206 | /** 1207 | * Delete pages matching the given selector 1208 | * @param mixed $selector 1209 | * @return void 1210 | */ 1211 | public function deletePages($selector) { 1212 | $pages = $this->pages->find($selector); 1213 | foreach($pages as $page) $this->deletePage($page); 1214 | } 1215 | 1216 | /* ##### permissions ##### */ 1217 | 1218 | /** 1219 | * Add a permission to given role 1220 | * 1221 | * @param string|int $permission 1222 | * @param string|int $role 1223 | * @return boolean 1224 | */ 1225 | public function addPermissionToRole($permission, $role) { 1226 | $role = $this->roles->get((string)$role); 1227 | if(!$role->id) return; 1228 | $role->of(false); 1229 | $role->addPermission($permission); 1230 | return $role->save(); 1231 | } 1232 | 1233 | /** 1234 | * Add an array of permissions to an array of roles 1235 | * 1236 | * @param array|string $permissions 1237 | * @param array|string $roles 1238 | * @return void 1239 | */ 1240 | public function addPermissionsToRoles($permissions, $roles) { 1241 | if(!is_array($permissions)) $permissions = [(string)$permissions]; 1242 | if(!is_array($roles)) $roles = [(string)$roles]; 1243 | foreach($permissions as $permission) { 1244 | foreach ($roles as $role) { 1245 | $this->addPermissionToRole($permission, $role); 1246 | } 1247 | } 1248 | } 1249 | 1250 | /** 1251 | * Remove a permission from given role 1252 | * 1253 | * @param string|int $permission 1254 | * @param string|int $role 1255 | * @return void 1256 | */ 1257 | public function removePermissionFromRole($permission, $role) { 1258 | $role = $this->roles->get((string)$role); 1259 | $role->of(false); 1260 | $role->removePermission($permission); 1261 | return $role->save(); 1262 | } 1263 | 1264 | /** 1265 | * Remove an array of permissions to an array of roles 1266 | * 1267 | * @param array|string $permissions 1268 | * @param array|string $roles 1269 | * @return void 1270 | */ 1271 | public function removePermissionsFromRoles($permissions, $roles) { 1272 | if(!is_array($permissions)) $permissions = [(string)$permissions]; 1273 | if(!is_array($roles)) $roles = [(string)$roles]; 1274 | foreach($permissions as $permission) { 1275 | foreach ($roles as $role) { 1276 | $this->removePermissionFromRole($permission, $role); 1277 | } 1278 | } 1279 | } 1280 | 1281 | /** 1282 | * Create permission with given name 1283 | * 1284 | * @param string $name 1285 | * @param string $description 1286 | * @return Permission 1287 | */ 1288 | public function createPermission($name, $description = null) { 1289 | // if the permission exists return it 1290 | $permission = $this->permissions->get((string)$name); 1291 | if(!$permission->id) $permission = $this->permissions->add($name); 1292 | $permission->setAndSave('title', $description); 1293 | return $permission; 1294 | } 1295 | 1296 | /** 1297 | * Delete the given permission 1298 | * 1299 | * @param Permission|string $permission 1300 | * @return void 1301 | */ 1302 | public function deletePermission($permission) { 1303 | $permission = $this->permissions->get((string)$permission); 1304 | if(!$permission->id) return; 1305 | $this->permissions->delete($permission); 1306 | } 1307 | 1308 | /** 1309 | * Create role with given name 1310 | * 1311 | * @param string $name 1312 | * @param array $permissions 1313 | * @return void 1314 | */ 1315 | public function createRole($name, $permissions = []) { 1316 | // if the role exists return it 1317 | $role = $this->roles->get((string)$name); 1318 | if(!$role->id) $role = $this->roles->add($name); 1319 | 1320 | // add permissions 1321 | foreach($permissions as $permission) $this->addPermissionToRole($permission, $role); 1322 | 1323 | return $role; 1324 | } 1325 | 1326 | /** 1327 | * Delete the given role 1328 | * 1329 | * @param Role|string $role 1330 | * @return void 1331 | */ 1332 | public function deleteRole($role) { 1333 | $role = $this->roles->get((string)$role); 1334 | if(!$role->id) return; 1335 | $this->roles->delete($role); 1336 | } 1337 | 1338 | /* ##### users ##### */ 1339 | 1340 | /** 1341 | * Create a PW user with given password 1342 | * 1343 | * If the user already exists it will return this user. 1344 | * 1345 | * @param string $username 1346 | * @param string $password 1347 | * @return User 1348 | */ 1349 | public function createUser($username, $password) { 1350 | $user = $this->users->get($username); 1351 | if($user->id) return $user; 1352 | 1353 | $user = $this->wire->users->add($username); 1354 | $user->pass = $password; 1355 | $user->save(); 1356 | return $user; 1357 | } 1358 | 1359 | /** 1360 | * Delete a PW user 1361 | * 1362 | * @param string $username 1363 | * @return void 1364 | */ 1365 | public function deleteUser($username) { 1366 | $user = $this->users->get($username); 1367 | if(!$user->id) return; 1368 | $u = $this->wire->users->delete($user); 1369 | } 1370 | 1371 | /** 1372 | * Add role to user 1373 | * 1374 | * @param string $role 1375 | * @param User|string $user 1376 | * @return void 1377 | */ 1378 | public function addRoleToUser($role, $user) { 1379 | /** @var User $user */ 1380 | $user = $this->users->get((string)$user); 1381 | if(!$user->id) throw new WireException("User not found"); 1382 | $user->of(false); 1383 | $user->addRole($role); 1384 | $user->save(); 1385 | } 1386 | 1387 | /** 1388 | * Add roles to user 1389 | * 1390 | * @param array $roles 1391 | * @param User|string $user 1392 | * @return void 1393 | */ 1394 | public function addRolesToUser($roles, $user) { 1395 | foreach($roles as $role) $this->addRoleToUser($role, $user); 1396 | } 1397 | 1398 | /* ##### modules ##### */ 1399 | 1400 | /** 1401 | * Set module config data 1402 | * 1403 | * @param string|Module $module 1404 | * @param array $data 1405 | * @return Module 1406 | */ 1407 | public function setModuleConfig($module, $data) { 1408 | $module = $this->modules->get((string)$module); 1409 | if(!$module) throw new WireException("Module not found!"); 1410 | $this->modules->saveConfig($module, $data); 1411 | } 1412 | 1413 | /** 1414 | * Update module config data 1415 | * 1416 | * @param string|Module $module 1417 | * @param array $data 1418 | * @return Module 1419 | */ 1420 | public function updateModuleConfig($module, $data) { 1421 | $module = $this->modules->get((string)$module); 1422 | if(!$module) throw new WireException("Module not found!"); 1423 | 1424 | $newdata = $this->getModuleConfig($module); 1425 | foreach($data as $k=>$v) $newdata[$k] = $v; 1426 | $this->modules->saveConfig((string)$module, $newdata); 1427 | } 1428 | 1429 | /** 1430 | * Get module config data 1431 | * 1432 | * @param string $module 1433 | * @return array 1434 | */ 1435 | public function getModuleConfig($module) { 1436 | $module = $this->modules->get($module); 1437 | return $this->modules->getModuleConfigData($module); 1438 | } 1439 | 1440 | /** 1441 | * Install module 1442 | * 1443 | * If an URL is provided the module will be downloaded before installation. 1444 | * 1445 | * @param string $name 1446 | * @param string $url 1447 | * @return void 1448 | */ 1449 | public function installModule($name, $url = null) { 1450 | // if the module is already installed we return it 1451 | $module = $this->modules->get((string)$name); 1452 | if($module) return $module; 1453 | 1454 | // if an url was provided, download the module 1455 | if($url) $this->downloadModule($url); 1456 | 1457 | // install and return the module 1458 | return $this->modules->install($name); 1459 | } 1460 | 1461 | /** 1462 | * Download module from url 1463 | * 1464 | * @param string $url 1465 | * @return void 1466 | */ 1467 | public function downloadModule($url) { 1468 | require_once($this->config->paths->modules . "Process/ProcessModule/ProcessModuleInstall.php"); 1469 | $install = $this->wire(new ProcessModuleInstall()); 1470 | $install->downloadModule($url); 1471 | } 1472 | 1473 | /** 1474 | * Uninstall module 1475 | * 1476 | * @param string|Module $name 1477 | * @return void 1478 | */ 1479 | public function uninstallModule($name) { 1480 | $this->modules->uninstall((string)$name); 1481 | } 1482 | 1483 | /** 1484 | * Delete module 1485 | * 1486 | * @param string $name 1487 | * @return void 1488 | */ 1489 | public function deleteModule($name) { 1490 | $module = $this->modules->get((string)$name); 1491 | $this->uninstallModule($name); 1492 | $this->files->rmdir($this->config->paths($module), true); 1493 | } 1494 | 1495 | /* ##### helpers ##### */ 1496 | 1497 | /** 1498 | * Fire the callback if the version upgrading to ($to) is higher or equal 1499 | * to provided version ($version) 1500 | * 1501 | * 0.0.1 --> 0.0.2, VERSION = 0.0.2 --> fires 1502 | * 0.0.1 --> 0.0.5, VERSION = 0.0.2 --> fires 1503 | * 0.0.4 --> 0.0.5, VERSION = 0.0.2 --> fires 1504 | * 0.0.4 --> 0.0.5, VERSION = 1.0.2 --> does not fire 1505 | * 1506 | * @return void 1507 | */ 1508 | public function fireSince($version, $to, $func) { 1509 | if($this->isLower($to, $version)) return; 1510 | $func->__invoke($this); 1511 | } 1512 | 1513 | /** 1514 | * Is v1 lower than v2? 1515 | * @return bool 1516 | */ 1517 | public function isLower($v1, $v2) { 1518 | return version_compare($v1, $v2) < 0; 1519 | } 1520 | 1521 | /** 1522 | * Is v1 higher than v2? 1523 | * @return bool 1524 | */ 1525 | public function isHigher($v1, $v2) { 1526 | return version_compare($v1, $v2) > 0; 1527 | } 1528 | 1529 | /** 1530 | * Is v1 the same as v2? 1531 | * @return bool 1532 | */ 1533 | public function isSame($v1, $v2) { 1534 | return version_compare($v1, $v2) === 0; 1535 | } 1536 | 1537 | /** 1538 | * Sanitize repeater matrix array 1539 | * @param array $data 1540 | * @return array 1541 | */ 1542 | private function getMatrixDataArray($data) { 1543 | $newdata = []; 1544 | foreach($data as $key=>$val) { 1545 | // make sure fields is an array of ids 1546 | if($key === 'fields') { 1547 | $ids = []; 1548 | foreach($val as $_field) { 1549 | $ids[] = $this->fields->get((string)$_field)->id; 1550 | } 1551 | $val = $ids; 1552 | } 1553 | $newdata[$key] = $val; 1554 | } 1555 | return $newdata; 1556 | } 1557 | 1558 | /** 1559 | * Reset repeaterFields property of matrix field 1560 | * @param Field $field 1561 | * @return Field 1562 | */ 1563 | private function resetMatrixRepeaterFields(Field $field) { 1564 | $ids = [$this->fields->get('repeater_matrix_type')->id]; 1565 | $n = 1; 1566 | while(array_key_exists("matrix{$n}_name", $field->getArray())) { 1567 | $ids = array_merge($ids, $field->get("matrix{$n}_fields") ?: []); 1568 | $n++; 1569 | } 1570 | $field->set('repeaterFields', $ids); 1571 | 1572 | // remove unneeded fields 1573 | $tpl = $this->getRepeaterTemplate($field); 1574 | foreach($tpl->fields as $f) { 1575 | if($f->name === 'repeater_matrix_type') continue; 1576 | if(in_array($f->id, $ids)) continue; 1577 | $this->removeFieldFromTemplate($f, $tpl); 1578 | } 1579 | 1580 | return $field; 1581 | } 1582 | 1583 | /* ##### config file support ##### */ 1584 | 1585 | /** 1586 | * Migrate PW setup based on config array 1587 | * 1588 | * The method returns the used config so that you can do actions after migration 1589 | * eg adding custom tags to all fields or templates that where migrated 1590 | * 1591 | * @return WireData 1592 | */ 1593 | public function migrate($config, $vars = []) { 1594 | $config = $this->getConfig($config, $vars); 1595 | 1596 | // trigger before callback 1597 | if(is_callable($config->before)) { 1598 | $config->before->__invoke($this); 1599 | } 1600 | 1601 | // setup fields 1602 | foreach($config->fields as $name=>$data) $this->createField($name, $data['type']); 1603 | foreach($config->fields as $name=>$data) $this->setFieldData($name, $data); 1604 | 1605 | // setup templates 1606 | foreach($config->templates as $name=>$data) $this->createTemplate($name, false); 1607 | foreach($config->templates as $name=>$data) $this->setTemplateData($name, $data, true); 1608 | 1609 | // setup pages 1610 | foreach($config->pages as $name=>$data) { 1611 | if(is_int($name)) { 1612 | // no name provided 1613 | $name = uniqid(); 1614 | } 1615 | 1616 | $d = $this->wire(new WireData()); /** @var WireData $d */ 1617 | $d->setArray($data); 1618 | $this->createPage( 1619 | $d->title ?: $name, 1620 | $name, 1621 | $d->template, 1622 | $d->parent, 1623 | $d->status, 1624 | $d->data); 1625 | } 1626 | 1627 | // trigger after callback 1628 | if(is_callable($config->after)) { 1629 | $config->after->__invoke($this); 1630 | } 1631 | 1632 | return $config; 1633 | } 1634 | 1635 | /** 1636 | * Get config data object 1637 | * @return WireData 1638 | */ 1639 | public function getConfig($config, $vars = []) { 1640 | $config = $this->getConfigArray($config, $vars); 1641 | $data = $this->wire(new WireData()); /** @var WireData $data */ 1642 | $config = $data->setArray($config); 1643 | return $config; 1644 | } 1645 | 1646 | /** 1647 | * Get config array 1648 | * @return array 1649 | */ 1650 | public function getConfigArray($config, $vars = []) { 1651 | if(is_string($config)) { 1652 | if(is_file($config)) { 1653 | $config = $this->files->render($config, $vars); 1654 | } 1655 | } 1656 | if(!is_array($config)) throw new WireException("Invalid config data"); 1657 | 1658 | // this ensures that $config->fields is an empty array rather than 1659 | // a processwire fields object (proxied from the wire object) 1660 | if(!array_key_exists("fields", $config)) $config['fields'] = []; 1661 | if(!array_key_exists("templates", $config)) $config['templates'] = []; 1662 | if(!array_key_exists("pages", $config)) $config['pages'] = []; 1663 | 1664 | return $config; 1665 | } 1666 | 1667 | /** 1668 | * Get pathinfo of file/directory as WireData 1669 | * @return WireData 1670 | */ 1671 | public function info($str) { 1672 | $config = $this->wire('config'); 1673 | $info = $this->wire(new WireData()); /** @var WireData $info */ 1674 | $info->setArray(pathinfo($str)); 1675 | $info->dirname = Paths::normalizeSeparators($info->dirname)."/"; 1676 | $info->path = "{$info->dirname}{$info->basename}"; 1677 | $info->url = str_replace($config->paths->root, $config->urls->root, $info->path); 1678 | $info->is_dir = is_dir($info->path); 1679 | $info->is_file = is_file($info->path); 1680 | $info->isDir = !$info->extension; 1681 | $info->isFile = !!$info->extension; 1682 | $info->exists = ($info->is_dir || $info->is_file); 1683 | if($info->is_file) $info->m = "?m=".filemtime($info->path); 1684 | return $info; 1685 | } 1686 | 1687 | /** 1688 | * Load classes in given folder 1689 | */ 1690 | public function loadClasses($dir, $namespace = null) { 1691 | foreach($this->files->find($dir, ['extensions' => ['php']]) as $file) { 1692 | $info = $this->info($file); 1693 | require_once($info->path); 1694 | $class = $info->filename; 1695 | if($namespace) $class = "\\$namespace\\$class"; 1696 | $tmp = new $class(); 1697 | if(method_exists($tmp, "init")) $tmp->init(); 1698 | } 1699 | } 1700 | 1701 | /* ##### languages ##### */ 1702 | 1703 | /** 1704 | * Language support via API is tricky! For the time it is recommended to 1705 | * enable language support manually and then do all further changes via API. 1706 | */ 1707 | 1708 | // /** 1709 | // * Install language support. 1710 | // * 1711 | // * It can be helpful to completely remove language support in some situations: 1712 | // * https://processwire.com/talk/topic/7207-can%C2%B4t-install-languagesupport/ 1713 | // * 1714 | // * @return void 1715 | // */ 1716 | // public function installLanguageSupport() { 1717 | // $this->modules->install('LanguageSupport'); 1718 | // $this->modules->install('LanguageSupportFields'); 1719 | // $this->modules->install('LanguageSupportPageNames'); 1720 | // $this->modules->install('LanguageTabs'); 1721 | // } 1722 | 1723 | // /** 1724 | // * Uninstall language support. 1725 | // * 1726 | // * @return void 1727 | // */ 1728 | // public function uninstallLanguageSupport() { 1729 | // $this->modules->uninstall('LanguageTabs'); 1730 | // $this->modules->uninstall('LanguageSupportPageNames'); 1731 | // $this->modules->uninstall('LanguageSupportFields'); 1732 | // $this->modules->uninstall('LanguageSupport'); 1733 | // } 1734 | 1735 | // /** 1736 | // * Reset language support. 1737 | // * This can help if you have trouble uninstalling language support manually: 1738 | // * https://processwire.com/talk/topic/7207-can%C2%B4t-install-languagesupport/ 1739 | // * 1740 | // * @return void 1741 | // */ 1742 | // public function resetLanguageSupport() { 1743 | // $setup = $this->pages->get('parent.id=2, name=setup'); 1744 | // $this->deletePage($this->pages->get([ 1745 | // 'name' => 'language-translator', 1746 | // 'parent' => $setup, 1747 | // ])); 1748 | // $this->deletePage($this->pages->get([ 1749 | // 'name' => 'languages', 1750 | // 'parent' => $setup, 1751 | // ])); 1752 | // $this->deleteField('language'); 1753 | // $this->deleteField('language_files'); 1754 | // $this->deleteTemplate('language'); 1755 | // $this->modules->uninstall('ProcessLanguageTranslator'); 1756 | // $this->modules->uninstall('ProcessLanguage'); 1757 | // @$this->modules->uninstall('LanguageSupport'); 1758 | // } 1759 | 1760 | public function __debugInfo() { 1761 | return []; 1762 | } 1763 | } 1764 | --------------------------------------------------------------------------------