database. * * @since 7.1.0 * * @param int $order_id ID of the order about to be deleted. * @param WC_Order $order Instance of the order that is about to be deleted. */ do_action( 'woocommerce_before_delete_order', $order_id, $order ); } $this->upshift_or_delete_child_orders( $order ); $this->delete_order_data_from_custom_order_tables( $order_id ); $this->delete_items( $order ); $order->set_id( 0 ); /** We can delete the post data if: * 1. The HPOS table is authoritative and synchronization is enabled. * 2. The post record is of type `shop_order_placehold`, since this is created by the HPOS in the first place. * * In other words, we do not delete the post record when HPOS table is authoritative and synchronization is disabled but post record is a full record and not just a placeholder, because it implies that the order was created before HPOS was enabled. */ $orders_table_is_authoritative = $order->get_data_store()->get_current_class_name() === self::class; if ( $orders_table_is_authoritative ) { $data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); if ( $data_synchronizer->data_sync_is_enabled() ) { // Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}. // Once we stop creating posts for orders, we should do the cleanup here instead. wp_delete_post( $order_id ); } else { $this->handle_order_deletion_with_sync_disabled( $order_id ); } } if ( $do_filters ) { /** * Fires immediately after an order is deleted. * * @since 2.7.0 * * @param int $order_id ID of the order that has been deleted. */ do_action( 'woocommerce_delete_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment } } else { if ( $do_filters ) { /** * Fires immediately before an order is trashed. * * @since 7.1.0 * * @param int $order_id ID of the order about to be trashed. * @param WC_Order $order Instance of the order that is about to be trashed. */ do_action( 'woocommerce_before_trash_order', $order_id, $order ); } $this->trash_order( $order ); if ( $do_filters ) { /** * Fires immediately after an order is trashed. * * @since 2.7.0 * * @param int $order_id ID of the order that has been trashed. */ do_action( 'woocommerce_trash_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment } } } /** * Handles the deletion of an order from the orders table when sync is disabled: * * If the corresponding row in the posts table is of placeholder type, * it's just deleted; otherwise a "deleted_from" record is created in the meta table * and the sync process will detect these and take care of deleting the appropriate post records. * * @param int $order_id Th id of the order that has been deleted from the orders table. * @return void */ protected function handle_order_deletion_with_sync_disabled( $order_id ): void { global $wpdb; $post_type = $wpdb->get_var( $wpdb->prepare( "SELECT post_type FROM {$wpdb->posts} WHERE ID=%d", $order_id ) ); if ( DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE === $post_type ) { $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->posts} WHERE ID=%d OR post_parent=%d", $order_id, $order_id ) ); } else { // phpcs:disable WordPress.DB.SlowDBQuery $wpdb->insert( self::get_meta_table_name(), array( 'order_id' => $order_id, 'meta_key' => DataSynchronizer::DELETED_RECORD_META_KEY, 'meta_value' => DataSynchronizer::DELETED_FROM_ORDERS_META_VALUE, ) ); // phpcs:enable WordPress.DB.SlowDBQuery // Note that at this point upshift_or_delete_child_orders will already have been invoked, // thus all the child orders either still exist but have a different parent id, // or have been deleted and got their own deletion record already. // So there's no need to do anything about them. } } /** * Set the parent id of child orders to the parent order's parent if the post type * for the order is hierarchical, just delete the child orders otherwise. * * @param \WC_Abstract_Order $order Order object. * * @return void */ private function upshift_or_delete_child_orders( $order ): void { global $wpdb; $order_table = self::get_orders_table_name(); $order_parent_id = $order->get_parent_id(); if ( $this->legacy_proxy->call_function( 'is_post_type_hierarchical', $order->get_type() ) ) { $wpdb->update( $order_table, array( 'parent_order_id' => $order_parent_id ), array( 'parent_order_id' => $order->get_id() ), array( '%d' ), array( '%d' ) ); } else { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $child_order_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM $order_table WHERE parent_order_id=%d", $order->get_id() ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared foreach ( $child_order_ids as $child_order_id ) { $child_order = wc_get_order( $child_order_id ); if ( $child_order ) { $child_order->delete( true ); } } } } /** * Trashes an order. * * @param WC_Order $order The order object. * * @return void */ public function trash_order( $order ) { global $wpdb; if ( 'trash' === $order->get_status( 'edit' ) ) { return; } $trash_metadata = array( '_wp_trash_meta_status' => 'wc-' . $order->get_status( 'edit' ), '_wp_trash_meta_time' => time(), ); $wpdb->update( self::get_orders_table_name(), array( 'status' => 'trash', 'date_updated_gmt' => current_time( 'Y-m-d H:i:s', true ), ), array( 'id' => $order->get_id() ), array( '%s', '%s' ), array( '%d' ) ); $order->set_status( 'trash' ); foreach ( $trash_metadata as $meta_key => $meta_value ) { $this->add_meta( $order, (object) array( 'key' => $meta_key, 'value' => $meta_value, ) ); } $data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); if ( $data_synchronizer->data_sync_is_enabled() ) { wp_trash_post( $order->get_id() ); } } /** * Attempts to restore the specified order back to its original status (after having been trashed). * * @param WC_Order $order The order to be untrashed. * * @return bool If the operation was successful. */ public function untrash_order( WC_Order $order ): bool { $id = $order->get_id(); $status = $order->get_status(); if ( 'trash' !== $status ) { wc_get_logger()->warning( sprintf( /* translators: 1: order ID, 2: order status */ __( 'Order %1$d cannot be restored from the trash: it has already been restored to status "%2$s".', 'woocommerce' ), $id, $status ) ); return false; } $previous_status = $order->get_meta( '_wp_trash_meta_status' ); $valid_statuses = wc_get_order_statuses(); $previous_state_is_invalid = ! array_key_exists( $previous_status, $valid_statuses ); $pending_is_valid_status = array_key_exists( 'wc-pending', $valid_statuses ); if ( $previous_state_is_invalid && $pending_is_valid_status ) { // If the previous status is no longer valid, let's try to restore it to "pending" instead. wc_get_logger()->warning( sprintf( /* translators: 1: order ID, 2: order status */ __( 'The previous status of order %1$d ("%2$s") is invalid. It has been restored to "pending" status instead.', 'woocommerce' ), $id, $previous_status ) ); $previous_status = 'pending'; } elseif ( $previous_state_is_invalid ) { // If we cannot restore to pending, we should probably stand back and let the merchant intervene some other way. wc_get_logger()->warning( sprintf( /* translators: 1: order ID, 2: order status */ __( 'The previous status of order %1$d ("%2$s") is invalid. It could not be restored.', 'woocommerce' ), $id, $previous_status ) ); return false; } /** * Fires before an order is restored from the trash. * * @since 7.2.0 * * @param int $order_id Order ID. * @param string $previous_status The status of the order before it was trashed. */ do_action( 'woocommerce_untrash_order', $order->get_id(), $previous_status ); $order->set_status( $previous_status ); $order->save(); // Was the status successfully restored? Let's clean up the meta and indicate success... if ( 'wc-' . $order->get_status() === $previous_status ) { $order->delete_meta_data( '_wp_trash_meta_status' ); $order->delete_meta_data( '_wp_trash_meta_time' ); $order->delete_meta_data( '_wp_trash_meta_comments_status' ); $order->save_meta_data(); return true; } // ...Or log a warning and bail. wc_get_logger()->warning( sprintf( /* translators: 1: order ID, 2: order status */ __( 'Something went wrong when trying to restore order %d from the trash. It could not be restored.', 'woocommerce' ), $id ) ); return false; } /** * Deletes order data from custom order tables. * * @param int $order_id The order ID. * @return void */ public function delete_order_data_from_custom_order_tables( $order_id ) { global $wpdb; $order_cache = wc_get_container()->get( OrderCache::class ); // Delete COT-specific data. foreach ( $this->get_all_table_names() as $table ) { $wpdb->delete( $table, ( self::get_orders_table_name() === $table ) ? array( 'id' => $order_id ) : array( 'order_id' => $order_id ), array( '%d' ) ); $order_cache->remove( $order_id ); } } /** * Method to create an order in the database. * * @param \WC_Order $order Order object. */ public function create( &$order ) { if ( '' === $order->get_order_key() ) { $order->set_order_key( wc_generate_order_key() ); } $this->persist_save( $order ); // Do not fire 'woocommerce_new_order' for draft statuses for backwards compatibility. if ( 'auto-draft' === $order->get_status( 'edit' ) ) { return; } /** * Fires when a new order is created. * * @since 2.7.0 * * @param int Order ID. * @param \WC_Order Order object. */ do_action( 'woocommerce_new_order', $order->get_id(), $order ); } /** * Helper method responsible for persisting new data to order table. * * This should not contain and specific meta or actions, so that it can be used other order types safely. * * @param \WC_Order $order Order object. * @param bool $force_all_fields Force update all fields, instead of calculating and updating only changed fields. * @param bool $backfill Whether to backfill data to post datastore. * * @return void * * @throws \Exception When unable to save data. */ protected function persist_save( &$order, bool $force_all_fields = false, $backfill = true ) { $order->set_version( Constants::get_constant( 'WC_VERSION' ) ); $order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() ); if ( ! $order->get_date_created( 'edit' ) ) { $order->set_date_created( time() ); } if ( ! $order->get_date_modified( 'edit' ) ) { $order->set_date_modified( current_time( 'mysql' ) ); } $this->persist_order_to_db( $order, $force_all_fields ); $this->update_order_meta( $order ); $order->save_meta_data(); $order->apply_changes(); if ( $backfill ) { self::$backfilling_order_ids[] = $order->get_id(); $r_order = wc_get_order( $order->get_id() ); // Refresh order to account for DB changes from post hooks. $this->maybe_backfill_post_record( $r_order ); self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) ); } $this->clear_caches( $order ); } /** * Method to update an order in the database. * * @param \WC_Order $order Order object. */ public function update( &$order ) { $previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status' ); $changes = $order->get_changes(); // Before updating, ensure date paid is set if missing. if ( ! $order->get_date_paid( 'edit' ) && version_compare( $order->get_version( 'edit' ), '3.0', '<' ) && $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment ) { $order->set_date_paid( $order->get_date_created( 'edit' ) ); } if ( null === $order->get_date_created( 'edit' ) ) { $order->set_date_created( time() ); } $order->set_version( Constants::get_constant( 'WC_VERSION' ) ); // Fetch changes. $changes = $order->get_changes(); $this->persist_updates( $order ); // Update download permissions if necessary. if ( array_key_exists( 'billing_email', $changes ) || array_key_exists( 'customer_id', $changes ) ) { $data_store = \WC_Data_Store::load( 'customer-download' ); $data_store->update_user_by_order_id( $order->get_id(), $order->get_customer_id(), $order->get_billing_email() ); } // Mark user account as active. if ( array_key_exists( 'customer_id', $changes ) ) { wc_update_user_last_active( $order->get_customer_id() ); } $order->apply_changes(); $this->clear_caches( $order ); // For backwards compatibility, moving an auto-draft order to a valid status triggers the 'woocommerce_new_order' hook. if ( ! empty( $changes['status'] ) && 'auto-draft' === $previous_status ) { do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment return; } // For backwards compat with CPT, trashing/untrashing and changing previously datastore-level props does not trigger the update hook. if ( ( ! empty( $changes['status'] ) && in_array( 'trash', array( $changes['status'], $previous_status ), true ) ) || ( ! empty( $changes ) && ! array_diff_key( $changes, array_flip( $this->get_post_data_store_for_backfill()->get_internal_data_store_key_getters() ) ) ) ) { return; } do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment } /** * Proxy to udpating order meta. Here for backward compatibility reasons. * * @param \WC_Order $order Order object. * * @return void */ protected function update_post_meta( &$order ) { $this->update_order_meta( $order ); } /** * Helper method that is responsible for persisting order updates to the database. * * This is expected to be reused by other order types, and should not contain any specific metadata updates or actions. * * @param \WC_Order $order Order object. * @param bool $backfill Whether to backfill data to post tables. * * @return array $changes Array of changes. * * @throws \Exception When unable to persist order. */ protected function persist_updates( &$order, $backfill = true ) { // Fetch changes. $changes = $order->get_changes(); if ( ! isset( $changes['date_modified'] ) ) { $order->set_date_modified( current_time( 'mysql' ) ); } $this->persist_order_to_db( $order ); $this->update_order_meta( $order ); $order->save_meta_data(); if ( $backfill ) { self::$backfilling_order_ids[] = $order->get_id(); $this->clear_caches( $order ); $r_order = wc_get_order( $order->get_id() ); // Refresh order to account for DB changes from post hooks. $this->maybe_backfill_post_record( $r_order ); self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) ); } return $changes; } /** * Helper method to check whether to backfill post record. * * @return bool */ private function should_backfill_post_record() { $data_sync = wc_get_container()->get( DataSynchronizer::class ); return $data_sync->data_sync_is_enabled(); } /** * Helper function to decide whether to backfill post record. * * @param \WC_Abstract_Order $order Order object. * * @return void */ private function maybe_backfill_post_record( $order ) { if ( $this->should_backfill_post_record() ) { $this->backfill_post_record( $order ); } } /** * Helper method that updates post meta based on an order object. * Mostly used for backwards compatibility purposes in this datastore. * * @param \WC_Order $order Order object. * * @since 7.0.0 */ public function update_order_meta( &$order ) { $changes = $order->get_changes(); $this->update_address_index_meta( $order, $changes ); } /** * Helper function to update billing and shipping address metadata. * * @param \WC_Abstract_Order $order Order Object. * @param array $changes Array of changes. * * @return void */ private function update_address_index_meta( $order, $changes ) { // If address changed, store concatenated version to make searches faster. foreach ( array( 'billing', 'shipping' ) as $address_type ) { $index_meta_key = "_{$address_type}_address_index"; if ( isset( $changes[ $address_type ] ) || ( is_a( $order, 'WC_Order' ) && empty( $order->get_meta( $index_meta_key ) ) ) ) { $order->update_meta_data( $index_meta_key, implode( ' ', $order->get_address( $address_type ) ) ); } } } /** * Return array of coupon_code => meta_key for coupon which have usage limit and have tentative keys. * Pass $coupon_id if key for only one of the coupon is needed. * * @param WC_Order $order Order object. * @param int $coupon_id If passed, will return held key for that coupon. * * @return array|string Key value pair for coupon code and meta key name. If $coupon_id is passed, returns meta_key for only that coupon. */ public function get_coupon_held_keys( $order, $coupon_id = null ) { $held_keys = $order->get_meta( '_coupon_held_keys' ); if ( $coupon_id ) { return isset( $held_keys[ $coupon_id ] ) ? $held_keys[ $coupon_id ] : null; } return $held_keys; } /** * Return array of coupon_code => meta_key for coupon which have usage limit per customer and have tentative keys. * * @param WC_Order $order Order object. * @param int $coupon_id If passed, will return held key for that coupon. * * @return mixed */ public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) { $held_keys_for_user = $order->get_meta( '_coupon_held_keys_for_users' ); if ( $coupon_id ) { return isset( $held_keys_for_user[ $coupon_id ] ) ? $held_keys_for_user[ $coupon_id ] : null; } return $held_keys_for_user; } /** * Add/Update list of meta keys that are currently being used by this order to hold a coupon. * This is used to figure out what all meta entries we should delete when order is cancelled/completed. * * @param WC_Order $order Order object. * @param array $held_keys Array of coupon_code => meta_key. * @param array $held_keys_for_user Array of coupon_code => meta_key for held coupon for user. * * @return mixed */ public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) { if ( is_array( $held_keys ) && 0 < count( $held_keys ) ) { $order->update_meta_data( '_coupon_held_keys', $held_keys ); } if ( is_array( $held_keys_for_user ) && 0 < count( $held_keys_for_user ) ) { $order->update_meta_data( '_coupon_held_keys_for_users', $held_keys_for_user ); } } /** * Release all coupons held by this order. * * @param WC_Order $order Current order object. * @param bool $save Whether to delete keys from DB right away. Could be useful to pass `false` if you are building a bulk request. */ public function release_held_coupons( $order, $save = true ) { $coupon_held_keys = $this->get_coupon_held_keys( $order ); if ( is_array( $coupon_held_keys ) ) { foreach ( $coupon_held_keys as $coupon_id => $meta_key ) { $coupon = new \WC_Coupon( $coupon_id ); $coupon->delete_meta_data( $meta_key ); $coupon->save_meta_data(); } } $order->delete_meta_data( '_coupon_held_keys' ); $coupon_held_keys_for_users = $this->get_coupon_held_keys_for_users( $order ); if ( is_array( $coupon_held_keys_for_users ) ) { foreach ( $coupon_held_keys_for_users as $coupon_id => $meta_key ) { $coupon = new \WC_Coupon( $coupon_id ); $coupon->delete_meta_data( $meta_key ); $coupon->save_meta_data(); } } $order->delete_meta_data( '_coupon_held_keys_for_users' ); if ( $save ) { $order->save_meta_data(); } } /** * Performs actual query to get orders. Uses `OrdersTableQuery` to build and generate the query. * * @param array $query_vars Query variables. * * @return array|object List of orders and count of orders. */ public function query( $query_vars ) { if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { $query_vars['no_found_rows'] = true; } if ( isset( $query_vars['anonymized'] ) ) { $query_vars['meta_query'] = $query_vars['meta_query'] ?? array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query if ( $query_vars['anonymized'] ) { $query_vars['meta_query'][] = array( 'key' => '_anonymized', 'value' => 'yes', ); } else { $query_vars['meta_query'][] = array( 'key' => '_anonymized', 'compare' => 'NOT EXISTS', ); } } try { $query = new OrdersTableQuery( $query_vars ); } catch ( \Exception $e ) { $query = (object) array( 'orders' => array(), 'found_orders' => 0, 'max_num_pages' => 0, ); } if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) { $orders = $query->orders; } else { $orders = WC()->order_factory->get_orders( $query->orders ); } if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { return (object) array( 'orders' => $orders, 'total' => $query->found_orders, 'max_num_pages' => $query->max_num_pages, ); } return $orders; } //phpcs:enable Squiz.Commenting, Generic.Commenting /** * Get the SQL needed to create all the tables needed for the custom orders table feature. * * @return string */ public function get_database_schema() { global $wpdb; $collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : ''; $orders_table_name = $this->get_orders_table_name(); $addresses_table_name = $this->get_addresses_table_name(); $operational_data_table_name = $this->get_operational_data_table_name(); $meta_table = $this->get_meta_table_name(); $max_index_length = $this->database_util->get_max_index_length(); $composite_meta_value_index_length = max( $max_index_length - 8 - 100 - 1, 20 ); // 8 for order_id, 100 for meta_key, 10 minimum for meta_value. $composite_customer_id_email_length = max( $max_index_length - 20, 20 ); // 8 for customer_id, 20 minimum for email. $sql = " CREATE TABLE $orders_table_name ( id bigint(20) unsigned, status varchar(20) null, currency varchar(10) null, type varchar(20) null, tax_amount decimal(26,8) null, total_amount decimal(26,8) null, customer_id bigint(20) unsigned null, billing_email varchar(320) null, date_created_gmt datetime null, date_updated_gmt datetime null, parent_order_id bigint(20) unsigned null, payment_method varchar(100) null, payment_method_title text null, transaction_id varchar(100) null, ip_address varchar(100) null, user_agent text null, customer_note text null, PRIMARY KEY (id), KEY status (status), KEY date_created (date_created_gmt), KEY customer_id_billing_email (customer_id, billing_email({$composite_customer_id_email_length})), KEY billing_email (billing_email($max_index_length)), KEY type_status_date (type, status, date_created_gmt), KEY parent_order_id (parent_order_id), KEY date_updated (date_updated_gmt) ) $collate; CREATE TABLE $addresses_table_name ( id bigint(20) unsigned auto_increment primary key, order_id bigint(20) unsigned NOT NULL, address_type varchar(20) null, first_name text null, last_name text null, company text null, address_1 text null, address_2 text null, city text null, state text null, postcode text null, country text null, email varchar(320) null, phone varchar(100) null, KEY order_id (order_id), UNIQUE KEY address_type_order_id (address_type, order_id), KEY email (email($max_index_length)), KEY phone (phone) ) $collate; CREATE TABLE $operational_data_table_name ( id bigint(20) unsigned auto_increment primary key, order_id bigint(20) unsigned NULL, created_via varchar(100) NULL, woocommerce_version varchar(20) NULL, prices_include_tax tinyint(1) NULL, coupon_usages_are_counted tinyint(1) NULL, download_permission_granted tinyint(1) NULL, cart_hash varchar(100) NULL, new_order_email_sent tinyint(1) NULL, order_key varchar(100) NULL, order_stock_reduced tinyint(1) NULL, date_paid_gmt datetime NULL, date_completed_gmt datetime NULL, shipping_tax_amount decimal(26,8) NULL, shipping_total_amount decimal(26,8) NULL, discount_tax_amount decimal(26,8) NULL, discount_total_amount decimal(26,8) NULL, recorded_sales tinyint(1) NULL, UNIQUE KEY order_id (order_id), KEY order_key (order_key) ) $collate; CREATE TABLE $meta_table ( id bigint(20) unsigned auto_increment primary key, order_id bigint(20) unsigned null, meta_key varchar(255), meta_value text null, KEY meta_key_value (meta_key(100), meta_value($composite_meta_value_index_length)), KEY order_id_meta_key_meta_value (order_id, meta_key(100), meta_value($composite_meta_value_index_length)) ) $collate; "; return $sql; } /** * Returns an array of meta for an object. * * @param WC_Data $object WC_Data object. * @return array */ public function read_meta( &$object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound $raw_meta_data = $this->data_store_meta->read_meta( $object ); return $this->filter_raw_meta_data( $object, $raw_meta_data ); } /** * Deletes meta based on meta ID. * * @param WC_Data $object WC_Data object. * @param \stdClass $meta (containing at least ->id). * * @return bool */ public function delete_meta( &$object, $meta ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound global $wpdb; if ( $this->should_backfill_post_record() && isset( $meta->id ) ) { // Let's get the actual meta key before its deleted for backfilling. We cannot delete just by ID because meta IDs are different in HPOS and posts tables. $db_meta = $this->data_store_meta->get_metadata_by_id( $meta->id ); if ( $db_meta ) { $meta->key = $db_meta->meta_key; $meta->value = $db_meta->meta_value; } } $delete_meta = $this->data_store_meta->delete_meta( $object, $meta ); $changes_applied = $this->after_meta_change( $object, $meta ); if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() && isset( $meta->key ) ) { self::$backfilling_order_ids[] = $object->get_id(); if ( is_object( $meta->value ) && '__PHP_Incomplete_Class' === get_class( $meta->value ) ) { $meta_value = maybe_serialize( $meta->value ); $wpdb->delete( _get_meta_table( 'post' ), array( 'post_id' => $object->get_id(), 'meta_key' => $meta->key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $meta_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ), array( '%d', '%s', '%s' ) ); wp_cache_delete( $object->get_id(), 'post_meta' ); $logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' ); $logger->warning( sprintf( 'encountered an order meta value of type __PHP_Incomplete_Class during `delete_meta` in order with ID %d: "%s"', $object->get_id(), var_export( $meta_value, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export } else { delete_post_meta( $object->get_id(), $meta->key, $meta->value ); } self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) ); } return $delete_meta; } /** * Add new piece of meta. * * @param WC_Data $object WC_Data object. * @param \stdClass $meta (containing ->key and ->value). * * @return int|bool meta ID or false on failure */ public function add_meta( &$object, $meta ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound $add_meta = $this->data_store_meta->add_meta( $object, $meta ); $meta->id = $add_meta; $changes_applied = $this->after_meta_change( $object, $meta ); if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) { self::$backfilling_order_ids[] = $object->get_id(); add_post_meta( $object->get_id(), $meta->key, $meta->value ); self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) ); } return $add_meta; } /** * Update meta. * * @param WC_Data $object WC_Data object. * @param \stdClass $meta (containing ->id, ->key and ->value). * * @return bool The number of rows updated, or false on error. */ public function update_meta( &$object, $meta ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound $update_meta = $this->data_store_meta->update_meta( $object, $meta ); $changes_applied = $this->after_meta_change( $object, $meta ); if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) { self::$backfilling_order_ids[] = $object->get_id(); update_post_meta( $object->get_id(), $meta->key, $meta->value ); self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) ); } return $update_meta; } /** * Perform after meta change operations, including updating the date_modified field, clearing caches and applying changes. * * @param WC_Abstract_Order $order Order object. * @param \WC_Meta_Data $meta Metadata object. * * @return bool True if changes were applied, false otherwise. */ protected function after_meta_change( &$order, $meta ) { method_exists( $meta, 'apply_changes' ) && $meta->apply_changes(); // Prevent this happening multiple time in same request. if ( $this->should_save_after_meta_change( $order, $meta ) ) { $order->set_date_modified( current_time( 'mysql' ) ); $order->save(); return true; } else { $order_cache = wc_get_container()->get( OrderCache::class ); $order_cache->remove( $order->get_id() ); } return false; } /** * Helper function to check whether the modified date needs to be updated after a meta save. * * This method prevents order->save() call multiple times in the same request after any meta update by checking if: * 1. Order modified date is already the current date, no updates needed in this case. * 2. If there are changes already queued for order object, then we don't need to update the modified date as it will be updated ina subsequent save() call. * * @param WC_Order $order Order object. * @param \WC_Meta_Data|null $meta Metadata object. * * @return bool Whether the modified date needs to be updated. */ private function should_save_after_meta_change( $order, $meta = null ) { $current_time = $this->legacy_proxy->call_function( 'current_time', 'mysql', 1 ); $current_date_time = new \WC_DateTime( $current_time, new \DateTimeZone( 'GMT' ) ); $should_save = $order->get_date_modified() < $current_date_time && empty( $order->get_changes() ) && ( ! is_object( $meta ) || ! in_array( $meta->key, $this->ephemeral_meta_keys, true ) ); /** * Allows code to skip a full order save() when metadata is changed. * * @since 8.8.0 * * @param bool $should_save Whether to trigger a full save after metadata is changed. */ return apply_filters( 'woocommerce_orders_table_datastore_should_save_after_meta_change', $should_save ); } } خطای 404: صفحه پیدا نشد- ویرا مزون