woocommerce
/
src
/
Internal
/
PushNotifications
/
DataStores
/
NotificationPreferencesDataStore.php
NotificationPreferencesDataStore.php
140 lines
| 1 | <?php |
| 2 | |
| 3 | declare( strict_types = 1 ); |
| 4 | |
| 5 | namespace Automattic\WooCommerce\Internal\PushNotifications\DataStores; |
| 6 | |
| 7 | defined( 'ABSPATH' ) || exit; |
| 8 | |
| 9 | use Automattic\WooCommerce\Internal\Utilities\Users; |
| 10 | use WC_Data_Exception; |
| 11 | use WP_Http; |
| 12 | |
| 13 | /** |
| 14 | * Persistence layer for per-user push notification preferences. |
| 15 | * |
| 16 | * Stores a single versioned envelope per user under the `wc_push_notification_preferences` |
| 17 | * user meta key. The key is automatically scoped to the current site by `Users::*_site_user_meta`, |
| 18 | * which prefixes the underlying meta key with the blog ID so preferences set on one site in a |
| 19 | * multisite network do not leak to other sites the same user belongs to. Owns schema migration |
| 20 | * on read and surfaces real DB write failures via `WC_Data_Exception`. |
| 21 | * |
| 22 | * @since 10.8.0 |
| 23 | */ |
| 24 | class NotificationPreferencesDataStore { |
| 25 | /** |
| 26 | * User meta key under which the preferences envelope is stored. |
| 27 | */ |
| 28 | const META_KEY = 'wc_push_notification_preferences'; |
| 29 | |
| 30 | /** |
| 31 | * Current preferences schema version. |
| 32 | * |
| 33 | * Bump when the envelope's `preferences` shape changes, and add a |
| 34 | * corresponding branch to `migrate()`. |
| 35 | */ |
| 36 | const CURRENT_SCHEMA_VERSION = 1; |
| 37 | |
| 38 | /** |
| 39 | * Read the stored envelope for a user. |
| 40 | * |
| 41 | * Migrates older schema versions on the fly and persists the upgrade so |
| 42 | * callers always receive a current-version envelope. Returns null when |
| 43 | * nothing is stored for the user. |
| 44 | * |
| 45 | * @param int $user_id The user ID. |
| 46 | * |
| 47 | * @return array|null Envelope with `schema_version` and `preferences` keys, or null if unstored. |
| 48 | * |
| 49 | * @throws WC_Data_Exception When persisting a migrated envelope fails. |
| 50 | * |
| 51 | * @since 10.8.0 |
| 52 | */ |
| 53 | public function read( int $user_id ): ?array { |
| 54 | $stored = Users::get_site_user_meta( $user_id, self::META_KEY ); |
| 55 | |
| 56 | if ( ! is_array( $stored ) || empty( $stored ) ) { |
| 57 | return null; |
| 58 | } |
| 59 | |
| 60 | $stored_version = isset( $stored['schema_version'] ) ? (int) $stored['schema_version'] : 0; |
| 61 | |
| 62 | if ( $stored_version < self::CURRENT_SCHEMA_VERSION ) { |
| 63 | $stored = $this->migrate( $stored, $stored_version ); |
| 64 | $this->write( $user_id, $stored ); |
| 65 | } |
| 66 | |
| 67 | return $stored; |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Persist an envelope for a user. |
| 72 | * |
| 73 | * No-ops when the stored value already matches the supplied envelope. |
| 74 | * Throws `WC_Data_Exception` when the underlying user meta write fails |
| 75 | * for a reason other than the value being unchanged. |
| 76 | * |
| 77 | * @param int $user_id The user ID. |
| 78 | * @param array $envelope The envelope to persist (must have `schema_version` and `preferences` keys). |
| 79 | * |
| 80 | * @return void |
| 81 | * |
| 82 | * @throws WC_Data_Exception When the user meta write fails for a non-no-op reason. |
| 83 | * |
| 84 | * @since 10.8.0 |
| 85 | */ |
| 86 | public function write( int $user_id, array $envelope ): void { |
| 87 | // Skip the write when the stored envelope already matches. This avoids |
| 88 | // the ambiguous `false` return from update_user_meta() that means |
| 89 | // either "value unchanged" or "DB write failed" — by short-circuiting |
| 90 | // the no-op case, a `false` from the call below unambiguously means |
| 91 | // the write itself failed and we can surface it. |
| 92 | $stored = Users::get_site_user_meta( $user_id, self::META_KEY ); |
| 93 | if ( $stored === $envelope ) { |
| 94 | return; |
| 95 | } |
| 96 | |
| 97 | $result = Users::update_site_user_meta( $user_id, self::META_KEY, $envelope ); |
| 98 | |
| 99 | if ( false === $result ) { |
| 100 | // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped |
| 101 | throw new WC_Data_Exception( |
| 102 | 'woocommerce_push_notification_preferences_save_failed', |
| 103 | 'Failed to save push notification preferences.', |
| 104 | WP_Http::INTERNAL_SERVER_ERROR |
| 105 | ); |
| 106 | // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Upgrade an envelope to the current schema version. |
| 112 | * |
| 113 | * Pure transformation — does not persist. Missing or malformed |
| 114 | * `preferences` entries are replaced with an empty array; the service |
| 115 | * layer is responsible for filling in defaults from the empty case. |
| 116 | * |
| 117 | * @param array $data The stored envelope (expected keys: `schema_version`, `preferences`). |
| 118 | * @param int $from_version The schema version currently on disk. |
| 119 | * |
| 120 | * @return array Envelope upgraded to `self::CURRENT_SCHEMA_VERSION`. |
| 121 | * |
| 122 | * @since 10.8.0 |
| 123 | */ |
| 124 | public function migrate( array $data, int $from_version ): array { |
| 125 | // Parameter reserved for future schema migrations. |
| 126 | unset( $from_version ); |
| 127 | |
| 128 | $preferences = isset( $data['preferences'] ) && is_array( $data['preferences'] ) |
| 129 | ? $data['preferences'] |
| 130 | : array(); |
| 131 | |
| 132 | // For v1 the envelope shape is stable; we only normalize the version tag. |
| 133 | |
| 134 | return array( |
| 135 | 'schema_version' => self::CURRENT_SCHEMA_VERSION, |
| 136 | 'preferences' => $preferences, |
| 137 | ); |
| 138 | } |
| 139 | } |
| 140 |