DeferredEmailQueue.php
1 month ago
EmailColors.php
11 months ago
EmailFont.php
1 year ago
EmailLogger.php
1 month ago
EmailStyleSync.php
11 months ago
OrderPriceFormatter.php
1 year ago
DeferredEmailQueue.php
291 lines
| 1 | <?php |
| 2 | |
| 3 | declare( strict_types = 1 ); |
| 4 | |
| 5 | namespace Automattic\WooCommerce\Internal\Email; |
| 6 | |
| 7 | use Automattic\WooCommerce\Internal\StockNotifications\Factory as StockNotificationFactory; |
| 8 | use Automattic\WooCommerce\Internal\StockNotifications\Notification as StockNotification; |
| 9 | |
| 10 | /** |
| 11 | * Handles deferred transactional email sending via Action Scheduler. |
| 12 | * |
| 13 | * Collects email callbacks during a request and dispatches each one as an |
| 14 | * individual Action Scheduler action on shutdown, replacing the legacy |
| 15 | * WC_Background_Emailer approach. |
| 16 | * |
| 17 | * @since 10.8.0 |
| 18 | */ |
| 19 | final class DeferredEmailQueue { |
| 20 | |
| 21 | /** |
| 22 | * Action Scheduler hook for processing a queued email. |
| 23 | */ |
| 24 | private const AS_HOOK = 'woocommerce_send_queued_transactional_email'; |
| 25 | |
| 26 | /** |
| 27 | * Action Scheduler group for email actions. |
| 28 | */ |
| 29 | private const AS_GROUP = 'woocommerce-emails'; |
| 30 | |
| 31 | /** |
| 32 | * Key for object references stored in queued email args. |
| 33 | */ |
| 34 | private const QUEUED_OBJECT_KEY = '__woocommerce_deferred_email_object'; |
| 35 | |
| 36 | /** |
| 37 | * Queue of email callbacks collected during the current request. |
| 38 | * |
| 39 | * @var array<int, array{filter: string, args: array}> |
| 40 | */ |
| 41 | private array $queue = array(); |
| 42 | |
| 43 | /** |
| 44 | * Whether the shutdown hook has been registered. |
| 45 | * |
| 46 | * @var bool |
| 47 | */ |
| 48 | private bool $shutdown_registered = false; |
| 49 | |
| 50 | /** |
| 51 | * Initialize hooks. |
| 52 | * |
| 53 | * @internal |
| 54 | */ |
| 55 | final public function init(): void { // phpcs:ignore Generic.CodeAnalysis.UnnecessaryFinalModifier.Found |
| 56 | // Registered unconditionally so previously-scheduled AS jobs can still |
| 57 | // be processed even if the feature is later disabled. |
| 58 | add_action( self::AS_HOOK, array( $this, 'send_queued_transactional_email' ), 10, 2 ); |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Add an email callback to the queue. |
| 63 | * |
| 64 | * Returns false when any argument cannot be represented in Action Scheduler |
| 65 | * storage, allowing callers to fall back to sending the email synchronously. |
| 66 | * |
| 67 | * @param string $filter The action hook name that triggered the email. |
| 68 | * @param array $args The arguments passed to the action hook. |
| 69 | * @return bool True if the email was queued. |
| 70 | */ |
| 71 | public function push( string $filter, array $args ): bool { |
| 72 | try { |
| 73 | $args = $this->prepare_arg_for_queue( $args ); |
| 74 | } catch ( \UnexpectedValueException $e ) { |
| 75 | return false; |
| 76 | } |
| 77 | |
| 78 | $this->queue[] = array( |
| 79 | 'filter' => $filter, |
| 80 | 'args' => $args, |
| 81 | ); |
| 82 | |
| 83 | if ( ! $this->shutdown_registered ) { |
| 84 | add_action( 'shutdown', array( $this, 'dispatch' ), 100 ); |
| 85 | $this->shutdown_registered = true; |
| 86 | } |
| 87 | |
| 88 | return true; |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Dispatch queued emails via Action Scheduler on shutdown. |
| 93 | * |
| 94 | * Each email is scheduled as an individual AS action for atomic |
| 95 | * processing and per-email failure isolation. |
| 96 | * |
| 97 | * @internal |
| 98 | */ |
| 99 | public function dispatch(): void { |
| 100 | if ( empty( $this->queue ) ) { |
| 101 | return; |
| 102 | } |
| 103 | |
| 104 | foreach ( $this->queue as $item ) { |
| 105 | \WC()->queue()->add( |
| 106 | self::AS_HOOK, |
| 107 | array( $item['filter'], $item['args'] ), |
| 108 | self::AS_GROUP |
| 109 | ); |
| 110 | } |
| 111 | |
| 112 | $this->queue = array(); |
| 113 | $this->shutdown_registered = false; |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * Process a single queued transactional email from Action Scheduler. |
| 118 | * |
| 119 | * @internal |
| 120 | * |
| 121 | * @param mixed $filter The action hook name. |
| 122 | * @param mixed $args The arguments for the email callback. |
| 123 | */ |
| 124 | public function send_queued_transactional_email( $filter, $args ): void { |
| 125 | if ( ! is_string( $filter ) || ! is_array( $args ) ) { |
| 126 | return; |
| 127 | } |
| 128 | |
| 129 | $args = $this->restore_args_from_queue( $args ); |
| 130 | if ( null === $args ) { |
| 131 | return; |
| 132 | } |
| 133 | |
| 134 | \WC_Emails::send_queued_transactional_email( $filter, $args ); |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Convert a queued argument to a JSON-safe value. |
| 139 | * |
| 140 | * @param mixed $arg The argument to convert. |
| 141 | * @return mixed |
| 142 | * @throws \UnexpectedValueException When a queued object argument cannot be prepared. |
| 143 | */ |
| 144 | private function prepare_arg_for_queue( $arg ) { |
| 145 | if ( is_array( $arg ) ) { |
| 146 | foreach ( $arg as $key => $value ) { |
| 147 | $arg[ $key ] = $this->prepare_arg_for_queue( $value ); |
| 148 | } |
| 149 | |
| 150 | return $arg; |
| 151 | } |
| 152 | |
| 153 | if ( is_object( $arg ) ) { |
| 154 | foreach ( $this->get_supported_object_types() as $type => $object_type ) { |
| 155 | if ( ! $arg instanceof $object_type['class'] ) { |
| 156 | continue; |
| 157 | } |
| 158 | |
| 159 | $id = $object_type['get_id']( $arg ); |
| 160 | |
| 161 | if ( empty( $id ) || ( ! is_int( $id ) && ! is_string( $id ) ) ) { |
| 162 | throw new \UnexpectedValueException( 'Queued email object argument cannot be prepared.' ); |
| 163 | } |
| 164 | |
| 165 | return array( |
| 166 | self::QUEUED_OBJECT_KEY => array( |
| 167 | 'type' => $type, |
| 168 | 'id' => $id, |
| 169 | ), |
| 170 | ); |
| 171 | } |
| 172 | |
| 173 | throw new \UnexpectedValueException( 'Queued email object argument cannot be prepared.' ); |
| 174 | } |
| 175 | |
| 176 | return $arg; |
| 177 | } |
| 178 | |
| 179 | /** |
| 180 | * Restore queued arguments after Action Scheduler storage. |
| 181 | * |
| 182 | * @param array $args The arguments for the email callback. |
| 183 | * @return array|null |
| 184 | */ |
| 185 | private function restore_args_from_queue( array $args ): ?array { |
| 186 | try { |
| 187 | foreach ( $args as $key => $arg ) { |
| 188 | $args[ $key ] = $this->restore_arg_from_queue( $arg ); |
| 189 | } |
| 190 | |
| 191 | return $args; |
| 192 | } catch ( \UnexpectedValueException $e ) { |
| 193 | return null; |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | /** |
| 198 | * Restore a queued argument after Action Scheduler storage. |
| 199 | * |
| 200 | * @param mixed $arg The argument to restore. |
| 201 | * @return mixed |
| 202 | * @throws \UnexpectedValueException When a queued object reference cannot be restored. |
| 203 | */ |
| 204 | private function restore_arg_from_queue( $arg ) { |
| 205 | if ( ! is_array( $arg ) ) { |
| 206 | return $arg; |
| 207 | } |
| 208 | |
| 209 | if ( ! array_key_exists( self::QUEUED_OBJECT_KEY, $arg ) ) { |
| 210 | foreach ( $arg as $key => $value ) { |
| 211 | $arg[ $key ] = $this->restore_arg_from_queue( $value ); |
| 212 | } |
| 213 | |
| 214 | return $arg; |
| 215 | } |
| 216 | |
| 217 | $reference = $arg[ self::QUEUED_OBJECT_KEY ]; |
| 218 | |
| 219 | if ( ! is_array( $reference ) || ! isset( $reference['type'], $reference['id'] ) ) { |
| 220 | throw new \UnexpectedValueException( 'Queued email object reference is invalid.' ); |
| 221 | } |
| 222 | |
| 223 | $id = $reference['id']; |
| 224 | |
| 225 | if ( ! is_int( $id ) && ! is_string( $id ) ) { |
| 226 | throw new \UnexpectedValueException( 'Queued email object reference is invalid.' ); |
| 227 | } |
| 228 | |
| 229 | $object_type = $this->get_supported_object_types()[ (string) $reference['type'] ] ?? null; |
| 230 | |
| 231 | if ( ! is_array( $object_type ) ) { |
| 232 | throw new \UnexpectedValueException( 'Queued email object reference is invalid.' ); |
| 233 | } |
| 234 | |
| 235 | $object = $object_type['fetch']( $id ); |
| 236 | |
| 237 | if ( ! is_object( $object ) ) { |
| 238 | throw new \UnexpectedValueException( 'Queued email object reference cannot be restored.' ); |
| 239 | } |
| 240 | |
| 241 | return $object; |
| 242 | } |
| 243 | |
| 244 | /** |
| 245 | * Get supported queued object types. |
| 246 | * |
| 247 | * @return array<string, array{class: class-string, get_id: callable, fetch: callable}> |
| 248 | */ |
| 249 | private function get_supported_object_types(): array { |
| 250 | return array( |
| 251 | 'product' => array( |
| 252 | 'class' => \WC_Product::class, |
| 253 | 'get_id' => static function ( $queued_object ) { |
| 254 | return $queued_object instanceof \WC_Product ? $queued_object->get_id() : null; |
| 255 | }, |
| 256 | 'fetch' => static function ( $id ) { |
| 257 | return \WC()->call_function( 'wc_get_product', $id ); |
| 258 | }, |
| 259 | ), |
| 260 | 'order' => array( |
| 261 | 'class' => \WC_Order::class, |
| 262 | 'get_id' => static function ( $queued_object ) { |
| 263 | return $queued_object instanceof \WC_Order ? $queued_object->get_id() : null; |
| 264 | }, |
| 265 | 'fetch' => static function ( $id ) { |
| 266 | return \WC()->call_function( 'wc_get_order', $id ); |
| 267 | }, |
| 268 | ), |
| 269 | 'payment_gateway' => array( |
| 270 | 'class' => \WC_Payment_Gateway::class, |
| 271 | 'get_id' => static function ( $queued_object ) { |
| 272 | return $queued_object instanceof \WC_Payment_Gateway ? $queued_object->id : null; |
| 273 | }, |
| 274 | 'fetch' => static function ( $id ) { |
| 275 | $gateways = \WC()->payment_gateways()->payment_gateways(); |
| 276 | return $gateways[ $id ] ?? null; |
| 277 | }, |
| 278 | ), |
| 279 | 'stock_notification' => array( |
| 280 | 'class' => StockNotification::class, |
| 281 | 'get_id' => static function ( $queued_object ) { |
| 282 | return $queued_object instanceof StockNotification ? $queued_object->get_id() : null; |
| 283 | }, |
| 284 | 'fetch' => static function ( $id ) { |
| 285 | return StockNotificationFactory::get_notification( (int) $id ); |
| 286 | }, |
| 287 | ), |
| 288 | ); |
| 289 | } |
| 290 | } |
| 291 |