PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.1
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.1
5.6.2 5.6.3 5.6.1 5.6.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 3.0.62 All 38 releases
double-opt-in / src / Migration / MigrationRegistry.php

MigrationRegistry.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.6.1, at src/Migration/MigrationRegistry.php

250 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Migration Registry
4 *
5 * @package Forge12\DoubleOptIn\Migration
6 * @since 4.3.0
7 */
8
9 namespace Forge12\DoubleOptIn\Migration;
10
11 use Forge12\Shared\LoggerInterface;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class MigrationRegistry
19 *
20 * @api
21 *
22 * Collects migration declarations from Core and addons, then applies any
23 * that have not yet run on this site. Applied migration IDs are persisted
24 * in the WordPress option `f12_doi_applied_migrations`.
25 *
26 * Addons register migrations during their `boot()` method; the registry
27 * does not schedule itself — the integrating provider calls
28 * {@see runPending()} once addons are registered (typically on
29 * `admin_init` after addon bootstrap).
30 */
31 final class MigrationRegistry {
32
33 private const OPTION_KEY = 'f12_doi_applied_migrations';
34
35 /**
36 * Singleton instance.
37 *
38 * @var MigrationRegistry|null
39 */
40 private static ?MigrationRegistry $instance = null;
41
42 /**
43 * Registered migrations, keyed by ID.
44 *
45 * @var array<string, MigrationInterface>
46 */
47 private array $migrations = array();
48
49 /**
50 * Logger.
51 *
52 * @var LoggerInterface|null
53 */
54 private ?LoggerInterface $logger = null;
55
56 private function __construct() {}
57
58 private function __clone() {}
59
60 /**
61 * @throws \Exception
62 */
63 public function __wakeup() {
64 throw new \Exception( 'Cannot unserialize singleton' );
65 }
66
67 public static function getInstance(): MigrationRegistry {
68 if ( self::$instance === null ) {
69 self::$instance = new self();
70 }
71 return self::$instance;
72 }
73
74 /**
75 * @internal Tests only.
76 */
77 public static function resetInstance(): void {
78 self::$instance = null;
79 }
80
81 public function setLogger( LoggerInterface $logger ): void {
82 $this->logger = $logger;
83 }
84
85 /**
86 * Register a migration.
87 *
88 * Duplicate IDs are refused with a warning — a migration ID is a
89 * primary key and accidentally re-using one would corrupt the
90 * applied-migrations bookkeeping.
91 *
92 * @param MigrationInterface $migration The migration to register.
93 * @return bool True if registered, false if ID collision.
94 */
95 public function register( MigrationInterface $migration ): bool {
96 $id = $migration->getId();
97
98 if ( isset( $this->migrations[ $id ] ) ) {
99 $this->log(
100 'warning',
101 'Migration ID collision — second registration ignored',
102 array(
103 'migration_id' => $id,
104 )
105 );
106 return false;
107 }
108
109 $this->migrations[ $id ] = $migration;
110
111 $this->log(
112 'debug',
113 'Migration registered',
114 array(
115 'migration_id' => $id,
116 )
117 );
118
119 return true;
120 }
121
122 /**
123 * All registered migration IDs.
124 *
125 * @return string[]
126 */
127 public function getRegisteredIds(): array {
128 return array_keys( $this->migrations );
129 }
130
131 /**
132 * IDs of migrations that have already been applied on this site.
133 *
134 * @return string[]
135 */
136 public function getAppliedIds(): array {
137 $applied = get_option( self::OPTION_KEY, array() );
138 return is_array( $applied ) ? array_values( $applied ) : array();
139 }
140
141 /**
142 * IDs of migrations that are registered but not yet applied.
143 *
144 * @return string[]
145 */
146 public function getPendingIds(): array {
147 return array_values(
148 array_diff(
149 $this->getRegisteredIds(),
150 $this->getAppliedIds()
151 )
152 );
153 }
154
155 /**
156 * Apply every registered migration that has not yet been applied.
157 *
158 * Failures in one migration do not abort the loop; each migration is
159 * isolated. A failing migration stays in the pending list and will
160 * be retried on the next bootstrap.
161 *
162 * @return array{applied: string[], failed: string[]}
163 */
164 public function runPending(): array {
165 global $wpdb;
166
167 $applied = $this->getAppliedIds();
168 $results = array(
169 'applied' => array(),
170 'failed' => array(),
171 );
172
173 foreach ( $this->getPendingIds() as $id ) {
174 $migration = $this->migrations[ $id ];
175
176 try {
177 $migration->up( $wpdb );
178
179 $applied[] = $id;
180 $results['applied'][] = $id;
181
182 $this->log(
183 'info',
184 'Migration applied',
185 array(
186 'migration_id' => $id,
187 'description' => $migration->getDescription(),
188 )
189 );
190 } catch ( \Throwable $e ) {
191 $results['failed'][] = $id;
192 $this->log(
193 'error',
194 'Migration failed — will retry next bootstrap',
195 array(
196 'migration_id' => $id,
197 'error' => $e->getMessage(),
198 'file' => $e->getFile(),
199 'line' => $e->getLine(),
200 )
201 );
202 }
203 }
204
205 // Persist the cumulative applied list in one write. We dedupe
206 // defensively in case a migration was recorded elsewhere and now
207 // also flows through here.
208 $applied = array_values( array_unique( $applied ) );
209 update_option( self::OPTION_KEY, $applied, false );
210
211 return $results;
212 }
213
214 /**
215 * Log helper.
216 *
217 * @param string $level Log level.
218 * @param string $message Message.
219 * @param array $context Context.
220 * @return void
221 */
222 private function log( string $level, string $message, array $context = array() ): void {
223 if ( $this->logger === null ) {
224 return;
225 }
226
227 $context = array_merge(
228 array(
229 'plugin' => 'double-opt-in',
230 'component' => 'migration-registry',
231 ),
232 $context
233 );
234
235 switch ( $level ) {
236 case 'error':
237 $this->logger->error( $message, $context );
238 break;
239 case 'warning':
240 $this->logger->warning( $message, $context );
241 break;
242 case 'info':
243 $this->logger->info( $message, $context );
244 break;
245 default:
246 $this->logger->debug( $message, $context );
247 }
248 }
249 }
250