step_logger = $step_logger; } /** * JSON-encodes notifications and fires a non-blocking POST to the internal * REST endpoint. * * @param Notification[] $notifications The notifications to dispatch. * @return void * * @since 10.7.0 */ public function dispatch( array $notifications ): void { if ( empty( $notifications ) ) { return; } $encoded = array_map( fn ( Notification $notification ) => $notification->to_array(), $notifications ); $body = wp_json_encode( array( 'notifications' => $encoded ) ); if ( false === $body ) { foreach ( $notifications as $notification ) { $this->step_logger->log_failure( $notification, 'loopback_requested', 'encode_failed', 'error', 'Failed to JSON-encode push notification payload.', array( 'batch_size' => count( $notifications ) ) ); } return; } $token = JsonWebToken::create( array( 'iss' => get_site_url(), 'exp' => time() + self::JWT_EXPIRY_SECONDS, 'body_hash' => hash( 'sha256', $body ), ), wp_salt( 'auth' ) ); /** * The request is non-blocking so the response is not handled anywhere. * If the request fails, the ActionScheduler safety net will pick up * unsent notifications after 60 seconds. * * The token travels both as an Authorization header and as a query * parameter: the receiver prefers the header and falls back to the * parameter on hosts that strip the header (see TOKEN_QUERY_PARAM). */ $response = wp_remote_post( add_query_arg( self::TOKEN_QUERY_PARAM, $token, rest_url( self::SEND_ENDPOINT ) ), array( 'blocking' => false, 'timeout' => 1, 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $token, ), 'body' => $body, ) ); /* * One request carries the whole batch, so one line records it, naming * each notification so a per-notification read can find it. The * identifier is carried as well as the type and resource ID, because a * subclass may build it from more: two stock events for one product * share a type and a resource ID and differ only there. */ $context = array( 'batch_size' => count( $notifications ), 'notifications' => array_map( fn( Notification $notification ) => array( 'type' => $notification->get_type(), 'resource_id' => $notification->get_resource_id(), 'identifier' => $notification->get_identifier(), ), $notifications ), ); if ( is_wp_error( $response ) ) { $this->step_logger->log_unattributed_failure( 'loopback_requested', 'request_failed', 'warning', sprintf( 'Loopback request failed: %s', $response->get_error_message() ), $context ); return; } $this->step_logger->log_batch_step( 'loopback_requested', 'ok', $context ); } }