PluginProbe
JetFormBuilder — Dynamic Blocks Form Builder / 3.6.6
JetFormBuilder — Dynamic Blocks Form Builder v3.6.6
3.6.6.1 3.6.6 3.6.5.4 3.6.5.3 3.6.5.2 3.6.5.1 3.6.5 3.6.4.2 3.6.4.1 3.6.4 3.6.3.1 3.6.3 3.6.2.2 3.6.2.1 3.6.2 3.6.1.1 3.6.1 3.6.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 All 134 releases
jetformbuilder / includes / migrations / auto-migrator.php

auto-migrator.php in JetFormBuilder — Dynamic Blocks Form Builder 3.6.6, at includes/migrations/auto-migrator.php

219 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 namespace Jet_Form_Builder\Migrations;
5
6 use Jet_Form_Builder\Classes\Instance_Trait;
7 use Jet_Form_Builder\Db_Queries\Execution_Builder;
8 use Jet_Form_Builder\Migrations\Versions\Version_3_6_5_2;
9 use Jet_Form_Builder\Migrations\Versions\Version_3_6_5_3;
10 use Jet_Form_Builder\Migrations\Versions\Version_3_6_6;
11
12 // If this file is called directly, abort.
13 if ( ! defined( 'WPINC' ) ) {
14 die;
15 }
16
17 /**
18 * Runs outstanding migrations automatically once the plugin files are updated to a
19 * newer version, without waiting for a manual "Update DB" click (that notice only
20 * covers form-record table migrations, not general ones like `Version_3_6_5_2`) and
21 * without relying on `register_activation_hook` (which does NOT fire on plugin update,
22 * only on manual (re)activation).
23 *
24 * Only migrations explicitly returned by `migration_instances()` are auto-run. This is
25 * intentionally separate from `Migrator::install()`: older schema migrations can be
26 * expensive and may require a deliberate/manual update, so an unrelated admin request
27 * must not start the full historical migration set.
28 *
29 * The selected migration classes stay in this registry permanently. Therefore a client
30 * may skip the release that introduced `Version_3_6_5_2` and update directly to 3.7.0 (or
31 * later): its stored DB version is still older than the running plugin, the auto-migrator
32 * runs, and `Base_Migration::install()` executes every selected migration that is not yet
33 * recorded in the migrations table. `Version_3_6_5_2` and `Version_3_6_5_3` each run a
34 * time-boxed batch loop per request and persist a resume cursor when their time budget is
35 * exceeded; an incomplete pass throws so the migration stays "not installed" and the next
36 * capable admin's `admin_init` request continues it automatically from that cursor — no
37 * AJAX or manual admin-page step is involved. A migration signaling an incomplete batch also
38 * issues its own intermediate `COMMIT` before throwing (to persist the resume cursor), which
39 * ends the transaction this class started; `install_migrations()` therefore stops immediately
40 * on that signal rather than attempting a later migration outside any transaction. The next
41 * capable admin's request retries from the beginning of `migration_instances()`, skipping
42 * whatever already recorded itself as installed, until every migration completes.
43 *
44 * @since 3.6.5.2
45 */
46 class Auto_Migrator {
47
48 use Instance_Trait;
49
50 const DB_VERSION_OPTION = 'jet_fb_db_version';
51 const LOCK_TRANSIENT = 'jet_fb_migrations_running';
52
53 public function init_hooks() {
54 add_action( 'admin_init', array( $this, 'maybe_run' ) );
55 }
56
57 /**
58 * Runs migrations if the plugin code is newer than the last migrated DB version.
59 * Cheap no-op on the vast majority of requests (single option read + version compare).
60 */
61 public function maybe_run() {
62 if ( ! $this->needs_upgrade() ) {
63 return;
64 }
65
66 // Only an admin-context request that can manage the site should trigger schema/data
67 // changes. Other admin requests (e.g. admin-ajax from a low-priv user) just skip;
68 // they'll be picked up on the next capable admin's page load.
69 if ( ! current_user_can( 'manage_options' ) ) {
70 return;
71 }
72
73 // Guard against two overlapping admin requests both running migrations at once.
74 if ( get_transient( self::LOCK_TRANSIENT ) ) {
75 return;
76 }
77
78 set_transient( self::LOCK_TRANSIENT, 1, MINUTE_IN_SECONDS * 5 );
79
80 try {
81 $this->run();
82 } finally {
83 delete_transient( self::LOCK_TRANSIENT );
84 }
85 }
86
87 public function needs_upgrade(): bool {
88 $stored = get_option( self::DB_VERSION_OPTION, '' );
89
90 // No stamp yet: either a genuine update from a pre-3.6.5.2 version (including a
91 // client that skipped straight to a later release), or a fresh install. `run()`
92 // checks the selected migrations individually, so both cases are safe.
93 if ( '' === $stored ) {
94 return true;
95 }
96
97 return version_compare( $stored, JET_FORM_BUILDER_VERSION, '<' );
98 }
99
100 /**
101 * Whether the auto-migration set (`migration_instances()`) has not yet fully completed
102 * for this site. On a small site this is true for at most a single `admin_init` request;
103 * on a large site it can span several requests while a time-boxed migration resumes
104 * itself batch by batch. Exposed so UI that depends on the migrated data being complete
105 * (e.g. the "Allowed Server-Side Callbacks" settings tab, whose `import_trusted_callbacks()`
106 * only runs once the whole scan finishes) can disable editing and show a wait state
107 * instead of racing a save against a migration batch still in flight.
108 *
109 * @since 3.6.5.3
110 */
111 public function is_migration_in_progress(): bool {
112 return $this->needs_upgrade() && ! $this->all_installed();
113 }
114
115 protected function run() {
116 // Nothing outstanding (e.g. fresh install already stamped by table creation) →
117 // just record the version and skip touching the DB in a transaction.
118 if ( $this->all_installed() ) {
119 $this->stamp_version();
120
121 return;
122 }
123
124 try {
125 if ( false === $this->transaction_start() ) {
126 throw new \RuntimeException( 'Failed to start the auto-migration transaction.' );
127 }
128
129 $this->install_migrations();
130
131 if ( false === $this->transaction_commit() ) {
132 throw new \RuntimeException( 'Failed to commit the auto-migration transaction.' );
133 }
134 } catch ( \Throwable $exception ) {
135 $this->transaction_rollback();
136
137 // Do NOT stamp the version on failure — leave `needs_upgrade()` true so the
138 // next capable admin request retries. Surface the reason for support.
139 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
140 error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
141 '[jet-form-builder] auto-migration failed: ' . $exception->getMessage()
142 );
143 }
144
145 return;
146 }
147
148 // Success → record the version so we don't re-scan on every admin load until the
149 // next update bumps the plugin version again.
150 $this->stamp_version();
151 }
152
153 /**
154 * Runs only the migration classes explicitly selected for automatic execution. Each
155 * migration is skipped internally if it is already recorded as installed, so keeping
156 * an old migration in this list is both safe and required for clients that skip the
157 * release where it was introduced.
158 *
159 * A migration signaling an incomplete batch (`Migration_Incomplete_Exception`) stops the
160 * loop immediately instead of continuing to the next migration. Time-boxed migrations
161 * (e.g. `Version_3_6_5_2`, `Version_3_6_5_3`) persist their own progress by issuing an
162 * intermediate `COMMIT` before throwing, so the outer transaction `run()` started is no
163 * longer intact once one of them yields — a later migration run in that same request
164 * would execute outside any transaction, and a genuine failure in it could no longer be
165 * rolled back. Re-throwing right away still leaves `needs_upgrade()` true (via `run()`'s
166 * catch block skipping `stamp_version()`) so the next capable admin request resumes the
167 * interrupted migration, and only then attempts the ones after it.
168 *
169 * @throws \Jet_Form_Builder\Migrations\Migration_Exception
170 * @throws \Jet_Form_Builder\Migrations\Migration_Incomplete_Exception
171 */
172 protected function install_migrations() {
173 foreach ( $this->migration_instances() as $migration ) {
174 $migration->install();
175 }
176 }
177
178 protected function all_installed(): bool {
179 foreach ( $this->migration_instances() as $migration ) {
180 if ( ! $migration->is_installed() ) {
181 return false;
182 }
183 }
184
185 return true;
186 }
187
188 /**
189 * Automatic migrations must remain listed in future plugin versions so sites that
190 * jump over the introducing release still receive them. Do not place regular/manual
191 * schema migrations here.
192 *
193 * @return \Jet_Form_Builder\Migrations\Versions\Base_Migration[]
194 */
195 protected function migration_instances(): array {
196 return array(
197 new Version_3_6_5_2(),
198 new Version_3_6_5_3(),
199 new Version_3_6_6(),
200 );
201 }
202
203 protected function stamp_version() {
204 update_option( self::DB_VERSION_OPTION, JET_FORM_BUILDER_VERSION, false );
205 }
206
207 protected function transaction_start() {
208 return Execution_Builder::instance()->transaction_start();
209 }
210
211 protected function transaction_commit() {
212 return Execution_Builder::instance()->transaction_commit();
213 }
214
215 protected function transaction_rollback() {
216 return Execution_Builder::instance()->transaction_rollback();
217 }
218 }
219