Add the AccessiblePrivateMethods trait (#34019)

This trait allows to mark private and protected methods as externally
accessible. There are also utility add_action and add_filter methods
that mark the method as accessible and use it as callback in one
single step.

Additionally, all the classes that were hooking to private methods using an
anonymous function in the 'src' directory are changed to use the new
AccessiblePrivateMethods trait instead, thus they can be unhooked now.
This commit is contained in:
Néstor Soriano 2022-09-15 09:24:35 +02:00 committed by GitHub
parent b73b2fff0a
commit 2b8d1fbe11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 885 additions and 332 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add the AccessiblePrivateMethods trait

View File

@ -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 );

View File

@ -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}"}();
}
}
/**

View File

@ -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
}
}

View File

@ -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 ] );
}

View File

@ -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';
}
/**

View File

@ -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();

View File

@ -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(
"<p><strong style='color: #E00000'>%s</strong></p><p>%s</p>",
__( '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(
"<p><strong style='color: #E00000'>%s</strong></p><p>%s</p>",
__( '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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace Automattic\WooCommerce\Internal\Traits;
use Automattic\WooCommerce\Utilities\ArrayUtil;
/**
* This trait allows making private methods of a class accessible from outside.
* This is useful to define hook handlers with the [$this, 'method'] or [__CLASS__, 'method'] syntax
* without having to make the method public (and thus having to keep it forever for backwards compatibility).
*
* Example:
*
* class Foobar {
* use AccessiblePrivateMethods;
*
* public function __construct() {
* self::add_action('some_action', [$this, 'handle_some_action']);
* }
*
* public static function init() {
* self::add_filter('some_filter', [__CLASS__, 'handle_some_filter']);
* }
*
* private function handle_some_action() {
* }
*
* private static function handle_some_filter() {
* }
* }
*
* For this to work the callback must be an array and the first element of the array must be either '$this', '__CLASS__',
* or another instance of the same class; otherwise the method won't be marked as accessible
* (but the corresponding WordPress 'add_action' and 'add_filter' functions will still be called).
*
* No special procedure is needed to remove hooks set up with these methods, the regular 'remove_action'
* and 'remove_filter' functions provided by WordPress can be used as usual.
*/
trait AccessiblePrivateMethods {
//phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
/**
* List of instance methods marked as externally accessible.
*
* @var array
*/
private $_accessible_private_methods = array();
/**
* List of static methods marked as externally accessible.
*
* @var array
*/
private static $_accessible_static_private_methods = array();
//phpcs:enable PSR2.Classes.PropertyDeclaration.Underscore
/**
* Register a WordPress action.
* If the callback refers to a private or protected instance method in this class, the method is marked as externally accessible.
*
* $callback can be a standard callable, or a string representing the name of a method in this class.
*
* @param string $hook_name The name of the action to add the callback to.
* @param callable|string $callback The callback to be run when the action is called.
* @param int $priority Optional. Used to specify the order in which the functions
* associated with a particular action are executed.
* Lower numbers correspond with earlier execution,
* and functions with the same priority are executed
* in the order in which they were added to the action. Default 10.
* @param int $accepted_args Optional. The number of arguments the function accepts. Default 1.
*/
protected static function add_action( string $hook_name, $callback, int $priority = 10, int $accepted_args = 1 ): void {
self::process_callback_before_hooking( $callback );
add_action( $hook_name, $callback, $priority, $accepted_args );
}
/**
* Register a WordPress filter.
* If the callback refers to a private or protected instance method in this class, the method is marked as externally accessible.
*
* $callback can be a standard callable, or a string representing the name of a method in this class.
*
* @param string $hook_name The name of the filter to add the callback to.
* @param callable|string $callback The callback to be run when the filter is called.
* @param int $priority Optional. Used to specify the order in which the functions
* associated with a particular filter are executed.
* Lower numbers correspond with earlier execution,
* and functions with the same priority are executed
* in the order in which they were added to the filter. Default 10.
* @param int $accepted_args Optional. The number of arguments the function accepts. Default 1.
*/
protected static function add_filter( string $hook_name, $callback, int $priority = 10, int $accepted_args = 1 ): void {
self::process_callback_before_hooking( $callback );
add_filter( $hook_name, $callback, $priority, $accepted_args );
}
/**
* Do the required processing to a callback before invoking the WordPress 'add_action' or 'add_filter' function.
*
* @param callable $callback The callback to process.
* @return void
*/
protected static function process_callback_before_hooking( $callback ): void {
if ( ! is_array( $callback ) || count( $callback ) < 2 ) {
return;
}
$first_item = $callback[0];
if ( __CLASS__ === $first_item ) {
static::mark_static_method_as_accessible( $callback[1] );
} elseif ( is_object( $first_item ) && get_class( $first_item ) === __CLASS__ ) {
$first_item->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 );
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,452 @@
<?php
namespace Automattic\WooCommerce\Tests\Internal\Traits;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\StringUtil;
/**
* Tests for the AccessiblePrivateMethods class.
*/
class AccessiblePrivateMethodsTest extends \WC_Unit_Test_Case {
/**
* Runs before each test.
*/
public function setUp(): void {
remove_all_filters( 'filter_handled_privately' );
remove_all_actions( 'action_handled_privately' );
remove_all_actions( 'action_handled_publicly' );
remove_all_filters( 'static_filter_handled_privately' );
remove_all_actions( 'static_action_handled_privately' );
remove_all_actions( 'static_action_handled_publicly' );
parent::setUp();
}
/**
* @testdox Public instance and static methods are still accessible in classes implementing the trait.
*/
public function test_public_methods_are_still_accessible() {
//phpcs:disable Squiz.Commenting
$sut = new class() {
use AccessiblePrivateMethods;
public function public_return_one() {
return 1;
}
public static function public_static_return_ten() {
return 10;
}
};
//phpcs:enable Squiz.Commenting
$this->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

View File

@ -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 );
}
}