diff --git a/plugins/woocommerce/changelog/add-accessible-private-methods-trait b/plugins/woocommerce/changelog/add-accessible-private-methods-trait new file mode 100644 index 00000000000..ebd52806c1a --- /dev/null +++ b/plugins/woocommerce/changelog/add-accessible-private-methods-trait @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add the AccessiblePrivateMethods trait diff --git a/plugins/woocommerce/includes/class-wc-query.php b/plugins/woocommerce/includes/class-wc-query.php index 4758db345fb..7318ae2987c 100644 --- a/plugins/woocommerce/includes/class-wc-query.php +++ b/plugins/woocommerce/includes/class-wc-query.php @@ -7,6 +7,7 @@ */ use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; defined( 'ABSPATH' ) || exit; @@ -15,6 +16,8 @@ defined( 'ABSPATH' ) || exit; */ class WC_Query { + use AccessiblePrivateMethods; + /** * Query vars to add to wp. * @@ -505,14 +508,7 @@ class WC_Query { self::$product_query = $q; // Additonal hooks to change WP Query. - add_filter( - 'posts_clauses', - function( $args, $wp_query ) { - return $this->product_query_post_clauses( $args, $wp_query ); - }, - 10, - 2 - ); + self::add_filter( 'posts_clauses', array( $this, 'product_query_post_clauses' ), 10, 2 ); add_filter( 'the_posts', array( $this, 'handle_get_posts' ), 10, 2 ); do_action( 'woocommerce_product_query', $q, $this ); diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php b/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php index 1ee7dcc288b..01b48ec8eaf 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php @@ -2,12 +2,15 @@ namespace Automattic\WooCommerce\Internal\Admin\Orders; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; /** * Controls the different pages/screens associated to the "Orders" menu page. */ class PageController { + use AccessiblePrivateMethods; + /** * Instance of the posts redirection controller. * @@ -88,15 +91,16 @@ class PageController { $this->set_action(); - // Perform initialization for the current action. - add_action( - 'load-woocommerce_page_wc-orders', - function() { - if ( method_exists( $this, 'setup_action_' . $this->current_action ) ) { - $this->{"setup_action_{$this->current_action}"}(); - } - } - ); + self::add_action( 'load-woocommerce_page_wc-orders', array( $this, 'handle_load_page_action' ) ); + } + + /** + * Perform initialization for the current action. + */ + private function handle_load_page_action() { + if ( method_exists( $this, 'setup_action_' . $this->current_action ) ) { + $this->{"setup_action_{$this->current_action}"}(); + } } /** diff --git a/plugins/woocommerce/src/Internal/Admin/ProductReviews/Reviews.php b/plugins/woocommerce/src/Internal/Admin/ProductReviews/Reviews.php index 37be1540bea..426aefc538f 100644 --- a/plugins/woocommerce/src/Internal/Admin/ProductReviews/Reviews.php +++ b/plugins/woocommerce/src/Internal/Admin/ProductReviews/Reviews.php @@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Internal\Admin\ProductReviews; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use WP_Ajax_Response; use WP_Comment; use WP_Screen; @@ -14,6 +15,8 @@ use WP_Screen; */ class Reviews { + use AccessiblePrivateMethods; + /** * Admin page identifier. */ @@ -38,59 +41,16 @@ class Reviews { */ public function __construct() { - add_action( - 'admin_menu', - function() { - $this->add_reviews_page(); - } - ); - - add_action( - 'admin_enqueue_scripts', - function() { - $this->load_javascript(); - } - ); + self::add_action( 'admin_menu', [ $this, 'add_reviews_page' ] ); + self::add_action( 'admin_enqueue_scripts', [ $this, 'load_javascript' ] ); // These ajax callbacks need a low priority to ensure they run before their WordPress core counterparts. - add_action( - 'wp_ajax_edit-comment', - function() { - $this->handle_edit_review(); - }, - -1 - ); + self::add_action( 'wp_ajax_edit-comment', [ $this, 'handle_edit_review' ], -1 ); + self::add_action( 'wp_ajax_replyto-comment', [ $this, 'handle_reply_to_review' ], -1 ); - add_action( - 'wp_ajax_replyto-comment', - function() { - $this->handle_reply_to_review(); - }, - -1 - ); - - add_filter( - 'parent_file', - function( $parent_file ) { - return $this->edit_review_parent_file( $parent_file ); - } - ); - - add_filter( - 'gettext', - function( $translation, $text ) { - return $this->edit_comments_screen_text( $translation, $text ); - }, - 10, - 2 - ); - - add_action( - 'admin_notices', - function() { - $this->display_notices(); - } - ); + self::add_filter( 'parent_file', [ $this, 'edit_review_parent_file' ] ); + self::add_filter( 'gettext', [ $this, 'edit_comments_screen_text' ], 10, 2 ); + self::add_action( 'admin_notices', [ $this, 'display_notices' ] ); } /** @@ -111,7 +71,7 @@ class Reviews { * @param string $capability The capability (defaults to `moderate_comments` for viewing and `edit_products` for editing). * @param string $context The context for which the capability is needed. */ - return (string) apply_filters( 'woocommerce_product_reviews_page_capability', 'view' === $context ? 'moderate_comments' : 'edit_products', $context ); + return (string) apply_filters( 'woocommerce_product_reviews_page_capability', $context === 'view' ? 'moderate_comments' : 'edit_products', $context ); } /** @@ -130,12 +90,7 @@ class Reviews { [ $this, 'render_reviews_list_table' ] ); - add_action( - "load-{$this->reviews_page_hook}", - function() { - $this->load_reviews_screen(); - } - ); + self::add_action( "load-{$this->reviews_page_hook}", array( $this, 'load_reviews_screen' ) ); } /** @@ -163,7 +118,7 @@ class Reviews { public function is_reviews_page() : bool { global $current_screen; - return isset( $current_screen->base ) && 'product_page_' . static::MENU_SLUG === $current_screen->base; + return isset( $current_screen->base ) && $current_screen->base === 'product_page_' . static::MENU_SLUG; } /** @@ -186,7 +141,7 @@ class Reviews { */ protected function is_review_or_reply( $object ) : bool { - $is_review_or_reply = $object instanceof WP_Comment && in_array( $object->comment_type, [ 'review', 'comment' ], true ) && 'product' === get_post_type( $object->comment_post_ID ); + $is_review_or_reply = $object instanceof WP_Comment && in_array( $object->comment_type, [ 'review', 'comment' ], true ) && get_post_type( $object->comment_post_ID ) === 'product'; /** * Filters whether the object is a review or a reply to a review. @@ -245,7 +200,7 @@ class Reviews { wp_die( esc_html( $updated->get_error_message() ) ); } - $position = isset( $_POST['position'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['position'] ) ) : -1; + $position = isset( $_POST['position'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['position'] ) ) : -1; $wp_list_table = $this->make_reviews_list_table(); ob_start(); @@ -290,12 +245,12 @@ class Reviews { } // Inline Review replies will use the `detail` mode. If that's not what we have, then let WordPress core take over. - if ( isset( $_REQUEST['mode'] ) && 'dashboard' === $_REQUEST['mode'] ) { + if ( isset( $_REQUEST['mode'] ) && $_REQUEST['mode'] === 'dashboard' ) { return; } // If this is not a a reply to a review, bail silently to let WordPress core take over. - if ( 'product' !== get_post_type( $post ) ) { + if ( get_post_type( $post ) !== 'product' ) { return; } @@ -317,8 +272,8 @@ class Reviews { $comment_author_email = wp_slash( $user->user_email ); $comment_author_url = wp_slash( $user->user_url ); // WordPress core already sanitizes `content` during the `pre_comment_content` hook, which is why it's not needed here, {@see wp_filter_comment()} and {@see kses_init_filters()}. - $comment_content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $comment_type = isset( $_POST['comment_type'] ) ? sanitize_text_field( wp_unslash( $_POST['comment_type'] ) ) : 'comment'; + $comment_content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $comment_type = isset( $_POST['comment_type'] ) ? sanitize_text_field( wp_unslash( $_POST['comment_type'] ) ) : 'comment'; if ( current_user_can( 'unfiltered_html' ) ) { if ( ! isset( $_POST['_wp_unfiltered_html_comment'] ) ) { @@ -336,7 +291,7 @@ class Reviews { wp_die( esc_html__( 'Sorry, you must be logged in to reply to a review.', 'woocommerce' ) ); } - if ( '' === $comment_content ) { + if ( $comment_content === '' ) { wp_die( esc_html__( 'Error: Please type your reply text.', 'woocommerce' ) ); } @@ -353,7 +308,7 @@ class Reviews { if ( ! empty( $_POST['approve_parent'] ) ) { $parent = get_comment( $comment_parent ); - if ( $parent && '0' === $parent->comment_approved && $parent->comment_post_ID == $comment_post_ID ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + if ( $parent && $parent->comment_approved === '0' && $parent->comment_post_ID === $comment_post_ID ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase if ( ! current_user_can( 'edit_comment', $parent->comment_ID ) ) { wp_die( -1 ); } @@ -539,16 +494,16 @@ class Reviews { protected function edit_review_parent_file( $parent_file ) { global $submenu_file, $current_screen; - if ( isset( $current_screen->id, $_GET['c'] ) && 'comment' === $current_screen->id ) { + if ( isset( $current_screen->id, $_GET['c'] ) && $current_screen->id === 'comment' ) { $comment_id = absint( $_GET['c'] ); - $comment = get_comment( $comment_id ); + $comment = get_comment( $comment_id ); if ( isset( $comment->comment_parent ) && $comment->comment_parent > 0 ) { $comment = get_comment( $comment->comment_parent ); } - if ( isset( $comment->comment_post_ID ) && 'product' === get_post_type( $comment->comment_post_ID ) ) { + if ( isset( $comment->comment_post_ID ) && get_post_type( $comment->comment_post_ID ) === 'product' ) { $parent_file = 'edit.php?post_type=product'; $submenu_file = 'product-reviews'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } @@ -573,7 +528,7 @@ class Reviews { } // Try to get comment from query params when not in context already. - if ( ! $comment && isset( $_GET['action'], $_GET['c'] ) && 'editcomment' === $_GET['action'] ) { + if ( ! $comment && isset( $_GET['action'], $_GET['c'] ) && $_GET['action'] === 'editcomment' ) { $comment_id = absint( $_GET['c'] ); $comment = get_comment( $comment_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } @@ -582,16 +537,16 @@ class Reviews { if ( isset( $comment->comment_parent ) && $comment->comment_parent > 0 ) { $is_reply = true; - $comment = get_comment( $comment->comment_parent ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $comment = get_comment( $comment->comment_parent ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } // Only replace the translated text if we are editing a comment left on a product (ie. a review). - if ( isset( $comment->comment_post_ID ) && 'product' === get_post_type( $comment->comment_post_ID ) ) { - if ( 'Edit Comment' === $text ) { + if ( isset( $comment->comment_post_ID ) && get_post_type( $comment->comment_post_ID ) === 'product' ) { + if ( $text === 'Edit Comment' ) { $translation = $is_reply ? __( 'Edit Review Reply', 'woocommerce' ) : __( 'Edit Review', 'woocommerce' ); - } elseif ( 'Moderate Comment' === $text ) { + } elseif ( $text === 'Moderate Comment' ) { $translation = $is_reply ? __( 'Moderate Review Reply', 'woocommerce' ) : __( 'Moderate Review', 'woocommerce' ); @@ -663,5 +618,4 @@ class Reviews { */ echo apply_filters( 'woocommerce_product_reviews_list_table', ob_get_clean(), $this->reviews_list_table ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - } diff --git a/plugins/woocommerce/src/Internal/Admin/ProductReviews/ReviewsCommentsOverrides.php b/plugins/woocommerce/src/Internal/Admin/ProductReviews/ReviewsCommentsOverrides.php index ae66f80dac5..99183f771ba 100644 --- a/plugins/woocommerce/src/Internal/Admin/ProductReviews/ReviewsCommentsOverrides.php +++ b/plugins/woocommerce/src/Internal/Admin/ProductReviews/ReviewsCommentsOverrides.php @@ -2,6 +2,7 @@ namespace Automattic\WooCommerce\Internal\Admin\ProductReviews; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use WP_Comment_Query; /** @@ -9,35 +10,17 @@ use WP_Comment_Query; */ class ReviewsCommentsOverrides { + use AccessiblePrivateMethods; + const REVIEWS_MOVED_NOTICE_ID = 'product_reviews_moved'; /** * Constructor. */ public function __construct() { - - add_action( - 'admin_notices', - function() { - $this->display_notices(); - } - ); - - add_filter( - 'woocommerce_dismiss_admin_notice_capability', - function( $default_capability, $notice_name ) { - return $this->get_dismiss_capability( $default_capability, $notice_name ); - }, - 10, - 2 - ); - - add_filter( - 'comments_list_table_query_args', - function( $args ) { - return $this->exclude_reviews_from_comments( $args ); - } - ); + self::add_action( 'admin_notices', array( $this, 'display_notices' ) ); + self::add_filter( 'woocommerce_dismiss_admin_notice_capability', array( $this, 'get_dismiss_capability' ), 10, 2 ); + self::add_filter( 'comments_list_table_query_args', array( $this, 'exclude_reviews_from_comments' ) ); } /** @@ -46,7 +29,7 @@ class ReviewsCommentsOverrides { protected function display_notices() : void { $screen = get_current_screen(); - if ( empty( $screen ) || 'edit-comments' !== $screen->base ) { + if ( empty( $screen ) || $screen->base !== 'edit-comments' ) { return; } @@ -123,7 +106,7 @@ class ReviewsCommentsOverrides { * @return string */ protected function get_dismiss_capability( $default_capability, $notice_name ) { - return self::REVIEWS_MOVED_NOTICE_ID === $notice_name ? Reviews::get_capability() : $default_capability; + return $notice_name === self::REVIEWS_MOVED_NOTICE_ID ? Reviews::get_capability() : $default_capability; } /** @@ -134,7 +117,7 @@ class ReviewsCommentsOverrides { */ protected function exclude_reviews_from_comments( $args ) : array { - if ( ! empty( $args['post_type'] ) && 'any' !== $args['post_type'] ) { + if ( ! empty( $args['post_type'] ) && $args['post_type'] !== 'any' ) { $post_types = (array) $args['post_type']; } else { $post_types = get_post_types(); @@ -142,7 +125,7 @@ class ReviewsCommentsOverrides { $index = array_search( 'product', $post_types ); - if ( false !== $index ) { + if ( $index !== false ) { unset( $post_types[ $index ] ); } diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php index c57e7bafc03..544cb848d51 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php @@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Internal\DataStores\Orders; use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; defined( 'ABSPATH' ) || exit; @@ -20,6 +21,8 @@ defined( 'ABSPATH' ) || exit; */ class CustomOrdersTableController { + use AccessiblePrivateMethods; + /** * The name of the option for enabling the usage of the custom orders tables */ @@ -90,89 +93,16 @@ class CustomOrdersTableController { * Initialize the hooks used by the class. */ private function init_hooks() { - add_filter( - 'woocommerce_order_data_store', - function ( $default_data_store ) { - return $this->get_data_store_instance( $default_data_store, 'order' ); - }, - 999, - 1 - ); - - add_filter( - 'woocommerce_order-refund_data_store', - function ( $default_data_store ) { - return $this->get_data_store_instance( $default_data_store, 'order_refund' ); - }, - 999, - 1 - ); - - add_filter( - 'woocommerce_debug_tools', - function( $tools ) { - return $this->add_initiate_regeneration_entry_to_tools_array( $tools ); - }, - 999, - 1 - ); - - add_filter( - 'woocommerce_get_sections_advanced', - function( $sections ) { - return $this->get_settings_sections( $sections ); - }, - 999, - 1 - ); - - add_filter( - 'woocommerce_get_settings_advanced', - function ( $settings, $section_id ) { - return $this->get_settings( $settings, $section_id ); - }, - 999, - 2 - ); - - add_filter( - 'updated_option', - function( $option, $old_value, $value ) { - $this->process_updated_option( $option, $old_value, $value ); - }, - 999, - 3 - ); - - add_filter( - 'pre_update_option', - function( $value, $option, $old_value ) { - return $this->process_pre_update_option( $option, $old_value, $value ); - }, - 999, - 3 - ); - - add_filter( - DataSynchronizer::PENDING_SYNCHRONIZATION_FINISHED_ACTION, - function() { - $this->process_sync_finished(); - } - ); - - add_action( - 'woocommerce_update_options_advanced_custom_data_stores', - function() { - $this->process_options_updated(); - } - ); - - add_action( - 'woocommerce_after_register_post_type', - function() { - $this->register_post_type_for_order_placeholders(); - } - ); + self::add_filter( 'woocommerce_order_data_store', array( $this, 'get_orders_data_store' ), 999, 1 ); + self::add_filter( 'woocommerce_order-refund_data_store', array( $this, 'get_refunds_data_store' ), 999, 1 ); + self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 999, 1 ); + self::add_filter( 'woocommerce_get_sections_advanced', array( $this, 'get_settings_sections' ), 999, 1 ); + self::add_filter( 'woocommerce_get_settings_advanced', array( $this, 'get_settings' ), 999, 2 ); + self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 ); + self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 ); + self::add_filter( DataSynchronizer::PENDING_SYNCHRONIZATION_FINISHED_ACTION, array( $this, 'process_sync_finished' ), 10, 0 ); + self::add_action( 'woocommerce_update_options_advanced_custom_data_stores', array( $this, 'process_options_updated' ), 10, 0 ); + self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 ); } /** @@ -221,14 +151,36 @@ class CustomOrdersTableController { * @return bool True if the custom orders table usage is enabled */ public function custom_orders_table_usage_is_enabled(): bool { - return 'yes' === get_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ); + return get_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) === 'yes'; } /** * Gets the instance of the orders data store to use. * - * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the woocommerce_order_data_store hooks). - * @param string $type The type of the data store to get. + * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the woocommerce_order_data_store hook). + * + * @return \WC_Object_Data_Store_Interface|string The actual data store to use. + */ + private function get_orders_data_store( $default_data_store ) { + return $this->get_data_store_instance( $default_data_store, 'order' ); + } + + /** + * Gets the instance of the refunds data store to use. + * + * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the woocommerce_order-refund_data_store hook). + * + * @return \WC_Object_Data_Store_Interface|string The actual data store to use. + */ + private function get_refunds_data_store( $default_data_store ) { + return $this->get_data_store_instance( $default_data_store, 'order_refund' ); + } + + /** + * Gets the instance of a given data store. + * + * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the appropriate hooks). + * @param string $type The type of the data store to get. * * @return \WC_Object_Data_Store_Interface|string The actual data store to use. */ @@ -296,7 +248,7 @@ class CustomOrdersTableController { */ private function create_custom_orders_tables() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput - if ( ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) ) { + if ( ! isset( $_REQUEST['_wpnonce'] ) || wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) === false ) { throw new \Exception( 'Invalid nonce' ); } @@ -347,7 +299,7 @@ class CustomOrdersTableController { * @return array The updated settings array. */ private function get_settings( array $settings, string $section_id ): array { - if ( ! $this->is_feature_visible() || 'custom_data_stores' !== $section_id ) { + if ( ! $this->is_feature_visible() || $section_id !== 'custom_data_stores' ) { return $settings; } @@ -365,7 +317,7 @@ class CustomOrdersTableController { ); $sync_status = $this->data_synchronizer->get_sync_status(); - $sync_is_pending = 0 !== $sync_status['current_pending_count']; + $sync_is_pending = $sync_status['current_pending_count'] !== 0; $settings[] = array( 'title' => __( 'Data store for orders', 'woocommerce' ), @@ -484,7 +436,7 @@ class CustomOrdersTableController { * @param mixed $value New value of the setting. */ private function process_updated_option( $option, $old_value, $value ) { - if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && 'no' === $value ) { + if ( $option === DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION && $value === 'no' ) { $this->data_synchronizer->cleanup_synchronization_state(); } } @@ -493,14 +445,14 @@ class CustomOrdersTableController { * Handler for the setting pre-update hook. * We use it to verify that authoritative orders table switch doesn't happen while sync is pending. * + * @param mixed $value New value of the setting. * @param string $option Setting name. * @param mixed $old_value Old value of the setting. - * @param mixed $value New value of the setting. * * @throws \Exception Attempt to change the authoritative orders table while orders sync is pending. */ - private function process_pre_update_option( $option, $old_value, $value ) { - if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || $value === $old_value || false === $old_value ) { + private function process_pre_update_option( $value, $option, $old_value ) { + if ( $option !== self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION || $value === $old_value || $old_value === false ) { return $value; } @@ -539,7 +491,7 @@ class CustomOrdersTableController { * @return bool */ private function auto_flip_authoritative_table_enabled(): bool { - return 'yes' === get_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ); + return get_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ) === 'yes'; } /** diff --git a/plugins/woocommerce/src/Internal/ProductAttributesLookup/DataRegenerator.php b/plugins/woocommerce/src/Internal/ProductAttributesLookup/DataRegenerator.php index ecb050c1d48..6e613dbbd98 100644 --- a/plugins/woocommerce/src/Internal/ProductAttributesLookup/DataRegenerator.php +++ b/plugins/woocommerce/src/Internal/ProductAttributesLookup/DataRegenerator.php @@ -5,9 +5,8 @@ namespace Automattic\WooCommerce\Internal\ProductAttributesLookup; -use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; -use Automattic\WooCommerce\Utilities\ArrayUtil; defined( 'ABSPATH' ) || exit; @@ -31,6 +30,8 @@ defined( 'ABSPATH' ) || exit; */ class DataRegenerator { + use AccessiblePrivateMethods; + public const PRODUCTS_PER_GENERATION_STEP = 10; /** @@ -55,28 +56,9 @@ class DataRegenerator { $this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup'; - add_filter( - 'woocommerce_debug_tools', - function( $tools ) { - return $this->add_initiate_regeneration_entry_to_tools_array( $tools ); - }, - 1, - 999 - ); - - add_action( - 'woocommerce_run_product_attribute_lookup_regeneration_callback', - function () { - $this->run_regeneration_step_callback(); - } - ); - - add_action( - 'woocommerce_installed', - function() { - $this->run_woocommerce_installed_callback(); - } - ); + self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 1, 999 ); + self::add_action( 'woocommerce_run_product_attribute_lookup_regeneration_callback', array( $this, 'run_regeneration_step_callback' ) ); + self::add_action( 'woocommerce_installed', array( $this, 'run_woocommerce_installed_callback' ) ); } /** @@ -446,7 +428,7 @@ class DataRegenerator { */ private function verify_tool_execution_nonce() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput - if ( ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) ) { + if ( ! isset( $_REQUEST['_wpnonce'] ) || wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) === false ) { throw new \Exception( 'Invalid nonce' ); } } @@ -519,7 +501,7 @@ class DataRegenerator { // If the lookup table has data, or if it's empty because there are no products yet, we're good. // Otherwise (lookup table is empty but products exist) we need to initiate a regeneration if one isn't already in progress. if ( $this->data_store->lookup_table_has_data() || ! $this->get_last_existing_product_id() ) { - $must_enable = 'no' !== get_option( 'woocommerce_attribute_lookup_enabled' ); + $must_enable = get_option( 'woocommerce_attribute_lookup_enabled' ) !== 'no'; $this->finalize_regeneration( $must_enable ); } else { $this->initiate_regeneration(); diff --git a/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php b/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php index 85e70c6c7e7..c561d1e9b00 100644 --- a/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php +++ b/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php @@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Internal\ProductAttributesLookup; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use Automattic\WooCommerce\Utilities\ArrayUtil; use Automattic\WooCommerce\Utilities\StringUtil; @@ -15,6 +16,8 @@ defined( 'ABSPATH' ) || exit; */ class LookupDataStore { + use AccessiblePrivateMethods; + /** * Types of updates to perform depending on the current changest */ @@ -46,89 +49,10 @@ class LookupDataStore { * Initialize the hooks used by the class. */ private function init_hooks() { - add_action( - 'woocommerce_run_product_attribute_lookup_update_callback', - function ( $product_id, $action ) { - $this->run_update_callback( $product_id, $action ); - }, - 10, - 2 - ); - - add_filter( - 'woocommerce_get_sections_products', - function ( $products ) { - if ( $this->check_lookup_table_exists() ) { - $products['advanced'] = __( 'Advanced', 'woocommerce' ); - } - return $products; - }, - 100, - 1 - ); - - add_action( - 'woocommerce_rest_insert_product', - function ( $product_post, $request ) { - $this->on_product_created_or_updated_via_rest_api( $product_post, $request ); - }, - 100, - 2 - ); - - add_filter( - 'woocommerce_get_settings_products', - function ( $settings, $section_id ) { - if ( 'advanced' === $section_id && $this->check_lookup_table_exists() ) { - $title_item = array( - 'title' => __( 'Product attributes lookup table', 'woocommerce' ), - 'type' => 'title', - ); - - $regeneration_is_in_progress = $this->regeneration_is_in_progress(); - - if ( $regeneration_is_in_progress ) { - $title_item['desc'] = __( 'These settings are not available while the lookup table regeneration is in progress.', 'woocommerce' ); - } - - $settings[] = $title_item; - - if ( ! $regeneration_is_in_progress ) { - $regeneration_aborted_warning = - $this->regeneration_was_aborted() ? - sprintf( - "
%s
%s
", - __( 'WARNING: The product attributes lookup table regeneration process was aborted.', 'woocommerce' ), - __( 'This means that the table is probably in an inconsistent state. It\'s recommended to run a new regeneration process or to resume the aborted process (Status - Tools - Regenerate the product attributes lookup table/Resume the product attributes lookup table regeneration) before enabling the table usage.', 'woocommerce' ) - ) : null; - - $settings[] = array( - 'title' => __( 'Enable table usage', 'woocommerce' ), - 'desc' => __( 'Use the product attributes lookup table for catalog filtering.', 'woocommerce' ), - 'desc_tip' => $regeneration_aborted_warning, - 'id' => 'woocommerce_attribute_lookup_enabled', - 'default' => 'no', - 'type' => 'checkbox', - 'checkboxgroup' => 'start', - ); - - $settings[] = array( - 'title' => __( 'Direct updates', 'woocommerce' ), - 'desc' => __( 'Update the table directly upon product changes, instead of scheduling a deferred update.', 'woocommerce' ), - 'id' => 'woocommerce_attribute_lookup_direct_updates', - 'default' => 'no', - 'type' => 'checkbox', - 'checkboxgroup' => 'start', - ); - } - - $settings[] = array( 'type' => 'sectionend' ); - } - return $settings; - }, - 100, - 2 - ); + self::add_action( 'woocommerce_run_product_attribute_lookup_update_callback', array( $this, 'run_update_callback' ), 10, 2 ); + self::add_filter( 'woocommerce_get_sections_products', array( $this, 'add_advanced_section_to_product_settings' ), 100, 1 ); + self::add_action( 'woocommerce_rest_insert_product', array( $this, 'on_product_created_or_updated_via_rest_api' ), 100, 2 ); + self::add_filter( 'woocommerce_get_settings_products', array( $this, 'add_product_attributes_lookup_table_settings' ), 100, 2 ); } /** @@ -172,7 +96,7 @@ class LookupDataStore { } $action = $this->get_update_action( $changeset ); - if ( self::ACTION_NONE !== $action ) { + if ( $action !== self::ACTION_NONE ) { $this->maybe_schedule_update( $product->get_id(), $action ); } } @@ -188,7 +112,7 @@ class LookupDataStore { * @param int $action The action to perform, one of the ACTION_ constants. */ private function maybe_schedule_update( int $product_id, int $action ) { - if ( 'yes' === get_option( 'woocommerce_attribute_lookup_direct_updates' ) ) { + if ( get_option( 'woocommerce_attribute_lookup_direct_updates' ) === 'yes' ) { $this->run_update_callback( $product_id, $action ); return; } @@ -268,7 +192,7 @@ class LookupDataStore { if ( in_array( 'catalog_visibility', $keys, true ) ) { $new_visibility = $changeset['catalog_visibility']; - if ( 'visible' === $new_visibility || 'catalog' === $new_visibility ) { + if ( $new_visibility === 'visible' || $new_visibility === 'catalog' ) { return self::ACTION_INSERT; } else { return self::ACTION_DELETE; @@ -663,7 +587,7 @@ class LookupDataStore { * @return bool True if a lookup table regeneration is already in progress. */ public function regeneration_is_in_progress() { - return 'yes' === get_option( 'woocommerce_attribute_lookup_regeneration_in_progress', null ); + return get_option( 'woocommerce_attribute_lookup_regeneration_in_progress', null ) === 'yes'; } /** @@ -701,7 +625,7 @@ class LookupDataStore { * @return bool True if the last lookup table regeneration process was aborted. */ public function regeneration_was_aborted(): bool { - return 'yes' === get_option( 'woocommerce_attribute_lookup_regeneration_aborted' ); + return get_option( 'woocommerce_attribute_lookup_regeneration_aborted' ) === 'yes'; } /** @@ -715,4 +639,75 @@ class LookupDataStore { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared return ( (int) $wpdb->get_var( "SELECT EXISTS (SELECT 1 FROM {$this->lookup_table_name})" ) ) !== 0; } + + /** + * Handler for 'woocommerce_get_sections_products', adds the "Advanced" section to the product settings. + * + * @param array $products Original array of settings sections. + * @return array New array of settings sections. + */ + private function add_advanced_section_to_product_settings( array $products ): array { + if ( $this->check_lookup_table_exists() ) { + $products['advanced'] = __( 'Advanced', 'woocommerce' ); + } + + return $products; + } + + /** + * Handler for 'woocommerce_get_settings_products', adds the settings related to the product attributes lookup table. + * + * @param array $settings Original settings configuration array. + * @param string $section_id Settings section identifier. + * @return array New settings configuration array. + */ + private function add_product_attributes_lookup_table_settings( array $settings, string $section_id ): array { + if ( $section_id === 'advanced' && $this->check_lookup_table_exists() ) { + $title_item = array( + 'title' => __( 'Product attributes lookup table', 'woocommerce' ), + 'type' => 'title', + ); + + $regeneration_is_in_progress = $this->regeneration_is_in_progress(); + + if ( $regeneration_is_in_progress ) { + $title_item['desc'] = __( 'These settings are not available while the lookup table regeneration is in progress.', 'woocommerce' ); + } + + $settings[] = $title_item; + + if ( ! $regeneration_is_in_progress ) { + $regeneration_aborted_warning = + $this->regeneration_was_aborted() ? + sprintf( + "%s
%s
", + __( 'WARNING: The product attributes lookup table regeneration process was aborted.', 'woocommerce' ), + __( 'This means that the table is probably in an inconsistent state. It\'s recommended to run a new regeneration process or to resume the aborted process (Status - Tools - Regenerate the product attributes lookup table/Resume the product attributes lookup table regeneration) before enabling the table usage.', 'woocommerce' ) + ) : null; + + $settings[] = array( + 'title' => __( 'Enable table usage', 'woocommerce' ), + 'desc' => __( 'Use the product attributes lookup table for catalog filtering.', 'woocommerce' ), + 'desc_tip' => $regeneration_aborted_warning, + 'id' => 'woocommerce_attribute_lookup_enabled', + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + ); + + $settings[] = array( + 'title' => __( 'Direct updates', 'woocommerce' ), + 'desc' => __( 'Update the table directly upon product changes, instead of scheduling a deferred update.', 'woocommerce' ), + 'id' => 'woocommerce_attribute_lookup_direct_updates', + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + ); + } + + $settings[] = array( 'type' => 'sectionend' ); + } + + return $settings; + } } diff --git a/plugins/woocommerce/src/Internal/Settings/OptionSanitizer.php b/plugins/woocommerce/src/Internal/Settings/OptionSanitizer.php index cd003be265d..2ecdba823fd 100644 --- a/plugins/woocommerce/src/Internal/Settings/OptionSanitizer.php +++ b/plugins/woocommerce/src/Internal/Settings/OptionSanitizer.php @@ -5,6 +5,8 @@ namespace Automattic\WooCommerce\Internal\Settings; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; + defined( 'ABSPATH' ) || exit; /** @@ -14,6 +16,8 @@ defined( 'ABSPATH' ) || exit; */ class OptionSanitizer { + use AccessiblePrivateMethods; + /** * OptionSanitizer constructor. */ @@ -27,11 +31,9 @@ class OptionSanitizer { ); foreach ( $color_options as $option_name ) { - add_filter( + self::add_filter( "woocommerce_admin_settings_sanitize_option_{$option_name}", - function( $value, $option ) { - return $this->sanitize_color_option( $value, $option ); - }, + array( $this, 'sanitize_color_option' ), 10, 2 ); @@ -62,5 +64,4 @@ class OptionSanitizer { return (string) $value; } - } diff --git a/plugins/woocommerce/src/Internal/Traits/AccessiblePrivateMethods.php b/plugins/woocommerce/src/Internal/Traits/AccessiblePrivateMethods.php new file mode 100644 index 00000000000..52beb6c1532 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Traits/AccessiblePrivateMethods.php @@ -0,0 +1,190 @@ +mark_method_as_accessible( $callback[1] ); + } + } + + /** + * Register a private or protected instance method of this class as externally accessible. + * + * @param string $method_name Method name. + * @return bool True if the method has been marked as externally accessible, false if the method doesn't exist. + */ + protected function mark_method_as_accessible( string $method_name ): bool { + // Note that an "is_callable" check would be useless here: + // "is_callable" always returns true if the class implements __call. + if ( method_exists( $this, $method_name ) ) { + $this->_accessible_private_methods[ $method_name ] = $method_name; + return true; + } + + return false; + } + + /** + * Register a private or protected static method of this class as externally accessible. + * + * @param string $method_name Method name. + * @return bool True if the method has been marked as externally accessible, false if the method doesn't exist. + */ + protected static function mark_static_method_as_accessible( string $method_name ): bool { + if ( method_exists( __CLASS__, $method_name ) ) { + static::$_accessible_static_private_methods[ $method_name ] = $method_name; + return true; + } + + return false; + } + + /** + * Undefined/inaccessible instance method call handler. + * + * @param string $name Called method name. + * @param array $arguments Called method arguments. + * @return mixed + * @throws \Error The called instance method doesn't exist or is private/protected and not marked as externally accessible. + */ + public function __call( $name, $arguments ) { + if ( isset( $this->_accessible_private_methods[ $name ] ) ) { + return call_user_func_array( array( $this, $name ), $arguments ); + } elseif ( is_callable( array( 'parent', '__call' ) ) ) { + return parent::__call( $name, $arguments ); + } elseif ( method_exists( $this, $name ) ) { + throw new \Error( 'Call to private method ' . get_class( $this ) . '::' . $name ); + } else { + throw new \Error( 'Call to undefined method ' . get_class( $this ) . '::' . $name ); + } + } + + /** + * Undefined/inaccessible static method call handler. + * + * @param string $name Called method name. + * @param array $arguments Called method arguments. + * @return mixed + * @throws \Error The called static method doesn't exist or is private/protected and not marked as externally accessible. + */ + public static function __callStatic( $name, $arguments ) { + if ( isset( static::$_accessible_static_private_methods[ $name ] ) ) { + return call_user_func_array( array( __CLASS__, $name ), $arguments ); + } elseif ( is_callable( array( 'parent', '__callStatic' ) ) ) { + return parent::__callStatic( $name, $arguments ); + } elseif ( 'add_action' === $name || 'add_filter' === $name ) { + $proper_method_name = 'add_static_' . substr( $name, 4 ); + throw new \Error( __CLASS__ . '::' . $name . " can't be called statically, did you mean '$proper_method_name'?" ); + } elseif ( method_exists( __CLASS__, $name ) ) { + throw new \Error( 'Call to private method ' . __CLASS__ . '::' . $name ); + } else { + throw new \Error( 'Call to undefined method ' . __CLASS__ . '::' . $name ); + } + } +} diff --git a/plugins/woocommerce/src/Utilities/ArrayUtil.php b/plugins/woocommerce/src/Utilities/ArrayUtil.php index 87865f9c58e..7c758003d97 100644 --- a/plugins/woocommerce/src/Utilities/ArrayUtil.php +++ b/plugins/woocommerce/src/Utilities/ArrayUtil.php @@ -168,5 +168,21 @@ class ArrayUtil { return array_map( $callback, $items ); } + + /** + * Push a value to an array, but only if the value isn't in the array already. + * + * @param array $array The array. + * @param mixed $value The value to maybe push. + * @return bool True if the value has been added to the array, false if the value was already in the array. + */ + public static function push_once( array &$array, $value ) : bool { + if ( in_array( $value, $array, true ) ) { + return false; + } + + $array[] = $value; + return true; + } } diff --git a/plugins/woocommerce/tests/php/src/Internal/Traits/AccessiblePrivateMethodsTest.php b/plugins/woocommerce/tests/php/src/Internal/Traits/AccessiblePrivateMethodsTest.php new file mode 100644 index 00000000000..32a14ea7297 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Internal/Traits/AccessiblePrivateMethodsTest.php @@ -0,0 +1,452 @@ +assertEquals( 1, $sut->public_return_one() ); + $this->assertEquals( 10, $sut::public_static_return_ten() ); + } + + /** + * @testdox Private and protected instance and static methods are still inaccessible by default in classes implementing the trait. + * + * @testWith ["protected_return_two"] + * ["private_return_three"] + * ["static_protected_return_twenty"] + * ["static_private_return_thirty"] + * + * @param string $method_name The name of the method to try to call. + */ + public function test_private_and_protected_methods_are_still_inaccessible_by_default( string $method_name ) { + //phpcs:disable Squiz.Commenting + $sut = new class() { + use AccessiblePrivateMethods; + + protected function protected_return_two() { + return 2; + } + + private function private_return_three() { + return 3; + } + + protected static function static_protected_return_twenty() { + return 20; + } + + private static function static_private_return_thirty() { + return 30; + } + }; + //phpcs:enable Squiz.Commenting + + $this->expectException( \Error::class ); + $this->expectExceptionMessage( 'Call to private method ' . get_class( $sut ) . '::' . $method_name ); + + if ( StringUtil::starts_with( $method_name, 'static' ) ) { + $sut::$method_name(); + } else { + $sut->$method_name(); + } + } + + /** + * @testWith [true] + * [false] + * + * @param bool $call_static_method True to call the non-existing method statically, false to call it in an object instance. + * + * @testdox Calling non-existing methods still throws an error if there's no __call or __callStatic method in the parent class. + */ + public function test_non_existing_methods_still_throw_error_if_no_call_method_in_parent( bool $call_static_method ) { + //phpcs:disable Squiz.Commenting + $sut = new class() { + use AccessiblePrivateMethods; + }; + //phpcs:enable Squiz.Commenting + + $this->expectException( \Error::class ); + $this->expectExceptionMessage( 'Call to undefined method ' . get_class( $sut ) . '::non_existing' ); + + if ( $call_static_method ) { + $sut::non_existing(); + } else { + $sut->non_existing(); + } + } + + /** + * @testdox Calling non-existing methods redirects to __call method in the parent class if available. + */ + public function test_non_existing_methods_redirect_to_parent_call_method_if_available() { + //phpcs:disable Squiz.Commenting + $sut = new class() extends BaseClass { + use AccessiblePrivateMethods; + }; + //phpcs:enable Squiz.Commenting + + $result = $sut->method_in_parent_class( 'foo' ); + $this->assertEquals( 'Argument: foo', $result ); + } + + /** + * @testdox Calling static non-existing methods redirects to __call method in the parent class if available. + */ + public function test_static_non_existing_methods_redirect_to_parent_call_method_if_available() { + //phpcs:disable Squiz.Commenting + $sut = new class() extends BaseClass { + use AccessiblePrivateMethods; + }; + //phpcs:enable Squiz.Commenting + + $result = $sut::static_method_in_parent_class( 'foo' ); + $this->assertEquals( 'Static argument: foo', $result ); + } + + /** + * @testdox Private and protected methods can be made accessible by calling mark_method_as_accessible. + */ + public function test_private_and_protected_methods_can_be_made_accessible() { + //phpcs:disable Squiz.Commenting + $sut = new class() { + use AccessiblePrivateMethods; + + public function __construct() { + $this->mark_method_as_accessible( 'protected_return_two' ); + $this->mark_method_as_accessible( 'private_return_three' ); + } + + //phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag + final public static function init() { + self::mark_static_method_as_accessible( 'protected_return_twenty' ); + self::mark_static_method_as_accessible( 'private_return_thirty' ); + } + + protected function protected_return_two() { + return 2; + } + + private function private_return_three() { + return 3; + } + + protected static function protected_return_twenty() { + return 20; + } + + private static function private_return_thirty() { + return 30; + } + }; + //phpcs:enable Squiz.Commenting + + $this->assertEquals( 2, $sut->protected_return_two() ); + $this->assertEquals( 3, $sut->private_return_three() ); + + $sut::init(); + + $this->assertEquals( 20, $sut::protected_return_twenty() ); + $this->assertEquals( 30, $sut::private_return_thirty() ); + } + + /** + * @testWith [true] + * [false] + * + * @param bool $call_static_method True to call the non-existing method statically, false to call it in an object instance. + * + * @testdox Trying to mark a non existing method as accessible with mark_method_as_accessible does nothing. + */ + public function test_accessibilizing_non_existing_method_does_nothing( bool $call_static_method ) { + //phpcs:disable Squiz.Commenting + $sut = new class() { + use AccessiblePrivateMethods; + + public function __construct() { + $this->mark_method_as_accessible( 'non_existing' ); + } + + //phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag + final public static function init() { + self::mark_static_method_as_accessible( 'non_existing' ); + } + }; + //phpcs:enable Squiz.Commenting + + $this->expectException( \Error::class ); + $this->expectExceptionMessage( 'Call to undefined method ' . get_class( $sut ) . '::non_existing' ); + + if ( $call_static_method ) { + $sut::non_existing(); + } else { + $sut->non_existing(); + } + } + + /** + * @testWith [true] + * [false] + * + * @testdox New add_(static_)action and add_(static_)filter methods can be used to register private and protected class methods as hook callbacks. + */ + public function test_private_and_protected_hook_handler_methods_can_be_made_accessible() { + //phpcs:disable Squiz.Commenting + $sut = new class() { + use AccessiblePrivateMethods; + + public $action_argument = null; + + public static $static_action_argument = null; + + public function __construct() { + self::add_action( 'action_handled_privately', array( $this, 'handle_action' ) ); + self::add_filter( 'filter_handled_privately', array( $this, 'handle_filter' ) ); + } + + //phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag + final public static function init() { + self::add_action( 'static_action_handled_privately', array( __CLASS__, 'handle_static_action' ) ); + self::add_filter( 'static_filter_handled_privately', array( __CLASS__, 'handle_static_filter' ) ); + } + + private function handle_action( $argument ) { + $this->action_argument = $argument; + } + + private function handle_filter( $argument ) { + return 'Filter argument: ' . $argument; + } + + private static function handle_static_action( $argument ) { + self::$static_action_argument = $argument; + } + + private static function handle_static_filter( $argument ) { + return 'Static filter argument: ' . $argument; + } + }; + //phpcs:enable Squiz.Commenting + + $sut::init(); + + //phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment + + do_action( 'action_handled_privately', 'foo' ); + $this->assertEquals( 'foo', $sut->action_argument ); + + $filter_result = apply_filters( 'filter_handled_privately', 'bar' ); + $this->assertEquals( 'Filter argument: bar', $filter_result ); + + do_action( 'static_action_handled_privately', 'fizz' ); + $this->assertEquals( 'fizz', $sut::$static_action_argument ); + + $filter_result = apply_filters( 'static_filter_handled_privately', 'buzz' ); + $this->assertEquals( 'Static filter argument: buzz', $filter_result ); + + //phpcs:enable WooCommerce.Commenting.CommentHooks.MissingHookComment + } + + /** + * @testdox add_action and add_filter methods can be used to register public class methods as hook callbacks, although that's not needed. + */ + public function test_accessibilizing_public_method_does_nothing() { + //phpcs:disable Squiz.Commenting + $sut = new class() { + use AccessiblePrivateMethods; + + public $action_argument = null; + + public static $static_action_argument = null; + + public function __construct() { + self::add_action( 'action_handled_publicly', array( $this, 'handle_action_publicly' ) ); + } + + //phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag + final public static function init() { + self::add_action( 'static_action_handled_publicly', array( __CLASS__, 'handle_static_action_publicly' ) ); + } + + public function handle_action_publicly( $argument ) { + $this->action_argument = $argument; + } + + public static function handle_static_action_publicly( $argument ) { + self::$static_action_argument = $argument; + } + }; + //phpcs:enable Squiz.Commenting + + $sut::init(); + + //phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment + do_action( 'action_handled_publicly', 'foo' ); + $this->assertEquals( 'foo', $sut->action_argument ); + + do_action( 'static_action_handled_publicly', 'bar' ); + $this->assertEquals( 'bar', $sut::$static_action_argument ); + + //phpcs:enable WooCommerce.Commenting.CommentHooks.MissingHookComment + } + + /** + * @testdox A hook attached to a private or protected method can be easily unhooked externally. + */ + public function test_unhooking_private_methods() { + //phpcs:disable Squiz.Commenting + $sut = new class() { + use AccessiblePrivateMethods; + + public $action_argument = null; + + public static $static_action_argument = null; + + public function __construct() { + self::add_action( 'action_handled_privately', array( $this, 'handle_action' ) ); + self::add_filter( 'filter_handled_privately', array( $this, 'handle_filter' ) ); + } + + //phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag + final public static function init() { + self::add_action( 'static_action_handled_privately', array( __CLASS__, 'handle_static_action' ) ); + self::add_filter( 'static_filter_handled_privately', array( __CLASS__, 'handle_static_filter' ) ); + } + + private function handle_action( $argument ) { + $this->action_argument = $argument; + } + + private function handle_filter( $argument ) { + return 'Filter argument: ' . $argument; + } + + private static function handle_static_action( $argument ) { + self::$static_action_argument = $argument; + } + + private static function handle_static_filter( $argument ) { + return 'Static filter argument: ' . $argument; + } + }; + //phpcs:enable Squiz.Commenting + + $sut::init(); + + //phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment + + $filter_result = apply_filters( 'filter_handled_privately', 'foo' ); + $this->assertEquals( 'Filter argument: foo', $filter_result ); + + $filter_result = apply_filters( 'static_filter_handled_privately', 'bar' ); + $this->assertEquals( 'Static filter argument: bar', $filter_result ); + + do_action( 'action_handled_privately', 'foo2' ); + $this->assertEquals( 'foo2', $sut->action_argument ); + + do_action( 'static_action_handled_privately', 'bar2' ); + $this->assertEquals( 'bar2', $sut::$static_action_argument ); + + remove_action( 'action_handled_privately', array( $sut, 'handle_action' ) ); + remove_filter( 'filter_handled_privately', array( $sut, 'handle_filter' ) ); + remove_action( 'static_action_handled_privately', array( get_class( $sut ), 'handle_static_action' ) ); + remove_filter( 'static_filter_handled_privately', array( get_class( $sut ), 'handle_static_filter' ) ); + + $filter_result = apply_filters( 'filter_handled_privately', 'fizz' ); + $this->assertEquals( 'fizz', $filter_result ); + + $filter_result = apply_filters( 'static_filter_handled_privately', 'buzz' ); + $this->assertEquals( 'buzz', $filter_result ); + + do_action( 'action_handled_privately', 'fizz2' ); + $this->assertEquals( 'foo2', $sut->action_argument ); + + do_action( 'static_action_handled_privately', 'buzz2' ); + $this->assertEquals( 'bar2', $sut::$static_action_argument ); + + //phpcs:enable WooCommerce.Commenting.CommentHooks.MissingHookComment + } + + /** + * @testWith ["action"] + * ["filter"] + * + * @testdox Trying to use 'add_action' or 'add_filter' statically throws an error hinting at the proper method names. + * + * @param string $action_or_filter 'action' or 'filter'. + * @return void + */ + public function test_instance_add_action_and_filter_methods_throw_error_with_hint_when_called_statically( $action_or_filter ) { + //phpcs:disable Squiz.Commenting + $sut = new class() { + use AccessiblePrivateMethods; + }; + + $method_name = "add_${action_or_filter}"; + $proper_method_name = "add_static_${action_or_filter}"; + + $this->expectException( \Error::class ); + $this->expectExceptionMessage( get_class( $sut ) . '::' . "$method_name can't be called statically, did you mean '$proper_method_name'?" ); + + $sut::$method_name( 'some_action', function() {} ); + } +} + +//phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound, Squiz.Classes.ClassFileName.NoMatch +/** + * Class used in the inherited __call method test. + */ +class BaseClass { + //phpcs:disable Squiz.Commenting.FunctionComment.Missing + public function __call( $name, $arguments ) { + if ( 'method_in_parent_class' === $name ) { + return 'Argument: ' . $arguments[0]; + } + } + + public static function __callStatic( $name, $arguments ) { + if ( 'static_method_in_parent_class' === $name ) { + return 'Static argument: ' . $arguments[0]; + } + } + //phpcs:enable Squiz.Commenting.FunctionComment.Missing +} + +//phpcs:enable Generic.Files.OneObjectStructurePerFile.MultipleFound, Squiz.Classes.ClassFileName.NoMatch diff --git a/plugins/woocommerce/tests/php/src/Utilities/ArrayUtilTest.php b/plugins/woocommerce/tests/php/src/Utilities/ArrayUtilTest.php index bc543919c47..2ba949d6cc7 100644 --- a/plugins/woocommerce/tests/php/src/Utilities/ArrayUtilTest.php +++ b/plugins/woocommerce/tests/php/src/Utilities/ArrayUtilTest.php @@ -251,4 +251,28 @@ class ArrayUtilTest extends \WC_Unit_Test_Case { $actual = ArrayUtil::select( $items, 'the_id', ArrayUtil::SELECT_BY_AUTO ); $this->assertEquals( array( 1, 2, 3 ), $actual ); } + + /** + * @testdox push_once doesn't alter the array and returns false if the item is already in the array. + */ + public function test_push_once_existing_value() { + $array = array( 1, 2, 3 ); + + $result = ArrayUtil::push_once( $array, 2 ); + + $this->assertFalse( $result ); + $this->assertEquals( array( 1, 2, 3 ), $array ); + } + + /** + * @testdox push_once pushes the value in the array and returns true if the value isn't yet in the array. + */ + public function test_push_once_new_value() { + $array = array( 1, 2, 3 ); + + $result = ArrayUtil::push_once( $array, 4 ); + + $this->assertTrue( $result ); + $this->assertEquals( array( 1, 2, 3, 4 ), $array ); + } }