*/ private static array $touched = []; /** * Whether the end-of-request snapshot is hooked. * * @var bool */ private static bool $hooked = false; /** * An order (or a refund of one) was saved. * * @param mixed $order The saved object. * * @internal Hooked to `woocommerce_after_order_object_save` and the refund equivalent. */ public function saved( $order ): void { if ( $order instanceof \WC_Order_Refund ) { $this->touch( (int) $order->get_parent_id() ); return; } if ( ! $order instanceof WC_Order ) { return; } // A block-checkout draft is a cart that reached the checkout page, not an // order. It is sent once it becomes one; until then it is only noise. if ( 'checkout-draft' === $order->get_status() ) { return; } $this->touch( $order->get_id() ); } /** * An order was refunded; the parent's totals changed. * * @param mixed $orderId Order id. * * @internal Hooked to `woocommerce_order_refunded`. */ public function refunded( $orderId ): void { $this->touch( (int) $orderId ); } /** * Status transitions are facts of their own (`order.status_changed`). * * @param mixed $orderId Order id. * @param mixed $from Old status. * @param mixed $to New status. * * @internal Hooked to `woocommerce_order_status_changed`. */ public function statusChanged( $orderId, $from, $to ): void { if ( 'checkout-draft' === $to ) { return; } Outbox::add( 'order.status_changed', [ 'order_id' => (string) (int) $orderId, 'from' => is_string( $from ) && '' !== $from ? Contract::status( $from ) : null, 'to' => Contract::status( (string) $to ), ] ); } /** * An order went to the bin, or was deleted for good. * * @param mixed $orderId Order id. * @param bool $permanent Deleted for good. * * @internal Hooked to `woocommerce_trash_order` and `woocommerce_delete_order`. */ public function removed( $orderId, bool $permanent ): void { unset( self::$touched[ (int) $orderId ] ); CatalogueWatcher::deleted( 'order', (int) $orderId, $permanent ); } /** * An order came back from the bin. * * @param mixed $orderId Order id. * * @internal Hooked to `woocommerce_untrash_order`. */ public function restored( $orderId ): void { $this->touch( (int) $orderId ); } /** * Snapshot every touched order. * * @internal Hooked to `shutdown` once something was touched. Runs before the * outbox writer, which is hooked later on the same action. */ public static function snapshotTouched(): void { $ids = array_keys( self::$touched ); self::$touched = []; foreach ( $ids as $id ) { $order = wc_get_order( $id ); // A refund is a WC_Abstract_Order, not a WC_Order, so this also skips refunds. if ( $order instanceof WC_Order ) { Outbox::add( 'order.upserted', OrderSnapshot::build( $order ) ); } } } /** * Remember an order id for the end of the request. * * @param int $orderId Order id. */ private function touch( int $orderId ): void { if ( $orderId <= 0 ) { return; } self::$touched[ $orderId ] = true; if ( ! self::$hooked ) { self::$hooked = true; // Priority 15: after the queue drain closed the response (0), before the // outbox writes this request's buffer (20). add_action( 'shutdown', [ self::class, 'snapshotTouched' ], 15 ); } } }